본문 바로가기

javascript/react

[React] redux에 대한 개념

Redux란?

리액트의 컴포넌트들은 상태 기반으로 동작한다. 특정 컴포넌트 내부의 상태가 어떤 이벤트에 의해 변경되면, 해당 상태가 갱신되면서 컴포넌트 함수가 다시 실행된다. 평가 결과 기존 virtual DOM 상에 표현된 컴포넌트의 모습과 다른 부분이 있다면, 해당 부분만을 다시 렌더링하고 실제 DOM상에 반영한다.

컴포넌트의 3가지 요소

컴포넌트의 구성요소는 크게 3가지로 나뉠 수 있다.

  • State : 컴포넌트 내부에서 관리되는 상태. 컴포넌트 함수의 재실행을 야기할 수 있다.
  • View : 현재 상태에 기반하여 선언적으로 표현되는 UI 요소. 보통 Action이 여기에 등록되어 있다.
  • Action : 유저의 입력(input)에 반응하여 나타나는 이벤트, 혹은 상태 갱신을 위한 트리거.

 세가지 구성요소는 서로 긴밀한 관계를 가지고 있으며, 각 요소들은 위와 같은 사이클을 가진다. 즉 State의 변화는 View에 영향을 주고, 사용자는 View와 반응하여 Action의 발생을 야기하며, Action은 State이 갱신되도록 돕는다. 해당 사이클 과정에서 데이터의 흐름이 단방향적으로 나타나므로 one-way data flow라고 하기도 한다.

 하나의 컴포넌트 내부에서의 데이터 흐름은 매우 간단하기 때문에 컴포넌트를 위와 같이 구성하더라도 큰 문제가 없다. 그러나 컴포넌트의 수가 증가함에 따라 State 참조 관계는 복잡해지고, 전반적인 App의 관리가 어려워진다. 

상태의 참조가 다수의 컴포넌트에서 나타날 수 있다.

 리액트에서 전통적으로 사용하는 컴포넌트간 데이터 교환은 프로퍼티 또는 콜백을 통해 이루어진다. 따라서 유저의 정보 같이 프로젝트 전반적으로 사용하는 데이터가 하나의 컴포넌트에 저장되어 있는 경우, 전통적인 방식을 이용하면 해당 정보를 사용하는 모든 컴포넌트에 대해 유저의 정보를 프로퍼티 형식으로 받거나, 콜백 형태로 넘겨줘야만 한다. 이런 과정은 귀찮을 뿐더러, 프로젝트의 규모가 증가함에 따라 데이터의 흐름을 파악하는데 큰 어려움을 가져온다.

 그렇다면, 위에서 나타난 문제점의 근본적인 원인이 무엇일까? 프로젝트 전반적으로 사용하는 데이터가 특정 컴포넌트에 종속되어 있으며, 이를 직접적으로 사용할 방법이 없다는 것이다. 따라서 이 문제를 해결하기 위해서는 컴포넌트에 종속된 상태를 추출하여 컴포넌트와 관계없는 공간에 저장하고, 해당 상태에 직접 접근할 수 있어야 한다. 이 글의 주제가 되는 Redux는 이러한 역할을 수행하기 위해 탄생했다.

 Redux는 상태 관리와 관련된 개념을 컴포넌트로부터 분리하여 View와 State 사이에 독립성을 유지할 수 있도록 설계된 상태 관리 도구로, 애플리케이션 전반적으로 사용되는 State을 Store라고 불리는 중앙 집중적인 단일 저장소에 저장하고, 미리 지정된 Action을 dispatch하여 상태 전이를 촉발한다. 저장되어 있는 State는 React처럼 (Immutability)을 가지며, Store 생성시 전달된 Reducer 함수에 정의된 로직 및 기존 상태에 기반하여 새로운 상태 객체를 생성한다.

 

Redux 공식 사이트에서 설명하는 Redux의 작동 방식. 유저의 Action을 Dispatch로 전달하면, 등록된 Reducer 함수가 작동한다.

 

Redux와 React Context

React 자체적으로도 상태의 전역적 사용을 위한 API인 Context가 존재한다. 실제 Redux와 Context + useReducer 조합의 사용법도 유사하기 때문에 두가지 방법에 혼동이 생길 수 있다. 그러나 두 방법에는 몇가지 차이점이 있다.

 Redux는 전역 상태 관리를 위한 라이브러리다. 그러나 Context는 상태 관리와 관계가 없다. 생각해보면 Context을 통해 전역적으로 사용되는 상태에 대한 조작은 해당 Context에 대한 Provider 함수를 만들고 내부에서 useState, useReducer 등의 hook을 이용하여 수행했다. Context API가 한 일은 Context.Provider에 value 프로퍼티로 함수 내부에서 관리되는 상태를 전달하고, 해당 Context가 사용되는 컴포넌트를 연결하기는 했지만 직접적으로 상태를 관리하지는 않았다. 즉, Context 자체만으로는 상태의 관리가 불가능하다. 해당 API를 통해 전역 변수를 각 컴포넌트들에게 직접 전달할 수는 있겠지만, 실제 상태 관리 로직은 Context 자체와는 전혀 관계가 없었다.

 또한, 현재 수준에서는 몇가지 문제가 있다고 한다. Redux와는 달리 Context는 자주 변하는 값에 대해 최적화 되어있지 않으며, Context 내부의 특정 상태가 변하면 해당 상태와 관계가 없다고 하더라도 Context를 등록한 모든 컴포넌트를 평가한다고 한다.

 Redux는 독립된 라이브러리로 js 환경이라면 언제나 쓸 수 있지만, Context는 React에 종속되어 있다는 특징도 있다. 다만 Context는 React에 기본적으로 포함되어 있어, React를 사용할 때 Context 기반으로 구현하면 다른 라이브러리에 대한 의존도가 낮아진다는 점이 장점으로 작용하기도 한다.

요약하면, Context + useState/useReducer ... hook 의 조합을 통해 Redux을 모방할 수 있기는 하나, Context 자체는 상태 관리 라이브러리가 아니며 아직까지 Redux를 완전히 대체할 수 있는 수준은 아니다.

https://blog.isquaredsoftware.com/2021/01/context-redux-differences/

Redux 사용법

https://redux.js.org/api/api-reference ( API 문서 주소 )

Redux의 대략적인 사용 방법은 다음과 같다. 개인적으로 자동완성 때문에 타입스크립트 환경 사용을 추천한다.

  1. Reducer 함수를 만든다. Redux는 초기값을 넘기지 않으므로 Reducer에서 초기값을 설정해야 한다.
  2. createStore에 Reducer을 실인자로 전달하여 store 객체를 만든다.
  3. 구독할 함수가 있다면 store.subscribe() 함수를 통해 구독한다.
  4. store.dispatch 함수에 action을 실인자로 제공하여 2에서 전달된 Reducer 함수를 실행한다.
  5. store.getState() 함수를 이용하여 저장된 상태에 접근하여 사용한다.
import redux from 'redux';

type counterActionType = { type: "increment" } | { type: "decrement" };
//Action으로 전달할 타입

interface counter { counter: number };
//State로 전달될 값

const counterReducer = (state: counter = { counter: 0 }, action: counterActionType): counter => {
    switch (action.type) {
        case 'increment':
            return {
                counter: state.counter + 1
            };
        case 'decrement':
            return {
                counter: state.counter - 1
            }
    }// 설정된 Action에 대해 나누고, 새로운 객체 반환.
    // throw new Error("Cannnot reach here!");
    // 맨 처음에는 action으로 undefined가 전달되므로, 이 점에 유의
}

const store = redux.createStore(counterReducer);
// 리듀서 함수 -> 새로운 상태에 대한 snapshot
// 초기 상태를 만들어 둬야 함
// 동일 input에 동일 output이 나와야 한다.
// side effect가 개입하면 안됨 (fetch 등이 없어야 함.)

const counterSubscriber = () => {
    const latestState = store.getState();
    console.log(latestState);
};
// store에 등록될 구독 함수

store.subscribe(counterSubscriber);
// 함수를 구독. state 변할 때마다 실행된다.

let count = 0;

const set = function f() {
    setTimeout(() => {
        if (count < 5) {
            store.dispatch({ type: "increment" });
            count++;
            f();
        }
    }, 1000);
};
set();

// 결과
//{ counter: 1 }
//{ counter: 2 }
//{ counter: 3 }
//{ counter: 4 }
//{ counter: 5 }

 

Reducer function

function Reducer(prevState, action) : typeof prevState {
    //some codes ...
    return state as typeof prevState;
}

 

Reducer 함수는 store에서 관리하던 상태 prevState 및 dispatch를 통해 전달받는 action 을 파라미터로 가진다. 두 파라미터에는 정말 어떤 값이든 다 들어갈 수 있기는 하나, 유지 관리를 이유로 보통 둘 다 객체에 값을 래핑하여 사용한다. 이때 prevState의 상태와 새로운 상태 state이 동일한 타입을 가져야 이후 Reducer 함수를 다시 사용하더라도 문제가 발생하지 않으므로 둘은 동일 타입을 가져야 한다.

프로그램이 맨 처음 시작하면 Reducer 함수가 동작한다. 이때 prevState에는 디폴트 파라미터가 오게 되고, action에는 리액트 자체적으로 설정한 랜덤한 값이 해당 위치를 채우게 된다. 이때 action은 절대 실행되지 않는 것이 목적이므로, 내가 설정한 action 이외의 상황에서는 에러를 발생하도록 프로그램을 구현하면 맨 처음에 반드시 에러가 발생한다. 이런 동작에 주의하여 프로그램을 구현하자.

맨 위 @@redux/INIT~ 은 랜덤한 문자열

createStore

const store = redux.createStore(Reducer);

 

위에서 언급한 Reducer 함수를 실인자로 받아들이고, store 객체를 반환한다. 이 과정에서 반환된 store 객체를 통해 State와 관련된 몇몇 작업을 수행할 수 있다.

 

subscriber function

function subscriber(): void {
    //any code you want
    //but do not return anything
}

store에 저장된 상태 정보가 변경될 때마다 실행될 함수이다. 파라미터 및 반환값이 없다는 조건을 만족하면 별다른 제약조건이 없는 편이다. 값이 바뀔때마다 로그를 남긴다든지 하는 방식으로 사용할 수 있을 것 같다.

 

store.subscribe

const unsubscribe = store.subscribe(subscriber);

실인자로 입력받은 함수를 구독한다. 구독된 함수는 상태가 변경될 때마다 실행된다. 현재 함수는 구독 취소 함수를 반환하는데, unsubscribe 함수를 실행하면 등록했던 subscriber이 더 이상 실행되지 않는다.

 

store.dispatch

store.dispatch(action);

실인자로 전달받은 action을 수행한다. action의 타입은 Reducer 함수의 로직 설정에 따라 달라지는데, 원칙적으로는 어떤 값을 전달하더라도 전혀 관계 없다.

 현재 함수의 action 및 이전에 store에 저장되어 있던 prevState을 Reducer function에 전달하여 새로운 state을 만든다.

 

결론 및 요약

 Redux는 어플리케이션에 대한 전역 상태 관리를 위한 라이브러리로, 프로젝트 전반의 상태를 하나의 단일 저장공간인 Store에 저장하고, dispatch를 통해 특정 action을 Reducer 함수에게 넘겨 상태 변화를 촉발할 수 있다. 

 

'javascript > react' 카테고리의 다른 글

[React] custom hook  (0) 2021.12.29
[React] react-redux  (0) 2021.12.24
[React] memo & useCallback  (0) 2021.12.21
[React] useImperativeHandle을 타입스크립트와 사용하는 방법  (0) 2021.12.19
[React] forwardRef & useImperativeHandle  (0) 2021.12.19