본문 바로가기

아직도 Redux인가?

1. 상태관리란?

상태 관리는 state와 같은 상태를 관리하는 방법입니다. 전역으로 관리하는 방법이 있고, 컴포넌트 단위로도 관리하는 방법이 있습니다. Redux는 전역으로 관리하는 툴입니다.


2. 아직도 Redux인가?

2025년 6월 기준으로도 아직 1위를 지키고 있지만 이제 곧 Zustand에 순위를 내줄 것으로 보입니다. 다만 그렇다 하더라도, 아직도 많은 채용공고에는 React와 함께 Redux가 올라온 상태입니다.

https://npmtrends.com

3. Redux란?

공식 홈페이지에서 설명하고 있는 리덕스의 정의를 한 번 보도록 합시다.

Redux 공식 홈페이지

리덕스는 리액트만을 위한 라이브러리는 아닙니다. React 뿐만 아니라 Vue와도 잘 결합되며, 또한 순수 JS와도 결합이 됩니다. 설치없이 실행할 수 있는 생활코딩의 간단한 예제를 통해 Redux의 속성을 알아봅시다.

3.1 바닐라와 함께 Redux 살펴보기

생활코딩은 CCL 라이선스 BY를 따르고 있습니다. 자료의 출처가 생활코딩임을 명시합니다. 자료가 이전 자료여서 Claude를 통해 현대 버전으로 수정하였습니다.


아래 코드는 리덕스를 사용하지 않은 코드입니다.

<html>
  <head></head>
  <body>
    <style>
      .container {
        border: 5px solid black;
        padding: 10px;
      }
    </style>
    <div id="red"></div>
    <div id="green"></div>
    <div id="blue"></div>

    <script>
      function red() {
        document.querySelector('#red').innerHTML = `
                <div class="container" id="component_red">
                    <h1>red</h1>
                    <input type="button" value="fire" onclick="
                    document.querySelector('#component_red').style.backgroundColor = 'red';
                    document.querySelector('#component_green').style.backgroundColor = 'red';
                    document.querySelector('#component_blue').style.backgroundColor = 'red';
                    ">
                </div>
            `;
      }
      red();
      function green() {
        document.querySelector('#green').innerHTML = `
                <div class="container" id="component_green">
                    <h1>green</h1>
                    <input type="button" value="fire" onclick="
                    document.querySelector('#component_red').style.backgroundColor = 'green';
                    document.querySelector('#component_green').style.backgroundColor = 'green';
                    document.querySelector('#component_blue').style.backgroundColor = 'green';
                    ">
                </div>
            `;
      }
      green();
      function blue() {
        document.querySelector('#blue').innerHTML = `
                <div class="container" id="component_blue">
                    <h1>blue</h1>
                    <input type="button" value="fire" onclick="
                    document.querySelector('#component_red').style.backgroundColor = 'blue';
                    document.querySelector('#component_green').style.backgroundColor = 'blue';
                    document.querySelector('#component_blue').style.backgroundColor = 'blue';
                    ">
                </div>
            `;
      }
      blue();
    </script>
  </body>
</html>

아래 코드는 리덕스를 사용한 코드입니다. 설치 없이 사용할 수 있도록 CDN을 사용했습니다. 주석 제거 버전입니다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>리덕스를 사용해서 상태 관리하기기</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.1/redux.js"></script>
    <style>
        .container {
            border: 5px solid black;
            padding: 10px;
        }
    </style>
</head>
<body>
    <div id="red-component"></div>
    <div id="green-component"></div>
    <div id="blue-component"></div>
    <div id="test-component"></div>
    <script>
        // 바리스타(상태, 주문서)
        const reducer = (state, action) => {
            // 초기 상태(state)를 정의
            if (state === undefined){
                return {
                    color: 'yellow'
                }
            }
            // 주문서(action)에 따라 상태(state)를 변경
            switch (action.type){
                case 'CHANGE_COLOR':
                    return {
                        ...state,
                        color: action.color
                    }
                default:
                    return state;
            }
        }

        // 커피숖
        const store = Redux.createStore(
            reducer,
            // Redux DevTools Extension을 사용하기 위한 설정
            window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
        );

        // 주문을 받는 점원(주문서를 생성하는 액션 크리에이터 함수수)
        const changeColorAction = (color) => {
            // 주문서(action)를 생성
            return {
                type: 'CHANGE_COLOR',
                color: color,
                timestamp: new Date().toISOString()
            }
        }

        const renderRedComponent = () => {
            const state = store.getState()
            document.getElementById('red-component').innerHTML = `
                <div class="container" id="component_red" style="background-color: ${state.color}">
                    <h1>Red Component</h1>
                    <input type="button" value="Red!" onclick="
                        handleColorChange('red');
                    ">
                </div>
            `
        }
        const renderGreenComponent = () => {
            const state = store.getState()
            document.getElementById('green-component').innerHTML = `
                <div class="container" id="component_green" style="background-color: ${state.color}">
                    <h1>Green Component</h1>
                    <input type="button" value="Green!" onclick="
                        handleColorChange('green');
                    ">
                </div>
            `
        }
        const renderBlueComponent = () => {
            const state = store.getState()
            document.getElementById('blue-component').innerHTML = `
                <div class="container" id="component_blue" style="background-color: ${state.color}">
                    <h1>Blue Component</h1>
                    <input type="button" value="blue!" onclick="
                        handleColorChange('blue');
                    ">
                </div>
            `
        }

        const renderTestComponent = () => {
            const state = store.getState()
            document.getElementById('test-component').innerHTML = `
                <div class="container" id="component_test" style="background-color: ${state.color}">
                    <h1>Test Component</h1>
                    <input type="button" value="Test!" onclick="
                        handleColorChange('yellow');
                    ">
                </div>
            `
        }

        const handleColorChange = (color) => {
            // 주문서를 생성하고 커피숖에 전달
            // store에 dispatch를 하게 되면 reducer가 호출됩니다.
            // reducer는 현재 상태(state)와 주문서(action)를 받아 새로운 상태를 반환합니다.
            store.dispatch(changeColorAction(color))
        }

        // 진동벨 받는 사람(고객)
        store.subscribe(renderRedComponent)
        store.subscribe(renderGreenComponent)
        store.subscribe(renderBlueComponent)

        // 초기 렌더링
        renderRedComponent()
        renderGreenComponent()
        renderBlueComponent()
        renderTestComponent() // 재렌더링 안되는 컴포넌트

    </script>
</body>
</html>

AI통해 주석을 넣은 버전입니다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Modern Redux Color Changer</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.1/redux.js"></script>
    <style>
      .container {
        border: 5px solid black;
        padding: 10px;
      }
    </style>
  </head>
  <body>
    <div id="red-component"></div>
    <div id="blue-component"></div>
    <div id="green-component"></div>

    <script>
      /**
       * =====================================================
       * REDUX CORE: REDUCER 함수
       * =====================================================
       * Redux의 핵심 - 순수 함수로 상태 변화를 처리
       *
       * 핵심 원칙:
       * 1. 순수 함수: 같은 입력에 대해 항상 같은 출력
       * 2. 불변성: 기존 state를 직접 수정하지 않고 새로운 객체 반환
       * 3. 부수 효과 없음: API 호출, DOM 조작 등 금지
       *
       * @param {Object} state - 현재 애플리케이션 상태
       * @param {Object} action - 상태 변경 명령 객체 (type 필수)
       * @returns {Object} 새로운 상태 객체
       */
      const reducer = (state, action) => {
        console.log('Reducer 호출:', {
          currentState: state,
          action: action,
          timestamp: new Date().toLocaleTimeString(),
        });

        // 초기 상태 설정
        // Redux store 생성 시 자동으로 호출되며, 이때 state는 undefined
        if (state === undefined) {
          const initialState = { color: 'yellow' };
          console.log('초기 상태 설정:', initialState);
          return initialState;
        }

        // Action 타입에 따른 상태 변경 처리
        switch (action.type) {
          case 'CHANGE_COLOR':
            console.log('색상 변경:', `${state.color} → ${action.color}`);

            // 불변성을 위해 spread operator 사용 (ES6+)
            // Object.assign 대신 더 현대적인 방식
            const newState = {
              ...state, // 기존 상태의 모든 속성 복사
              color: action.color, // color 속성만 새 값으로 덮어쓰기
            };

            console.log('새로운 상태 생성:', newState);
            return newState;

          default:
            // 알 수 없는 action type인 경우 기존 상태 그대로 반환
            console.log('알 수 없는 액션 타입:', action.type);
            return state;
        }
      };

      /**
       * =====================================================
       * REDUX STORE 생성 및 설정
       * =====================================================
       * Redux store: 애플리케이션의 상태를 관리하는 중앙 저장소
       */
      const store = Redux.createStore(
        reducer,
        // Redux DevTools Extension 연동 (브라우저 확장 프로그램)
        // 개발 중에만 사용, 프로덕션에서는 제거 권장
        window.__REDUX_DEVTOOLS_EXTENSION__ &&
          window.__REDUX_DEVTOOLS_EXTENSION__(),
      );

      console.log('Redux Store 생성 완료');
      console.log('초기 상태:', store.getState());

      /**
       * =====================================================
       * UTILITY FUNCTIONS
       * =====================================================
       */

      /**
       * 액션 생성자 함수 (Action Creator)
       * 액션 객체 생성을 위한 팩토리 함수
       * @param {string} color - 변경할 색상
       * @returns {Object} Redux 액션 객체
       */
      const changeColorAction = (color) => ({
        type: 'CHANGE_COLOR',
        color: color,
        timestamp: Date.now(), // 추가 메타데이터
      });

      /**
       * =====================================================
       * COMPONENT FUNCTIONS (View Layer)
       * =====================================================
       * 각 컴포넌트는 자신만의 렌더링 로직을 가짐
       * Redux 상태가 변경될 때마다 자동으로 리렌더링
       */

      /**
       * Red 컴포넌트 렌더링 함수
       * store.subscribe()에 의해 상태 변경 시 자동 호출
       */
      const renderRedComponent = () => {
        console.log('Red 컴포넌트 렌더링 시작');

        // 현재 상태를 store에서 가져오기
        const currentState = store.getState();

        // 대상 DOM 요소 찾기
        const targetElement = document.querySelector('#red-component');

        // HTML 템플릿 리터럴을 사용한 동적 렌더링
        targetElement.innerHTML = `
                <div class="container" style="background-color: ${currentState.color}">
                    <h1>red</h1>
                    <input type="button" value="fire" onclick="handleColorChange('red')">
                </div>
            `;

        console.log('Red 컴포넌트 렌더링 완료');
      };

      /**
       * Blue 컴포넌트 렌더링 함수
       */
      const renderBlueComponent = () => {
        console.log('Blue 컴포넌트 렌더링 시작');

        const currentState = store.getState();
        const targetElement = document.querySelector('#blue-component');

        targetElement.innerHTML = `
                <div class="container" style="background-color: ${currentState.color}">
                    <h1>blue</h1>
                    <input type="button" value="fire" onclick="handleColorChange('blue')">
                </div>
            `;

        console.log('Blue 컴포넌트 렌더링 완료');
      };

      /**
       * Green 컴포넌트 렌더링 함수
       */
      const renderGreenComponent = () => {
        console.log('Green 컴포넌트 렌더링 시작');

        const currentState = store.getState();
        const targetElement = document.querySelector('#green-component');

        targetElement.innerHTML = `
                <div class="container" style="background-color: ${currentState.color}">
                    <h1>green</h1>
                    <input type="button" value="fire" onclick="handleColorChange('green')">
                </div>
            `;

        console.log('Green 컴포넌트 렌더링 완료');
      };

      /**
       * =====================================================
       * EVENT HANDLERS
       * =====================================================
       */

      /**
       * 색상 변경 이벤트 핸들러
       * 버튼 클릭 시 호출되어 Redux 액션을 디스패치
       * @param {string} color - 변경할 색상
       */
      const handleColorChange = (color) => {
        console.log(`색상 변경 요청: ${color}`);

        // 액션 생성자를 사용하여 액션 객체 생성
        const action = changeColorAction(color);

        // Redux store에 액션 디스패치
        // 이로 인해 reducer가 호출되고, 상태가 변경되며, 구독자들이 실행됨
        store.dispatch(action);

        console.log('액션 디스패치 완료:', action);
      };

      /**
       * =====================================================
       * SUBSCRIPTION & INITIALIZATION
       * =====================================================
       * Redux store 구독 설정 및 초기 렌더링
       */

      // 상태 변경 감지를 위한 구독자 등록
      // store.subscribe()는 상태가 변경될 때마다 등록된 함수를 자동 실행
      console.log('컴포넌트 구독자 등록 중...');

      store.subscribe(renderRedComponent);
      store.subscribe(renderBlueComponent);
      store.subscribe(renderGreenComponent);

      console.log('모든 구독자 등록 완료');

      /**
       * =====================================================
       * INITIAL RENDER
       * =====================================================
       * 애플리케이션 시작 시 초기 렌더링 수행
       */
      console.log('초기 렌더링 시작...');

      // 각 컴포넌트 초기 렌더링
      renderRedComponent();
      renderBlueComponent();
      renderGreenComponent();

      console.log('애플리케이션 초기화 완료!');

      /**
       * =====================================================
       * REDUX 동작 흐름 정리
       * =====================================================
       *
       * 1️⃣ 사용자 인터랙션
       *    - 사용자가 버튼 클릭
       *    - handleColorChange() 함수 실행
       *
       * 2️⃣ 액션 생성 및 디스패치
       *    - changeColorAction()으로 액션 객체 생성
       *    - store.dispatch()로 Redux store에 액션 전달
       *
       * 3️⃣ Reducer 실행
       *    - Redux가 자동으로 reducer 함수 호출
       *    - 현재 상태(state)와 액션(action)을 매개변수로 전달
       *
       * 4️⃣ 상태 업데이트
       *    - reducer가 새로운 상태 객체 반환
       *    - Redux가 상태 변경을 감지
       *
       * 5️⃣ 구독자 실행
       *    - store.subscribe()로 등록된 모든 함수들이 자동 실행
       *    - 각 컴포넌트 렌더링 함수가 순차적으로 호출
       *
       * 6️⃣ UI 업데이트
       *    - store.getState()로 새로운 상태 조회
       *    - DOM 요소들이 새로운 상태에 맞게 업데이트
       *
       * 🔄 이 과정이 매번 반복되어 상태와 UI의 동기화 유지
       */

      // 전역 함수로 등록 (onclick 핸들러에서 사용하기 위함)
      window.handleColorChange = handleColorChange;
    </script>
  </body>
</html>

우리가 앞에서 배운 리액트에서 클릭 이벤트가 발생했을 때, 해당 값들은 Hook을 사용해서 값의 상태를 바꿔준 후 화면에 재렌더링하여서 값들을 보여주었습니다. 즉, useState를 사용하여 리액트에게 값의 변경을 알리면 리액트가 UI를 업데이트 하였습니다.

리덕스도 useState와 마찬가지로 변하는 값들을 관리해주는 관리 시스템입니다. 차이는 useState가 컴포넌트 단위로 상태를 관리했다면, 리덕스는 전역으로 데이터 상태를 관리합니다. 데이터들의 값이 바뀌었을 때 그 값들을 사용하는 모든 컴포넌트들에게 변경된 값을 전달해주고 그 값들을 다시 화면에 보여줍니다.


다음 내용을 살펴보기 전에 리덕스 데브툴스를 설치해 주세요. 그리고 생활코딩의 코드를 VSC의 live Server로 켜놓고 해당 툴을 실행해 보세요. 상태를 다운로드 받아 다시 import하면 그 상태 그대로를 재생할 수 있습니다.

Redux DevTools
개발자도구 > Redux 탭

개발자 도구를 열어보면 요소, 콘솔, 소스 메뉴 옆으로 >>가 보이실 것입니다. 누르시면 Redux라는 메뉴를 보실 수 있습니다.

  1. 클릭한 이벤트를 표시합니다. 이를 통해 어떤 이벤트가 발생되었는지 디버깅이 가능합니다. 또한 이 이벤트를 jump나 skip할 수 있어요.
  2. 동영상 보듯이 play가 가능합니다. 이를 통해 어떠한 이벤트가 순차적으로 발생되었는지 확인할 수 있습니다.
  3. 이벤트들을 밖으로 내보내거나 가져올 수 있습니다.