01.13 React Redux(리덕스)

React.js

01.13. React Redux

리액트로 프로젝트에서는 최상위 컴포넌트에서 상태 관리 로직이 너무 많아져 코드가 길어지고 가독성이 떨어진다. 

불필요한 리렌더링을 방지하기 위해서 하위 컴포넌트에서 shouldComponentUpdate를 구현하여 방지한다 하더라도 프로젝트가 복잡해질 경우 여러 컴포넌트를 거쳐 porps를 전달하는 것은 비효율적이다. 

작업을 진행하면서 헷갈릴 수 있고, 실제로 컴포넌트 자기 자신은 필요하지 않은데 하위 컴포넌트 때문에 필요한 props 개수가 많아지기 때문이다. 추가로 형제 컴포넌트에서 불필요한 렌더링이 일어나기 때문에 이 역시 상황에 따라 방지를 해주어야한다.

이러한 문제점들은 리덕스라는 상태 관리 도구를 사용하면 매끄럽게 해결할 수 있다.

01.13.1. Redux 개념

상태를 더 효율적으로 관리하는데 사용하는 상태 관리 라이브러리이다.
리덕스는 상태 관리의 로직을 컴포넌트 밖에서 처리하는 것이다. 리덕스를 사용하면 스토어(store)라는 객체 내부에 상태를 담게 된다.
리덕스 적용 구조
리덕스를 사용하면 스토어에서 모든 상태 관리가 일어난다. 상태에 어떤 변화를 일으켜야 할 때는 액션(action)을 스토어에 전달한다. 

액션은 객체 형태로 되어 있으며, 상태를 변화시킬 때 이 객체를 참조하여 변화를 일으킨다. 액션을 전달하는 과정을 디스패치(dispatch)라고 한다.

스토어가 액션을 받으면 리듀서(reducer)가 전달받은 액션을 기반으로 상태를 어떻게 변경시켜야할지 정한다. 액션을 처리하면 새 상태를 스토어에 저장한다.

스토어 안에 있는 상태가 바뀌면 스토어를 구독하고 있는 컴포넌트에 바로 전달한다. 부모 컴포넌트로 props를 전달하는 작업은 생략하며, 리덕스에 연결하는 함수를 사용하여 컴포넌트를 스토어에 구독시킨다.

  • 스토어 : 애플리케이션의 상태 값들을 내장하고 있다.
  • 액션 : 상태 변화를 일으킬 때 참조하는 객체이다.
  • 디스패치 : 액션을 스토어에 전달하는 것을 의미한다.
  • 리듀서 : 상태를 변화시키는 로직이 있는 함수이다.
  • 구독 : 스토어 값이 필요한 컴포넌트는 스토어를 구독한다.
※ 리덕스는 리액트에서 사용하려고 만든 상태 관리 라이브러리지만, 리액트에 의존하지는 않는다.

01.13.1.1. 액션과 액션 생성 함수

액션(action)은 스토어에서 상태 변화를 일으킬 때 참조하는 객체이다.
이 객체는 type 값을 반드시 가지고 있어야한다.

{
     type : 'REQUIRED'
    ,optional1 : 'not required'
    ,optional2 : {
         id: 1
        ,text:'anything'
    }
}
액션 객체 안에서 type 값은 필수이고, 나머지는 있어도 되고 없어도 되며 형식도 자유이다.
액션을 새로 만들 때마다 직접 객체를 만든다면 액션 형식을 모두 알고 있어야 하므로 불편하다. 그래서 액션을 만들어 주는 함수를 사용한다. 이를 액션 생성 함수(action creator)라고 한다.

액션 생성 함수를 정의하려면 액션 타입을 상수 값으로 정의해주어야한다.
const INC = 'INCREMENT';
const DEC = 'DECREMENT';
액션 생성 함수를 만든다.
const incFnc = () => ({
    type: INC
});

const decFnc = () => ({
    type: DEC
});
파라미터를 추가할 경우에는
const incFnc = (diff) => ({
     type: INC
    ,diff: diff
})

const decFnc = (diff) => ({
     type: DEC
    ,diff: diff
})
위의 코드처럼 파라미터를 전달하면 된다.

01.13.1.2. 리듀서

리듀서는 상태에 변화를 일으키는 함수이다.
리듀서는 파라미터를 두 개 받는다. 첫번째 파라미터는 현재 상태이고, 두번째 파라미터는 액션 객체이다.

함수 내부에서는 switch 문을 사용하여 action.type에 따라 새로운 상태를 만들어서 반환해야한다.
리듀서가 초기에 사용할 초기 상태 initialState 값부터 먼저 설정해야 리듀서를 만들 수 있다.
const initialState = {
    num : 0
};
초기 상태를 설정 후 리듀서 함수를 생성한다.

function counter(state = initialState, action) {
    switch(action.type) {
        case INC:
            return { number:state.num + action.diff };
        case DEC:
            return {number:state.num - action.diff };
        default:
            return state;
    }
}
※ counter 함수의 첫번째 파라미터 state = initialState 는 ES6 문법으로, state 값이 undefined 라면 initialState를 기본 값으로 사용한다는 의미이다.

함수 내부에서 action.type에 따라 현재 상태와 액션 객체를 참조하여 새 객체를 만들어주었다.
지금 number 값만 설정했는데 다른 값이 있을 경우에는 아래와 같이 한다.
const initialState = {
     num: 1
    ,foo: 'bar'
    ,bar: 'foo'
};

function counter(state = initialState, action) {
    switch(action.type) {
        case INC:
            return Object.assign({}, state, {
                num: state.num + action.diff
            });
        case DEC:
            return Object.assign({}, state, {
                num: state.num - action.diff
            });
        default:
            return state;
    }
}
리덕스에서 상태를 업데이트할 때는 컴포넌트의 state를 다룰 때처럼 값을 직접 수정하면 안된다. 새로운 객체를 만든 후 그 안에 상태를 정의해야한다. Object.assign 함수를 실행하면 파라미터로 전달된 객체들을 순서대로 합쳐준다. ES6의 전개 연산자(...) 를 사용하면 더욱 깔끔하게 코드를 입력할 수 있다.
const initialState = {
     num: 1
    ,foo: 'bar'
    ,bar: 'foo'
};

function counter(state = initialState, action) {
    switch(action.type) {
        case INC:
            return {
                 ...state
                ,num: state.num + action.diff
            };
        case DEC:
            return {
                 ...state
                ,num: state.num - action.diff
            };
        default:
            return state;
    }
}
리덕스를 사용하면 리듀서 함수를 직접 실행하는 일은 없다. 이 함수를 실행하는 작업은 리덕스가 처리한다.

01.13.1.3. 스토어

액션과 리듀서가 준비되면 리덕스 스토어를 만들 수 있다.

import {createStore} from 'redux';

const store = createStore(counter);
 스토어를 생성할 때는 createStore 함수를 사용한다. 파라미터로는 리듀서 함수가 들어가고, 두번째 파라미터를 설정하면 해당 값을 스토어의 기본 값으로 사용한다. 이 파라미터를 생략하면 리듀서 초기값을 스토어 기본 값으로 사용한다.

01.13.1.4. 구독

리액트 컴포넌트에서 리덕스 스토어를 구독(subscribe)하는 작업은 react-redux의 connect 함수가 대신한다. 리덕스의 내장 함수 subscribe을 직접적으로 사용할 일은 별로 없다.

01.13.1.5. dispatch

스토어에 액션을 넣을 때는 store.dispatch 함수를 사용한다.

store.dispatch(incFnc(1));
그러면 액션이 디스패치될 때마다 구독할 때 등록했던 함수를 실행할 것이다.

01.13.2. 리덕스의 규칙

01.13.2.1. 스토어는 단 한 개

스토어는 언제나 단 한 개이다. 스토어를 여러개 생성하여 상태를 관리하면 안된다. 그 대신 리듀서를 여러개만들어 관리할 수 있다.

01.13.2.2 state는 읽기 전용

리덕스의 상태(state) 값은 읽기 전용으로 이 값을 절대로 직접 수정하면 안된다. 그렇게 하면 리덕스의 구독 함수를 제대로 실행하지 않거나 컴포넌트의 리렌더링이 되지 않을 수 있기 때문이다. Object.assign을 사용한다고 해서 이전에 사용하던 객체들이 메모리에 누적되지 않는다. 상태 레퍼런스가 사라지면 자동으로 메모리 관리를 한다. 또 nested된 객체가 있을 때 (여러 괄호로 감싼) 그 내부의 깊은 값까지 복사하는 것이 아니라, 객체 내부의 키 레퍼런스만 복사하므로 객체가 복잡하다고 해서 성능이 악화되지는 않는다.

01.13.2.3. 변화는 순수 함수로 구성

모든 변화는 순수 함수로 구성해야한다. 여기에서 함수란 리듀서 함수를 의미한다. 순수 함수에서 결과 값을 출력할 때는 파라미터 값에만 의존해야 하며, 같은 파라미터는 언제나 같은 결과를 출력해야한다.
예를 들어 리듀서 함수 내부에서 외부 네트워크와 데이터베이스에 직접 접근하면 안된다. 요청이 실패할 수 있고, 외부 서버의 반환 값이 변할 수 있기 때문이다.

01.13.3. 정리

리덕스는 스토어에 상태 정보를 가진 객체를 넣어 두고, 액션이 디스패치되었을 때 리듀서 함수를 이용하여 상태를 변화시키는 것이 주요 역할이다. 그리고 상태가 변화될 때마다 스토어에 구독된 함수를 실행시킨다.

댓글

이 블로그의 인기 게시물

01.11 리액트 컴포넌트 CSS 적용 ( CSS Module, Sass, styled-components )

01.9 React 컴포넌트 라이프사이클

01.7 React ref (DOM에 이름 달기)