Redux Concepts

김동현·2022년 3월 14일
0

Redux

목록 보기
2/5
post-thumbnail

Design Pattern

소프트웨어 설계시 자주 발생하는 문제에 대한 모법답안을 디자인 패턴이라고 합니다. 이런 디자인 패턴 중에는 일부 코드를 해결하기 위한 비교적 작은 범위를 다루는 것들도 존재하며 애플리케이션 전체를 설계할 때 사용하는 큰 범위를 다루는 것도 존재합니다.

애플리케이션 전체를 다루기 위한 디자인 패턴들은 여러 작은 범위의 디자인 패턴들과 함께 사용해서 만들어지기에 복합패턴이라고도 부릅니다. 모든 복합 패턴의 근간이라고 부를 수 있는 패턴이 MVC 패턴입니다.

MVC

MVC 패턴은 Model, View, Controller 세 가지 구성요소로 나눠서 애플리케이션을 설계하는 패턴입니다.

구성요소들의 역할은 아래와 같습니다.

  1. Model : 데이터의 형태를 정의하고, 사용자의 입력을 받아 Controller에게 전달

  2. Controller : 사용자의 입력을 받아서 애플리케이션 내에서 어떻게 처리할지 판단 및 가공해서 Model 또는 View를 조작

  3. View : Model을 UI로 표현하고, 사용자의 입력을 받아 Controller에게 전달

예를 들어, 사용자가 View에 존재하는 add to cart 버튼을 클릭하면 이를 Contoller에게 전달하고 Controller는 Model에게 item을 cart에 추가하라고 전달합니다. Model은 cart에 item을 추가하고 Model이 View에게 cart에 item이 추가되었다고 알려주면 View는 그에 맞게 화면을 업데이트합니다.

하지만 모든 동작이 위와 같이 Model을 거치지 않을 수도 있습니다. View가 Controller에게 사용자 입력을 전달하고 Controller가 바로 View에게 업데이트되었다고 알려줄 수도 있습니다. 즉, View와 Controller가 직접적으로 소통할 수 있습니다.

MVC 패턴은 각 구성요소가 서로에게 접근할 수 있는 "양방향 통신"이 발생합니다.

MVC 패턴은 애플리케이션의 구성 요소를 역할에 따라 분리하고 있습니다. 그리고 모든 애플리케이션은 결국 본질적으로 데이터(Model)을 잘 조작하여 화면(View)에 보여주는 것이기 때문에 대부분 모든 디자인 패턴들의 근간이 MVC 패턴이 되었습니다.

하지만 애플리케이션 규모가 커지고 세분화되면서 MVC 패턴을 그대로 사용하기에는 어려움을 겪기도 합니다. 프론트엔드 내에서는 사용자와 수많은 상호작용이 발생하고, 데이터를 빈번하게 수정해야 했으며 서버로부터 받아오는 데이터와 클라이언트단에서 관리하는 데이터들을 모두 잘 관리해야 하는 복잡한 요구사항에 직면하게 됩니다.

MVC 패턴은 각 구성요소들끼리 양방향으로 통신하기 때문에 애플리케이션 규모가 커질 수록 동작 흐름을 분석하거나 예측할 수 없는 문제가 발생하게 됩니다.

위 그림처럼 애플리케이션의 규모가 커지면 많은 Model과 View들이 생기게 되면서 특정 View A에서 발생한 동작이 Model A를 수정하게 되고, Model A가 수정되었으니 이에 영향을 받는 View B가 수정되고, View B가 수정되니 Model B를 수정하게 되는 양방향성으로 인한 연쇄적인 변화가 발생하게 됩니다.

Flux

Flux 디자인 패턴의 핵심은 단방향성입니다. 앞서 MVC 패턴 문제의 원인인 양방향성으로 인한 연쇄적인 변화로 규정했기 때문에 Flux는 단방향으로 애플리케이션 변화의 흐름을 최대한 단순화하고 예측가능하게 하는데에 목표를 두었습니다.

Flux 패턴은 4가지 구성요소로 이루어져 있습니다. 그리고 각 구성요소들은 한가지 방향으로만 상호작용할 수 있습니다.

  1. Action : 어떤 변화를 발생시킬지 정의하는 type 프로퍼티변화에 필요한 데이터를 갖고 있는 단순한 객체입니다.

  2. Dispatcher : Action 객체를 받아 Store에 전달하는 역할을 합니다. Flux 패턴에서 가장 핵심이 되는 역할을 합니다. Action을 여러 개가 될 수 있지만 Dispatcher는 하나로 모든 Action을 전달받기 때문에 예측가능하게 만들어주는 가장 핵심이 됩니다.

  3. Store : 애플리케이션의 데이터를 저장하고, Dispatch가 전달한 Action에 따라 데이터를 수정합니다.

  4. View : Store에 저장된 데이터를 받아 UI로 표현하고, 사용자의 입력에 따라 Action을 생성합니다.

단방향성으로 인해 애플리케이션은 특정 순서에 따라서만 데이터와 UI가 변화하게 되었으며 개발자들은 애플리케이션에서 어떤 변화가 일어났는지 파악하기 쉬워졌으며, 나아가 애플리케이션의 동작을 예측하기도 쉬워졌습니다.

Redux

Redux는 Flux, CQRS, Event Sourcing의 개념을 사용해서 만든 라이브러리로 "JavaScript 앱을 위한 예측 가능한 상태 컨테이너"를 핵심 가치로 삼고 있습니다.

Redux는 모든 상태를 관리하는 컨테이너 역할을 하며 애플리케이션 내 구성요소들은 컨테이너에 접근해서 상태를 읽어올 수 있기에 자바스크립트 앱에서 전역 상태 관리를 수행하기 위해서 사용할 수 있습니다.

Redux는 Flux 패턴의 단방향성을 차용했기에 Redux내 발생하는 모든 상태 변화를 예측할 수 있습니다. 이런 특성으로 인해 프론트엔드에서 발생하는 복잡한 상태들의 변화를 관리하는데에 적합합니다.

Redux의 3가지 원칙

1. "Single source of truthy"

Redux 내의 모든 전역 상태는 하나의 객체 안에 트리구조로 저장되고, 이 객체를 Store라고 부릅니다.

모든 상태를 하나의 객체에 저장하기에 애플리케이션이 단순해지고 예측하기 쉬워집니다. 또한 하나의 객체 변화만을 추적하면 되기에 Undo, Redo 등의 기능을 구현하는데에도 쉽습니다.

2. "Sate is read-only"

Redux의 상태를 변화시키기 위한 유일한 방법은 "Action 객체를 Dispatch 통해 전달하는 것입니다". 그 외에 Store에 직접 접근해서 상태를 수정하는 등의 행위는 허용되지 않습니다.

Redux는 위와 같이 state를 불변하게 다루고, 변화시킬 수 있는 방법을 제약함으로서 안정성과 예측 가능성을 증대시킵니다.

모든 변화는 Dispatch를 거치게 되고 순서대로 수행되기에 여러곳에서 동시에 데이터를 수정하면서 발생하는 race condition 문제 등이 발생하지 않습니다.

또한 Action을 통해 변화의 의도, 목적을 표현합니다. Action은 단순한 객체로 이를 추적하거나 로깅, 저장하는 등의 동작을 수행하기에 용이함으로 디버깅을 손쉽게 할 수 있으며, 추후 테스트 코드를 작성하는데에도 용이합니다.

3. "Change are made with pure function"

Redux의 State 변화하는 유일한 방법은 Action 객체를 Dispath에게 전달하는 것입니다. 실질적으로 Action을 통해서 Store을 변경시키는 동작은 Reducer라고 불리는 순수함수를 통해서 수행됩니다.

Reducer는 이전 State 값과 Action 객체를 인자로 전달받아 새로운 State를 반환하는 순수함수입니다.

여기서 주목해야할 점은 새로운 State를 반환하는 점입니다. 즉, 기존 State 객체를 변경하는 것이 아니라 기존 State를 이용하여 새로운 State 객체를 만들어내는 방식으로 동작한다는 점입니다.

Redux의 Store가 하나이기에 이를 관리하는 Reducer 또한 하나여야 합니다. 하지만 각기 다른 관심사가 하나의 함수에 모두 들어가게 되면 유지보수측면에서 좋지 않기 때문에 애플리케이션이 커지면 여러개의 Reducer 함수(slice reducer)로 분리해서 코드를 작성한 다음에 하나의 Reducer(root reudcer)로 통합하는 방식을 활용합니다.

Redux의 구성요소

Redux는 크게 전역 상태를 보관하고 있는 "store 객체", 상태 저장소에 접근하여 상태를 변경하는 "reducer 함수", reducer에게 상태 변경을 지시하는 "dispatch 함수", 상태 변경하는데 필요한 정보를 갖고 있는 "action 객체" 개념이 존재합니다.

1. Store 객체

Redux는 하나의 애플리케이션을 위한 "하나의 중앙 데이터 저장소(Store)"를 갖습니다. 여기서 데이터는 상태를 의미합니다. 절대 하나 이상의 Store를 갖지 않으며 애플리케이션 내 존재하는 하나의 Store가 모든 상태를 관리합니다.

즉, Store 내부에 존재하는 상태들은 하나의 컴포넌트에 종속되지 않으며 상태 관리를 React가 아닌 Redux에서 할 수 있게 도와줍니다.

Store 내 저장된 상태값들은 컴포넌트가 직접적으로 접근하여 변경할 수 없습니다. Store 내 저장된 상태값을 변경하기 위해서는 reducer 함수를 사용해야 합니다. 즉, Store 내 상태값들은 reducer 함수만이 직접적으로 접근할 수 있으며 상태값을 변경할 수 있습니다.

store는 redux의 createStore 호출을 통해 생성되며, 호출할 때 인수로 reducer 함수를 전달해주어야 합니다. 실질적으로 상태를 관리하는 로직은 reducer 함수가 갖고 있기 때문에 store와 reducer를 서로 연결시켜주어야 합니다.

// store 생성시 인수로 상태를 관리하는 reducer 함수 전달
const store = redux.createStore(reducer);

store를 정리하자면 아래와 같습니다.

  • 하나의 애플리케이션에는 오직 "하나의 Store"만을 갖습니다.

  • Redux의 Store가 "애플리케이션 전역 상태"를 보관하고 관리합니다.

  • Store의 상태 변경은 오직 reducer 함수만을 통해 변경이 가능합니다. store 객체는 불변 객체로서 컴포넌트가 직접 store의 상태를 접근하여 변경할 수 없습니다.

2. reducer 함수

reducer 함수는 "store의 상태를 관리"하는 로직을 갖고 있습니다. 즉, 실제로 상태를 관리하는 로직을 갖고 있는 순수함수입니다.

reducer 함수는 우리가 직접 호출하는 것이 아니라 action 이라는 객체를 dispatch 함수의 인수로 전달하면 Redux가 reducer 함수를 실행하여 상태값을 변경합니다.

reducer 함수는 두 개의 인수를 Redux로부터 전달받아 실행합니다.

// reducer 함수는 기존 상태 객체와 action 객체를 전달받아 실행
// 반환값으로 작성된 객체로 상태 객체를 대체(replace)
cost reducer = (stateObj, action) => {
    ,,,
    return newStateObj;
}
  1. 첫 번째 인수로는 "기존 상태 객체"를 전달받습니다.

  2. 두 번째 인수로는 dispatch된 "action 객체"를 전달받습니다. 그리고 언제나 새로운 상태 객체를 반환해야 합니다.

상태값들을 갖고 있는 상태 객체는 reducer 함수의 반환값으로 작성한 상태 객체로 "대체(replace)"됩니다. 대체되기 때문에 상태 객체에 존재하는 모든 상태를 반환되는 상태 객체에 모두 작성해주어야 합니다. 반환되는 객체에 작성되지 않은 상태는 잃어버리게 됩니다.

이때 첫 번째 인수로 전달받는 기존 상태 객체에 대해서 "get 엑세스만 허용"되며 set 엑세스는 허용되지 않습니다. 즉, 기존 상태 객체에 대한 상태값 참조만 가능하며, 상태값 변경하기 위해선 변경된 상태값을 갖는 새로운 상태 객체를 반환값으로 작성해주어야 합니다.

상태값 변경 유무는 기존 상태 변경 함수처럼 상태 객체에 존재하는 이전 상태값과 reducer 함수가 반환한 상태 객체의 각 상태값들을 "단순 비교(=== 연산자)"를 통해 일치하지 않는 경우 상태가 변경되었다고 처리됩니다.

참고로 reducer 함수는 "순수 함수"여야 합니다. 순수함수란 언제나 동일한 입력을 넣으면 동일한 출력값을 반환하는 함수이며, 내부 로직에는 side-effect가 없는 함수를 의미합니다. 즉, reudcer 함수는 언제나 동일한 입력에 대해서 동일한 출력값을 반환해야 하며 Http 요청과 같은 side-effect도 없어야 합니다.


reducer 함수의 특징은 아래와 같습니다.

  • reudcer 함수는 Redux가 호출하는 것으로 우리는 action 객체를 dispatch 함수에 전달하면서 호출하면 Redux가 reducer 함수를 실행합니다.

  • reducer 함수는 Redux로부터 "이전 상태 객체""action 객체"를 인수로 전달받고, 새로운 상태 객체를 반환**해주어야 합니다.
    반환된 객체로 상태 객체가 대체되므로 반환한 객체에는 모든 프로퍼티(상태)를 작성해주저야 합니다.

  • reducer 함수가 전달받는 기존 상태 객체에 대해서는 get 엑세스만 허용됩니다.
    즉, 상태값 참조만 가능하며 상태값 변경을 위해서는 변경된 상태값을 갖는 새로운 상태 객체를 반환하면 기존 상태 객체를 반환된 상태 객체로 대체합니다.

  • reducer 함수가 반환하는 객체와 이전 상태 객체의 각 프로퍼티 값(상태값)을 서로 단순 비교(===연산자)하여 일치하지 않는 경우 상태가 변경된 것으로 처리된다.

  • reducer 함수는 "순수함수"로 작성해야 한다.

3. dispatch 함수

dispatch 함수는 store 내부 메서드인 dispatch 메서드에 action 객체를 전달하여 호출하면 Redux가 reducer 함수를 실행합니다.

// reducer 함수에게 action 객체 전달되면서 호출
store.dispatch(action);

4. action 객체

action 객체는 "상태를 업데이트하는데 필요한 정보"를 갖고 있는 객체입니다.

일반적으로 action 객체는 상태 변경에 대한 동작을 나타내는 type 프로퍼티를 갖고 있으며 이외 상태 변경에 필요한 정보(데이터)를 추가적인 프로퍼티로 갖고 있습니다.

type 프로퍼티의 값은 string 타입이며, 통상 domain/eventName의 형태를 따릅니다. domain 파트는 이 이벤트가 어떤 카테고리에 속하는지 표시하기 위함이고 eventName은 어떤 일이 발생했는지를 표현합니다.

type 프로퍼티는 필수이며, 그 외에 추가적인 데이터들은 통상적으로 payload 프로퍼티로 전달합니다.

우리는 action 객체를 dispath 함수의 인수로 전달하면서 호출하면 action 객체가 reducer 함수에게 전달되면서 Redux가 실행하고, 상태값을 변경합니다.

// action 객체의 type 프로퍼티에 상태 변경 동작의 식별자 작성
// 추가적인 정보는 추가적인 프로퍼티 작성
const action = { type: 'domain/eventName' , ,,, };

redux.dispatch(action);

5. action creator 함수

Action Creator는 Action을 생성하는 함수입니다. 매번 Action 객체를 작성하는 것보다는 Action Creator를 통해서 생성하는 것이 권장됩니다.

const addTodo = (todo) => {
    return {
        type: 'TODO/ADD_TODO',
        payload: todo
    };
};

6. subscription

subscription 함수는 "상태값이 변경된 이후에 호출"되는 함수입니다.

action 객체를 dispatch 함수에 전달하면서 호출하면 Redux가 reudcer 함수를 호출하고, reducer 함수의 반환값으로 작성된 객체로 상태 객체가 대체됩니다.

이때 이전 상태 객체의 각 프로퍼티 값과 대체된 상태 객체의 각 프로퍼티 값을 서로 단순 비교하여 상태값 변경 유무를 판단하는데 일치하지 않는 프로퍼티 값이 존재하는 경우 해당 상태값이 변경되었다고 판단하여 Redux가 subscription 함수를 실행합니다.

store 내부 메서드 중 subscribe이라는 메서드가 존재하는데 이 메서드에 인수로 전달한 함수가 바로 subscription 함수입니다.
store 내부 존재하는 상태값이 변경된 이후에 Redux가 자동으로 subscription 함수를 실행합니다.

// subsciption 함수는 상태 객체의 상태값 변경된 경우 호출되는 함수
redux.subscribe(subscription);

Redux 사용하기

Redux는 React에만 국한되어 사용되는 라이브러리가 아닙니다. 자바스크립트로 작성되는 모든 프로젝트에서 사용 가능한 라이브러리입니다. 지금은 React App이 아닌 순수 자바스크립트 프로젝트에서 Redux 라이브러리를 사용하여 Redux의 작동 방식에 대해서 알아보겠습니다.

1. redux 패키지 설치

먼저 redux 라이브러리를 설치하기 위해서 터미널에 아래 명령어를 입력합니다.

npm install redux

2. store 생성

먼저 애플리케이션 전역에서 사용될 상태를 갖는 store를 만들기 위해서는 가져온 redux 객체를 이용합니다. redux 객체에서 "createStore 메서드"를 호출하여 store을 생성합니다.

const redux = require('redux');

  // store 생성
const store = redux.createStore();

3. reducer 함수 정의

store 내부에 존재하는 상태는 reducer 함수가 결정합니다. reducer 함수의 반환값으로 상태 객체가 "대체"되기 때문입니다.

reducer 함수가 실행될 때 reducer 함수는 항상 두 개의 인수를 전달받습니다. 첫 번째 인수로는 "기존 상태 객체"를 전달받고, 두 번째 인수로는 dispath된 "action 객체"를 전달받습니다.

그리고 reducer 함수가 반환하는 객체가 기존 상태 객체를 대체하므로 반환되는 객체에는 모든 프로퍼티(상태)를 작성해주어야 합니다.

const redux = require('redux');

  // reducer 함수 정의
const reducer = (stateObj, action) => {
    return { counter: stateObj.counter + 1 };
}

  // reducer 함수를 createStore 함수의 인수로 전달하여 연결
const store = reducer.createStore(reducer);

정의한 reducer 함수를 store를 생성하는 createStore 메서드의 인수로 전달해줍니다.

실질적으로 store의 상태값 변경과 관련된 로직들은 모두 reducer 함수가 갖고 있기 때문에 store를 생성할 때 인수로 reducer 함수를 전달하여 서로 연결시킬 수 있습니다.

4. subscription 함수 등록

subscription 함수는 store에 저장된 상태값이 변경된 이후에 호출되는 함수입니다. subscription 함수도 우리가 직접 호출하는 것이 아니라 Redux가 호출힙나다.

store 객체의 "subscribe 메서드"에 함수를 인수로 전달해줍니다. 이때 인수로 전달한 함수가 바로 subscription 함수입니다.
이후 Redux가 store 내부 상태값이 변경된 경우 subscribe 메서드의 인수로 전달한 콜백함수(subscriontion)을 호출합니다.

subscription 함수는 "상태가 업데이트된 이후"에 호출됩니다. 이를 확인하기 위해서 subscribtion 함수 내부에서 store.getState 메서드를 호출합니다.
getState 메서드는 store 내 저장된 현재 상태 객체를 가져오는 메서드입니다.

const redux = require('redux');

  // reducer 함수
const reducer = (stateObj, action) => {
    return { counter: stateObj.counter + 1 };
}

  // store 생성
const store = reducer.createStore(reducer);

  // subscription 함수
const subscription = () => {
    const latestState = store.getState();
    console.log(latestState);
};

  // subscription 등록
store.subscribe(subscription);

5. 초기 상태 객체 설정

createStore 메서드가 호출되고 reducer 함수가 실행됩니다. reducer가 실행되는 시점에서는 아직 stateObj가 정의되어 있지 않습니다(undefined).

초기 상태값이 없으므로 우리는 reducer 함수의 첫 번째 매개변수에 "매개변수 기본값"을 사용하여 초기값을 설정할 수 있습니다.

  // 초기 상태 객체
const initialState = { counter: 0 };

  // stateObj 변수에 매개 변수 초기값을 작성
const reducer = (stateObj = initialState, action) => {
    return { counter: stateObj.counter + 1 };
};

초기 상태 객체는 reducer가 처음 실행될 때만 사용되는 값이며, 이후에는 해당 값으로 초기화되지 않습니다. 매개변수에 부여한 초기값은 인수가 전달되지 않거나, undefined가 전달되는 경우에만 해당 초기값으로 초기화되는 문법입니다.

6. action 객체 dispatch

action 객체를 store 객체의 "dispatch 메서드"의 인수로 전달하면 redux가 reducer 함수를 실행하며, reducer 함수의 반환값으로 상태 객체가 대체됩니다.

대체된 상태 객체의 프로퍼티 값과 이전 상태 객체의 프로퍼티 값을 서로 단순 비교하여 일치하지 않는 경우 상태가 변경된 것이며 이후에 subscribe 메서드의 인수로 전달한 subscription 함수를 Redux가 호출합니다.

action 객체는 일반적으로 type 프로퍼티가 존재하는 객체입니다. type 프로퍼티는 reducer 함수 내부에서 상태 변경의 동작을 나타내는 식별자처럼 사용됩니다.

store.dispatch({ type: 'increment' });

우리는 action.type에 정의한 값을 통해 reducer 함수 내부에서는 action.type의 값에 따라 다른 상태 객체가 반환되도록 작성할 수 있습니다.

const reducer = (stateObj = initialState, action) => {
    if (action.type === 'increment') {
        return { counter: stateObj.counter + 1 };
    }
    
    if (action.type === 'decrement') {
        return { counter: stateObj.counter - 1 };
    }
    
    return stateObj;
};

action.type을 if 문으로 검사하여 일치하는 코드 블록을 실행하게 되고, 반환값으로 작성된 상태 객체로 대체합니다.
이때 이전 상태 객체의 프로퍼티 값과 서로 단순 비교하여 일치하지 않는 경우 subscription 함수를 redux가 호출합니다.

profile
Frontend Dev

0개의 댓글