[Redux] 기초 개념 정리

소고기는레어·2022년 3월 2일
0

Front-end 🖥

목록 보기
13/19

Redux 기초 개념

리덕스는 크게 세가지 파트로 구분할 수 있다.

  1. 액션
    • 스토어의 상태를 변경하는 용도로 사용된다.
    • 직접 상태를 변경하지는 않고 어떤 상태로 변경할 것인지 결정하는 역할만 한다.
  2. 리듀서
    • state를 정의하고 전달 받은 액션에 따라 스토어의 상태 변경이 이루어진다.
    • 실제 스토어의 상태 변경은 리듀서에서 이루어진다.
  3. 스토어
    • 액션과 리듀서의 연결 지점이다.
    • 또한 모든 것을 총괄하는 지점이다.
    • Redux는 단일 스토어 하나만을 갖는다.

Action

액션은 스토어의 상태 변경을 결정하지만, 실제로 스토어의 상태 변경이 이루어지는 곳은 아니다.

식당을 예시로 들면 액션주문서이다.
어떤 메뉴를 만들어야 할지 결정되는 부분이지만, 실제 메뉴가 만들어지는 곳은 주방이지 주문서가 아니다.


액션이 갖는 기본적인 형태는 아래와 같다.
참고로 모든 액션은 객체로 이루어진다.

{
	type: "타입"
}

type은 액션의 이름이며, 주문서에 적힌 메뉴의 이름이다.

어떤 메뉴를 조리할 것인지, 즉 스토어의 어떤 상태를 변경할 것인지 결정한다. 모든 액션이 필수로 가지고 있어야 하는 프로퍼티이다.


또한 type 외에 다른 프로퍼티가 추가로 들어가는 경우도 있다.

{
	type: "타입",
    params: "파라미터"
}

이 프로퍼티는 옵션이다. 주문서에 적힌 맵기, 재료 추가 등의 추가 요청사항이다.

해당 메뉴를 어떻게 조리할 것인지, 즉 해당 상태를 무엇을 이용해 어떻게 변경할 것인지 결정한다. 이 프로퍼티는 있을 수도 없을 수도 있다.


액션 생성자

액션은 생성자 함수를 통해 만든다.
액션의 타입은 미리 정의해두는 것이 좋다. 코드 작성에서 실수를 방지할 수 있기 때문이다.

// 타입 정의
export const PLUS_NUM = "PLUS_NUM"
export const MINUS_NUM = "MINUS_NUM";

// 액션 생성자
export function plusNum(num) {
  return {
  	type: PLUS_NUM,
    num
  };
}

export function minusNum(num) {
  return {
  	type: MINUS_NUM,
  	num
  };
}


Reducer

리듀서는 state를 정의하고 스토어의 상태 변경을 담당하는 곳이다.

식당에서 주문서(action)를 받으면 주문서는 주방으로 전달되고 주방에서 조리를 시작한다. 리듀서는 바로 이 주방과 같은 곳이다.

액션이 리듀서로 전달되면 리듀서는 액션의 type 등을 확인하고 그에 따른 로직을 처리하여 스토어의 상태를 변경하게 된다.

// 액션 타입을 불러온다.
// 오타가 발생하는 것을 방지
import { MINUS_NUM, PLUS_NUM } from "../actions";

// state 초기값 정의
const initialState = 0;

// 리듀서
export default function calcApp(prevState = initialState, action) {
  //액션의 타입에 따른 로직 정의
  if (action.type === PLUS_NUM) {
    return prevState + parseInt(action.num);
  } else if (action.type === MINUS_NUM) {
    return prevState - parseInt(action.num);
  }
	
  // 해당 없을 경우 이전 state 반환
  return prevState;
}

주의할 점이 있는데, 리듀서는 기존의 state를 변경해서는 안된다. 기존의 state를 이용해 새로운 state를 반환해야 한다. 이는 기존의 state와 새로운 state를 비교해서 변경 사항이 있는지 없는지를 판단하는 redux의 인지 방식과 관련이 있다.



Store

스토어는 액션과 리듀서의 연결고리와 같은 역할이며, 모든 것을 총괄하는 곳이기도 하다. 식당 그 자체라고 이해할 수 있다.

Redux의 제어는 대부분 이 store와 store의 메소드를 사용하게 된다. store의 메소드를 이용해 액션과 리듀서를 제어한다.

스토어 생성 & 리듀서 연결

createStore() 함수를 redux에서 import하여 사용할 수 있다.

이 함수에는 3개의 인자를 전달할 수 있다.

  1. 리듀서
  2. 초기 state
  3. enhancer

인자로 리듀서를 전달하여 스토어와 리듀서를 연결하게 된다. 초기 state와 enhancer는 아직 몰라도 된다.

import {createStore} from "redux";
import calcApp from "calcApp.js";

// 스토어를 생성하고 calcApp 리듀서 연결
const store = createStore(calcApp);

스토어의 생성과 리듀서 연결을 마쳤다면 이제 남은 것은 액션의 연결이다.

리듀서는 스토어와 항상 연결되어 있는 반면에 액션은 스토어의 상태 변경이 필요할 때만 전달하면 된다. 액션의 전달은 아래에서 설명할 스토어의 메소드를 통해 이루어진다.

액션 전달 & 여러 메소드

액션의 전달은 다음 메소드를 통해 이루어진다.

store.dispatch(action);

해당 메소드의 인자로 액션을 전달하면 스토어는 인자로 받은 액션을 리듀서에 전달한다.


이 외에도 스토어는 여러가지 메소드를 갖고 있다.

  • store.getState();
    - 스토어의 state를 불러온다.

  • store.subscribe();
    - 스토어의 변화를 감시하고 변화가 발생하면 콜백을 실행한다.
    - store의 이벤트 리스너이다.
    - 이 메소드의 반환값은 함수이며, 이 함수를 변수에 할당해 두었다가 필요시 호출하여 감시를 중단할 수 있다.

// 스토어 감시 시작
// 변경이 발생하면 state를 출력한다.
const unsubscribe = store.subscribe(() => {
	console.log(store.getState());
})

// 스토어에 액션 전달
store.dispach(plusNum(1));

// 스토어 감시 중단
unsubscribe();

// 감시만 중단할 뿐 액션 전달과 상태 변경은 정상적으로 이루어진다.
store.dispach(plusNum(5));


리듀서 분리

프로젝트의 규모가 커지면 리듀서 또한 규모가 커지게 되어 관리에 어려움이 생길 수 있다.

이 때 리듀서를 역할별로 분리하여 관리할 수 있는데, 이렇게 분리된 리듀서들을 스토어에 연결하기 위해서는 다시 하나로 합칠 필요가 있다.

이 때 combineReducers() 메소드를 사용하여 리듀서들을 하나로 합칠 수 있다.

import {combineReducers} from "redux";
import reducerA from "reducerA.js";
import reducerB from "reducerB.js";

const reducer = combineReducers(reducerA, reducerB);

export default reducer;

분리된 리듀서들은 모두 각각의 state를 갖게 되고, 마찬가지로 각각의 초기값을 정의해 주어야 한다.



react-redux

$ npm i react-redux

react-redux를 이용하면 리액트와 리덕스 간의 연결을 간단하게 진행할 수 있다.

연결 과정은 아래와 같다.

  1. Provider로 store 연결
  2. store에서 데이터 불러오기

1. Provider로 store 연결

Provider는 컴포넌트이다.
Provider의 하위 컴포넌트는 모두 store에 접근이 가능하다.
store는 Provider의 prop으로 전달한다.

<Provider store={store}>
  <App />
</Provider>

2. store에서 데이터 불러오기

스토어에서 불러와야 할 데이터는 statedispatch이다.

dispatch로 state를 변경하고 이 state를 사용해 앱을 굴려야 하기 때문이다.

state와 dispatch를 불러오는데 사용할 메소드는 각각 아래와 같다.

  • useSelector()
  • useDispatch()

컴포넌트에서 위의 메소드를 호출하여 원하는 데이터에 접근할 수 있다.

이 때, 보통은 스토어에서 데이터를 불러와서 처리하는 컴포넌트와 해당 데이터를 화면에 출력하는 컴포넌트를 분리하여 작성한다.

스토어에서 데이터를 불러오는 컴포넌트를 컨테이너라고 부른다. 컨테이너는 스토어에서 데이터를 불러온 뒤 출력 담당 컴포넌트에 데이터를 전달하여 반환한다.

state 컴포넌트

컨테이너 컴포넌트는 아래와 같은 구조를 갖는다.

import { useSelector } from "react-redux";
import Num from "../components/Num";

export default function NumContainer() {
  const num = useSelector((state) => state);

  return <Num num={num} />;
}

출력을 담당하는 컴포넌트는 아래와 같이 데이터를 prop으로 받아와서 출력하게 된다.

export default function Num({ num }) {
  return <div>{num}</div>;
}

dispatch 컴포넌트

dispatch는 아래와 같은 로직으로 불러와서 사용이 가능하다.

보통 불러온 뒤 사용하기 쉬운 간단한 메소드 형태로 만들어서 반환한다.

import { useCallback } from "react";
import { useDispatch } from "react-redux";
import CalcBtn from "../components/CalcBtn";
import { plusNum, minusNum } from "../redux/actions";

export default function CalcContainer() {
  const dispatch = useDispatch();

  const plus = useCallback(
    (num) => {
      dispatch(plusNum(num));
    },
    [dispatch]
  );

  const minus = useCallback(
    (num) => {
      dispatch(minusNum(num));
    },
    [dispatch]
  );

  return <CalcBtn plus={plus} minus={minus} />;
}

dispatch의 출력 담당 컴포넌트

export default function CalcBtn({ plus, minus }) {
  const onPlusClick = (e) => {
    plus(e.target.value);
  };
  const onMinusClick = (e) => {
    minus(e.target.value);
  };
  return (
    <div>
      <button value={1} onClick={onPlusClick}>
        +1
      </button>
      <button value={2} onClick={onPlusClick}>
        +2
      </button>
      <button value={1} onClick={onMinusClick}>
        -1
      </button>
      <button value={2} onClick={onMinusClick}>
        -2
      </button>
    </div>
  );
}
profile
https://www.rarebeef.co.kr/

0개의 댓글