00. Redux-Saga?

TL;DR

  • redux는 단순하게 말해 state 관리 라이브러리이다.
  • redux-thunk와 redux-saga는 부수효과를 제어하는 redux middleware이다.
  • redux-saga는 비동기 코드를 동기적으로 작성할 수 있게 해준다.
  • redux의 러닝커브는 높지 않다. 하지만 redux-saga의 러닝커브는 높다.
  • 하지만 redux-thunk를 사용하지 않고 redux만 써왔다면 redux-saga 러닝커브는 비교적 높지 않다.

Intro

react에 입문해서 props와 state의 의미를 알고 컴포넌트를 구성하는 게 익숙해질 무렵이면 슬슬 여기저기서 redux라는 녀석을 거론하고 있다는 걸을 눈치챈다. 하지만 컴포넌트도 만드는데 무엇이 더 두려우랴, 하고 redux를 파고들다 보면 이런 생각이 들 것이다. 일단 나는 그랬다.
"이거 왜 쓰는 거지? 꼭 써야 하나?"

Redux

최근에는 redux의 낮지 않은 러닝커브와 Reactive Extension 명세의 인기에 힘입어 mobx라는 경쟁자가 나타나긴 했지만 mobx를 쓰는 개발자들도 redux라는 녀석이 있고 그 쓰임새는 무엇인지 대충은 알고 있을 것이다. redux란 놈의 위상이 그렇다.
어차피 프로젝트를 진행하며 다시 자세하게 썰을 푸는 날이 오겠지만, redux는 상태 관리, 즉 state 관리를 해주는 라이브러리일 뿐이다. 뭐 flux 아키텍쳐가 어떻고 redux는 flux 아키텍쳐에서 아이디어를 가져와서 reducer의 역할을 주목해서 어쩌구 저쩌구 하는 이야기는 좀 더 내공 있는 분들의 좀 더 좋은 글들이 있으니 그쪽을 참고하길.

Redux와 러닝커브

흔히들 redux는 어렵다고 한다.
redux를 처음 접하면, 분명 '이걸 왜 써야하나' 하는 생각이 들 것이다. 왜냐면 component는 state랑 props만 있어도 무적이라고 생각할테니까. 나같은 경우 redux는 서버 api와 통신할 일이 없으면 쓸 필요가 전혀 없는 물건이라 생각했던 시절도 있었다(내 예전 글을 보면 그게 느껴진다). 혹시나 나같은 오해를 하고 있는 사람이 또 있을까 싶어 다시 한 번 강조한다.
redux 그 자체는 그저 state 관리를 해주는 라이브러리일뿐이다.
그리고 redux의 러닝커브는 생각보다 높지 않다. 단지 보일러 플레이트가 많을 뿐이다.

Redux 보일러 플레이트

  1. action
  2. reducer
  3. [constant?]
사실 세번째는 어거지로 우겨넣은 거고, action과 reducer만 있으면 redux는 잘 작동한다. 물론 여러 redux 관련 필수 라이브러리가 있어야 하고 리액트 앱의 최상단 root 부모 컴포넌트에 store도 전달해주고 그래야하지만 어쨌든 그런 구성을 다 완료하고 나면 결국 action과 reducer 뿐이다.
뭐 action이 dispatch 되면 reducer가 순수 함수를 실행하여 store에 접근해서 state를 조작하고 어쩌고 하는 이야기는 조금만 찾아보면 엄청나게 많이 찾아볼 수 있다. 그리고 솔직히 그건 모를 때는 당장 봐도 전혀 이해가 안 된다. 모를 땐 그냥 이렇게 투박하게 이해하자.
  • action은 reducer에게 명령한다.
  • reducer는 action의 명령으로 state를 변경하는 일을 수행한다.
여기까지 이해가 되었다면, 이런 시나리오도 가능하다는 걸 알아두자.
  • action은 reducer에게 명령하면서, reducer가 state를 변경할 때 사용할 수 있는 재료를 같이 준다.
  • reducer는 action의 명령에 따라 state를 변경하는데, 이 때 action에게 받은 재료를 토대로 state를 변경하는 일을 수행한다.
그럼 내가 세번째로 적은 constant는 뭐하는 놈인가 싶을텐데, 앞서도 말했지만 얘는 없어도 상관없다. 다만 좀 더 편하게 action이 reducer에게 명령하고 싶을 때, 그 명령을 constant로 하면 된다. 이 부분은 사실 best practice가 없이 각자가 자기 맘대로 constant를 만들어서 쓰기 때문에 설명하기가 곤란한 감이 있다. 여기가 이해 안 되면 constant는 잊자. 중요한 건 action과 reducer다.

Redux-thunk, Side-Effect

그런데 막상 다른 글들을 보거나, 보일러 플레이트 프로젝트를 보거나 하면 action이 명령만 내리는 게 아니라 뭔가를 수행한다는 느낌을 받을 것이다. 여기서부터 꼬이기 시작하는 것이다.
reducer는 reducer대로 state를 변경하는 일들을 수행하는데, action은 action대로 뭔가를 수행한다. 거기다가 더 난해한 건, action은 자기 멋대로 뭔가를 수행한 후에, 그 결과값을 reducer에게 또 재료로 전달한다.
이쯤되면 action이 명령만 내리는 위치가 아니라, 뭔가 reducer보다 많은 일을 수행하는 것처럼 착각하게 된다. 뭔가 reducer가 하는 일이 뭔지도 잘 모르겠고 실제로 state에 반영해야 하는 값들은 action이 이미 가지고 있는 거 같다. 이게 대체 뭐지? 뭐긴 뭐야 redux-thunk를 썼을 때 일어나는 일이다.
redux-thunk를 사용하면, 위의 시나리오처럼 action이 단순히 명령만 내리고, 재료를 전달하는 역할로 끝나지 않고 action 또한 뭔가 으쌰으쌰 일을 수행한다. 그리고 그 action이 하는 일은 Side-Effect(부수효과)를 만들어낸다.
예를 들어보면, redux-thunk가 없고 Side-Effect가 없는 상태를 먼저 보자.
  1. action은 reducer에게 '문을 열어줘'라고 명령을 내린다.
  2. reducer는 명령을 듣고 state의 값을 'close'에서 'open'으로 변경하여 문을 연다.
내친 김에 action이 state 변경에 도움이 되는 재료를 던져주는 상황도 살펴보자.
  1. action은 reducer에게 'irrationnelle'(재료)이 오면 'Hello'라고 인사해(명령) 라고 명령한다.
  2. reducer는 action에게 인사하라는 명령을 받아 state 값을 ' '에서 'Hello'로 변경하고 하는데, 그 뒤에 재료를 받은 걸 덧붙여서 'Hello irrationnelle'라고 최종적으로 state 값을 변경하여 인사하는 명령을 수행한다.
이 때 action은 'irrationnelle'을 재료로 전달할 수도 있고, 'radiohead'를 전달할 수도 있다. 어쨌든 action이 명령과 함께 미리 정의해둔 재료를 전달하면 reducer는 재료와 함께 명령을 잘 수행한다. 여기서 부수효과는 일어나지 않는다.
하지만 redux-thunk를 사용해서 부수효과가 발생한다면? 오해의 여지가 있어 부연설명을 하면 redux-thunk를 사용하기 때문에 부수효과가 발생하는 것이 아니라 서버와 api 통신을 하기 때문에 부수효과가 발생하는 것이다.
  1. action은 reducer에게 인사하라고 명령을 하기 전에 action 자신이 먼저 해야 할 일을 수행한다.
  2. action 자신이 먼저 해야 할 일이란, reducer에게 명령과 함께 전달할 재료를 먼저 서버 api와 통신하여 받아오는 일이다.
  3. action이 무사히 서버 api와 통신하여 재료를 받아오면, 문제는 없다. 위의 예시대로 action은 명령을 내리고 재료를 전달하며, reducer는 해당 명령을 재료와 함께 수행한다.
  4. 그런데 action이 서버 api와 통신을 실패하여 재료를 받아오지 못해도, action은 재료랍시고 null이나 undefined 같은 걸 reducer에게 전달하며 명령을 내린다. 바로 이 지점이 부수효과가 발생하는 곳이다. action이 기존에 다루기로 미리 정의해둔 재료만 다룬다면 적어도 null이나 undefined 같은 이질적인 것을 reducer에게 전달하진 않았을 것이다.
사실 영어사전에서 Side-Effect는 부작용이라고 보통 설명하는데, 여기서는 부수효과라고 하는 것은 말그대로 '예상치 못한 효과'이기 때문이다. 부작용은 아무래도 부정적인 뉘앙스가 좀 더 강하다보니.

다시, Redux

물론 부수효과 자체는 redux-thunk를 쓰든 이 프로젝트에서 사용할 redux-saga를 쓰든 발생한다.
부수효과는 어디까지나 '예기치 못한 효과'이며, 우리가 결국 서버 api와 통신하거나 잠시동안 함수 호출 시점을 미뤘는데 그 사이에 다른 함수가 실행된다거나 등등의 상황이 생기면 결국 부수효과는 발생한다.
그리고 위 파트에서는 redux-thunk가 부수효과를 조장한다는 듯한 뉘앙스를 받았을지 모르는데, 내가 redux-thunk에 대해 다소 부정적인 태도를 취한 것은 맞지만 오히려 redux-thunk는 부수효과로 일어날 수 있는 상황을 promise를 통해 잘 제어할 수 있다.
redux-thunk의 단점은 부수효과가 아니라 redux의 단순했던 역할 분배인
  1. action은 명령한다
  2. reducer는 명령에 따라 state 변경을 수행한다.
  3. 때로는 action이 명령과 함께 재료를 주고 reducer는 재료를 받아서 state 변경을 수행한다.
이 구조를 박살내고 action에게 과도한 책임을 준다는 점과 그 책임으로 인해 action을 나타내는 코드를 이해하기 힘든 상태로 만든다는 점이다.
이 때문에 redux를 처음 보는 사람은 action과 reducer의 알 수 없는 모호한 책임 관계에서 서로가 하는 일을 혼동하는 일이 많다. 거기다가 constant까지 따로 관리하게 되는 경우 보일러 플레이트 코드가 급격하게 증가하면서 redux의 기본 구조에 익숙해지는 것이 매우 힘들어진다.
하지만 이런 상황에도 불구하고, redux를 처음 접하는 사람은 단순히 redux만 접하는 게 아니라 redux middleware 중 하나인 바로 이 redux-thunk를 함께 보게 되는 경우가 많다.
왜냐면 redux-thunk처럼 서버와의 비동기 통신 등의 역할을 맡는 녀석이 없다면 redux만으로 만들 수 있는 앱은 숫자 세기 카운터 정도가 한계이기 때문에.
물론 서버와의 비동기 통신 등을 컴포넌트 내부의 메소드에 구현해도 된다. 그런데 그럴거면 Redux를,
"이거 왜 쓰는 거지? 꼭 써야 하나?"
컴포넌트 내부에 비동기 통신을 마치고 갱신된 state들이 난무하는데, 그것들은 무시하고 redux를 써야 한다면,
"이거 왜 쓰는 거지? 꼭 써야 하나?"

그래서 Redux-Saga

위의 문제를 해결하기 위해 상황을 다시 파악해보자.
  • redux를 이용하여 state 관리를 수월하게 해야한다
  • action은 reducer에게 명령을 내리고 재료를 전달하는 정도의 역할만 가진다.
  • 위의 원칙을 포기하지 않은 상태에서 서버 api와의 비동기 통신 등에서 발생할 수 있는 부수효과를 잘 관리하여 redux를 이용하고 싶다.
이런 상황의 대안으로 제시된 것 중 하나가 redux-saga이다.
그리고 redux-saga는 위의 문제를 해결해줄 뿐만 아니라 redux-saga만의 강점이 있는데
  • es6의 generator에서 착안한 라이브러리이기 때문에 비동기로 작동하는 코드를 동기적인 코드로 나타낼 수 있다.
  • 동기적인 코드 구조를 지니기 때문에 디버깅과 테스트에 매우 용이하다.
  • callback에서 자유로워 지기 때문에 가독성이 높은 코드를 작성할 수 있다.
  • promise와 조합하여 사용할 경우 예외 처리를 하는데에도 좀 더 직관적으로 코드를 작성할 수 있다.
등등이 있다.
하지만 문제가 있다. 앞서 나는 redux의 러닝 커브가 높지 않다고 했다.
그런데 redux-saga는 러닝 커브가 매우 높다.
다시 말해 redux-saga는 진짜 사용하기 어렵다.

Redux-Saga와 러닝커브

이 장황한 글의 목적은 사실 이 파트다.
redux-saga는 정말 react를 공부하며 가장 어렵고 막막했던 부분이었다. 사실 굳이 redux-saga를 써야 하나, 그냥 흔하게 쓰는 redux-thunk를 쓰면 되지 않나 생각해본 것이 한 두번이 아니었다.
일단 가장 어려운 점은, redux-saga는 기존에 redux-thunk에 익숙한 사람들에게 redux의 action과 reducer 관계를 다시 생각해볼 것을 요구한다. 즉, redux middleware인 redux-thunk를 기준으로 redux를 바라볼 것이 아니라 redux 자체의 기본 구조를 기준으로 redux를 이해할 것을 요구한다.
어찌보면 당연한 이야기인데, 쉽지는 않다. 여기서 쉽지 않다는 이야기는 러닝 커브의 이야기가 아니다.
앞서 말했지만 props와 state만으로도 무적이라 생각했던 컴포넌트에 굳이 redux를 써야 하는지 의문도 들고, 결국 redux는 redux-thunk같은 미들웨어를 잘 조합해서 부수효과 관리 목적으로 써도 되는 거 아닌가 싶으니까. 그리고 최근에는 mobx라는 좀 더 접근성 낮은 대체 라이브러리도 나왔는데 굳이 아득바득 redux를 공부해야 할 이유도 없다.
솔직히 나도 4개월 전에 시작한 사이드 프로젝트에서 지금까지 충분히 redux-saga를 굴려보고 테스트해보고 익숙해진 끝에 드디어 오늘 메인 프로젝트에 redux-saga를 이전 사이드 프로젝트보다 좀 더 나은 디렉토리 구조와 설계와 함께 도입할 수 있었다. 이것이 의미하는 바는 바로 메인 프로젝트는 지금까지 redux없이 state와 props 관리를 해오고 부수효과를 모든 컴포넌트 내부에서 관리했다는 이야기다. 덕분에 버그 하나 생기거나 수정 사항이 생기거나 하면 지옥을 경험하게 되지만.
어쨌거나 redux 없이도 프로젝트를 굴릴 수 있는데 굳이 redux 본연의 철학...까진 아니더라도 쓰임새를 이해하고 파악해야 한다는 것에 거부감이나 부담을 느낄 수 있으리라 생각한다.
또한 redux-saga의 러닝 커브를 높이는 요소로 es6의 generator 개념을 이해해야 하고, 해당 라이브러리의 effect 메소드인 take나 fork, put, call, select 같은 메소드들을 이해해야 한다는 점이다.
사실 라이브러리의 주요 메소드들을 파악해야 하는 건 모든 라이브러리의 공통사항인데 굳이 이걸 지적한 이유는,
실상 거의 모든 react 라이브러리의 문제라고 나는 생각하는 바인데,
redux-saga는 아직까지도 best practice가 존재하지 않기 때문이다.
덕분에 라이브러리의 effect인 메소드들을 쓰는 방식들이 저마다 다 다르고, 그 방식에 맞춰 코드 구조 또한 바뀌게 된다. 어떤 코드는 내가 watcher함수라 부르는 함수를 사용하지 않고 바로 take 메소드를 사용하여 while문 내부에 코드를 전개한다. 어떤 코드는 (내가 사용하는 방식인데) watcher 함수가 takeLatest 메소드를 이용하여 action을 주시하고 해당 action이 dispatch되면 그 때에야 제네레이터가 적용된 함수를 호출하며 while문 같은 건 사용하지 않는다.
거기에 어떤 코드는 take 메소드(put 메소드였을 수도 있다 기억이 잘...)를 사용해서 action이나 payload를 초기화하고, 어떤 코드는 call 메소드를 사용해서 초기화한다.
어찌보면 각자의 스타일대로 코딩할 수 있는 팔방미인 라이브러리일 수 있겠지만
일단은 어떻게 써먹는지는 알아야 자기 스타일대로 사용하든 말든 결정할 것이 아닌가.
바로 이런 점 때문에 이 프로젝트를 진행하기로 결정했다.
비록 내가 Best Practice는 제공하지 못할지라도, 적어도 전체적인 프로젝트에서 redux-saga가 어떤 일을 하는지, redux-saga와 결합했을 때 redux의 reducer와 action 구조는 어떤 형태를 지니는지 등등을 완성된 프로젝트로 보여준다면 아무래도 나처럼 공부하는데 난감했던 사람들에게 도움이 되지 않을까 생각했던 것이다.

Outro

이해하기 쉽게 전달한다고 개념을 상당히 단순화시켜서 이야기한 부분이 많았다.
그리고 절대 redux-thunk를 폄하하는 건 아니며, 뭔가 내가 오해하고 있는 부분이 있거나 글에서 이해가 가지 않는 부분은 얼마든지 피드백 환영.
TDD에 대한 썰은 다음 번에 풀기로 하고, 참고로 아직도 어떤 프로젝트를 할지는 정하지 못했다는 거. 일단은 서버는 만들지 않고 json-server로 간단한 mock rest api를 구축해서 만들 생각.
오늘도 뻘글 쓴다고 새벽까지 못 잤는데 그래도 금요일이라 다행... 다들 불금 되시길.