본문으로 건너뛰기

Redux Essentials, Part 1: Redux 개요와 핵심 개념

비공식 베타 번역

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

학습 내용
  • Redux가 무엇이며 사용하는 이유
  • Redux의 핵심 용어와 개념
  • Redux 앱에서의 데이터 흐름

소개

Redux Essentials 튜토리얼에 오신 것을 환영합니다! 이 튜토리얼은 Redux를 소개하고 최신 권장 도구와 모범 사례를 사용해 올바른 사용법을 가르칩니다. 수료 시점에는 여기서 배운 도구와 패턴으로 직접 Redux 애플리케이션을 구축할 수 있을 것입니다.

Part 1에서는 Redux 사용에 필요한 핵심 개념과 용어를 다루며, Part 2: Redux 앱 구조에서는 일반적인 React + Redux 앱을 살펴보고 각 요소가 어떻게 조화되는지 확인합니다.

Part 3: 기본 Redux 데이터 흐름부터는 이 지식을 활용해 실제 기능을 가진 소규모 소셜 미디어 피드 앱을 구축하며, 각 요소가 실제로 어떻게 작동하는지 확인하고 Redux 사용의 중요한 패턴과 가이드라인에 대해 논의합니다.

튜토리얼 활용 방법

이 튜토리얼은 올바른 Redux 사용법을 보여주는 데 초점을 맞추며, Redux 앱을 올바르게 구축하는 방법을 이해할 수 있도록 개념을 설명합니다.

초보자도 이해할 수 있도록 설명하려 노력했지만, 이미 알고 있어야 할 몇 가지 사항이 있습니다:

필수 사전 지식

해당 주제들에 익숙하지 않다면, 먼저 충분히 숙지한 후 Redux 학습을 재개할 것을 권장합니다. 준비되면 다시 만나요!

또한 브라우저에 React 및 Redux DevTools 확장 프로그램이 설치되어 있는지 확인하세요:

Redux란?

"Redux"가 무엇인지 기본적인 이해가 필요합니다. 어떤 역할을 하며 어떤 문제를 해결해주나요? 왜 사용해야 할까요?

Redux는 전역 애플리케이션 상태 관리를 위한 패턴과 라이브러리로, UI가 발생한 사건을 설명하는 "액션" 이벤트를 트리거하면 별도의 업데이트 로직인 "리듀서"가 응답으로 상태를 업데이트합니다. 애플리케이션 전체에서 사용해야 하는 상태를 위한 중앙 저장소 역할을 하며, 상태가 예측 가능한 방식으로만 업데이트되도록 보장하는 규칙을 제공합니다.

왜 Redux를 사용해야 하나요?

Redux는 애플리케이션의 여러 부분에서 필요한 "전역" 상태 관리를 도와줍니다.

Redux가 제공하는 패턴과 도구는 애플리케이션 상태가 언제, 어디서, 왜, 어떻게 업데이트되는지, 그리고 변경 발생 시 애플리케이션 로직이 어떻게 동작할지 이해하기 쉽게 합니다. Redux는 예측 가능하고 테스트 가능한 코드 작성을 유도하여 애플리케이션이 예상대로 작동할 것이라는 확신을 줍니다.

언제 Redux를 사용해야 할까요?

Redux는 공유 상태 관리를 도와주지만, 다른 도구와 마찬가지로 장단점이 있습니다. 배워야 할 개념이 더 많고 작성해야 할 코드도 더 많습니다. 또한 코드에 약간의 간접성을 추가하며 특정 제약을 따르도록 요구합니다. 이는 단기 생산성과 장기 생산성 사이의 균형 문제입니다.

Redux는 다음과 같은 경우에 더 유용합니다:

  • 애플리케이션의 여러 부분에서 필요한 대량의 상태가 존재할 때

  • 애플리케이션 상태가 시간에 따라 자주 업데이트될 때

  • 상태 업데이트 로직이 복잡할 때

  • 중간 규모 이상의 코드베이스를 가진 애플리케이션이며 여러 사람이 함께 작업할 때

모든 앱이 Redux를 필요로 하는 것은 아닙니다. 여러분이 구축하려는 앱의 종류를 생각해보고, 해결하려는 문제에 가장 적합한 도구가 무엇인지 결정하는 시간을 가지세요.

더 알아보기

Redux가 여러분의 앱에 적합한 선택인지 확실하지 않다면, 다음 자료들이 추가적인 지침을 제공합니다:

Redux 라이브러리와 도구

Redux의 핵심은 작은 독립형 JS 라이브러리입니다. 일반적으로 다른 패키지들과 함께 사용됩니다:

Redux Toolkit

Redux Toolkit은 Redux 로직 작성을 위해 권장하는 접근 방식입니다. Redux 앱 구축에 필수적이라고 생각하는 패키지와 함수들을 포함하고 있습니다. Redux Toolkit은 권장하는 모범 사례를 내장하고, 대부분의 Redux 작업을 단순화하며, 일반적인 실수를 방지하고, Redux 애플리케이션 작성을 더 쉽게 만듭니다.

React-Redux

Redux는 어떤 UI 프레임워크와도 통합될 수 있으며, 가장 빈번하게 React와 함께 사용됩니다. React-Redux는 React 컴포넌트가 상태 일부를 읽고 스토어를 업데이트하기 위한 액션을 디스패치하여 Redux 스토어와 상호작용할 수 있도록 하는 공식 패키지입니다.

Redux DevTools 확장 프로그램

Redux DevTools 확장 프로그램은 시간에 따른 Redux 스토어의 상태 변경 이력을 보여줍니다. 이를 통해 "시간 여행 디버깅"과 같은 강력한 기법을 포함하여 애플리케이션을 효과적으로 디버깅할 수 있습니다.

Redux 용어와 개념

실제 코드를 살펴보기 전에, 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): 앱을 구동하는 진리의 원천(source of truth)

  • 뷰(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')

React와 Redux는 모든 상태 업데이트가 불변 방식으로 수행될 것을 기대합니다. 이 내용의 중요성과 불변 업데이트 로직을 더 쉽게 작성하는 방법은 조금 뒤에 살펴보겠습니다.

더 알아보기

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

용어 정리

계속하기 전에 반드시 숙지해야 할 중요한 Redux 용어들입니다:

액션(Actions)

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

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

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

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

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

액션 생성자(Action Creators)

액션 생성자는 액션 객체를 생성하고 반환하는 함수입니다. 매번 액션 객체를 직접 작성하지 않도록 주로 사용합니다:

const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}

리듀서(Reducers)

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

정보

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

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

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

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

  • 순수 함수(pure)여야 합니다 - 비동기 로직 처리, 랜덤 값 생성, 기타 "사이드 이펙트"를 발생시켜서는 안 됩니다

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

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

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

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

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

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
// Check to see if the reducer cares about this action
if (action.type === 'counter/increment') {
// 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/increment' },
{ type: 'counter/increment' },
{ type: 'counter/increment' }
]

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/increment' })

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

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

일반적으로 올바른 액션을 디스패치하기 위해 액션 생성자(action creator)를 호출합니다:

const increment = () => {
return {
type: 'counter/increment'
}
}

store.dispatch(increment())

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

셀렉터(Selectors)

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

const selectCounterValue = state => state.value

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

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

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

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

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

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

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

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

  • 초기 설정:

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

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

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

Redux 데이터 흐름 다이어그램

학습 내용 요약

Redux에는 기억해야 할 새로운 용어와 개념이 많습니다. 지금까지 배운 내용을 요약해 보겠습니다:

요약
  • Redux는 전역 애플리케이션 상태 관리를 위한 라이브러리입니다
    • Redux는 일반적으로 React-Redux 라이브러리와 함께 사용되어 Redux와 React 통합
    • Redux Toolkit은 Redux 로직 작성을 위한 표준 방식
  • Redux 업데이트 패턴은 "무슨 일이 발생했는지"와 "상태가 어떻게 변하는지"를 분리합니다
    • 액션(Actions)type 필드를 가진 일반 객체로 앱에서 "무슨 일이 발생했는지" 설명
    • 리듀서(Reducers) 는 이전 상태와 액션을 기반으로 새 상태 값을 계산하는 함수
    • Redux 스토어(store) 는 액션이 디스패치(dispatched) 될 때마다 루트 리듀서 실행
  • Redux는 "단방향 데이터 흐름" 앱 구조를 사용합니다
    • 상태는 특정 시점의 앱 상태를 나타내며 UI는 이 상태를 기반으로 렌더링
    • 앱에서 이벤트 발생 시:
      • UI가 액션 디스패치
      • 스토어가 리듀서를 실행하고 발생한 사건에 따라 상태 업데이트
      • 스토어가 상태 변경을 UI에 알림
    • UI가 새 상태를 기반으로 리렌더링

다음 단계

이제 Redux 앱의 개별 조각들을 모두 살펴봤습니다. 다음으로 2부: Redux Toolkit 앱 구조에서 전체 예제를 통해 이러한 조각들이 어떻게 함께 작동하는지 알아보겠습니다.