Redux를 사용하여 상태 관리하기

nasagong·2023년 3월 11일
0

React

목록 보기
15/15
post-thumbnail

📚 들어가며

리덕스를 사용하여 직접 리액트의 상태를 관리해보자. 컴포넌트가 어떻게 리액트 스토어와 연결되는지 주목해보면 좋을 것 같다.

Ducks 패턴

리덕스 코드를 작성할 때는 디렉터리를 여러개 생성해야 한다. 이 때 다양한 패턴을 사용할 수 있는데, 나는 modules 디렉터리 안에 액션 타입/ 액션 생성함수/ 리듀서를 전부 작성하는 Ducks패턴에 따라 작성할 예정이다. 왜냐면 아는 게 이것 뿐이기 때문..

Counter 기능 구현하기

모듈 만들기

리덕스를 사용해 간단하게 카운터를 구현해보자. 우선 앞서 언급한 Ducks 패턴을 사용해 만든 모듈을 먼저 확인해보자.

const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";

export const increase = () => ({type:INCREASE});
export const decrease = () => ({type:DECREASE});

const iniitialState = {
  number: 0,
};

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

export default counter;

차근차근 위에서 아래로 읽어보자.

우선 액션 타입을 아래처럼 정의한 것을 확인할 수 있다.

const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";

이 포스팅의 예시 프로젝트는 카운터 기능 하나만 구현하기에 다른 모듈을 만들 필요가 없지만, 실제 프로젝트에선 여러가지 기능을 구현해야 하기에 다양한 리듀서를 포함하고 있는 모듈들이 포함될 것이다. 따라서 중복을 피하기 위해 액션 타입은 모듈이름/액션타입 형태로 작성한다.

export const increase = () => ({type:INCREASE});
export const decrease = () => ({type:DECREASE});

액션 생성 함수다. 미리 선언해둔 액션 타입을 반환한다. 추후 디스패치 함수를 통해 액션이 리듀서에 전달되어 상태를 변화시킬 거다.

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

리듀서 함수다. 디스패치를 통해 액션이 전달되면 타입을 조회하여 타입에 따라 상태값을 변화시킨다.

루트 리듀서 만들기

여러개의 리듀서가 한 프로젝트에서 사용되고 있을 때 이를 하나로 묶어둘 수는 없을까? 루트 리듀서를 사용하면 가능하다.

// index.js
import { combineReducers } from 'redux';
import counter from './counter';

const rootReducer = combineReducers({
    counter,
});

export default rootReducer;

리덕스에서 제공하는 combineReducers를 사용해 리듀서들을 합칠 수 있다. 여기선 하나의 리듀서만 포함시켰지만 보통은 두개 이상의 리듀서를 통합시켜 사용할 것이다.

파일명을 rootReducer.js 가 아닌 index.js 로 지었는데, 이렇게 설정해두면 나중에 불러올 때 디렉터리 이름까지만 입력하면 된다. 굿.

리액트에 리덕스 적용하기

이제 앞서 작성한 리듀서를 리덕스 스토어에 연결시킨 후, 스토어를 리액트 애플리케이션에 적용해보자.

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import rootReducer from './modules';
import {Provider} from 'react-redux';

const store = createStore(rootReducer);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

createStore를 통해 rootReducer가 연결된 스토어를 생성했다. 이제 이 스토어를 어플리케이션에서 사용할 수 있도록 store를 props로 갖고 있는 Provider 컴포넌트로 App을 감싸주면 된다.

컨테이너 컴포넌트 만들기

import Counter from "../components/Counter";
import {increase,decrease} from '../modules/counter';
import { connect } from 'react-redux';

const CounterContainer = ({number,increase,decrease}) =>{
    return(
        <Counter number={number} onIncrease={increase} onDecrease={decrease}/>
    )
}
export default connect(
    ({counter})=>({
        number:counter.number
    }),
    {
        increase,
        decrease,
    }
)(CounterContainer);

presentational 컴포넌트와 스토어 사이를 이어주는(?) 컨테이너 컴포넌트를 만들어보자. 우선 connect 함수에 주목해보자. connect는 컴포넌트와 스토어를 연결해주는 역할을 한다.

첫번째 파라미터는 리더스 스토어 안의 상태를 컴포넌트의 props로 넘겨주는 역할을 하고, 두번째 파라미터는 액션 생성 함수를 컴포넌트의 props로 넘겨주는 역할을 한다.

액션 생성 함수 / 리듀서 쉽게 만들기

createActionhandleActions를 사용해 보다 쉽게 리덕스 코드를 작성할 수 있다.

createAction

//기존 코드
const INCREASE = () => ({type:INCREASE});
const DECREASE = () => ({type:DECREASE});

//createAction 사용
const INCREASE = createAction(INCREASE);
const DECREASE = createAction(DECREASE);

화살표 함수를 하나하나 만들어 줄 필요 없이 react-redux에 포함된 createAction을 사용하면 액션 생성 함수를 간단하게 만들 수 있다.

handleActions

//기존 코드
function counter(state=iniitialState, action){
  switch(action.type){
    case INCREASE:
      return{
        number:state.number+1
      };
    case DECREASE:
      return{
        number:state.number-1
      };
    default:
      return state;
  }
}

//handleActions 사용
const counter = handleActions(
  {
    [INCREASE]: (state,action) => ({number:state.number+1}),
    [DECREASE]: (state,action) => ({number:state.number-1}),
  },
  iniitialState
);

리듀서 코드는 정말 작성하기 편해졌다.

다만, counter의 경우는 액션 함수를 생성하는 데 아무런 파라미터도 필요하지 않았지만, 꼭 필요한 경우에는 어떻게 작성해야 할까.

const MY_ACTION = 'sample/MY_ACTION';
const myActoin = createAction(MY_ACTION);
const action = myAction('hello world');
/*
  결과:
  { type: MY_ACTION, payload: 'hello world' }
*/

액션에 필요한 추가 데이터는 payload라는 이름을 사용해 저장된다. 액션 생성 함수에서 받은 파라미터를 변형한 후 넘기고 싶다면 아래처럼 미리 변형방식을 정의하는 함수를 설정해주면 된다.

export const changeInput = 
createAction(CHANGE_INPUT, input => input);

위 코드 자체는 payload값을 받은 그대로 넘겨주고 있다. 생략해도 무방하지만 암튼 이런 식으로 미리 방식을 설정해둘 수 있다고 한다.

이번엔 리듀서에서 payload값을 다루는 법도 확인해보자.

const todos = handleActions(
 {
    [CHANGE_INPUT]:(state,action) => ({...state, input:action.payload}),
(...)
 },intialState,
);

action.payload로 접근해주면 된다. coutnet 리듀서에서 사용되지 않은 actoin파라미터는 이렇게 사용된다.

[CHANGE_INPUT]:(state,{payload:input}) =>
({...state,input})

비구조화 할당을 사용해 payload이름을 새로 설정해주면 보다 가독성 좋게 작성할 수 있다.

Hooks 사용하기

당연하게도 리덕스 관련 Hooks가 존재한다. 먼저 useSelector를 사용해 상태를 조회하는 법을 알아보자. connect함수를 사용해 스토어에 접근한 후 props로 상태값을 받아와 사용하던 기존 방식과 달리 손쉽게 상태에 접근해 props로 넘길 수 있다.

const CounterContainer = () =>{
    const number = useSelector(state=>state.counter.number);
    return(
        <Counter number={number} />
    );
};
export default CounterContainer;

새롭게 작성한 CounterContainer다. connect없이 간결하게 작성됐다. 다만 useSelector만으로는 디스패치가 이루어지지 않는다. 이를 위해 useDispatch가 존재한다.

const CounterContainer = () =>{
    const number = useSelector(state=>state.counter.number);
    const dispatch = useDispatch();
    return(
        <Counter 
        number={number}
        onIncrease={()=>dispatch(increase())}
        onDecrease={()=>dispatch(decrease())}
        />
    )
}

export default CounterContainer;

역시나 매우 간단하다. 기본 사용법은

const dispatch = useDispatch();
dispatch({ type: 'SAMPLE_ACTION })l

이런 식이다. 미리 import해온 액션 생성 함수들을 바로 호출해주면 액션값을 반환하니 적절하게 디스패치가 가능하다.

useSelector와 connect의 차이점

connect함수를 사용해 컨테이너 컴포넌트를 만든다면 컨테이너의 부모 컴포넌트가 리렌더링될 때 컨테이너의 props가 변경되지 않으면 리렌더링이 발생하지 않는다. 반면 useSelector는 이러한 최적화가 자동으로 이루어지지 않기에 React.memo를 사용해줘야 한다.

import React from 'react';
import Counter from "../components/Counter";
import {increase,decrease} from '../modules/counter';
import { useDispatch, useSelector } from 'react-redux';

const CounterContainer = () =>{
    const number = useSelector(state=>state.counter.number);
    const dispatch = useDispatch();
    return(
        <Counter 
        number={number}
        onIncrease={()=>dispatch(increase())}
        onDecrease={()=>dispatch(decrease())}
        />
    )
}

export default React.memo(CounterContainer);

When you use useSelector to extract data from the Redux store state in your functional component, you are essentially creating a dependency on that state. This means that the component will re-render whenever the state changes. To prevent unnecessary re-renders, you can wrap your component with React.memo. This will memoize the component so that it only re-renders when its props change.

profile
잘쫌해

0개의 댓글