📗

2.1 Redux 개념

2.1.1 Redux

1) Redux

Redux(리덕스)는 JavaScript(자바스크립트) 상태관리 라이브러리입니다. 보통 리덕스는 리액트에서 가장 사용률이 높은 상태관리 라이브러리로 알려져 있기 때문에 리액트에서만 사용한다고 착각하기 쉽습니다. 하지만 리덕스는 라이브러리이기 때문에 다양한 UI, Library들과 함께 사용이 가능합니다. 즉, 리덕스는 리액트에 종속된 라이브러리가 아닌 어떤 UI에도 구애받지 않고, React, Vue, vanilla JS, Angular 등 어디든 붙여 사용할 수 있을 정도로 유연한 성격을 가지고 있습니다.
 
리덕스는 전역 상태관리를 위한 도구이자 상태(State)를 효율적으로 관리하고 사용할 수 있도록 도와주는 도구 중 하나입니다. 즉 애플리케이션의 상태를 하나의 중앙 저장소에 저장하고 관리합니다. 이렇게 관리함으로써 상태가 여러 컴포넌트 간에 공유되고, 상태에 대한 업데이트가 일관되게 이루어집니다.
 
리덕스는 상태의 변화를 예측 가능한 방식으로 관리합니다. 상태의 변경은 오직 액션(Action)이라는 객체를 통해 이루어지기 때문에 애플리케이션의 동작을 예측하기 쉽습니다. 또한 리덕스는 Redux DevTools를 통해 상태 변화를 모니터링하고 디버깅이 가능해지기 때문에 앱의 상태가 언제, 어디서, 어떻게 바뀌었는지 쉽게 추적이 가능합니다.

2) Redux를 사용하는 이유

리액트는 상위 컴포넌트에서 하위 컴포넌트로 전달시키는 구조로 되어 있기 때문에 상위 컴포넌트에서 렌더링이 발생하면 그 자식 컴포넌트들에게도 불필요한 리렌더링이 발생합니다. 또한 프로젝트 규모가 커지고 수많은 컴포넌트 간에 상태값을 공유하거나 변경해야 한다면 작성해야 하는 코드가 늘어나면서 코드의 가독성이 떨어지고 유지보수가 힘들어집니다. 이런 문제들을 개선하기 위해 리덕스를 사용하게 되었습니다.
 
리덕스는 상태 데이터를 하나의 데이터 저장소인 스토어에 저장하고 그 스토어에서 데이터를 불러오는 방식을 사용합니다. 이러한 방식을 사용하면서 불필요하게 상태를 다른 컴포넌트에 전달시키는 과정이 사라지고 더 효율적인 관리가 가능해졌습니다.
 
리덕스를 사용하면 컴포넌트의 상태 업데이트에 관련된 코드를 다른 파일로 분리해서 데이터를 효율적으로 관리할 수 있습니다. 또한, 컴포넌트끼리 똑같은 상태를 공유해야 할 때도 여러 컴포넌트를 거치지 않고 손쉽게 상태 값을 전달하거나 업데이트를 할 수 있습니다. 즉, 컴포넌트 간의 상태를 전달하는 과정이 간소화됩니다.

3) Redux는 언제 사용될까?

(1) 프로젝트의 규모가 클 경우

리덕스 사용 시, 리액트의 단점이었던 수많은 상태 변경과 상태 전달 과정을 간편화할 수 있으며, 상태 관리의 번거로움을 줄여줄 수 있습니다.
 

(2) 상태 관리가 복잡한 경우

애플리케이션의 상태가 여러 컴포넌트 간에 공유되고, 상태 업데이트를 하는 로직이 복잡한 경우 리덕스가 유용합니다. 특히, 상태가 다수의 컴포넌트에 걸쳐 있거나 깊은 중첩구조를 가지는 경우에 많이 활용됩니다.
 

(3) 중앙화된 상태 관리가 필요한 경우

리덕스는 중앙화된 상태 저장소를 제공하므로 상태가 한곳에 모여있습니다. 이는 상태를 예측 가능하게 만들어 주고 여러 컴포넌트 간에 일관성을 유지할 수 있도록 도와줍니다.
 

(4) 개발자 도구를 통한 디버깅이 필요한 경우

Redux DevTools를 통해 상태 변화를  모니터링하고 디버깅할 수 있습니다. 이는 개발과 디버깅의 효율을 높여줍니다.
 

(5) 프론트엔드 라이브러리/프레임워크와 통합해 사용하는 경우

React, Vue, Angular와 함께 사용하여 상태 관리를 통합적으로 처리할 수 있습니다. 특히, 리액트는 리덕스와 함께 자주 사용되는데, 리액트의 상태 관리만으로는 대규모 앱에서 한계를 가지기 때문에 리덕스를 함께 사용합니다.
리덕스는 상태관리를 효율적으로 할 수 있게 도와주는 하나의 도구로 상태 구조가 간단하고 복잡하지 않은 경우, 굳이 리덕스를 사용할 필요는 없습니다. 간단한 형태의 애플리케이션을 구성해 리액트만으로 충분히 상태를 관리할 수 있는 경우에는 사용하지 않아도 됩니다.
 

2.1.2 리덕스의 주요 구성요소

1) Store

상태를 보관하는 중앙 저장소이며, createStore() 함수로 생성합니다. 스토어는 애플리케이션의 모든 상태를 하나의 객체로 관리합니다. 따라서 한 애플리케이션 당 하나의 스토어를 가집니다. 상태 값이 하나의 스토어에 저장되기 때문에 에러가 발생했을 때 각각의 컴포넌트를 확인할 필요 없이 스토어만 관리하면 된다는 장점이 있습니다.
 
스토어를 생성하는 createStore() 함수는 최상위 export로 아래와 같이 import해 사용하며, 상태를 변경하는 reducer 함수를 파라미터로 받습니다.
import { createStore } from 'redux' const store = redux.createStore(reducer);

2) State

리덕스의 모든 상태 업데이트는 불변으로 수행될 것을 기대하기 때문에, 값을 업데이트할 때 기존 객체/배열의 복사본을 만든 다음 복사본을 수정합니다. 따라서 애플리케이션 상태를 항상 일반 JavaScript 객체 및 배열로 유지할 수 있습니다. 상태는 스토어에 의해 관리되며 스토어의 내장함수인 getState() 를 통해 확인할 수 있습니다.
// 현재 스토어의 상태를 가져옵니다. store.getState()
 
리액트에서는 리덕스 저장소에서 상태를 읽을 때 useSelector hook을 사용합니다. useSelector를 사용하면 리덕스 스토어의 상태를 선택적으로 추출해 컴포넌트에서 사용할 수 있으며 아래와 같은 형태를 가집니다.
import React from 'react'; // react-redux로부터 useSelector를 import 해 사용합니다. import { useSelector } from 'react-redux'; function UserProfile() { // useSelector를 사용해 Redux 스토어에서 필요한 상태를 선택합니다. const userName = useSelector((state) => state.user.name); return ( <div> <h2>User Profile</h2> <p>User Name: {userName}</p> </div> ); } export default UserProfile;
 

3) Action

Action은 변경 사항을 설명하는 객체로, 상태에 어떠한 변화가 필요할 때 발생시킵니다. 발생한 일을 설명하는 type 프로퍼티를 필수적으로 가지고 있어야 하고, type은 읽을 수 있는 문자열이어야 합니다. 그 외의 값은 상황에 따라 개발자가 자유롭게 넣어줄 수 있습니다. 액션은 스토어의 내장함수 중 하나인 dispatch()의 파라미터로 전달됩니다.
{ type: "ADD_TODO", data: { id: 0, text: "공부하기" } } { type: "CHANGE_GREETING", text: "안녕하세요. 만나서 반갑습니다." }
 
컴포넌트에서는 액션을 발생시킬 때 액션 생성함수를 사용할 수 있습니다. 매번 작업 개체를 직접 작성할 필요가 없어 편리하며 액션 생성함수는 아래와 같은 형태를 가집니다.
export function addTodo(data) { return { type: "ADD_TODO", data }; } // 화살표 함수로 생성도 가능합니다. export const changeGreeting = text => ({ type: "CHANGE_GREETING", text });
함수 앞에 export 키워드를 붙여 내보내고 다른 파일에서 import 키워드로 가져오기 합니다. 리덕스를 사용할 때 필수적으로 액션 생성함수를 사용해야 하는 것은 아닙니다. 액션을 발생시킬때마다 액션 객체를 직접 작성해도 됩니다.
 

4) dispatch()

스토어의 내장함수 중 하나인 디스패치는 상태를 변경하기 위해 사용합니다. 디스패치를 통해 액션을 발생시키면 리듀서가 호출되어 상태가 업데이트됩니다. 디스패치 함수는 파라미터로 액션 객체를 가집니다.
store.dispatch({ type: "ADD_TODO", data:{id: 0,text: "공부하기" }) // 액션 생성 함수를 넣어서 실행도 가능합니다. store.dispatch(changeInput);
 
react-redux에서는 액션을 발생시킬 때 useDispatch라는 hook을 사용합니다. useDispatch hook은 스토어의 디스패치 방식을 제공하며 아래와 같은 형태를 가집니다.
import React, { useState } from 'react' // react-redux로부터 useDispatch를 import 해 사용합니다. import { useDispatch } from 'react-redux' const Header = () => { const [text, setText] = useState(''); // useDispatch 훅을 사용하여 dispatch 함수를 가져옵니다. const dispatch = useDispatch(); const handleChange = e => setText(e.target.value); const handleKeyDown = e => { const trimmedText = e.target.value.trim() if (e.key === 'Enter' && trimmedText) { // dispatch로 "todo added" 액션을 발생시킵니다. dispatch({ type: 'todos/todoAdded', payload: trimmedText }) setText('') } } return ( <input type="text" placeholder="What needs to be done?" autoFocus={true} value={text} onChange={handleChange} onKeyDown={handleKeyDown} /> ) } export default Header;
 

5) reducer()

리듀서(reducer)는 현재 상태와 액션을 받아 새로운 상태를 반환하는 함수입니다. 리덕스에서 모든 상태 변경은 리듀서를 통해 이루어집니다. 리덕스의 상태는 불변을 기대하기 때문에, 리듀서는 원본 상태 값의 복사본 만 만들 수 있으며 복사본을 변경해 사용합니다. 리듀서는 기존상태와 액션을 파라미터로 받습니다.
// 리듀서 예시 export default function todoReducer(state = initialState, action) { // 액션의 타입에 따라 실행하는 명령이 달라집니다. switch (action.type) { case 'todos/todoAdded': { return { // 원본을 복사해 사용합니다. ...state, todos: [ ...state.todos, { id: nextTodoId(state.todos), text: action.payload, completed: false }] } } case 'todos/todoToggled': { return { ...state, todos: state.todos.map(todo => { if (todo.id !== action.payload) { return todo; } return { ...todo, completed: !todo.completed }; }) }; } default: return state; } }
 
리덕스를 사용할 때 여러 개의 리듀서를 만든 뒤 이를 합쳐 루트 리듀서 (Root Reducer)를 생성할 수도 있습니다. combineReducers() 함수를 사용해 루트 리듀서를 생성하며, 생성된 루트 리듀서는 createStore()함수의 파라미터로 전달됩니다. combineReducers() 함수는 최상위 export로 아래와 같이 import해 사용합니다.
import { combineReducers } from "redux"; import todoReducer from "./todoReducer"; import themeReducer from "./themeReducer"; const rootReducer = combineReducers({ appReducer, themeReducer } ); export default rootReducer;
 

6) subscribe()

스토어의 내장함수 중 하나인 subscribe() 함수를 실행시켜 변경 사항에 대한 리스너를 추가할 수 있습니다. subscribe() 함수는 스토어를 주시하고 있다가 액션이 디스패치 될 때, 전달된 함수를 호출합니다. 스토어의 데이터가 변할 때마다 실행되며 함수 형태의 값을 인자로 가집니다. subscribe()의 파라미터 함수 안에서 현재의 상태를 가져오려면 getState() 함수를 사용합니다.
function select(state) { return state.some.deep.property; } let currentValue function handleChange() { let previousValue = currentValue; // 현재의 상태 가져오기 currentValue = select(store.getState()); if (previousValue !== currentValue) { console.log('Some deep nested property changed from', previousValue,'to',currentValue); } } const unsubscribe = store.subscribe(handleChange); unsubscribe();
 

2.1.3 Redux의 3원칙

                                     [그림] Redux 작동 흐름                                      [그림] Redux 작동 흐름
[그림] Redux 작동 흐름
 
간단한 그림으로 리덕스의 작동 흐름을 살펴보면 이벤트가 발생할 때 변경될 상태의 정보가 담긴 액션객체가 생성되고, 호출된 액션이 리듀서로 전달됩니다. 리듀서함수는 디스패치된 액션객체의 상태 값을 확인하고, 그 값에 따라 전역 상태 저장소인 스토어의 상태를 변경시킵니다. 변경된 상태가 렌더링 되어 어플리케이션에 보여지게됩니다. 이 내용을 기반으로 리덕스 사용 시 준수해야만 하는 리덕스의 3원칙에 대해 살펴보겠습니다.

1) Single source of truth

하나의 애플리케이션에서 모든 상태는 하나의 스토어 안에서 관리한다는 원칙입니다. 하나의 스토어에 애플리케이션 내의 모든 상태를 보관하게 되면 어떤 컴포넌트에서 상태를 변경하거나 업데이트될 때 별도의 컴포넌트마다 동기화할 필요 없이 상태 변경에 관련된 로직을 일괄적으로 처리할 수 있습니다. 즉, 효율적으로 상태를 관리하고 컴포넌트 간의 데이터 공유를 쉽게 처리할 수 있습니다.
 

2) State is read-only

상태는 읽기 전용으로, 리액트에서 상태 갱신 함수로만 컴포넌트의 상태를 업데이트할 수 있던 것처럼, 리덕스에서는 액션을 리듀서함수에 디스패치하여 상태 변경을 요청할 수 있습니다. 요청받은 리듀서 함수는 기존의 상태를 변경하지 않고 새로운 상태 객체를 생성해야 합니다. 이 과정에서 상태를 불변하게 유지하면서 새로운 상태를 생성하기 때문에, 내부적으로  이전 상태와 새로운 상태 간의 데이터 차이를 추적할 수 있고, 필요한 경우에만 렌더링을 트리거 하기 때문에 최적화에 도움을 줄 수 있습니다.
 

3) Changes are made with pure functions

상태를 변경하는 리듀서 함수는 반드시 순수 함수로 작성되어야 한다는 원칙입니다. 여기서 말한 순수 함수란 동일한 입력에 대해 항상 동일한 출력을 반환하는 함수를 의미합니다. 즉, 리듀서는 이전 상태와  액션을  파라미터로 받아 새로운 상태를 반환해야 하며, 외부 변수나 다른 작업으로 상태를 변경해서는 안 됩니다. 하지만 비동기 작업이나 일부 작업을 수행할 땐  순수 함수로 처리하기 어려울 수 있습니다. 이때의 작업은 리듀서 함수 외부에서 미들웨어를 사용하여 처리를 할 수 있습니다. 미들웨어는 특정 조건에 따라 액션을 수정하고 다른 액션을 디스패치 할 수 있으며, 리듀서함수가 호출되기 전에 지정된 작업을 사전에 처리하여 부가적인 기능을 추가할 수 있습니다.
 

2.1.4 Redux의 작동 흐름

                                                                [그림] Redux 흐름도                                                                [그림] Redux 흐름도
[그림] Redux 흐름도
 
 
이전에 간단하게 살펴봤던 리덕스의 작동 흐름을 자세히 살펴보도록 하겠습니다.
먼저 사용자와의 인터랙션을 통해서 화면을 업데이트하는 컴포넌트는 상태를 직접 가지지 않고, 하나의 저장소인 스토어에서 관리하게 됩니다. 스토어를 구독한 컴포넌트의 상태가 업데이트가 되거나 전달되는 props를 변경해야 할 때 상태 값을 변경해 줄 리듀서 함수를 생성하고, 디스패치를 사용하여 상태와 액션 객체를 전달받습니다. 액션 객체는 미리 정의돼 있는 정보 패키지로 객체여야 하며 그 key 이름은 항상 type이고 , 어떤 종류의 액션인지 알 수 있습니다. payload는 전달된 액션에 필요한 데이터를 말합니다. 그 값에 따라 리듀서 함수가 상태를 수정하면, 변경된 상태가 렌더링되어 어플리케이션에 보여지게됩니다.