본문 바로가기
JavaScript/React

React) 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기 ① - 준비 과정

by 박채니 2022. 12. 9.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[리액트를 다루는 기술]의 책을 참고하여 포스팅한 개인 공부 내용입니다.

 

리덕스를 사용하여 리액트 애플리케이션 상태 관리하기

 

리액트에서 리덕스 사용 시, 상태 업데이트 관련 로직을 모듈로 분리하여 컴포넌트 파일과 별개로 관리할 수 있으므로

코드를 유지 보수하는 데 도움이 된다고 배웠습니다.

이외에도 여러 컴포넌트에서 동일한 상태를 공유할 때 유용하고, 실제 업데이트가 필요한 컴포넌트만 리렌더링되도록 최적화 해줄 수도 있습니다.

 

이전 포스팅에서는 store.dispatch와 store.subscribe를 사용했지만,

리액트에서 리덕스를 사용할 땐 react-redux 라이브러리에서 제공하는 connectProvider를 사용합니다.

 


작업 환경 설정

 

$ yarn create react-app react-redux-tutorial
$ cd react-redux-tutorial
$ yarn add redux react-redux

 

.prettierrc

{
    "singleQuote": true,
    "semi": true,
    "useTabs": false,
    "tabWidth": 2,
    "trailingComma": "all",
    "printWidth": 80
}

 


UI 준비하기

 

리덕스 사용 시 많이 사용되는 패턴은 아래와 같습니다.

  • 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트 분리

프레젠테이셔널 컴포넌트?

- 주로 상태 관리가 이루어지지 않고, props를 받아 화면에 UI를 보여주는 컴포넌트

 

컨테이너 컴포넌트?

- 리덕스와 연동되어 있는 컴포넌트

- 리덕스로부터 상태를 받아 오거나 리덕스 스토어에 액션을 디스패치

 

UI에 관련된 프레젠테이션 컴포넌트는 src/components 경로에 저장

리덕스와 연동된 컨테이너 컴포넌트는 src/containers 경로에 저장해보겠습니다.

 

카운터 컴포넌트 만들기

 

components/Counter.js

const Counter = ({ number, onIncrease, onDecrease }) => {
    return(
        <div>
            <h1>{number}</h1>
            <div>
                <button onClick={onIncrease}>+1</button>
                <button onClick={onDecrease}>-1</button>
            </div>
        </div>
    )
}

export default Counter;

 

App.js

import Counter from "./components/Counter";

function App() {
  return (
    <div>
      <Counter number={0} />
    </div>
  );
}

export default App;

 

할 일 목록 컴포넌트 만들기

 

components/Todos.js (할 일 추가, 체크, 삭제 등을 실행)

const TodoItem = ({ todo, onToggle, onRemove }) => {
    return(
        <div>
            <input type="checkbox" />
            <span>예제 텍스트</span>
            <button>삭제</button>
        </div>
    )
};

const Todos = ({
    input, // 인풋에 입력되는 텍스트
    todos, // 할 일 목록이 들어있는 객체
    onChangeInput,
    onInsert,
    onToggle,
    onRemove
}) => {
    const onSubmit = e => {
        e.preventDefault();
    }

    return(
        <div>
            <form onSubmit={onSubmit}> {/* 제출 방지 */}
                <input />
                <button type="submit">등록</button>
            </form>
            <div>
                <TodoItem />
                <TodoItem />
                <TodoItem />
                <TodoItem />
                <TodoItem />
            </div>
        </div>
    );
};

export default Todos;

TodoItem과 Todos를 분리해도 되지만, 컴포넌트를 합쳐주었습니다.

 

App.js

import Counter from "./components/Counter";
import Todos from "./components/Todos";

function App() {
  return (
    <div>
      <Counter number={0} />
      <hr />
      <Todos />
    </div>
  );
}

export default App;


리덕스 관련 코드 작성하기

 

리덕스를 사용 시 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해야 하고, 작성하는 방법이 두 가지 있습니다.

 

① 각각 다른 파일에 작성 (일반적인 구조)

actions, constants, reducers 디렉터리를 생성하고 안에 기능별로 파일을 생성합니다.

정리가 되기 때문에 편리할 수 있지만, 새로운 액션 생성마다 3개의 파일을 수정해야하므로 불편할 수도 있습니다.

 

② 기능별로 묶어서 하나의 파일로 작성 (Ducks 패턴)

액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 작성하는 방식입니다.

일반적인 구조로 관리함에 있어 불편함을 느끼는 개발자들이 사용합니다.

 

Ducks 패턴을 이용하여 코드 작성을 해보겠습니다.

 

counter 모듈 작성하기

 

Ducks 패턴을 사용액션 타입, 액션 생성 함수, 리듀서를 작성한 코드를 '모듈'이라고 합니다.

counter 모듈을 만들어보겠습니다.

 

액션 타입 정의하기

modules/counter.js

// 액션 타입 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

액션 타입대문자로 정의, 내용'모듈이름/액션이름'으로 작성하여 추후 액션 이름의 충돌을 방지해줍니다.

 

액션 생성 함수 만들기

modules/counter.js

// 액션 타입 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

// 액션 생성 함수 만들기
// export 키워드 -> 해당 함수를 다른 파일에서 불러와 사용 가능!
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

export 키워드를 이용해 해당 함수를 다른 파일에서도 불러와 사용 가능하도록 하였습니다.

 

초기 상태 및 리듀서 함수 만들기

modules/counter.js

// 액션 타입 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

// 액션 생성 함수 만들기
// export 키워드 -> 해당 함수를 다른 파일에서 불러와 사용 가능!
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

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

// 리듀서 함수 생성
function counter(state = initialState, action) {
    switch(action.type) {
        case INCREASE : return { number: state.number + 1 };
        case DECREASE : return { number: state.number - 1 };
        default: return state;
    }
}

export default counter; // 한 개만 보낼 수 있음!

초기 상태와 리듀서 함수를 생성하여 각 액션 별로 객체를 리턴해주었습니다.

export default 또한 함수를 내보내주는 것인데, export는 여러 개가 가능하고 export default는 단 하나만 가능합니다.

 

따라서 불러오는 방식도 다릅니다.

import counter from './counter'; // export default 키워드 이용 시 
import { increase, decrease } from './counter'; // export 키워드 이용 시
// 한꺼번에 불러오고 싶을 때
import counter, { increase, decrease } from './counter';

 

todos 모듈 만들기

 

액션 타입 정의 및 액션 생성 함수 만들기

modules/todos.js

// 액션 타입 정의하기
const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 인풋 값 변경
const INSERT = 'todos/INSERT'; // 새로운 todo 등록
const TOGGLE = 'todos/TOGGLE'; // todo를 체크/체크 해제
const REMOVE = 'todos/REMOVE'; // todo 제거

// 액션 생성 함수 만들기
export const changeInput = input => ({
    type: CHANGE_INPUT,
    input
});

let id = 3; // insert가 호출될 때마다 1씩 증가, todo객체 두 개를 사전에 넣을거라 3으로 지정
export const insert = text => ({
    type: INSERT,
    todo: {
        id: id++,
        text,
        done: false
    }
});

export const toggle = id => ({
    type: TOGGLE,
    id
});

export const remove = id => ({
    type: REMOVE,
    id
});

전달받은 파라미터를 액션 객체 안에 추가 필드로 들어간 것을 확인할 수 있습니다.

 

초기 상태 및 리듀서 함수 만들기

// 액션 타입 정의하기
const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 인풋 값 변경
const INSERT = 'todos/INSERT'; // 새로운 todo 등록
const TOGGLE = 'todos/TOGGLE'; // todo를 체크/체크 해제
const REMOVE = 'todos/REMOVE'; // todo 제거

// 액션 생성 함수 만들기
export const changeInput = input => ({
    type: CHANGE_INPUT,
    input
});

let id = 3; // insert가 호출될 때마다 1씩 증가, todo객체 두 개를 사전에 넣을거라 3으로 지정
export const insert = text => ({
    type: INSERT,
    todo: {
        id: id++,
        text,
        done: false
    }
});

export const toggle = id => ({
    type: TOGGLE,
    id
});

export const remove = id => ({
    type: REMOVE,
    id
});

// 초기 상태
const initialState = {
    input: '',
    todos: [
        {
            id: 1,
            text: '리덕스 기초 배우기', 
            done: true
        },
        {
            id: 2,
            text: '리액트와 리덕스 사용하기',
            done: false
        }
    ]
};

// 리듀서 함수 생성
function todos(state = initialState, action) {
    switch(action.type) {
        case CHANGE_INPUT:
            return {
                ...state,
                input: action.input
            };
        case INSERT:
            return {
                ...state,
                todos: state.todos.concat(action.todo)
            };
        case TOGGLE: 
            return {
                ...state,
                todos: state.todos.map(todo => todo.id === action.id ? { ...todo, done: !todo.done } : todo)
            };
        case REMOVE:
            return {
                ...state,
                todos: state.todos.filter(todo => todo.id !== action.id)
            };
        default: return state;
    }
}

export default todos;

불변성 유지를 위해 spread 연산자를 이용하였습니다.

 

루트 리듀서 만들기

 

createStore 함수를 사용해 스토어를 만들 때리듀서를 하나만 사용해야 합니다.

따라서 기존에 만들었던 리듀서들을 하나로 합쳐줘야 합니다. combineReducers 유틸함수를 사용하면 처리할 수 있습니다.

 

modules/index.js

import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';

const rootReducer = combineReducers({
    counter,
    todos
});

export default rootReducer;

combineReducers를 이용하여 하나의 리듀서로 만들어주었습니다.

또한, index.js로 설정해주면 간편하게 불러올 수 있습니다.

import rootReducer from './modules';