Redux 배워보기

sean·2023년 3월 19일
0

Web

목록 보기
20/22

기본 개념

액션(Action)

상태에 어떠한 변화가 필요하게 될 때, 우리는 '액션'이라는 것을 발생시킨다.
이는 하나의 객체로 표현되는데, 액션 객체는 다음과 같은 형식으로 이루어져 있다.

{
  type: 'TOGGLE_VALUE'
}

액션 객체는 type 필드를 필수적으로 가지고 있어야 하고, 그 외의 값들은 개발자 마음대로 넣어줄 수 있다.

{
  type: "ADD_TODO",
  data: {
    id: 0,
    text: "리덕스 배우기",
  }
}
{
  type: "CHANGE_INPUT",
  text: "안녕하세요"
}

액션 생성 함수

액션 생성 함수는 액션을 만드는 함수이다.
파라미터를 받아와서 액션 객체 형태로 만들어주는 역할을 한다.

export const addTodo = (data) => ({
  type: "ADD_TODO",
  data,
});

export const changeInput = (text) => ({
  type: "CHANGE_INPUT",
  text,
});

이러한 액션 생성 함수를 만들어서 사용하는 이유는 나중에 컴포넌트에서 더욱 쉽게 액션을 발생시키기 위함이다. 그래서 보통 함수 앞에 export 키워드를 붙여서 다른 파일에서 불러와서 사용한다.

리듀서(reducer)

리듀서는 변화를 일으키는 함수이다. 리듀서는 두 가지의 파라미터, 즉 '현재의 상태''전달받은 액션'을 참고하여 새로운 상태를 만들어서 반환하는 함수이다.

만약 카운터를 위한 리듀서를 작성한다면 다음과 같이 작성할 수 있다.

export default const counterState = (state, action) => {
  switch(action.type) {
    case 'INCREASE':
      return state + 1;
    case 'DECREASE':
      return state - 1;
    default:
      return state;
  }
}

Redux를 사용할 때에는 여러 개의 리듀서를 만들고, 이를 합쳐서 루트 리듀서(rootReducer)를 만들 수 있다. (루트 리듀서 안의 작은 리듀서들은 서브 리듀서라고 부른다.)

스토어(Store)

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

디스패치(Dispatch)

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

파일 관리 패턴

Redux를 사용할 때는 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해야 하는데, 이 코드들을 각각 다른 파일에 작성하는 방법도 있고, 기능별로 묶어서 파일 하나에 작성하는 방법도 있다.

일반적인 구조

  • /actions
    • counter.js
    • todos.js
  • /constants
    • ActionTypes.js
  • /reducers
    • counter.js
    • todos.js

위의 일반적인 구조는 코드를 종류에 다라 다른 파일에 작성하여 정리할 수 있어서 보기엔 깔끔하지만, 새로운 액션을 만들 때마다 세 종류의 파일을 모두 수정해야 하기 때문에 번거롭기도 하다.

Ducks 패턴

  • /modules
    • counter.js
    • todos.js

위의 패턴에서는 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 싹 다 몰아서 작성하는 방식이다. 이러한 방식을 Ducks 패턴이라고 부르며, 앞서 설명한 일반적인 구조로 리덕스를 사용하다가 불편함을 느낀 개발자들이 자주 사용하는 패턴이라고 한다.

이렇게 Ducks 패턴을 이용하여 액션 타입, 액션 생성 함수, 리듀서를 몰아서 쓴 파일을 '모듈(module)'이라고 한다.

모듈 작성하기

액션 타입(Action type) 정의하기

/src/store 디렉토리에 /modules 폴더를 만들고 거기에 우리가 연습해볼 counter 모듈 counter.js를 만들자.

가장 먼저 해야 할 일은 바로 '액션 타입(Action type)'을 정의하는 것이다.

  • 액션 타입은 대문자 문자열로 정의한다.
  • 문자열 내용은 '모듈 이름/액션 이름'과 같은 형태로 작성한다. (나중에 프로젝트가 커졌을 때 액션의 이름이 충돌되지 않게 해준다. 예를 들어, SHOW 혹은 INITIALIZE라는 이름을 가진 액션은 쉽게 중복될 수 있다. 하지만, 앞에 모듈 이름을 붙여주면 액션 타입 이름이 겹치는 것을 걱정하지 않아도 된다.)
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

액션 생성 함수 만들기

  • 액션 타입을 정의한 다음에는 '액션 생성 함수'를 만들어줘야 한다.
  • 중요) 함수 앞부분에 export 키워드를 붙여줘서 다른 파일에서 import해서 사용할 수 있도록 한다.
// 액션 타입 (Action type)
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

// 액션 생성 함수
export const increase = () => ({ type: 'INCREASE'});
export const decrease = () => ({ type: 'DECREASE'});

초기 상태(initialState)와 리듀서(reducer) 만들기

이제 counter 모듈의 초기 상태와 리듀서 함수를 만들어주자.
이 모듈의 초기 상태에는 number 값을 설정해 주었으며, 리듀서 함수에는 현재 상태를 참조하여 새로운 객체를 생성하여 반환하는 코드를 작성해준다. 마지막으로, export default 키워드를 사용하여 함수를 내보내준다.

// 액션 타입 (Action type)
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

// 초기 상태 (initialState)
const initialState = { number: 0 };

// 리듀서 (reducer)
export default const counterState = (state = initialState, action) => {
  switch(action.type) {
    case 'INCREASE':
      return {
        number: state.number + 1,
      };
    case 'DECREASE':
      return {
        number: state.number - 1,
      };
    default:
      return state;
  }
}

그런데, 여기에서 1씩 증가하고 1씩 감소하는 것이 아니라, 지정해준 값만큼 증가하고 감소하는 기능도 추가하고 싶다면 어떻게 해야 할까? 다음과 같이 액션을 추가한다.

// 추가된 액션
const INCREASE_BY_VALUE = 'counter/INCREASE_BY_VALUE';
const DECREASE_BY_VALUE = 'counter/DECREASE_BY_VALUE';

// 추가된 액션 생성 함수
export const increase_by = (num) => ({
  type: INCREASE_BY_VALUE,
  value: num,
});
export const decrease_by = (num) => ({
  type: DECREASE_BY_VALUE,
  value: num,
})

// 초기 상태는 그대로
const initialState = { number : 0 };

// 리듀서에서 추가된 코드
export default const counterState = (state = initialState, action) => {
  switch(action.type) {
    case 'INCREASE_BY_VALUE':
      return {
        ...state,
        number: state.number + action.value,
      };
    case 'DECREASE_BY_VALUE':
      return {
        ...state,
        number: state.number - action.value,
      };
  }
}

완성된 전체 counter.js 모듈의 코드는 다음과 같다.

// 액션 타입 (Action type)
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
const INCREASE_BY_VALUE = 'counter/INCREASE_BY_VALUE';
const DECREASE_BY_VALUE = 'counter/DECREASE_BY_VALUE';

// 액션 생성 함수
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increase_by = (num) => ({
  type: INCREASE_BY_VALUE,
  value: num,
});
export const decrease_by = (num) => ({
  type: DECREASE_BY_VALUE,
  value: num,
})

// 초기 상태 (initialState)
const initialState = { number: 0 };

// 리듀서 (reducer)
export default const counterState = (state = initialState, action) => {
  switch(action.type) {
    case 'INCREASE':
      return {
        number: state.number + 1,
      };
    case 'DECREASE':
      return {
        number: state.number - 1,
      };
    case 'INCREASE_BY_VALUE':
      return {
        ...state,
        number: state.number + action.value,
      };
    case 'DECREASE_BY_VALUE':
      return {
        ...state,
        number: state.number - action.value,
      };
    default:
      return state;
  }
}

루트 리듀서 (rootRecucer) 만들기

Redux를 사용할 때 위처럼 모듈을 딱 하나만 사용하진 않을 것이다. /src/store/modules 디렉토리에 counter.js를 비롯한 많은 모듈들이 들어가게 될 것인데, 해당 모듈들에는 각각 리듀서들이 존재하게 되므로 전체적으로 봤을 때는 리듀서가 여러 개 존재하게 된다.

그런데 나중에 createStore 함수를 사용하여 스토어를 만들 때는 리듀서를 단 하나만 파라미터로 넘겨줄 수 있다. 그렇기 때문에, 기존에 만들었던 리듀서들을 하나로 합쳐주어야 하는데, 이 작업은 Redux에서 제공하는 combineReducers라는 유틸 함수를 사용하면 쉽게 처리할 수 있다.

/src/store 디렉토리에 index.js를 만들고 다음과 같이 작성하자.

import { combineReducers, createStore } from 'redux';
import counterState from './modules/counter.js`;

const rootReducer = combineReducers({
  counterState,
});

export const store = createStore(rootReducer);
profile
여러 프로젝트보다 하나라도 제대로, 깔끔하게.

0개의 댓글