안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

리덕스 (Redux)

[Redux, Redux-Thunk, Redux-Saga] 주요 개념 요약

피그브라더 2021. 1. 27. 02:19

개인적으로 React 프로젝트를 진행하면서 Redux, Redux-Thunk, Redux-Saga에 대해 공부한 내용들을 기록한 것이다. 원래는 프로젝트 폴더에 문서 형태의 파일로 아카이빙 해두던 것이었는데 이번 기회에 티스토리로 옮겨두기로 결정했다. 여기에 기록한 내용들은 주로 해당 라이브러리의 핵심 개념 혹은 동작 원리가 주를 이룬다. 필자만의 기록이라 다른 사람들에게도 도움이 될지는 모르겠으나, 혹여나 한 사람이라도 이를 참고로 살펴보고 도움될 만한 내용을 얻어갈 수 있다면 참 좋겠다. 혹시 잘못된 내용이 있다면 댓글로 지적 바란다.

 

1. Redux, React-Redux 주요 개념 요약

 

1-1. 스토어

  • 애플리케이션의 상태를 트리 구조로 관리한다.
  • 상태를 읽으려면 getState() 함수를 호출한다.
  • 상태를 바꾸려면 dispatch() 함수를 호출해서 액션을 디스패치 한다.
  • 리덕스는 스토어가 생성되는 즉시 더미 액션을 디스패치 하여 스토어의 초기 상태를 설정한다.

 

1-2. 미들웨어  store => next => dispatch 

  • 리덕스의 기능을 확장하기 위한 수단으로, dispatch() 함수를 래핑 하는 역할을 수행한다.
  • 다음 미들웨어의 dispatch() 함수를 next 인자로 받아서 새로운 dispatch() 함수를 만들어낸다.
  • 마지막 미들웨어는 스토어의 기본 dispatch() 함수를 인자로 받아서 미들웨어 체인을 종료시킨다.
  • 이러한 체인의 결과로 만들어지는 최종적인 dispatch() 함수를 리덕스의 스토어에 부착하게 된다.
  • 각 미들웨어는 디스패치 된 액션을 직접 처리하거나 다음 미들웨어의 dispatch() 함수를 호출한다.
  • 이때, 미들웨어는 store 인자로 스토어를 받기 때문에 스토어와도 직접적인 상호작용이 가능하다.
  • 예를 들어, 썽크 미들웨어는 디스패치 된 썽크의 호출 시 스토어의 메소드들을 인자로 전달해준다.

 

1-3. 스토어 인핸서  storeCreator => storeCreator 

  • 스토어 생성자를 인자로 받아서 새로운 (개선된) 스토어 생성자를 만들어낸다.
  • 일반적으로, createStore() 함수의 마지막 인자로 스토어 인핸서를 넘겨준다.

 

1-4. 리덕스 동작 원리

  • 리덕스의 스토어는 Context로 구성되어 Provider 컴포넌트의 value props로 전달된다.
  • 해당 Context는 구체적으로 { store, subscription }과 같은 형태를 띠고 있다.
  • connect() 함수에 의해 만들어진 컴포넌트나 useSelector() 함수를 사용한 컴포넌트는 액션이 디스패치 된 후 (어떠한 구독 메커니즘에 의해) 강제적으로 리렌더링이 유발된다. 이때 해당 컴포넌트는 Provider 컴포넌트가 제공해주는 리덕스 스토어의 상태를 읽는다.
  • 참고로 스토어는 늘 참조값이 같은 가변 객체에 해당하며, Provider 컴포넌트는 중간에 스토어가 다른 걸로 바뀌지 않는 이상 늘 같은 참조값을 가지는 객체를 Context로 구성하도록 구현된다. 따라서 액션을 디스패치 하든 무엇을 하든, Provider 컴포넌트에 의해 자식 컴포넌트들이 전부 리렌더링 될 일은 없다. 즉, 리렌더링은 오로지 구독 메커니즘에 의존하여 발생할 뿐이다.

 

1-5. useSelector()

  • 액션이 디스패치 될 때마다 useSelector() 함수를 실행하여 새로운 상태 값을 계산한다.
  • 만약 이전 값과 새로 계산된 값이 다르면 해당 컴포넌트의 리렌더링이 강제로 유발된다.

 

1-6. useDispatch()

  • 참조값이 늘 같은 dispatch() 함수를 반환한다.

 

2. Redux-Thunk 주요 개념 요약

2-1. 썽크

  • 객체가 아닌, 동기 또는 비동기 작업을 수행할 수 있는 함수를 말한다.
  • 썽크를 사용하려면 액션 생성자는 객체가 아닌 이 함수를 반환해야 한다. 
  • 이는 리덕스 스토어와의 상호작용을 위해 getState(), dispatch() 함수를 인자로 받는다.
  • 따라서 리덕스 스토어의 상태에 접근하거나 또 다른 액션을 디스패치 하는 것이 가능하다.

 

2-2. 썽크 미들웨어

  • 디스패치 된 액션의 타입이 객체가 아닌 함수(= 썽크)인 경우, 이를 직접 처리한다.
  • 이때 인자로 store.dispatch(), store.getState() 함수를 넘기며 해당 썽크를 호출한다.
  • 참고로 여기서 전달받은 dispatch() 함수를 호출하면 다음 미들웨어의 dispatch() 함수를 호출하는 것이 아니라 스토어의 dispatch() 함수를 호출하는 것이기 때문에 미들웨어 체인의 맨 처음으로 다시 돌아가게 된다.
  • 만약 액션의 타입이 객체라면, 다음 미들웨어의 dispatch() 함수로 이를 처리한다(다음 미들웨어에게 넘긴다).

 

3. Redux-Saga 주요 개념 요약

 

3-1. 사가 (= 제네레이터 함수)

  • 제네레이터를 반환하는 함수이다.
  • 해당 제네레이터의 next() 함수를 호출함으로써 사가를 실행할 수 있다.
  • 사가는 yield 표현식을 만날 때마다 실행이 중단되며, 해당 표현식의 값은 next() 함수의 반환 값에 포함된다.
  • next() 함수의 인자로 넘기는 값은 해당 사가의 실행이 중단되어 있는 위치의 yield 표현식 자리를 채워준다.


3-2. 사가의 실행 흐름

  • 사가 미들웨어의 run 메소드가 엔트리 포인트에 해당하는 루트 사가를 실행한다.
  • 사가의 실행은 해당 제네레이터 함수를 호출하여 반복 가능한 제네레이터를 획득하는 것으로 시작한다.
  • 이제 해당 제네레이터의 next() 함수를 통해 이펙트를 읽고 지시된 동작을 수행하는 작업을 반복한다.
  • 그러다가 fork 이펙트를 읽으면, 해당 사가를 실행하는 또 다른 새로운 실행 맥락을 하나 만들어낸다.


3-3. 사가 미들웨어의 구현

  • 사가 미들웨어도 다른 미들웨어와 똑같이 store => next => dispatch 시그니쳐를 가지는 함수이다.
  • 그런데 사가 미들웨어에는 특별히 루트 사가의 실행을 위한 하나의 함수가 run 메소드로서 부착된다.
  • 그리고 사가 미들웨어는 호출되는 순간 그 함수에 store의 두 메소드를 바인딩시켜주도록 구현된다.
  • 따라서 run 메소드는 루트 사가를 실행하는 동안, 리덕스의 스토어와 직접적인 상호작용이 가능하다.
  • 그래서 select, put 이펙트를 각각 getState(), dispatch() 함수로 처리할 수 있게 되는 것이다.


3-4. 일반적인 사가 사용 패턴

  • 루트 사가는 여러 개의 Watcher 사가를 fork 한다. 각 Watcher 사가는 특정 액션을 기다린다.
  • 각 Watcher 사가는 특정 액션이 디스패치 될 때 이를 감지하여 해당 Worker 사가를 fork 한다.


3-5. 사가 미들웨어의 역할

  • 사가 미들웨어는 디스패치 된 액션을 캐치해서 이를 Watcher 사가에게 알리는 역할만 수행할 뿐이다.
  • 따라서 해당 액션이 리듀서에 도달했는지 다른 미들웨어에 의해 중간에 변형되었는지는 알지 못한다.
  • 즉, 다른 미들웨어들과 달리 액션을 디스패치 하는 과정 자체에는 직접적으로 관여하지 않는다.

 

3-6. 이펙트

  • 사가를 실행하는 실행부에게 어떠한 동작을 수행해야 할지 알려주는 일반 객체(Plain Object)이다.
  • 일반적으로 put(), call(), select() 등의 헬퍼 함수를 호출함으로써 해당 이펙트 객체를 생성한다.
  • 이펙트 자체는 단순 객체이므로 아무런 사이드 이펙트를 발생시키지 않는다. 따라서 테스트가 용이하다.

 

3-7. 주요 이펙트

  • call(fn, ...args) : (비동기 혹은 동기) 함수 fn을 호출한다.
  • select(selector) : selector를 이용하여 리덕스 스토어의 상태를 읽어온다. (= store.getState() 함수)
  • put(action) : action을 디스패치 한다. (= store.dispatch() 함수)
  • take(actionType) : actionType의 액션이 디스패치 될 때까지 기다린다.
  • fork(saga, ...args) : 새로운 실행 맥락으로 saga를 실행한다.
  • takeEvery(actionType, saga, ...args) : 사가를 하나 fork 하는 헬퍼 함수이다. 해당 사가는 actionType의 액션을 기다렸다가 saga를 fork 하는 작업을 무한히 반복한다.