Redux는 Flux인가?

김채은·2024년 7월 27일
post-thumbnail

들어가며

사내 React Deep Dive 스터디에서 React 공식문서를 공부하고 있다. 이번주 주제인 State 관리하기 챕터를 읽으면서 Reducer와 Context의 필요와 사용법에 대해 명확히 알게 되었고, 특히 상태관리 라이브러리의 필요를 체감하게 됐다. 이 글은 Redux와 Flux 패턴에 대해 좀 더 공부하다가 알게 된 내용을 작성했다.

Reducer와 Context로 앱 확장하기
Reducer를 사용하면 컴포넌트의 state 업데이트 로직을 통합할 수 있다. Context를 사용하면 다른 컴포넌트들에 정보를 전달할 수 있다. Reducer와 context를 함께 사용하여 복잡한 화면의 state를 관리할 수 있다.

사람들이 State를 관리할 때 Reducer를 잘 쓰지 않는 이유가 한 컴포넌트에서 state를 관리할 때 Reducer를 사용할 만큼 큰 어려움을 느끼지 않기 때문이라고 생각한다.
Reducer는 혼자 쓰기보다 Context와 함께 사용하며 여기저기 산재된 state를 관리할 때 큰 시너지를 낸다. Reducer + Context를 편하게 쓰게 해주는 게 상태관리 라이브러리다. 그 중에서도 Reducer와 유사한 동작 방식을 가진 Redux에 대해 공부해보았다.

Redux

  • 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너
  • 앱의 상태 전부를 저장소(store) 안에 있는 객체 트리에 저장
  • 상태를 변경하는 유일한 방법: 무엇이 일어날지 서술하는 객체인 액션(action)을 보내는 것
  • 액션이 상태 트리를 어떻게 변경할지 명시하기 위해 리듀서(reducers)를 작성해야 함

3가지 원칙

  1. 진실은 하나의 근원으로부터
    • 애플리케이션의 모든 상태는 하나의 저장소 안에 하나의 객체 트리 구조로 저장된다.
    • 범용적인 애플리케이션(universal application, 하나의 코드 베이스로 다양한 환경에서 실행 가능한 코드)을 만들기 쉽다.
    • 디버깅에 유리하다.
    • undo/redo를 손쉽게 구현 가능하다.
  2. 상태는 읽기 전용이다
    • 상태를 변화시키는 유일한 방법은 무슨 일이 벌어지는 지를 묘사하는 액션 객체를 전달하는 방법뿐이다.
    • 뷰나 네트워크 콜백에서 결코 상태를 직접 바꾸지 못한다.
    • 모든 액션은 엄격한 순서에 의해 하나하나 실행되기 때문에, 신경써서 관리해야 할 미묘한 경쟁 상태는 없다.
    • 액션은 그저 평범한 객체이므로, 기록을 남길 수 있고 시리얼라이즈할 수 있으며, 저장할 수 있고, 이후에 테스트나 디버깅을 위해 재현할 수 있다.
  3. 변화는 순수 함수로 작성되어야 한다
    • 액션에 의해 상태 트리가 어떻게 변화하는 지를 지정하기 위해 프로그래머는 순수 리듀서를 작성해야 한다.
    • 이전 상태를 변경하는 대신 새로운 상태 객체를 생성해서 반환해야 한다.
    • 리듀서는 그저 함수이기 때문에 호출되는 순서를 정하거나 추가적인 데이터를 넘길 수 있다.
    • 페이지네이션과 같이 일반적인 재사용 가능한 리듀서를 작성할 수 있다.

Redux는 Flux인가?

나는 Redux가 Flux 패턴을 대표하는 라이브러리라고 생각해왔다. 그런데 Redux 공식문서의 Prior Art 파트를 읽다가 Redux가 Flux의 구현체라고는 할 수 없다는 것을 알게 됐다.

Prior Art - Flux | Redux
Redux를 Flux의 구현 중 하나라고 생각할 수 있을까요? 그렇기도 하고, 아니기도 합니다.
Redux는 Flux의 중요한 특징들로부터 영감을 얻었습니다. Flux와 마찬가지로 Redux에서는 애플리케이션의 특정 레이어에 있을 모델 업데이트 로직에 집중할 수 있도록 해줍니다(Flux의 '저장소', Redux의 '리듀서'). 저장소나 리듀서는 애플리케이션 코드가 직접 데이터를 조작하는 대신 액션이라고 불리는 평범한 객체로만 모든 데이터 변화를 묘사하도록 강제합니다.

Flux

Flux는 사용자 입력을 기반으로 액션을 만들고 액션을 디스패처에 전달하여 스토어의 데이터를 변경한 뒤 뷰에 반영하는 단방양 흐름으로 애플리케이션을 만드는 아키텍처이다.

스토어는 뷰에게 자신의 상태가 변경됐다고 알리는 이벤트를 발생시켜야 한다. 이것을 도와주는 것이 EventEmitter이다.

EventEmitter는 Node.js에 내장되어 있는 이벤트 드리븐 아키텍처를 위한 API이다(일종의 옵저버 패턴).

  • .emit('EVENT_NAME'): 'EVENT_NAME'으로 정의된 이벤트를 발생시킴
  • .on('EVENT_NAME', callback): 'EVENT_NAME'으로 정의된 이벤트가 발생됨을 감지하여 callback 호출

on()으로 스토어에 이벤트 리스너를 등록하고, 이벤트 발생 시 emit()으로 콜백을 호출하여 뷰를 업데이트한다.

Flux와 EventEmitter로 구현한 Todo List

  • todoStore.js
    스토어는 데이터와 데이터가 변경됐을 때 뷰에 알릴 수 있는 event emitter로 구성했다.
import EventEmitter from "events";

class TodoStore {
  CHANGE_EVENT = "CHANGE";
  todos = [];
  todoEmitter = new EventEmitter();

  // event emitter에 변경 이벤트와 콜백 등록
  addChangeListener(callback) {
    this.todoEmitter.on(this.CHANGE_EVENT, callback);
  }

  // 변경 이벤트 제거
  removeChangeListner(callback) {
    this.todoEmitter.on(this.CHANGE_EVENT, callback);
  }

  // 변경 이벤트 발생시킴
  emitChange() {
    this.todoEmitter.emit(this.CHANGE_EVENT);
  }

  // todos 반환
  getTodos() {
    return structuredClone(this.todos);
  }
}

const todoStore = new TodoStore();

export default todoStore;
  • dispatcher.js
    dispatcher 객체에는 액션을 등록할 수 있다. CREATE_TODO 액션이 실행되면 todos에 action으로 받아온 todo를 추가하고, 변경 이벤트를 호출한다.
import { Dispatcher } from "flux";
import todoStore from "./todoStore";

const dispatcher = new Dispatcher();

dispatcher.register((action) => {
  switch (action.type) {
    case "CREATE_TODO":
      todoStore.todos.push(action.todo);
      todoStore.todoEmitter.emit(todoStore.CHANGE_EVENT);
      break;
    default:
      throw new Error("올바른 액션 타입이 아닙니다.");
  }
});

export default dispatcher;
  • Todos.jsx
  1. 컴포넌트가 마운트되면 todoStore의 변경 이벤트 핸들러로 handleTodosChange를 추가한다.
  2. Todo 추가로 handleTodoSubmit이 호출되면 dispatch를 통해 CREATE_TODO 액션을 발생시킨다.
  3. 스토어에 todo가 추가되고 변경 이벤트가 호출되어 handleTodosChange가 실행되고 새로운 todos를 가져와 상태를 업데이트한다.
import React, { useEffect, useState } from "react";
import todoStore from "./todoStore";
import dispatcher from "./Dispatcher";

const Todos = () => {
  const [typing, setTyping] = useState("");
  const [todos, setTodos] = useState(todoStore.getTodos());

  useEffect(() => {
    todoStore.addChangeListener(handleTodosChange);

    return () => todoStore.removeChangeListner(handleTodosChange);
  }, []);

  const handleTodosChange = () => {
    setTodos(todoStore.getTodos());
  };

  const handleTodoSubmit = (e) => {
    e.preventDefault();

    dispatcher.dispatch({
      type: "CREATE_TODO",
      todo: typing,
    });
    
    setTyping("");
  };

  const handleTypingChange = (e) => {
    setTyping(e.target.value);
  };

  return (
    <div>
      <form onSubmit={handleTodoSubmit}>
        <input value={typing} onChange={handleTypingChange} />
        <button type="submit">확인</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <li key={todo}>{todo}</li>
        ))}
      </ul>
    </div>
  );
};

export default Todos;

예전에 Flux 패턴을 공부할 때는 디스패처의 존재가 모호하게 느껴졌는데 직접 Flux 패턴으로 상태관리를 구현해보니 디스패처의 역할에 대해 명확히 이해가 됐다.

단일 dispatcher
dispatcher는 Flux 어플리케이션의 중앙 허브로 모든 데이터의 흐름을 관리한다. 본질적으로 store의 콜백을 등록하는데 쓰이고 action을 store에 배분해주는 간단한 작동 방식으로 그 자체가 특별하게 똑똑한 것은 아니다. 각각의 store를 직접 등록하고 콜백을 제공한다. action creator가 새로운 action이 있다고 dispatcher에게 알려주면 어플리케이션에 있는 모든 store는 해당 action을 앞서 등록한 callback으로 전달 받는다.

Flux와 Redux의 차이

그래서 Redux랑 Flux랑 뭐가 다르냐. Flux와 달리 Redux에는 디스패처 개념이 없다. Redux는 event emitter보다 순수 함수들에 의존한다.
Flux는 dispatcher가 사이드 이펙트를 발생시키지만, Redux는 사용자가 결코 데이터의 상태를 바꾸지 않는다고 가정한다.

Redux의 리듀서

Redux로 Todo List 만드는 예제는 검색을 하면 굉장히 많이 나오기 때문에 생략하고 리듀서만 살펴보자.

import { ADD } from "./actions";

const initialState = {
  todos: [],
};

export const reducer = (state = initialState, action) => {
  switch (action.type){
    case ADD:
      return {
        todos: [...state.todos, action.todo],
      };
  }
};

Flux의 디스패처가 상태를 변경하고 이벤트를 발생시키는 것과 달리 Redux의 리듀서는 이전의 상태로부터 순수한 상태를 리턴한다.

리듀서(reducer)가 reduce로부터 온 용어라는 것을 생각하면 좀 더 쉽게 이해할 수 있다. reduce는 초기값을 단일 값으로 누적하는 연산을 한다. 이런 reduce로 전달하는 함수가 reducer인데, 지금까지의 결과와 현재 아이템을 인자로 받고 다음 결과를 반환한다.

const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
  (result, number) => result + number
); // 1 + 2 + 3 + 4 + 5

그러니까 Redux가 말하는 상태는 데이터에 액션을 누적한 결과이다. 이러한 사이드 이펙트가 없는 액션을 누적하여 관리할 수 있기 때문에 undo/redo와 디버깅에 용이한 것이다.

결론, Redux는 Flux의 구현 그 자체는 아니지만, Redux 팀에서 말하는 것처럼 Flux의 원리에 기반한 상태관리 라이브러리이다.

profile
배워서 남주는 개발자 김채은입니다 ( •̀ .̫ •́ )✧

0개의 댓글