상세 컨텐츠

본문 제목

[React.js 1팀] 16장. 리덕스 라이브러리 이해하기

24-25/React.js 1

by mingging17 2025. 1. 31. 10:00

본문

728x90

 

1. 개념 미리 정리하기

리덕스는 가장 많이 사용하는 리액트 상태 관리 라이브러리로, 컴포넌트의 상태 업데이트 관련 로직을 다른 파일로 분리시켜 효율적인 파일 관리를 가능하게 한다. 리덕스를 사용하면 컴포넌트끼리 똑같은 상태를 공유할 때도 여러 컴포넌트를 거치지 않고 손쉽게 값을 전달하거나 업데이트 할 수 있다.

 

Context API를 사용하는 경우에도 전역 상태 관리가 가능하지만, 리덕스를 사용하면 코드의 유지 보수성 향상으로 작업 효율이 증가하고, 상태를 더 체계적으로 관리할 수 있기 때문에 규모가 큰 프로젝트에 리덕스를 주로 사용한다. 또한, 리덕스는 비동기 작업을 훨씬 효율적으로 관리할 수 있는 미들웨어 등의 개발자 도구도 지원한다.

 

리덕스 실습에 들어가기 전, 사용할 개념을 미리 정리해보자.

 

1-(1). 액션 (action)

상태에 변화가 필요하면 액션이 발생한다. 이는 하나의 객체로 표현되는데, 액션 객체의 형식은 아래와 같다.

{
	type: 'TOGGLE_VALUE'
}

 

 

이 type 필드는 액션 객체의 이름 역할로, 액션 객체에서 필수 요소이다. 그리고 type 외의 값은 상태 업데이트 시에 참고할 값으로, 작성자에 의해 결정된다.

 

1-(2). 액션 생성 함수 (action creator)

 

액션 생성 함수액션 객체를 만들어 주는 함수이다. 변화가 필요할 때마다 액션 객체를 생성하는데, 이때 매번 액션 객체를 작성자가 직접 작성하기는 번거로울 뿐더러 실수가 발생할 수 있으므로, 액션 생성 함수를 사용하는 것이다.

function addTodo(data) {
return {
    type: 'ADD_TODO',
  data
};
}

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

 

1-(3). 리듀서 (reducer)

 

리듀서변화를 일으키는 함수다. 액션이 발생하면 리듀서가 현재 상태와 전달받은 액션 객체를 파라피터로 받아온다. 그 후 두 값을 기반으로 새로운 상태를 생성해 반환한다.

const initialState = {
  counter: 1
};

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

 

1-(4). 스토어 (store)

 

프로젝트를 리덕스에 적용하기 위해 스토어를 만든다. 한 개의 프로젝트는 단 하나의 스토어만 가질 수 있고, 스토어 안에는 현재 애플리케이션의 상태와 리듀서가 들어간다. 또한, 몇 가지의 중요한 내장 함수를 가진다.

 

1-(5). 디스패치 (dispatch)

디스패치는 앞서 언급한 스토어의 내장 함수 중 하나로, 액션을 발생시키는 주체이다. 이 함수는 dispatch(action)과 같이 액션 객체를 파라미터로 넣은 형태로 호출한다. 이 함수가 호출되면 스토어가 리듀서 함수를 실행시켜 새로운 상태를 생성한다.

 

1-(6). 구독 (subscribe)

구독도 스토어의 내장 함수이다. 이는 subscribe 함수 안에 리스너 함수를 파라미터로 넣어주어, 액션이 디스패치되어 상태가 업데이트될 때마다 이 리스너 함수가 호출된다.

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

unsubscribe(); // 구독 비활성화

 

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

 

리덕스는 리액트에 사용하기 위해 만들어졌지만, 꼭 리액트에만 사용 가능한 것은 아니다. 다른 UI 라이브러리나 프레임 워크와도 함께 사용 가능하다. 또한, 라이브러리나 프레임워크가 없는 바닐라 자바스트립트와 사용도 가능하다.

 

이번에는 리덕스의 기능과 작동 원리를 이해하기 위해 바닐라 자바스크립트 환경에서 리덕스를 사용해보자.

 

2-(1). Parcel로 프로젝트 만들기

우리는 프로젝트를 구성하기 위해 Parcel이라는 도구를 사용할 것이다. 이를 사용하면 쉽고 빠른 웹 애플리케이션 프로젝트를 구성이 가능하다.

  1. parcel-bundler 설치
    $ yarn global add parcel-bundler

    yarn global이 잘 설치되지 않는다면 npm install -g parcel-bundler 명령어 사용

  2. 프로젝트 디렉터리 생성 후, package.json 파일 생성
    $ mkdir vanilla-redux # 프로젝트 디렉터리 생성
    $ cd vanilla-redux # 디렉터리로 이동
    $ yarn init -y # package.json 파일 생성

  3.  에디터로 해당 디렉터리를 열어 index.html과 index.js 파일을 생성
    <index.html>
    <html>
      <body>
        <div>바닐라 자바스크립트</div>
        <script src="./index.js"></script>
      </body>
    </html>

    <index.js>
    console.log('hello parcel');
  4. 개발용 서버 실행
    $ parcel index.html

    파일을 저장할 때마다 자동으로 새로고침된다. 
  5. 리덕스 모듈 설치
    $ yarn add redux

2-(2). 간단한 UI 구성하기

index.css 파일과 index.html 파일을 다음과 같이 수정하여 UI를 구성하자.

<index.css>

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

.toggle.active {
  background: yellow;
}

 

<index.html>

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

 

 

 

2-(3). DOM 레퍼런스 만들기

이번 프로젝트에서 UI를 구성할 때 별도의 라이브러리를 사용하지 않으므로 DOM을 직접 수정해야 한다. 기존 코드를 지운 후, 아래와 같이 자바스크립트 파일 상단에 수정할 DOM 노드를 지칭하는 값을 미리 선언해준다.

 

<index.js>

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

 

2-(4). 액션 타입과 액션 생성 함수 정의

액션에 이름을 정의해주자. 액션 이름(타입)은 문자열 형태로, 주로 대문자를 사용하며 고유한 이름을 가져야 한다. 이름이 중복된다면 의도하지 않은 결과가 발생할 수 있다.

 

<index.js>

const TOGGLE_SWITCH = 'TOGGLE_SWITCH';
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';

 

그 다음 액션 생성 함수를 작성해준다. 액션 객체에서 type 필드는 필수적이며, 그 외의 필드는 상태를 업데이트 할 때 참고하고 싶은 값을 넣으면 된다.

 

<index.js>

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

 

2-(5). 초깃값 설정

이 프로젝트에서 사용할 초깃값을 정의하자. 초깃값은 숫자일 수도 있고, 문자열일 수도 있고, 객체일 수도 있다.

 

<index.js>

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

 

2-(6). 리듀서 함수 정의

리듀서 함수를 생성하자. 함수의 파라미터로 state와 action 값을 넣자.

 

<index.js>

// state가 undefined일 때는 initialState를 기본값으로 사용
function reducer(state = initialState, action) {
  // action.type에 따라 다른 작업을 처리함
  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;
  }
}

 

리듀서 함수가 처음 호출될 때는 state 값이 undefined이다. 해당 값이 undefined인 경우, initialState를 기본값으로 성정하기 위해 함수의 파라미터에 기본값이 설정되어 있다. 리듀서는 상태의 불변성을 유지하면서 데이터에 변화를 주어야하기 때문에 spread 연산자(...)를 사용하면 편리하다. 하지만 객체의 구조가 복잡해지면 이 방법은 번거롭고 가독성을 해치므로 필요에 따라 선택하자. 객체의 구조가 복잡해지거나 배열을 사용하는 경우에는 immer 라이브러리를 추천한다.

 

2-(7). 스토어 만들기

스토어를 만들 때는 createStore 함수를 사용한다. 코드 상단에 import하여 리덕스로부터 해당 함수를 불러와 사용한다. 함수의 파라미터에는 reducer 함수를 넣어준다.

 

<index.js>

import { createStore } from 'redux';

(...)

const store = createStore(reducer);

 

 

2-(8). render 함수 만들기

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

 

<index.js>

(...)

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

 

2-(9). 구독하기

스토어의 상태가 바뀔 때마다 방금 만든 render 함수가 호출되도록 해줄 것이다. 이를 위해 스토어의 내장 함수인 subcribe를 사용하자. 

 

다음 예시와 같이 subscribe 함수의 파라미터로는 함수 형태의 값을 전달한다. 이렇게 전달된 함수는 액션이 발생하여 상태가 업데이트될 때마다 호출된다.

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

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

 

이번 프로젝트에서는 subcribe 함수를 직접 사용하나, 컴포넌트에서 리덕스 상태를 조회하는 과정에서 react-redux라는 라이브러리가 이 작업을 대신하기 때문에 이 방식은 잘 사용하지 않는다.

 

이제 상태가 업데이트될 때마다 render 함수를 호출하도록 수정하자.

<index.js>

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

render();
store.subscribe(render);

 

 

2-(10). 액션 발생시키기

스토어의 내장 함수 dispatch를 사용하여 액션을 발생시키자. 이때, 파라미터에는 액션 객체를 넣는다.

 

다음과 같이 DOM 요소들에 클릭 이벤트를 설정하고, 이벤트 함수 내부에서 dispatch를 사용해 액션을 스토어에게 전달하는 코드를 작성하자.

 

<index.js>

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

 

모두 제대로 작동한다면 화면에 나타나는 원과 하단의 증감 버튼을 눌렀을 때 상태 변화를 확인할 수 있다.

 

3. 리덕스의 세 가지 규칙

이번에는 프로젝트에서 리덕스를 사용할 때 지켜야할 세 가지 규칙을 알아보자.

 

3-(1). 단일 스토어

하나의 애플리케이션 안에는 하나의 스토어가 들어있다. 여러 개의 스토어를 사용하는 것이 불가능한 것은 아니다. 특정한 업데이트가 빈번하게 일어나거나 애플리케이션의 특정 부분을 완전히 분리시킬 때에는 여러 개의 스토어를 만들 수 있지만, 이런 경우 상태 관리가 복잡해지므로 권장하진 않는다.

 

3-(2). 읽기 전용 상태

리덕스 상태는 읽기 전용이다. 상태를 업데이트할 때 기존의 객체를 건드리지 않고 새로운 객체를 생성해야 한다. 리덕스에서 불변성을 유지해야 하는 이유는 내부적으로 데이터 변경을 감지하기 위해 얕은 비교(shallow equality) 검사를 진행하기 때문이다. 

 

3-(3). 리듀서는 순수한 함수

리듀서 함수는 순수한 함수여야 한다.

 

<순수한 함수의 조건>

  1. 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받는다.
  2. 이전 상태는 건드리지 않고, 변화를 준 새로운 상태 객체를 만들어서 반환한다.
  3. 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환한다.
  4. 파라미터 외의 값에는 의존하지 않는다.

리듀서 함수 내부에서 랜덤 값을 만들거나, Date 함수를 사용하여 현재 시간을 가져오거나, 네트워크 요청을 한다면, 파라미터가 같아도 다른 결과를 만들어 낼 수 있기 때문에 사용하면 안 된다. 따라서 해당 작업은 리듀서 함수 바깥에서 처리해 주어야 한다. 액션을 만드는 과정이나 리덕스 미들웨어에서 처리한다. 또한, 주로 네트워크 요청과 같은 비동기 작업은 미들웨어를 통해 관리한다.

 


출처 :  김민준, 『리액트를 다루는 기술』, 길벗(2019).

Corner React.js 1

Editor: Mingging

 

728x90

관련글 더보기