상세 컨텐츠

본문 제목

[React.js 2팀] 16장. 리덕스 라이브러리 이해하기 ~ 17장. 리덕스로 리액트 애플리케이션 상태 관리하기

24-25/React.js 2

by 엔초_ 2025. 1. 31. 10:00

본문

728x90

 

16장. 리덕스 라이브러리 이해하기

 리덕스는 가장 많이 사용하는 리액트 상태 관리 라이브러리로, 컴포넌트의 상태 업데이트와 관련된 로직을 다른 파일로 분리시켜 더욱 효율적인 관리를 가능하게 한다. 또한 리덕스를 사용하면 컴포넌트끼리 동일한 상태를 공유해야 하는 경우에도 여러 컴포넌트를 거치지 않고 상태 값을 쉽게 전달하거나 업데이트 할 수 있다는 장점이 있다.

 특히 리덕스를 사용하면 상태를 더욱 체계적으로 관리할 수 있기 때문에 규모가 큰 프로젝트에서는 리덕스를 사용하는 것이 좋다.

1. 개념 정리하기

리덕스를 이해하기 위해 미리 정리해야 할 개념은 크게 6가지이다.

 

 ① 액션 

상태에 어떠한 변화가 필요하면 액션(action)이란 것이 발생한다. 액션은 하나의 객체로 표현되며, 다음과 같은 형식으로 이루어진다.

{
  type: 'TOGGLE_VALUE'
}

액션 객체는 type 필드 외에도 다른 값을 작성자 임의대로 추가할 수 있다. 그러나 주의해야 할 점이 있다면, type 필드는 반드시 포함되어야 하며 이 값이 액션의 이름이 된다는 것이다. 그 외의 값들은 상태 업데이트 시 참고해야 하는 값이 된다.

 

 ② 액션 생성 함수 

액션 생성 함수(action creator)는 액션 객체를 만들어주는 함수이다. 액션 객체를 함수로 만들어 관리하면 매번 액션 객체를 만드는 과정에서 발생하는 실수를 방지할 수 있다.

다음은 액션 생성 함수를 활용하는 예시이다.

function addTodo(data) {
return {
	type: 'ADD_TODO',
  	data
  };
}
// 화살표 함수로도 만들 수 있다.
const changeInput = text => ({ 
    type: 'CHANGE_INPUT',
	text
});

 

 ③ 리듀서 

리듀서(reducer)는 변화를 일으키는 함수이다. 액션을 만들어서 발생시키면 리듀서는 현재 상태와 전달받은 액션 객체를 파라미터로 받아온다. 그리고 두 값을 참고하여 새로운 상태를 만들어서 반환한다. 다음은 리듀서 코드의 예시이다.

const initialState = {
  counter: 1
};
function reducer(state = initialState, action) {
  switch (action.type) {
    case INCREMENT:
      return {
          counter: state.counter + 1
      };
    default:
      return state;
  }
}

 

 ④ 스토어 

프로젝트에 리덕스를 적용하기 위해서는 스토어(store)를 만들어야 한다. 한 개의 프로젝트는 단 하나의 스토어만 가진다는 점에 주의하자. 스토어 안에는 현재 애플리케이션 상태와 리듀서가 들어가 있으며, 그 외에도 몇 가지 중요한 내장 함수를 포함한다. 

 

 ⑤ 디스패치 

디스패치(dispatch)는 스토어의 내장 함수 중 하나로, '액션을 발생시키는 것'이라고 이해할 수 있다. 

이 함수는 dispatch(action)과 같은 형태로 액션 객체를 파라미터로 넣어서 호출하며, 디스패치가 호출되면 스토어는 리듀서 함수를 실행시켜 새로운 상태를 만들어 준다.

 

 ⑥ 구독 

구독(subscribe) 또한 스토어의 내장 함수 중 하나이다.

const listener = () => {
console.log('상태가 업데이트됨');  
}
const unsubscribe = store.subscribe(listener);

unsubscribe(); // 추후 구독을 비활성화할 때 함수를 호출

subscribe 함수 안에 리스너 함수(listener)를 파라미터로 넣어 호출해주면, 이 리스너 함수가 액션이 디스패치되어 상태가 업데이트 될 때마다 호출된다. 

 

2. 리액트 없이 쓰는 리덕스

 리덕스는 리액트에 종속되는 라이브러리가 아니다. 리덕스는 리액트에서 사용하기 위해 만들어졌으나 실제로 다른 UI 라이브러리와 프레임워크(angular-redux, ember redux, Vue 등)에서 함께 사용할 수도 있다.

 또한 리덕스는 바닐라(vanilla) 자바스크립트(라이브러리/프레임워크 없이 사용하는 순수 자바스크립트 그 자체)와 함께 사용할 수 있다. 이번에는 바닐라 자바스크립트 환경에서 리덕스를 사용하여 리덕스의 핵심 기능과 작동 원리를 알아보자.

 

[ Parcel로 프로젝트 만들기 ]

먼저, 프로젝트 구성을 위해 Parcel이라는 도구를 사용한다. Parcel을 사용하면 쉽고 빠르게 웹 애플리케이션을 구성할 수 있다.

1. Parcel-bundler 설치

$ yarn global add parcel-bundler
#yarn global이 잘 설치되지 않는다면 npm install -g parcel-bundler

 다만, 여기서 경고 메시지가 뜬다면 설치된 일부 패키지들이 최신 버전과 맞지 않거나 더 이상 관리되지 않기 때문이다 (버전 확인 결과 v1이었다). 이 경우, 기존 Parcel을 제거하고 'npm install -g parcel'로 Parcel v2를 설치한다. 

설치가 완료되면 다음과 같은 메시지가 뜬다.

 

2. 프로젝트 디렉터리 생성 후 package.json 파일 생성

$ mkdir vanilla-redux
$ cd vanilla-redux

# package.json 파일 생성.
$ yarn init -y

 나의 경우, 실습 과정에서 Yarn이 설치되어 있지 않아 오류가 발생하였다. 따라서 'npm install -g yarn'을 통해 Yarn을 전역으로 설치한 후 다시 시도하였다. 

 

3. 에디터로 해당 디렉터리를 열고 index.html과 index.js 파일 생성

<html>
  <body>
    <div>바닐라 자바스크립트</div>
    <script src="./index.js"></script>
  </body>
</html>

index.html

console.log('hello parcel');

index.js

모두 작성 후 다음 명령어를 실행하면 개발용 서버가 실행된다. 

개발 서버의 주소는 http://localhost:1234/ 이며, 파일을 저장할 때마다 자동으로 새로고침된다.

링크를 클릭하여 들어가면 아래와 같은 페이지가 나타난다.

4. yarn을 사용하여 리덕스 모듈 설치

$ yarn add redux

 

 

[ 간단한 UI 구성하기 ]

 간단한 UI 구성을 위해 index.css 파일을 생성하고 index.html을 수정한다.

.toggle {
	border: 2px solid black;
    width: 64px;
    height: 64px;
    border-radius: 32px;
    box-sizing: border-box;
}
.toggle.active {
	background: yellow;
}

index.css

<html>
  <head>
    <link rel="stylesheet" type="text/css" href="index.css" />
  </head>
  <body>
    <div class="toggle"></div>
    <hr />
    <h1>0</h1>
    <button id="increase">+1</button>
    <button id="decrease">-1</button>
    <script src="./index.js"></script>
  </body>
</html>

index.html

그러면 다음과 같은 UI를 구성할 수 있다.

[ DOM 레퍼런스 만들기 ]

 이번 프로젝트에서는 UI를 관리할 때 별도의 라이브러리를 사용하지 않으므로 DOM을 직접 수정해야 한다. 

다음과 같이 index.js 파일을 수정하여 자바스크립트 파일 상단에 수정할 DOM 노드를 가리키는 값을 미리 선언한다.

const divToggle = document.querySelector('.toggle');
const counter = document.querySelector('h1');
const btnIncrease = document.querySelector('#increase');
const btnDecrease = document.querySelector('#decrease');

index.js

 

[ 액션 타입과 액션 생성 함수 정의 ]

먼저, 액션에 이름을 정의해 주자. 액션 이름은 문자열 형태로 지정하며, 주로 대문자로 작성한다. 이름이 중복되면 의도하지 않은 결과가 발생할 위험이 있으므로 액션 이름은 고유해야 한다.

(...)
const TOGGLE_SWITCH = 'TOGGLE_SWITCH';
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';

index.js

그 다음은 액션 이름을 사용하여 액션 생성 함수를 작성한다. 앞에서 설명했듯, 액션 객체는 type 값을 반드시 포함해야 한다.

(...)
const toggleSwitch = () => ({ type: TOGGLE_SWITCH});
const increase = difference => ({ type: INCREASE, difference });
const decrease = () => ({ type: DECREASE });

index.js

 

[ 초깃값 설정 ]

초깃값의 형태는 숫자, 문자열, 객체 등 작성자 마음대로 정할 수 있다.

(...)
const initialState = {
	toggle: false,
    counter: 0
};

index.js

 

[ 리듀서 함수 정의 ]

리듀서는 변화를 일으키는 함수로, state action 값을 파라미터로 받는다.

(...)
//state가 undefined일 때는 initialState를 기본값으로 사용
function reducer(state=initialState, action){
    switch (action.type){
        case TOGGLE_SWITCH:
            return{
                ...state, //불변성 유지
                toggle: !state.toggle
            };
        case INCREASE:
            return{
                ...state,
                counter: state.counter + action.difference
            };
        case DECREASE:
            return{
                ...state,
                counter: state.counter - 1
            };
        default:
            return state;
    }
}

index.js

  • state 값이 undefined인 경우 initialState를 기본값으로 설정하기 (함수의 파라미터 부분)
  • 스프레드 연산자(...)를 사용하여 상태의 불변성을 유지하면서 데이터에 변화 일으키기

 리듀서에서는 상태의 불변성을 유지하면서 데이터에 변화를 일으켜주어야 한다. 이 과정에서 스프레드 연산자를 사용하면 편리하다. 그러나 객체의 구조가 복잡해지면 업데이트가 번거로워지고 가독성이 나빠지므로 리덕스의 상태는 최대한 깊지 않은 구조로 진행하는 것이 좋다.

 객체의 구조가 복잡해지거나 배열을 함께 다루는 경우 immer 라이브러리를 사용하면 조금 더 쉽게 리듀서 작성이 가능하다.

 

 

[ 스토어 만들기 ]

스토어를 만들 때는 createStore 함수를 사용한다. 이 함수를 사용하려면 리덕스에서 해당 함수를 import해야 하고, 함수의 파라미터에는 리듀서 함수를 넣어주어야 한다.

import { createStore } from 'redux';
(...)
const store = createStore(reducer);

index.js

이번에는 스토어 내장 함수들을 사용해보자.

 

[ render 함수 만들기 ]

render 함수는 상태가 업데이트 될 때마다 호출되며, 리액트의 render 함수와 다르게 이미 html을 사용하여 만들어진 UI의 속성을 상태에 따라 변경해준다. 

(...)

const store = createStore(reducer);

const render = () => {
  const state = store.getState(); // 현재 상태를 불러오기
  // 토글 처리
  if (state.toggle) {
    divToggle.classList.add('active');
  } else {
    divToggle.classList.remove('active');
  }
  // 카운터 처리
  counter.innerText = state.counter;
};

render();

index.js

  • store.getStore(): 상태 관리 라이브러리(redux)에서 현재 애플리케이션 상태를 가져온다. state는 애플리케이션의 모든 상태를 포함하는 객체가 된다.
  • state.toggle 값에 따라 특정 DOM 요소(divToggle)에 active 클래스를 추가하거나 제거한다. 
  • 현재 상태의 counter 값을 DOM 요소 counter의 텍스트로 설정하여 업데이트한다.

 

[ 구독하기 ]

이번에는 스토어의 상태가 바뀔 때마다 render 함수가 호출되도록 할 것이다. 이 작업은 스토어의 내장 함수 subscribe를 사용하여 수행한다.

subscribe 함수의 파라미터로는 함수 형태의 값을 전달해주며, 전달된 함수는 액션이 발생하여 상태가 업데이트 될 때마다 호출된다.

const listener = () => {
  console.log('상태가 업데이트됨');  
}
const unsubscribe = store.subscribe(listener);

unsubscribe(); // 추후 구독을 비활성화할 때 함수를 호출

 상태가 업데이트 될 때마다 render 함수를 호출하도록 하려면 아래와 같이 코드를 작성한다.

(...)
const render = () => {
  (...)
render();
store.subscribe(render);

index.js

 

[ 액션 발생시키기 ]

액션을 발생시키는 것을 디스패치라고 한다. 디스패치를 할 때는 스토어의 내장 함수 dispatch를 사용하며, 파라미터는 액션 객체를 넣어주면 된다. 아래와 같이 각 DOM 요소에 클릭 이벤트를 설정하고 이벤트 함수 내부에서 dispatch 함수를 사용하여 액션을 스토어에 전달해보자.

(...)
divToggle.onclick = () => {
  store.dispatch(toggleSwitch());
};
btnIncrease.onclick = () => {
  store.dispatch(increase(1));
};
btnDecrease.onclick = () => {
  store.dispatch(decrease());
};

index.js

나의 경우, 이 과정에서 아래와 같은 오류가 생겼다.

  • ' Browser scripts cannot have imports or exports ' : 이 경우는 Parcel이 기본적으로 브라우저 환경에서 실행되는 스크립트를 처리할 때 import와 export를 허용하지 않는 경우 발생한다. 이를 해결하기 위해 HTML 파일의 <script> 태그에 type="module"을 추가한다.
  • ' Cannot find module 'redux' ' : 이 경우는 Parcel이 redux 모듈을 찾을 수 없다는 의미로, 'npm install redux'로 redux를 설치해야 한다. 

위 과정을 통해 문제를 해결할 수 있었다. 아래와 같이 원과 버튼을 클릭할 때 상태 변화가 잘 일어나고 있다.

3. 리덕스의 세 가지 규칙

① 단일 스토어 : 하나의 애플리케이션 안에는 하나의 스토어가 들어있다. 

② 읽기 전용 상태 : 리덕스 상태는 읽기 전용이므로 상태를 업데이트 할 때 기존의 객체는 건드리지 않고 새로운 객체를 생성한다.

③ 리듀서는 순수한 함수 : 변화를 일으키는 리듀서 함수는 순수한 함수여야 한다. 순수한 함수는 다음 조건을 만족한다.

  • 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받는다.
  • 파라미터 외의 값에는 의존할 수 없다.
  • 이전 상태는 절대 건드리지 않고, 변화를 준 새로운 상태 객체를 만들어 반환한다. (랜덤, Date 함수, 네트워크 요청 등의 작업은 리듀서 함수 바깥에서 처리해야 한다.)
  • 똑같은 파라미터로 호출된 리듀서 함수는 항상 같은 결과 값을 반환한다.

 


 

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

 이번 장에서는 리덕스를 사용해 애플리케이션의 상태를 관리하는 방법을 배운다. 소규모 프로젝트의 경우 컴포넌트가 가진 state를 사용하는 것만으로도 충분하지만, 프로젝트의 규모가 커질수록 상태 관리가 번거로워질 수 있다.

 

 리액트 애플리케이션에서 리덕스를 사용했을 때의 장점

  • 상태 업데이트에 관한 로직을 모듈로 따로 분리하여 컴포넌트 파일과 별개로 관리할 수 있기 때문에 코드를 유지보수하는 데 있어 도움이 된다.
  • 여러 컴포넌트에서 동일한 상태를 공유할 때 매우 유용하며, 실제 업데이트가 필요한 컴포넌트만 리렌더링 되게끔 쉽게 최적화할 수도 있다.

 

1. 작업 환경 설정

 리액트 프로젝트를 생성한 뒤, 프로젝트에 리덕스를 적용해 보자.

우선, create-react-app를 사용해 새로운 리액트 프로젝트를 생성한다. 다음으로 아래 yarn 명령어를 사용하여 리덕스와 react-redux 라이브러리를 설치한다.

$ yarn create react-app react-redux-tutorial

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

 

 

 또한 Prettier를 적용하고 싶은 경우, 디렉터리에 다음과 같이 .prettierrc 파일을 작성할 수 있다.

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

 

2. UI 준비하기

 리액트 프로젝트에서 리덕스를 사용할 때 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것이다.

  • 프레젠테이셔널 컴포넌트: 주로 상태 관리가 이루어지지 않고, 그저 props를 받아와서 화면에 UI를 보여주기만 하는 컴포넌트
  • 컨테이너 컴포넌트: 프레젠테이셔널 컴포넌트와 달리 리덕스와 연동되어 있어 리덕스로부터 상태를 받아 오기도 하고 리덕스 스토어에 액션을 디스패치하기도 하는 컴포넌트이다. 

 이러한 패턴은 리덕스를 사용하는 데 필수 사항은 아니지만, 이 패턴을 사용하면 코드의 재사용성이 높아지고 관심사의 분리가 이루어져 UI를 작성할 때 좀 더 집중할 수 있게 된다. 이번 장의 프로젝트에서는 이 패턴을 사용하여 코드를 작성한다.

 

 UI에 관련된 프레젠테이셔널 컴포넌트는 src/components 경로에 저장하고, 리덕스와 연동된 컨테이너 컴포넌트는 src/containers 컴포넌트에 작성한다.

 


[카운터 컴포넌트 만들기]

 

 숫자를 더하고 뺄 수 있는카운터 컴포넌트를 만들어 보자. components 디렉터리를 생성한 뒤, 디렉터리에 Counter 컴포넌트를 Counter.js로 다음과 같이 작성한다.

import React from 'react';

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 컴포넌트에서 다음과 같이 렌더링한다.

import React from 'react';
import Counter from './components/Counter';

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

export default App;

 

 렌더링이 완료되면 yarn start 명령어를 입력하여 개발 서버를 실행해 보자. 다음과 같이 카운터 컴포넌트가 반영된 카운터 UI가 나타날 것이다!

카운터 UI

 

 

[할 일 목록 컴포넌트 만들기]

 

 해야할 일을 추가, 체크, 삭제할 수 있는 할 일 목록 컴포넌트를 만들어 보자. 앞서 생성했던 components 디렉터리에 Todos 컴포넌트를 다음과 같이 작성한다. Todos 컴포넌트와 TodoItem 컴포넌트를 파일 두 개로 분리해도 되고, 아래 코드처럼 파일 하나에 두 컴포넌트를 선언할 수도 있다. 해당 컴포넌트들이 받아오는 props는 이후 사용하도록 한다.

import React from 'react';

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;

 

 컴포넌트 작성이 끝나면 App 컴포넌트에서 앞서 작성한 카운터 아래에 다음과 같이 코드를 입력한 뒤 렌더링한다. hr태그를 사용해 사이에 구분선을 그려주자!

import React from 'react';
import Counter from './components/Counter';
import Todos from './components/Todos';

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

export default App;

 

 렌더링이 완료되면 동일하게 yarn start 명령어를 입력하여 개발 서버를 실행하자! 다음과 같이 컴포넌트가 반영된 할일 목록 UI가 나타나게 된다.

할 일 목록 UI

 


3. 리덕스 관련 코드 작성하기

 

 이제 프로젝트에 리덕스를 사용해 보자! 리덕스를 사용할 때는 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해서 준비해야 한다. 이 코드들을 각각 다른 파일에 작성할 수도 있고, 기능별로 묶어 파일 하나에 작성하는 방법도 있다.

 

 

[패턴 구조의 종류와 장단점]

 

 리덕스 관련 코드에 대한 디렉터리 구조는 정해진 방법이 없기 때문에 마음대로 작성해도 무방하다. 하지만 아래 두 가지 방법이 주로 사용되며, 곧 소개할 Ducks 패턴을 사용하여 진행하려고 한다!

 

 아래는 리덕스 공식 문서에서 사용되는 가장 일반적이고 기본적인 구조로, actions, constants, reducers라는 세 개의 디렉터리를 만들고 그 안에 기능별로 파일을 하나씩 만드는 방식이다. 이렇게 하면 코드를 종류에 따라 다른 파일에 작성할 수 있어 편리하다는 장점이 있지만, 새로운 액션을 만들 때마다 아래 세 종류의 파일을 모두 수정해야 하기 때문에 불편한 점도 있다.

 

actions

counter.js

todos.js

constants

ActionTypes.js

reducers

counter.js

todos.js

 

 다른 구조는 다음과 같이 액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 다 작성하는 방식이다. 이러한 방식을 Ducks 패턴이라고 부르며, 앞서 설명한 구조의 대안이 되는 패턴이다. 하나에 다 몰아서 작성하면 위의 일반적인 방식과 달리 새로운 액션을 만들 때마다 여러 파일을 일일히 수정해야 할 필요가 없어 핀리하다.

 

modules

counter.js

todos.js

 

 

 

[counter 모듈 작성하기]

 

 Ducks 패턴을 사용하여 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드모듈이라고 한다. 먼저 이 counter 모듈을 작성하기 위해 다음의 단계를 순서대로 진행하자.

 

1.  modules 디렉터리를 생성한 뒤 counter.js 파일을 다음과 같이 작성한다.

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

 

 

2.  액션 타입을 정의한다. 액션 타입은 대문자로 정의하고, 문자열의 내용은 '모듈 이름/액션 이름'과 같은 형태로 작성한다. 문자열 안에 모듈 이름을 넣어서 나중에 프로젝트의 규모가 커졌을 때 액션의 이름이 서로 충돌하지 않도록 한다.

 

 

3.  액션 생성 함수를 다음과 같이 작성하여 생성한다. 이때 앞부분의 export라는 키워드를 주목해야 하는데, 추후 이 함수를 다른 함수에서 불러와 사용할 수 있게 하는 역할을 한다.

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

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

 

 

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

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

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;

 

 

 

[todos 모듈 만들기]

 

1.  modules 디렉터리를 생성한 뒤 todos.js 파일을 생성한다.

 

2.  counter 모듈을 작성할 때와 마찬가지로 먼저 액션 타입을 정의한다.

const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 인풋 값을 변경함
const INSERT = 'todos/INSERT'; // 새로운 todo를 등록함
const TOGGLE = 'todos/TOGGLE'; // todo를 체크/체크 해제함
const REMOVE = 'todos/REMOVE'; // todo를 제거함

 

3.  액션 생성 함수를 다음과 같이 생성한다. counter 모듈을 작성할 때와는 달리, 이번에는 액션 생성 함수에 파라미터가 필요하다. 전달받은 파라미터는 액션 객체 안에 추가 필드로 들어가게 된다. 아래의 insert 함수는 액션 객체를 만들 때 파라미터 외에 사전에 이미 선언되어 있는 id라는 값에도 의존한다. 이 id 값은 각 todo 객체가 들고 있게 될 고유값이며, 함수가 호출될 때마다 id 값에 1씩 더해지게 된다.

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씩 더해집니다.
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
});

 

 

4.  모듈의 초기 상태와 리듀서 함수를 작성한다. todos 모듈의 경우, 객체에 한 개 이상의 값이 들어가기 때문에 불변성을 유지해줘야 해서 업데이트 방식이 조금 까다로워진다. 이를 spread 연산자를 활용하여 다음과 같이 작성할 수 있다. 또한, 배열 내장함수를 사용해 배열 변화를 구현할 수 있다. 

(...)
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;

 

 

 

[루트 리듀서 만들기]

 

 추후 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;

 

 참고로, 파일명을 index.js로 설정하면 추후 파일을 불러올 때 다음과 같이 디렉터리 이름까지만 입력하여 불러올 수 있다!

import rootReducer from './modules';


4. 리액트 애플리케이션에 리덕스 적용하기

 리액트 앱에 리덕스를 적용하기 위해 다음 단계들을 순서대로 진행하자!

 

1. 우선 스토어를 생성한다. src 폴더의 index.js에서 아래 코드를 작성해 스토어를 생성하자.

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import rootReducer from './modules';

const store = createStore(rootReducer);

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.unregister();

 

 

2. 다음으로, Provider 컴포넌트를 사용프로젝트에 리덕스를 적용한다. 리액트 컴포넌트에서 스토어를 사용할 수 있도록 App 컴포넌트를 react-redux에서 제공하는 Provider 컴포넌트로 감싼다. 이 컴포넌트를 사용할 땐 store를 props로 전달해야 한다.

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import rootReducer from './modules';

const store = createStore(rootReducer);

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

serviceWorker.unregister();

 

 

3. Redux DevTools를 설치한다. Redux DevTools는 리덕스 개발자 도구이고, 크롬 확장프로그램으로 설치해서 사용할 수 있다. 웹스토어에서 Redux DevTools를 검색해서 설치하자.

 

 

4. yarn을 사용해 redux-devtools-extension을 설치하자. 이 패키지를 설치하면 코드를 훨씬 깔끔하게 관리할 수 있다.

 

5. 설치가 끝나면 다음과 같이 적용까지 마친다! 이제 브라우저에서 크롬 개발자도구를 실행하면 Redux의 개발자도구가 나타난다.

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import rootReducer from './modules';

const store = createStore(rootReducer, composeWithDevTools());

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

serviceWorker.unregister();

 


5. 컨테이너 컴포넌트 만들기 

리덕스 스토어와 연동된 컴포넌트를 컨테이너 컴포넌트라고 한다. 

 

 

[ CounterContainer 만들기 ]

import React from 'react';
import Counter from '../components/Counter';

const CounterContainer = () => {
  return <Counter />;
};

export default CounterContainer;

src/containers/CounterContainer.js

 

이 컴포넌트를 리덕스와 연동하려면 react-redux에서 제공하는 connect 함수를 사용해야 하고 사용법은 아래와 같다.

여기서 mapStateToProps는 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수이고, mapDispatchToProps는 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수이다. 이렇게 connect 함수를 호출하고 나면 또 다른 함수를 반환한다. 반환된 함수에 컴포넌트를 파라미터로 넣어 주면 리덕스와 연동된 컴포넌트가 만들어 진다. 

 

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

const CounterContainer = ({ number, increase, decrease }) => {
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};

const mapStateToProps = state => ({
  number: state.counter.number,
});
const mapDispatchToProps = dispatch => ({
  increase: () => {
    dispatch(increase());
  },
  decrease: () => {
    dispatch(decrease());
  },
});
export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(CounterContainer);

src/containers/CounterContainer.js

connect 함수를 사용하고 액션 생성 함수를 불러와서 액션 객체를 만들고 디스패치해 주었다. 앞서 설치한  리덕스 개발자 도구를 통하면 잘 작동하는 것을 확인해 볼 수 있다. 

 

CounterContainer.js

 

 

 

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

const CounterContainer = ({ number, increase, decrease }) => {
  return (
    <Counter number={number} onIncrease={increase} onDecrease={decrease} />
  );
};

export default connect(
  state => ({
    number: state.counter.number,
  }),
  {
    increase,
    decrease,
  },
)(CounterContainer);

src/containers/CounterContainer.js

 

mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성 함수로 이루어진 객체 형태로 넣어 주면 더 간결하게 사용할 수 있다. 두 번째 파라미터를 아예 객체 형태로 넣어 주면 connect 함수가 내부적으로 bindActionCreators 작업을 대신해 준다. 

 

 

 

 

[ TodosContainer 만들기]

 

connect 함수를 사용하고, mapDispatchToProps를 짧고 간단하게 쓰는 방법을 적용해서 코드를 작성했다. 

import React from 'react';
import { connect } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/todos';
import Todos from '../components/Todos';

const TodosContainer = ({
  input,
  todos,
  changeInput,
  insert,
  toggle,
  remove,
}) => {
  return (
    <Todos
      input={input}
      todos={todos}
      onChangeInput={changeInput}
      onInsert={insert}
      onToggle={toggle}
      onRemove={remove}
    />
  );
};

export default connect(
  // 비구조화 할당을 통해 todos를 분리하여
  // state.todos.input 대신 todos.input을 사용
  ({ todos }) => ({
    input: todos.input,
    todos: todos.todos,
  }),
  {
    changeInput,
    insert,
    toggle,
    remove,
  },
)(TodosContainer);

src/containers/TodosContainer.js

 

 

 

그 다음 Todos 컴포넌트에서 받아온 props를 사용하도록 코드를 수정한다. 

import React from 'react';

const TodoItem = ({ todo, onToggle, onRemove }) => {
  return (
    <div>
      <input
        type="checkbox"
        onClick={() => onToggle(todo.id)}
        checked={todo.done}
        readOnly={true}
      />
      <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
        {todo.text}
      </span>
      <button onClick={() => onRemove(todo.id)}>삭제</button>
    </div>
  );
};

const Todos = ({
  input, // 인풋에 입력되는 텍스트
  todos, // 할 일 목록이 들어 있는 객체
  onChangeInput,
  onInsert,
  onToggle,
  onRemove,
}) => {
  const onSubmit = e => {
    e.preventDefault();
    onInsert(input);
    onChangeInput(''); // 등록 후 인풋 초기화
  };
  const onChange = e => onChangeInput(e.target.value);
  return (
    <div>
      <form onSubmit={onSubmit}>
        <input value={input} onChange={onChange} />
        <button type="submit">등록</button>
      </form>
      <div>
        {todos.map(todo => (
          <TodoItem
            todo={todo}
            key={todo.id}
            onToggle={onToggle}
            onRemove={onRemove}
          />
        ))}
      </div>
    </div>
  );
};

export default Todos;

src/components/Todos.js

 

마지막으로 App 컴포넌트에서 컨테이너 컴포넌트를 렌더링 하도록 수정하고 결과를 확인한다. 

import React from 'react';
import CounterContainer from './containers/CounterContainer';
import TodosContainer from './containers/TodosContainer';

const App = () => {
  return (
    <div>
      <CounterContainer />
      <hr />
      <TodosContainer />
    </div>
  );
};

export default App;

src/App.js

 

TodosContainer.js

 

 

6. 리덕스 더 편하게 사용하기

 

[ redux-actions ]

 

redux-actions를 사용하면 액션 생성 함수를 더 짧은 코드로 작성할 수 있고 리듀서를 작성할 때도 switch/case문이 아닌 handleActions라는 함수를 사용하여 각 액션마다 업데이트 함수를 설정하는 형식으로 작성해 줄 수 있다.

$ yarn add redux-actions

 

✅ createAction()

import { createAction } from 'redux-actions';

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

(...)

src/modules/counter.js

createAction은 액션 생성 함수로, 이 함수를 사용하면 매번 객체를 직접 만들어 줄 필요 없이 더욱 간단하게 액션 생성 함수를 선언할 수 있다.

 


✅ handleAction()

import { createAction, handleActions } from 'redux-actions';

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

const initialState = {
  number: 0,
};

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

export default counter;

src/modules/counter.js

함수의 첫 번째 파라미터에는 각 액션에 대한 업데이트 함수를 넣어 주고, 두 번째 파라미터에는 초기 상태를 넣어 준다.

 

 

 

[ immer ]

 

리듀서에서 상태를 업데이트할 때는 불변성을 지켜야 하기 때문에 앞에서는 spread 연산자(…)와 배열의 내장 함수를 활용했다. 그러나 모듈의 상태가 복잡해질수록 불변성을 지키기가 까다로워진다. 따라서 모듈의 상태를 설계할 때는 객체의 깊이가 너무 깊어지지 않도록 주의해야 한다.

현재 프로젝트에 immer 설치하기

$ yarn add immer


객체의 구조가 복잡해지거나 객체로 이루어진 배열을 다룰 경우, immer를 사용하면 훨씬 편리하게 상태를 관리할 수 있다.

 

 

 

7. Hooks를 사용하여 컨테이너 컴포넌트 만들기

 

리덕스 스토어와 연동된 컨테이너 컴포넌트를 만들 때 connect 함수를 사용하는 대신 react-redux에서 제공하는 Hooks를 사용할 수도 있다.

 

 

✅ useSelector로 상태 조회하기

useSelector Hook을 사용하면 connect 함수를 사용하지 않고도 리덕스의 상태를 조회할 수 있다. useSelector의 사용법은 다음과 같다.

const 결과 = useSelector(상태 선택 함수);

 

여기서 상태 선택 함수는 mapStateToProps와 형태가 똑같다.

 

 

 

✅useDispatch를 사용하여 액션 디스패치하기

이 Hook은 컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해준다. 컨테이너 컴포넌트에서 액션을 디스패치해야 한다면 이 Hook을 사용하면 된다.

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

 

 

 

✅useStore를 사용하여 리덕스 스토어 사용하기

useStore Hooks를 사용하면 컴포넌트 내부에서 리덕스 스토어 객체를 직접 사용할 수 있다. 사용법은 다음과 같다.

const store = useStore();
store.dispatch({ type: 'SAMPLE_ACTION '});
store.getState();

 

useStore는 컴포넌트에서 정말 어쩌다가 스토어에 직접 접근해야 하는 상황에만 사용해야 한다.

 

 

 

 

 

[ connect 함수와의 주요 차이점 ]


컨테이너 컴포넌트를 만들 때 connect 함수를 사용해도 되고, useSelector와 useDispatch를 사용해도 된다. 리덕스 관련 Hook이 있다고 해서 기존 connect 함수가 사라지는 것은 아니므로, 더 편한 것을 사용하면 된다.

하지만 Hooks를 사용하여 컨테이너 컴포넌트를 만들 때 잘 알아 두어야 할 차이점이 있다. connect 함수를 사용하여 컨테이너 컴포넌트를 만들었을 경우, 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 자동으로 방지되어 성능이 최적화된다.

반면 useSelector를 사용하여 리덕스 상태를 조회했을 때는 이 최적화 작업이 자동으로 이루어지지 않으므로, 성능 최적화를 위해서는 React.memo를 컨테이너 컴포넌트에 사용해야 한다.

 

 

 

 

 



Quiz

  1. 액션을 만들어서 발생시키면 리듀서는 (_____)와 전달받은 (_____)를 파라미터로 받아온다.
  2. 랜덤 값 생성, Date 함수 사용, 네트워크 요청 등은 리듀서 함수 (__)에서 처리해야 한다.
  3. 액션을 발생시키는 것은 (_____)이며, 파라미터로 (_____)를 받는다.
  4. 프레젠테이셔널 컴포넌트와 달리 리덕스와 연동되어 있어 리덕스로부터 상태를 받아오기도 하고, 리덕스 스토어에 액션을 디스패치하기도 하는 것은 (___________)이다.
  5. (_______)은 기존의 리덕스 구조의 대안으로 만들어진 패턴이며, 일반적인 방식과 달리 새로운 액션을 만들 때마다 여러 파일을 일일히 수정해야 할 필요가 없기 때문에 편리하다.
  6. 루트 리듀서를 만드는 이유는 스토어에서 리듀서를 하나만 사용해야 하기 때문이다. (O/X)
  7. _______ 함수를 사용하면 부모 컴포넌트가 리렌더링 될 때 해당 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 자동으로 방지되어 성능이 최적화 된다.

 

 

**코드 작성 문제**

1. 구독하기 기능의 예시 코드를 완성하여라

const listener = () => {
  console.log('상태가 업데이트됨');  
}
const unsubscribe = (____________________); //빈칸에 코드 추가

unsubscribe();

 

2. 아래의 counter 모듈의 리듀서 함수 부분을  switch/case문이 아닌 handleActions() 함수를 사용하여 각 액션마다 업데이트 함수를 설정하는 형식으로 작성해라

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;
  }
}

 

 

 


빈칸 답 ) 1. 현재 상태, 액션 객체   2. 밖   3. 디스패치, 액션 객체   4. 컨테이너 컴포넌트   5. Ducks 패턴   6. O   7.connect 

코드 문제 ) 1. store.subscribe(listener) 

2.

더보기

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


출처:  김민준, 『리액트를 다루는 기술』, 길(2023), https://thebook.io/080203/0530/
Corner React.js 2
Editor: Encho, Borybop, ahyohyo

728x90

관련글 더보기