본문 바로가기
전역상태관리/Redux

상태 관리 라이브러리 : Redux

by whale in milktea 2023. 2. 24.

"상태관리 라이브러리의 핵심은! 여러 상태를 한 개의 창고에 넣어서 보관하고 사용하는 것!"

상태 관리 Toolkit Redux

Redux는 Toolkit이라는 이름에서 알 수 있듯, 상태를 관리하기 위한 여러 컴포넌트 및 메서드가 포함된 라이브러리를 의미한다.

 

출처 : https://yahooeng.tumblr.com/post/152078809581/refactoring-components-for-redux-performance

기본적으로 Redux는 트리 형식으로 props를 하위 컴포넌트로 전달하는 방식이며, state를 통해 상태를 업데이트하며 관리하기 때문에, 중간에 컴포넌트에서 변경된 state가 쓸데없이 다른 컴포넌트에 전달되며 불필요한 자원소모 및 렌더링을 필연적으로 유발한다.(props drilling)

 

이를 관리하기 위해 중간에 위치한 컴포넌트의 변경사항을 즉시 최상위 컴포넌트에 적용하기 위한 Toolkit인 Redux, Recoil 등이 필요해졌다.


Redux 데이터 흐름

출처 : GeektoGeeks

Redux는 Action → Dispatch → Reducer → Store로 이어지는 단방향적 데이터 흐름을 갖는 것이 특징이다.

각각의 지점은 다음과 같은 역할을 맡는다.

1. Action : states의 상태 변화를 정의한 "객체" ===> 컴포넌트 내에서 일어난 상호작용의 결과를 객체로 Action에 담는다.
2. Dispatch : Action 객체를 Reducer에 전달한다.
3. Reducer : 최상위 컴포넌트로 전달하기 전 새로운 states를 생성하는 "순수함수"이다.
4. Store : 상태가 저장된 Redux의 저장소이다.
이렇게 Store로 전달된 states는 useSelector()를 통해 최상위 컴포넌트에서 접근할 수 있게 된다.

 

Redux 3대 원칙

1. Single Source of Truth(단일 진실의 근원) : 애플리케이션의 모든 상태는 하나의 저장소 안에 하나의 객체 트리 구조로 저장된다.
2. State is Read-Only(상태는 읽기 전용이다) : 상태를 직접 변경하지 않고, 액션 객체를 통해 변경할 수 있도록 하는 것
3. Changes are Made with Pure Functions(순수 함수를 이용해 변경) : 리듀서는 반드시 순수함수로만 작성되어야 한다.

Redux의 3대 원칙은 개발자가 애플리케이션의 복잡성을 줄이고, 요지보수와 확장성을 향상시킬 수 있도록 돕는다.

하나의 저장소 원칙은 저장소를 기반으로 한 집중된 상태관리를 가능케 하고, 상태는 읽기 전용이기 때문에 불필요한 SideEffect를 방지할 수 있으며, 순수함수를 활용함으로 예측가능하고 부작용이 없는 코드를 작성할 수 있도록 한다.

 

Redux 기본문법

1. Redux 설치

# 이미 설치된 react 프로젝트에 redux toolkit 추가하기
npm install redux
npm install react-redux


# 많은 경우, 상태관리 toolkit을 사용하기에 번들러로 react 프로젝트를 생성할 때 함께 생성할 수 있다.
create-react-app project-name --template redux
create-react-app project-name --template redux-typescript

2. STORE 만들기

Redux의 핵심 개념은 하나의 저장소를 만들어 해당 저장소에 state를 보관한 뒤 이를 사용하는 것이다.

따라서, 코드에 저장소를 만들어 주어야 한다.

실제 이 저장소는 최상위 컴포넌트에 위치한 Provider 컴포넌트에 위치한다 ==> 해당 컴포넌트 예시는 맨 마지막에 정리하면서 작성

// app.js
// rootReducer는 여러 개의 reducer를 편하게 활용하기 위한 코드이다.
import { createStore } from 'redux';

const store = createStore(rootReducer);

 

3. Action 정의하기

Action은 어떤 액션을 취할 것인지 정의해놓은 객체다. 즉, 어떤 행동을 취할 것인지 의미상으로 명확하게 객체로 만든 것이라 할 수 있다.

Action에서 가장 중요하게 파악해야 하는 속성은, typepayload이다.

 

type 은 이벤트 유형을 식별하는 문자열로, redux에서 이벤트에 대한 reducer 함수를 실행할 때, type 속성을 기준으로 파악하게 된다.

payload 는 이벤트와 함께 전달되는 데이터를 의미한다. payload는 이벤트 객체와 함께 reducer 함수에서 처리된다.

 

다음은, Todo를 추가하는 action 객체의 예시이다.

// 액션 타입을 정의
const ADD_TODO = 'ADD_TODO'

// 액션 생성자 함수
function addTodoAction(todo) {
  return {
    type: ADD_TODO,
    payload: todo
  };
}

4. Reducer 함수 정의하기

기본적으로 reducer 함수는 인자로 2가지를 받는다. 현재 상태를 의미하는 state, 변경하고자 하는 상태인 action이다.

reducer 함수가 수행되는 순서는 다음과 같다.

 

1. 처음 앱이 실행되면, store의 상태는 초기 상태값으로 설정된다.

2. 이 때, state의 상태는 initalState이다.

3. reducer 함수는 "state"에 새로운 값을 할당하는 것이 아닌 새로운 상태의 객체를 반환하도록 짜야 한다.

4. 이러한 특징은 redux의 3대 원칙은 "Changes are Made with Pure Functions"에 입각한다.

 

다음은 위의 todo action 상태를 활용한 reducer 함수의 예시다.

// 리듀서 함수
function todoReducer(state = initialState, action) { // 인자로 초기 상태와 액션을 받는다.
  switch (action.type) { // 정의된 액션이 여러 개일 경우, 문자열로 구성된 type은 식별자로 기능한다.
    case ADD_TODO:
      return {
        ...state,
        todos: [...state.todos, action.payload] // 기존의 상태에 새로운 값을 추가!
      };
    default:
      return state;
  }
}

해당 코드에서 보면 알 수 있듯, Action 객체에서 type은 일종의 식별자와 같은 역할을 하고 payload에는 실제 값이 담기게 된다.

 

5. Dispatch 실행하기

Dispatch는 redux의 흐름에서 봤을 때 action이라는 객체를 reducer에 전달하는 역할을 한다.

redux에서 dispatch를 사용해야만 하는 이유는, state는 읽기 전용이라는 원칙 때문이다.

 

읽기 전용인 state를 별도의 처리과정 없이 바로 reducer로 전달하게 되면, 이러한 원칙을 어기게 될 가능성이 높다.

dispatch는 action 객체를 새롭게 생성하고 reducer를 호출하여 새롭게 생성된 action을 전달한다.

 

일반적으로 dispatch는 store에 정의되어 있는 dispatch 메서드를 호출한 뒤 action의 객체의 인자로 새로운 상태를 전달하면 된다.

store.dispatch(addTodoAction({ id: 1, text: 'Buy groceries' }));

물론 이렇게 사용해도 store에 변화된 상태를 전달할 수 있지만, react 프로젝트 내에서 다시 호출하는데 여러 코드를 복잡하게 작성해야 한다. 하지만, react는 이에 대응하는 hooks를 제공하고 있다. 해당 hooks는 대표적으로 2가지가 있다.

1. useSelector() : store에 있는 state를 불러온다.
2. useDispatch() : dispatch를 호출하여 action을 reducer로 전달한다.

이를 활용한 예시 코드는 다음과 같다.

// react-redux에 명시된 hooks이다!
import { useDispatch, useSelector } from 'react-redux'; 
// 다른 파일에서 가져오는 것을 가정! (사실, action은 actions라는 별도의 파일에 보관되는 것이 보편적이라 한다..
import { addTodoAction } from '../actions/todoActions'; 

function TodoList() {

  const todos = useSelector(state => state.todos);
	// useSelector는 기본적으로 store의 전체 값을 가져온다
    // 따라서 특별한 어떤 state의 값에 적응하려면 todos와 같은 필드값을 넣어줘야 한다
  const dispatch = useDispatch();

  const addTodo = (todo) => {
    dispatch(addTodoAction(todo));
  }

6. Provider 설정

* 원래 Provider는 redux를 가져올 때, 한꺼번에 설정하게 된다.

Provider는 모든 컴포넌트의 최상위 컴포넌트를 감싸고, 해당 컴포넌트의 props로 store를 전달하게 되면 하위 컴포넌트에서 props로 redux에서 상태를 전달받아 사용할 수 있게 된다.

import React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import todoReducer from './reducers/todoReducer';
import TodoList from './components/TodoList';

const store = createStore(todoReducer);

function App() {
  return (
    <Provider store={store}>
      <TodoList />
    </Provider>
  );
}

export default App;

 

'전역상태관리 > Redux' 카테고리의 다른 글

Flux와 MVC (단방향 데이터 흐름) (1)  (0) 2023.04.10
React 상태 관리  (0) 2023.02.23