본문으로 건너뛰기

Redux 핵심 개념, 파트 2: 개념과 데이터 흐름

비공식 베타 번역

이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →

학습 내용
  • Redux 사용을 위한 주요 용어와 개념
  • Redux 애플리케이션에서의 데이터 흐름

소개

파트 1: Redux 개요에서는 Redux가 무엇인지, 사용하는 이유, 그리고 일반적으로 Redux 코어와 함께 사용되는 다른 라이브러리들을 살펴보았습니다. 또한 실제 동작하는 Redux 애플리케이션의 모습과 애플리케이션을 구성하는 요소들을 확인했습니다. 마지막으로 Redux에서 사용되는 일부 용어와 개념을 간략히 소개했습니다.

이번 섹션에서는 이러한 용어와 개념을 더 자세히 살펴보고, Redux 애플리케이션에서 데이터가 어떻게 흐르는지에 대해 알아보겠습니다.

비공식 베타 번역

이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →

주의

참고: 이 튜토리얼은 Redux의 원리와 개념을 설명하기 위해 의도적으로 오래된 스타일의 Redux 로직 패턴을 보여줍니다. 이 패턴은 오늘날 우리가 Redux 앱 구축을 위한 올바른 접근법으로 가르치는 '현대적인 Redux(modern Redux)' 패턴(Redux Toolkit 사용)보다 더 많은 코드가 필요합니다. 이 튜토리얼은 프로덕션 환경에서 바로 사용할 수 있는 프로젝트가 아닙니다.

'현대적인 Redux'와 Redux Toolkit 사용법을 배우려면 다음 페이지를 참조하세요:

배경 개념

실제 코드를 살펴보기 전에, Redux를 사용하기 위해 알아야 할 몇 가지 용어와 개념에 대해 이야기해 보겠습니다.

상태 관리

작은 React 카운터 컴포넌트를 살펴보면서 시작해 봅시다. 이 컴포넌트는 컴포넌트 상태로 숫자를 추적하고, 버튼을 클릭하면 숫자를 증가시킵니다:

function Counter() {
// State: a counter value
const [counter, setCounter] = useState(0)

// Action: code that causes an update to the state when something happens
const increment = () => {
setCounter(prevCounter => prevCounter + 1)
}

// View: the UI definition
return (
<div>
Value: {counter} <button onClick={increment}>Increment</button>
</div>
)
}

이는 다음과 같은 부분을 가진 독립형 앱입니다:

  • 상태(state): 애플리케이션을 구동하는 진리의 원천

  • 뷰(view): 현재 상태를 기반으로 한 UI의 선언적 설명

  • 액션(actions): 사용자 입력을 기반으로 앱에서 발생하는 이벤트로, 상태 업데이트를 트리거함

이는 **"단방향 데이터 흐름"**의 작은 예시입니다:

  • 상태(state)는 특정 시점의 애플리케이션 상태를 설명합니다

  • UI는 해당 상태를 기반으로 렌더링됩니다

  • 사용자 버튼 클릭 같은 이벤트 발생 시, 발생한 내용을 기반으로 상태가 업데이트됩니다

  • UI는 새로운 상태를 기반으로 다시 렌더링됩니다

단방향 데이터 흐름

그러나 동일한 상태를 공유하고 사용해야 하는 여러 컴포넌트가 존재할 때는 이 간단한 구조가 깨질 수 있습니다. 특히 이러한 컴포넌트들이 애플리케이션의 서로 다른 위치에 있을 때 더욱 그렇습니다. 때로는 "상태 끌어올리기"로 부모 컴포넌트로 이동시켜 해결할 수 있지만, 항상 효과적이지는 않습니다.

이를 해결하는 한 가지 방법은 공유 상태를 컴포넌트에서 추출해 컴포넌트 트리 외부의 중앙 집중식 위치에 두는 것입니다. 이렇게 하면 컴포넌트 트리가 하나의 큰 "뷰"가 되며, 트리 내 어디에 위치하든 모든 컴포넌트가 상태에 접근하거나 액션을 트리거할 수 있습니다!

상태 관리 관련 개념을 정의하고 분리하며, 뷰와 상태 간 독립성을 유지하는 규칙을 적용함으로써 코드 구조와 유지보수성을 개선할 수 있습니다.

이것이 Redux의 기본 아이디어입니다: 애플리케이션의 전역 상태를 담을 단일 중앙 저장소와, 코드 예측 가능성을 높이기 위한 상태 업데이트 패턴을 제공합니다.

불변성(Immutability)

"변경 가능(Mutable)"은 "수정할 수 있음"을 의미합니다. "불변(Immutable)"은 절대 변경될 수 없음을 뜻합니다.

JavaScript 객체와 배열은 기본적으로 모두 변경 가능합니다. 객체를 생성하면 필드 내용을 변경할 수 있고, 배열을 생성하면 요소 역시 수정할 수 있습니다:

const obj = { a: 1, b: 2 }
// still the same object outside, but the contents have changed
obj.b = 3

const arr = ['a', 'b']
// In the same way, we can change the contents of this array
arr.push('c')
arr[1] = 'd'

이를 객체나 배열을 *변경(Mutating)*한다고 표현합니다. 메모리 상의 동일한 객체/배열 참조지만 내부 내용이 변경된 것입니다.

값을 불변 방식으로 업데이트하려면 기존 객체/배열의 복사본을 만들고, 이 복사본을 수정해야 합니다.

JavaScript의 배열/객체 전개 연산자나 원본 배열을 변경하지 않고 새 복사본을 반환하는 배열 메서드를 사용하면 됩니다:

const obj = {
a: {
// To safely update obj.a.c, we have to copy each piece
c: 3
},
b: 2
}

const obj2 = {
// copy obj
...obj,
// overwrite a
a: {
// copy obj.a
...obj.a,
// overwrite c
c: 42
}
}

const arr = ['a', 'b']
// Create a new copy of arr, with "c" appended to the end
const arr2 = arr.concat('c')

// or, we can make a copy of the original array:
const arr3 = arr.slice()
// and mutate the copy:
arr3.push('c')

Redux는 모든 상태 업데이트가 불변(immutable) 방식으로 처리될 것을 기대합니다. 이 중요성과 간편한 불변 업데이트 로직 작성 방법에 대해서는 잠시 후에 자세히 다루겠습니다.

더 알아보기

JavaScript에서 불변성이 작동하는 방식에 대한 자세한 정보:

Redux 용어

계속 진행하기 전에 반드시 익혀야 할 중요한 Redux 용어들이 있습니다:

액션(Actions)

액션type 필드를 가진 평범한 JavaScript 객체입니다. 액션은 애플리케이션에서 발생한 사건을 설명하는 이벤트로 생각할 수 있습니다.

type 필드는 "todos/todoAdded"처럼 액션을 설명하는 문자열이어야 합니다. 일반적으로 "domain/eventName" 형식으로 작성하며, 앞부분은 해당 액션이 속한 기능/카테고리, 뒷부분은 발생한 구체적 사건을 나타냅니다.

액션 객체에는 발생한 사건에 대한 추가 정보를 담은 다른 필드들이 포함될 수 있습니다. 관례상 이 정보는 payload 필드에 넣습니다.

전형적인 액션 객체의 예시:

const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}

리듀서(Reducers)

리듀서는 현재 stateaction 객체를 받아 필요한 경우 상태를 업데이트하는 방법을 결정하고 새로운 상태를 반환하는 함수입니다: (state, action) => newState. 리듀서를 수신된 액션(이벤트) 타입을 기반으로 이벤트를 처리하는 이벤트 리스너로 생각할 수 있습니다.

정보

"리듀서" 함수의 이름은 Array.reduce() 메서드에 전달하는 콜백 함수와 유사하기 때문에 붙여졌습니다.

리듀서는 반드시 다음과 같은 특정 규칙을 항상 따라야 합니다:

  • 오직 stateaction 인수를 기반으로 새 상태 값을 계산해야 합니다

  • 기존 state를 수정할 수 없습니다. 대신 기존 state를 복사하고 복사본에 변경 사항을 적용하는 _불변성 업데이트_를 수행해야 합니다

  • 비동기 로직 실행, 랜덤 값 계산, 기타 "사이드 이펙트" 발생을 금지합니다

이 규칙들의 중요성과 올바른 적용 방법에 대해서는 나중에 자세히 설명하겠습니다.

리듀서 함수 내부 로직은 일반적으로 동일한 단계를 따릅니다:

  • 현재 액션을 처리할지 확인합니다

    • 처리한다면 상태를 복사하고, 복사본을 새로운 값으로 업데이트한 후 반환합니다
  • 그렇지 않다면 기존 상태를 변경 없이 반환합니다

다음은 각 리듀서가 따라야 할 단계를 보여주는 간단한 예시입니다:

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
// Check to see if the reducer cares about this action
if (action.type === 'counter/incremented') {
// If so, make a copy of `state`
return {
...state,
// and update the copy with the new value
value: state.value + 1
}
}
// otherwise return the existing state unchanged
return state
}

리듀서 내부에서는 새로운 상태를 결정하기 위해 if/else, switch, 반복문 등 어떤 종류의 로직도 사용할 수 있습니다.

Detailed Explanation: Why Are They Called 'Reducers?'

The Array.reduce() method lets you take an array of values, process each item in the array one at a time, and return a single final result. You can think of it as "reducing the array down to one value".

Array.reduce() takes a callback function as an argument, which will be called one time for each item in the array. It takes two arguments:

  • previousResult, the value that your callback returned last time
  • currentItem, the current item in the array

The first time that the callback runs, there isn't a previousResult available, so we need to also pass in an initial value that will be used as the first previousResult.

If we wanted to add together an array of numbers to find out what the total is, we could write a reduce callback that looks like this:

const numbers = [2, 5, 8]

const addNumbers = (previousResult, currentItem) => {
console.log({ previousResult, currentItem })
return previousResult + currentItem
}

const initialValue = 0

const total = numbers.reduce(addNumbers, initialValue)
// {previousResult: 0, currentItem: 2}
// {previousResult: 2, currentItem: 5}
// {previousResult: 7, currentItem: 8}

console.log(total)
// 15

Notice that this addNumbers "reduce callback" function doesn't need to keep track of anything itself. It takes the previousResult and currentItem arguments, does something with them, and returns a new result value.

A Redux reducer function is exactly the same idea as this "reduce callback" function! It takes a "previous result" (the state), and the "current item" (the action object), decides a new state value based on those arguments, and returns that new state.

If we were to create an array of Redux actions, call reduce(), and pass in a reducer function, we'd get a final result the same way:

const actions = [
{ type: 'counter/incremented' },
{ type: 'counter/incremented' },
{ type: 'counter/incremented' }
]

const initialState = { value: 0 }

const finalResult = actions.reduce(counterReducer, initialState)
console.log(finalResult)
// {value: 3}

We can say that Redux reducers reduce a set of actions (over time) into a single state. The difference is that with Array.reduce() it happens all at once, and with Redux, it happens over the lifetime of your running app.

스토어(Store)

현재 Redux 애플리케이션 상태는 스토어(store) 라는 객체에 저장됩니다.

스토어는 리듀서를 전달하여 생성되며, 현재 상태 값을 반환하는 getState 메서드를 가지고 있습니다:

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}

디스패치(Dispatch)

Redux 스토어에는 dispatch 메서드가 있습니다. 상태를 업데이트하는 유일한 방법은 store.dispatch()를 호출하고 액션 객체를 전달하는 것입니다. 스토어는 리듀서 함수를 실행하고 새로운 상태 값을 저장하며, getState()를 호출하여 업데이트된 값을 가져올 수 있습니다:

store.dispatch({ type: 'counter/incremented' })

console.log(store.getState())
// {value: 1}

액션 디스패치를 애플리케이션에서 "이벤트 트리거"로 생각할 수 있습니다. 무언가 발생했고, 스토어가 이를 인지해야 합니다. 리듀서는 이벤트 리스너처럼 동작하며, 자신이 관심 있는 액션을 감지하면 상태를 업데이트하여 응답합니다.

셀렉터(Selectors)

셀렉터(Selector) 는 스토어 상태 값에서 특정 정보를 추출하는 방법을 알고 있는 함수입니다. 애플리케이션이 커질수록 앱의 다른 부분이 동일한 데이터를 읽어야 할 때 로직 반복을 피하는 데 도움이 됩니다:

const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

핵심 개념과 원칙

전반적으로 Redux 설계의 의도를 세 가지 핵심 개념으로 요약할 수 있습니다:

단일 진실 공급원(Single Source of Truth)

애플리케이션의 **전역 상태(global state)**는 단일 스토어(store) 내부에 객체 형태로 저장됩니다. 모든 데이터는 여러 곳에 중복되지 않고 단일 위치에만 존재해야 합니다.

이렇게 하면 상태 변경 시 애플리케이션 디버깅과 상태 검사가 용이해지며, 전체 애플리케이션과 상호작용해야 하는 로직을 중앙 집중화할 수 있습니다.

이것이 애플리케이션의 모든 상태가 Redux 스토어에 들어가야 한다는 의미는 아닙니다! 상태가 필요한 위치를 기반으로 상태를 Redux에 넣을지 UI 컴포넌트에 둘지 결정해야 합니다.

상태는 읽기 전용(State is Read-Only)

상태를 변경하는 유일한 방법은 **액션(action)**을 디스패치(dispatch)하는 것입니다. 액션은 발생한 사건을 설명하는 객체입니다.

이렇게 하면 UI가 실수로 데이터를 덮어쓰는 것을 방지할 수 있으며, 상태 업데이트 원인을 추적하기 쉬워집니다. 액션은 평범한 JS 객체이므로 로깅, 직렬화, 저장이 가능하며 디버깅이나 테스트 목적으로 재현할 수 있습니다.

순수 리듀서 함수로 변경 수행(Changes are Made with Pure Reducer Functions)

액션을 기반으로 상태 트리를 업데이트하는 방법을 지정하기 위해 리듀서(reducer) 함수를 작성합니다. 리듀서는 이전 상태와 액션을 받아 다음 상태를 반환하는 순수 함수입니다. 다른 함수들과 마찬가지로 리듀서를 더 작은 함수로 분할하거나 일반적인 작업을 위한 재사용 가능한 리듀서를 작성할 수 있습니다.

Redux 애플리케이션 데이터 흐름

앞서 "단방향 데이터 흐름"에 대해 설명했는데, 이는 애플리케이션을 업데이트하는 다음 단계 순서를 의미합니다:

  • 상태(state)는 특정 시점의 애플리케이션 상태를 설명합니다

  • UI는 해당 상태를 기반으로 렌더링됩니다

  • 사용자 버튼 클릭 같은 이벤트 발생 시, 발생한 내용을 기반으로 상태가 업데이트됩니다

  • UI는 새로운 상태를 기반으로 다시 렌더링됩니다

Redux의 경우 이 단계를 더 세분화할 수 있습니다:

  • 초기 설정:

    • 루트 리듀서 함수를 사용해 Redux 스토어 생성
    • 스토어가 루트 리듀서를 한 번 호출하고 반환값을 초기 state로 저장
    • UI가 처음 렌더링될 때 UI 컴포넌트는 Redux 스토어의 현재 상태에 접근하여 렌더링 내용을 결정합니다. 또한 향후 스토어 업데이트를 구독하여 상태 변경 시 알림을 받습니다.
  • 업데이트:

    • 사용자가 버튼을 클릭하는 등 애플리케이션에서 이벤트 발생
    • 앱 코드가 dispatch({type: 'counter/incremented'})처럼 Redux 스토어에 액션 디스패치
    • 스토어가 이전 state와 현재 action으로 리듀서 함수를 다시 실행하고, 반환값을 새로운 state로 저장
    • 스토어 업데이트를 구독 중인 모든 UI 부분에 변경 사실 알림
    • 스토어 데이터가 필요한 각 UI 컴포넌트는 필요한 상태 부분의 변경 여부 확인
    • 데이터 변경을 감지한 각 컴포넌트는 새 데이터로 강제 리렌더링하여 화면 갱신

이 데이터 흐름을 시각적으로 나타내면 다음과 같습니다:

Redux 데이터 흐름 다이어그램

학습 내용 요약

요약
  • Redux의 핵심 원칙은 세 가지로 요약 가능
    • 전역 애플리케이션 상태를 단일 스토어에서 관리
    • 스토어 상태는 앱 내 다른 부분에서 읽기 전용
    • 액션에 대한 응답으로 리듀서 함수가 상태 업데이트 수행
  • Redux는 "단방향 데이터 흐름" 아키텍처 사용
    • 상태는 특정 시점의 애플리케이션 상태를 나타내며, UI는 해당 상태 기반으로 렌더링
    • 애플리케이션에서 이벤트 발생 시:
      • UI가 액션 디스패치
      • 스토어가 리듀서 실행하고 발생한 이벤트 기반으로 상태 업데이트
      • 스토어가 UI에 상태 변경 알림
    • UI가 새 상태 기반으로 리렌더링

다음 단계

이제 Redux 애플리케이션의 다양한 부분을 설명하는 핵심 개념과 용어에 익숙해지셨을 것입니다.

이제 3부: 상태, 액션, 리듀서에서 새 Redux 애플리케이션을 구축하며 이 요소들이 어떻게 협력하는지 살펴보겠습니다.