Redux Saga를 어떻게 써야할까
Redux에서의 비동기 로직 작성을 가능하게 해주는 Redux Saga가 참 잘 쓰기 힘든 기술이라는 생각을 많이 합니다. Saga에 대한 생각도 자주 바뀌었습니다. Saga를 배우고 조금 사용해본 시점에서는 비동기로 불러온 값들을 지금 쓸 것도 아닌데 왜 굳이 다 저장하고, 상태를 하나하나 매겨야 하나 이해를 하지 못하고 Saga를 쓰기 위한 타이핑이 쓸데없이 많다는 생각을 가지고 있었습니다.
프로덕션 레벨의 제품을 만져보고 나서 Saga가 좋은 구조를 가진, 예측 가능한 웹앱을 만드는데 도움을 줄 수 있다는 깨달음을 얻었습니다. 그 이후에 Saga를 잘 활용해보고 싶었지만 예기치 않은 난관에 부닥치는 경우가 많아 잘 사용하는데 실패한 경험이 많은 것 같습니다.
이번 포스팅에서는 Saga를 잘 쓰는 법에 대한 고민과 생각의 결과를 정리합니다. Saga와 관련해 몇가지 생각할 거리들을 놓고 글을 써볼 예정입니다.
Redux+Saga가 무엇인지 설명하는 글은 아닙니다. Redux+Saga를 프로젝트에 적용해보셨거나, 지식이 있는 상태로 읽으시면 좋습니다!
Saga를 쓰는/쓰지 않는 이유
store 만들기
=>action 선언하기
=>action, payload 리턴하는 함수 만들기
=>reducer 함수 만들기
=>비동기 로직이라면 이 과정 3번 반복(fetch,success,failure)
=>store에 비동기 데이터 저장하는 Saga 작성하기
=>컴포넌트에서 사용하 기
솔직히 말하면 이 과정이 쓸데없는 낭비라고 생각했던 적이 있습니다. 꼭 나중에 또 쓸 것도 아닌데 비동기 데이터를 전부 redux store에 모아두는 거지? 그냥 컴포넌트단에서 async await 하면 되는데... 타이핑은 왜 이렇게 많지?
비즈니스 로직과 UI 로직의 분리
비즈니스 로직과 UI로직은 분리할수록 좋고, Redux + Saga를 사용하는 것은 한 가지 방법입니다. 컴포넌트에서 직접 async/await 문법을 통해 서버에 요청을 날리는게 가장 간편한 방법으로 보이긴 합니다. 하지만 많이 쓸수록 컴포넌트에 비즈니스 로직이 직접 의존하는 경우가 많아지고, 얽히고 섥혀서 UI를 수정하기 위해 비즈니스 로직을 같이 건드려야 하거나 그 반대의 경우도 생길 수 있습니다.
앱의 크기가 작다면 로직이 섞여서 발생하는 복잡성을 충분히 처리할 수도 있겠지만, 꽤 큰 앱의 경우에는 이렇게 로직이 섞였을 경우 유지보수가 점점 힘들어집니다.
소프트웨어 아키텍처의 고전인, 클린 아키텍처 에서도 주장하는 것이 웹앱인지 모바일 앱인지, UI에 데이터가 어떤 모양으로 뿌려지는지는 크게 중요하지 않다는 것입니다. 클라이언트의 형태는 관련된 의사결정을 미룰 수도 있는 부수사항일 뿐입니다.
UI를 실질적으로 채우는 데이터를 다루는 로직들은 앱의 중심, 즉 코어를 이룹니다. 데이터와 관련된 로직인 이 코어를 그대로 둔 채, 클라이언트는 얼마든지 바꿔 끼울 수 있습니다. 그리고 Redux + Saga는 이러한 앱의 코어가 아래 그림처럼 UI로직과는 확실히 분리된 채로 작동할 수 있도록 만드는 도구입니다. React로 앱을 만들 때 비즈니스 로직을 모두 Saga에 몰아넣고 컴포넌트 단에서는 dispatch만 하는 식의 구현도 가능합니다.
비즈니스 로직을 더 믿을만하게 만드는 제네레이터 문법
Saga를 사용하게 되는 또 하나의 이유이기도 합니다. Saga의 아이덴티티라고도 할 수 있는 제네레이터 문법은 필요한 로직들이 순차적이고 엄밀하게 작동할 수 있도록 돕습니다. yield
를 사용하면 비동기 로직 뿐 아니라, 새로운 액션 발행(put
), 동기적인 함수 호출(call
) 까지 모두 차례 대로 수행할 수 있도록 보장하여 예상치 못한 사이드 이펙트 발생을 줄입니다.
앱이 복잡해지면 컴포넌트 단에서 특정 함수 호출이 완료된 직후에 수행해야만 하는 로직이 생길 수 있는데요. 컴포넌트 단에서 순차적인 동작을 구현하려면 async/await과 더불어 useEffect로 특정 값을 구독해 변화를 관찰해야할 수도 있어 로직이 복잡해지거나, 불필요한 재랜더링이 발생할 여지도 충분히 존재합니다. Saga를 사용하면 컴포넌트 내부의 로직이 복잡해지는 것을 막고, dispatch
한 번으로 비즈니스 로직을 성공적으로 수행할 수 있습니다.
비동기 API 호출의 재사용성 극대화
프론트엔드 개발을 진행하다보면 초기 계획과는 완전 다르게, 주로 백엔드 API의 생김새 때문에 정말 생각지도 못한 곳에서 생각지도 못한 데이터가 필요할 수도 있습니다. 이때 async await로 비동기를 컴포넌트 단에 붙여서 처리하고 있었다면 똑같은 로직을 다른 컴포넌트에 한 번 더 써야하죠.
서버도 처음부터 완성되있는 것은 아니기 때문에 어떤 response에서 UI에 필요한 모든 데이터를 보내주지는 못하는 경우도 종종 생깁니다. 작은 팀일수록 그런 상황에 부닥치기 쉬운데요.
백엔드가 수정되기 전까지 프론트엔드에서는 다른 비동기 로직 호출을 통해 UI가 필요한 데이터를 적당히 때워(?) 줘야 하는 상황도 충분히 가능합니다. 웹앱의 여러 곳에서 특정한 데이터 패칭이 필요하면 필요할수록 Saga를 사용하기 위해 지출했던 비용에 상회하는 이득을 얻을 수 있다고 봅니다.
물론 모든 API 호출 함수를 util을 모아놓듯 한 곳에 정리하는 방식으로 재사용하기 쉽게 만들수도 있어 Redux+Saga만의 장점이라고 말하기엔 좀 애매하긴 합니다. 더 좋은 점이 있다면, Saga에서는 앱의 UI를 바꿀 수 있는 store의 상태까지 건드릴 수 있다는 것 정도겠습니다.
캐싱을 활용할 수 있는 여지
유저가 앱을 사용하는 흐름 안에서 store에 이미 데이터가 이미 있는 경우 굳이 fetch를 하지 않아도 되는 상황이 있습니다. store에 이런 식으로 데이터를 저장해 놓는다면, 불필요한 패칭을 하지 않는 방향으로 앱을 최적화할 수 있습니다.