본문 바로가기

공부 정리

22.11.30: Redux + thunk, saga

반응형

발췌 from 벨로퍼트와 함께하는 모던 리액트

 

오늘은 리덕스에 대해서 정리해보고자 한다. 

처음에 리덕스에 대한 개념 정리가 제대로 되지 않은 상태로 리덕스툴킷으로 넘어가버려서 리덕스를 제대로 이해하고있지 못하고 있다. 특히 미들웨어에 대해서...

 

Redux 

 

리덕스는 현재까지 가장 사용률이 높은 상태관리 라이브러리임. 우리가 만드는 컴포넌트들의 상태관리를 따로 분리시켜서 효율적으로 관리가 가능하고, 전역 상태 관리도 쉽다. 

 

Redux vs Context API

*Context API: 리액트 내장 API로 컴포넌트와 상관없이 값을 전역으로 관리 가능함 (useReducer와 함께 상태관리도구 역할 가능)

 

1. 미들웨어의 존재

 

리덕스에는 Middleware라는 것이 있다. 이것을 사용하면 액션 객체가 리듀서에서 처리되기 전에 우리가 원하는 작업들을 할 수 있다.

 

  • 특정 조건에 따라 액션을 무시되게 만들기
  • 액션을 콘솔에 출력하거나 서버쪽에 로깅
  • 액션이 디스패치 됐을 때 이를 수정해 리듀서에게 전달되도록 하기
  • 특정 액션 발생시 이에 기반한 다른 액션이 발생되게 할 수 있음
  • 특정 액션 발생시 특정 자바스크립트 함수를 실행시킬 수 있음

미들웨어는 주로 비동기 작업을 처리 할 때 많이 사용된다. (useReducer Hook에서도 외부 라이브러리 사용시 미들웨어 사용 가능, 자주 사용되진 않는다.)

 

2. 유용한 함수, Hooks 

 

connect라는 함수를 사용하면 리덕스의 상태, 또는 액션 생성 함수를 컴포넌트의 props로 받아올 수 있고, useSelector, useDispatch, useStore같은 Hooks를 사용하면 손쉽게 상태를 조회하거나 액션을 디스패치 할 수 있다.

connect 함수와 useSelector 함수에는 내부적으로 최적화가 잘 이루어져 있어 실제 상태가 바뀔때만 컴포넌트가 리렌더링 되지만 Context API를 쓰면 이런 최적화가 자동으로 이루어져있지 않아 Context의 상태가 바뀌면 Provider 내부 컴포넌트들이 모두 리렌더링 된다. 

 

3. 하나의 커다란 상태

 

Context API 사용해 글로벌 상태 관리시, 일반적으로 기능별로 Context를 만들어서 사용하는 것이 일반적이다. 하지만 리덕스는 모든 글로벌 상태를 하나의 커다란 상태 객체에 넣어서 사용하는 것이 필수적이다. 이로써 매번 Context를 새로 만드는 수고로움을 덜 수 있다.

 

그럼 Redux, Context API는 각각 언제 써야 할까?

 

1. 프로젝트의 규모가 크면 리덕스, 아니면 Context API

2. 비동기 작업을 자주 하면 리덕스, 아니면 Context API

3. 리덕스 써보니까 편하면 리덕스, 아니면 Context API

 

리덕스의 키워드 알아보기

 

Action : 상태에 어떤 변화가 필요할 때, 액션을 발생시킨다. 이것은 하나의 객체임.

type은 필수로 가져야하고 그 외 값들은 개발자 맘대로 넣을 수 있다.

 

{
type: "TOGGLE_VALUE",
data: {
   id: 0,
   text: "리덕스 배우자"
  }
}

 

 

1. Action Creator 액션 생성함수

액션을 만드는 함수. 단순히 파라미터를 받아와 액션 객체 형태로 만들어 줌.

 

export function addTodo(data) {
  return {
    type: "ADD_TODO",
    data
  };
}

// 화살표 함수로도 만들 수 있다.
export const changeInput = text => ({
  type: "CHANGE_INPUT",
  text
})

 

export 키워드를 붙이면 다른 파일에서 불러와 사용가능하다. 리덕스 사용시 액션생성함수를 사용하는것이 필수는 아님. 액션 발생시킬 때마다 직접 액션 객체를 작성 할수도 있다.

 

2. Reducer 리듀서

리듀서는 변화를 일으키는 함수다. 두가지 파라미터를 받음. state와 action

 

function reducer(state, action) {
  // 상태 업데이트 로직
  return alteredState;
}

 

리듀서는 현재 상태 (state), 전달받은 action을 참고해 새로운 상태(alteredState)를 만들어 반환한다. 이 리듀서는 useReducer를 사용할때 작성하는 리듀서와 형태가 같다.

 

만약 카운터를 위한 리듀서를 작성한다? 아래와 같이 작성하면 된다.

 

function counter(state, action) {
  switch (action.type) {
    case 'INCREASE':
      return state + 1;
    case 'DECREASE':
      return state -1;
    default:
      return state;
  }
}

 

useReducer에서는 default: 부분에 throw new Error('Unhandled Action')같은 에러를 발생시키도록 처리하는게 일반적이나 리덕스의 리듀서는 기존 state를 그대로 반환하도록 작성한다. 리덕스를 사용 할 땐 여러개의 리듀서를 만들고 이를 합쳐 루트 리듀서를 만들 수 있다. (루트리듀서 안에 작은 리듀서들을 서브 리듀서라 함) 

 

루트리듀서: combineReducers라는 함수로 여러개의 리듀서를 1개로 합쳐준다.

Ducks Pattern: 액션 타입, 액션 생성 함수, 리듀서를 하나의 모듈처럼 한 파일에 작성하자는 제안

1. Must export default a function called reducer() 

    반드시 리듀서 함수를 default export해라

2. Must export its action creators as functions

    반드시 액션생성 함수를 export 해라

3. Must have action types in the form npm-module-or-app/reducer/ACTION_TYPE

    반드시 접두사를 붙인 형태로 액션 타입을 정의해야 한다

4. May export its action types as UPPER_SNAKE_CASE, if an external reducer needs to listen for them, or if it is
     a
published reusable library

    외부 리듀서가 모듈 내 액션 타입을 바라보고 있거나, 모듈이 재사용 가능한 라이브러리로 쓰인다면 액션타입을         
    UPPER_SNAKE_CASE로 쓰고 export 해라

 

3. Store 스토어

한 애플리케이션당 하나의 스토어를 만든다. 스토어안에는 현재 앱의 상태, 리듀스가 들어가있고, 추가적으로 몇가지 내장 함수들이 있다.

 

4. Dispatch 디스패치

스토어의 내장함수 중 하나. 디스패치는 액션을 발생시킨다. 액션을 파라미터로 전달함.  dispatch(action) 이렇게 호출시, 스토어는 리듀서 함수를 실행시켜서 해당 액션을 처리하는 로직이 있으면 액션을 참고하여 새로운 상태를 만들어준다.

 

5. Subscribe 구독

구독도 스토어의 내장함수 중 하나. subscribe 함수는 함수 형태의 값을 파라미터로 받아옴. subscribe 함수에 특정 함수를 전달하면 액션이 디스패치 되었을 때 마다 전달해준 함수가 호출됨. 보통 react-redux의 connect 함수 또는 useSelector로 리덕스 스토어 상태를 구독한다. 

 

리덕스에는 규칙이 있다

 

1. 하나의 애플리케이션 안에는 하나의 스토어가 있다.

단 하나의 스토어를 만들어 사용한다. 여러개의 스토어를 사용하는 것이 가능은 하나, 권장되진 않음. 개발 도구를 활용하지 못하게되기 때문

 

2. 상태는 읽기전용이다.

리액트에서 상태변화 할때,

state업데이트 시 setState 사용

배열 업데이트시 배열 자체에 push 직접 하지 않고 contcat 같은 함수로 새로운 배열을 만들어 교차하는 방식

객체 업데이트시 Object.assign이나 spread 연산자로 업데이트

 

리덕스도 마찬가지로 불변성을 지켜주면 개발자 도구를 이용해 뒤로 돌리거나 앞으로 돌릴 수 있다.

불변성을 지켜주는 이유는 내부적으로 데이터가 변경되는 것을 감지하기위해 shallow equality 검사를 하기 때문이다.

 

3. 변화를 일으키는 함수인 리듀서는 순수한 함수여야 한다.

리듀서는 이전 상태와, 액션 객체를 파라미터로 받는다.

이전 상태는 건들지 않고, 변화를 일으킨 새로운 상태 객체를 만들어 반환한다.

똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값을 반환해야 한다.

 

만약 순수 함수가 아니라면 리덕스 미들웨어를 사용한다. (new Date(), 난수 생성, 네트워크 요청...)

 

리덕스 미들웨어

 

https://react.vlpt.us/redux-middleware/

 

 

일반적으로 미들웨어 라이브러리를 설치해 사용하는데, 비동기 작업에 관련된 미들웨어 라이브러리는 

redux-thunk, redux-saga, redux-observable, redux-promise-middleware 등이 있다.

 

redux-saga나 redux-observable은 특정 액션을 모니터링 할 수 있어서 특정 액션이 디스패치되었을때 원하는 함수를 호출, 또는 라우터를 통해 다른 주소로 이동하는 것이 가능하다.

 

리덕스 미들웨어의 템플릿

 

const middleware = sotre => next => action => {
 // 하고 싶은 작업 ...
}

 

미들웨어는 함수를 연달아 두번 리턴하는 하나의 함수이다. 위의 함수는 아래와 같이 쓸 수도 있다.

 

function middleware (store) {
  return function (next) {
    return function (action) {
      // 하고 싶은 작업...
    }; 
  };
};

 

미들웨어 안에서는 뭐든 할 수 있다. 액션 값을 객체가 아닌 함수로 받아오게 만들어 액션이 함수타입이면 이를 실행시키게끔 할 수도 있다.

 

const thunk = store => next => action =>
  typeof action === 'function'
    ? action(store.dispatch, store.getState)
    : next(action)

 

 

각 함수에서 받아오는 파라미터의 의미

 

store: 리덕스 스토어 인스턴스. 이 안에 dispatch, getState, subscribe 내장함수들이 들어있다.

next: 액션을 다음 미들웨어에게 전달한다. next(action) 이런 형태로 사용. 다음 미들웨어가 없다면 리듀서에게 액션을 전달해준다.

          만약 next를 호출하지 않으면 액션이 무시처리되어 리듀서에게 전달되지 않음.

action: 현재 처리하고 있는 액션 객체이다.

 

Redux의 middleware 라이브러리들

1. redux-thunk

리덕스에서 비동기 처리시 가장 많이 사용되는 미들웨어이다. 이 미들웨어를 사용하면 액션 객체가 아닌 함수를 디스패치 할 수 있다.

함수를 디스패치할 때, 해당 함수에서 dispatch와 getState를 파라미터로 받아와야 한다. 이 함수를 만들어주는 함수를 thunk라고 부른다.

 

const getComments = () => (dispatch, getState) => {
  // 이 안에서 액션을 dispatch 하거나
  // getState를 사용해 현재 상태도 조회 할 수 있다.
  const id = getState().post.activeId;
  
  // 요청이 시작했음을 알리는 액션
  dispatch({ type: 'GET_COMMENTS' });
  
  // 댓글을 조회하는 프로미스를 반환하는 getComments 가 있다고 가정
  api
    .getComments(id) // 요청 하고
    .then(comments => dispatch({ type: 'GET_COMMENTS_ERROR', error: e})); // 실패시
};

 

thunk 함수에서 async/await도 가능

 

const getComments = () => async (dispatch, getState) => {
  const id = getState().post.activeId;
  dispatch({  type: 'GET_COMMENTS' });
  try {
    const comments = await api.getComments(id);
    dispatch({ type: 'GET_COMMENTS_SUCCESS', id, comments });
  } catch (e) {
    dispatch({ type: 'GET_COMMENTS_ERROR', error: e});
  }
}

 

2. react-saga

redux-thunk 다음으로 가장 많이 사용되는 라이브러리이다.

 

redux-thunk의 경우에 함수를 디스패치 할 수 있게 해주고, redux-saga는 액션을 모니터링하고 있다가, 특정 액션이 발생하면 이에 따라 특정 작업을 하는 방식으로 사용한다. 예를들면 특정 자바스크립트를 실행 한다던가, 다른 액션을 디스패치 한다던가, 현재 상태를 불러오는 것 일수도있다. 순수 함수로 이루어지다보니, 사이드 이펙트도 적고 테스트 코드를 작성하기에 용이하다.

 

redux-saga는 되고 redux-thunk는 안되는 것들

 

1. 비동기 작업시 기존 요청을 취소 처리 할 수 있다.

2. 특정 액션이 발생했을 때 이에 다라 다른 액션이 디스패치되게끔 하거나, 자바스크립트 코드를 실행 할 수 있다.

3. 웹소켓을 사용하면 Channel 이라는 기능을 사용해 더욱 효율적으로 코드를 관리 할 수 있다.

4. API 요청이 실패했을 때 재요청하는 작업을 할 수 있다.

 

등등의 비동기 작업들을 처리 할 수 있다. 

 

Generator 문법

핵심기능

 

함수를 작성 할 때 함수를 특정 구간에 멈춰 놓을 수도 있고, 원할 때 다시 돌아가게 할 수도 있다. 그리고 결과값을 여러번 반환 할 수도 있다. 

 

예를 들어 다음과 같은 함수가 있다고 가정 해보자.

 

function weirdFunction() {
  return 1;
  return 2;
  return 3;
  return 4;
  return 5;
}

 

함수에서 값을 여러번에 걸쳐서 반환하는 것은 일반적으로 불가능하다. 위 함수는 호출 될때 무조건 1을 반환한다. 하지만 제너레이터 함수를 사용하면 함수에서 값을 순차적으로 반환한다. 함수의 흐름을 도중에 멈춰놓았다가 나중에 이어서 진행 할 수도 있다.

 

function* generatorFunction() {
  console.log('안녕하세요');
  yield 1;
  console.log('제너레이터 함수');
  yield 2;
  console.log('function*');
  yield 3;
  return 4;
}

 

제너레이터 함수를 만들 때 function* 이라는 키워드를 사용한다.

제너레이터 함수를 호출했을때 한 객체가 반환되는데, 이를 제너레이터라고 부른다.

 

const generator = generatorFunction();

 

제너레이터 함수를 호출한다고 해서 해당 함수 안의 코드가 바로 시작되진 않고 generator.next()를 호출해야 코드가 실행된다.

yield를 한 값을 반환하고 코드의 흐름을 멈춘다.

 

console에서 실행

또다른 예시

 

function* sumGenerator() {
  console.log('sumGenerator이 시작됐습니다.');
  let a = yield;
  console.log('a값을 받았습니다');
  let b = yield;
  console.log('b값을 받았습니다');
  yield a + b;
}

 

 

Generator로 액션을 모니터링 할 수 있다.

다음을 크롬 개발자도구 콘솔에 작성하고

 

function* watchGenerator() {
  console.log('모니터링 시작');
  while(true) {
    const action = yield;
    if (action.type === 'HELLO') {
        console.log('안녕하세요?');
    }
    if (action.type === 'BYE') {
        console.log('안녕히가세요');
    }
  }
}

 

 

이런 원리로 액션을 모니터링하고, 특정 액션이 발생했을때 우리가 원하는 자바스크립트 코드를 실행시켜준다.

 

'redux-saga/effects'의 다양한 유틸 함수들

put: 새로운 액션을 디스패치

takeEvery: 특정 액션 타입에 디스패치되는 모든 액션들을 처리

takeLatest: 특정 액션 타입에 대해 디스패치된 가장 마지막 액션만을 처리

ex) 특정 액션을 처리하고 있는 동안 동일한 타입의 새로운 액션이 디스패치되면 기존에 하던 작업을 무시 처리하고 새로운 작업을 시작

 

 

 

Redux-toolkit은 Redux-saga 를 내장라이브러리로 포함하고 있지 않다.

출처 https://redux-toolkit.js.org/introduction/getting-started

redux-thunk, immer library 등을 포함하고 있으나 redux-saga는 포함되어있지 않아서 redux-toolkit과 redux-saga를 연동해서 쓰기도 한다.

반응형