본문으로 건너뛰기

Redux 핵심 개념, 파트 3: 기본 Redux 데이터 플로우

비공식 베타 번역

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

학습 내용
  • React 애플리케이션에서 Redux 스토어 설정 방법
  • createSlice로 Redux 스토어에 리듀서 로직 "슬라이스" 추가 방법
  • useSelector 훅으로 컴포넌트에서 Redux 데이터 읽기
  • useDispatch 훅으로 컴포넌트에서 액션 디스패치하기
선수 지식

소개

파트 1: Redux 개요 및 개념에서 Redux가 전역 앱 상태를 중앙 집중식으로 관리하여 유지보수 가능한 앱을 구축하는 방법을 살펴보았습니다. 또한 액션 객체 디스패치, 새로운 상태 값을 반환하는 리듀서 함수 사용, thunk를 이용한 비동기 로직 작성 같은 핵심 Redux 개념도 다루었습니다. 파트 2: Redux Toolkit 앱 구조에서는 Redux Toolkit의 configureStore, createSlice API와 React-Redux의 Provider, useSelector가 함께 작동하여 Redux 로직을 작성하고 React 컴포넌트에서 해당 로직과 상호작용하는 방식을 확인했습니다.

이제 여러분은 이러한 조각들이 무엇인지 어느 정도 알게 되었으니, 이 지식을 실전에 적용해 볼 차례입니다. 실제 사용 사례를 보여주는 여러 기능을 포함한 소규모 소셜 미디어 피드 앱을 구축할 예정입니다. 이를 통해 여러분이 직접 애플리케이션에서 Redux를 사용하는 방법을 이해하는 데 도움이 될 것입니다.

코드를 작성할 때는 TypeScript 문법을 사용할 것입니다. Redux는 일반 JavaScript로도 사용할 수 있지만, TypeScript를 사용하면 흔히 발생하는 실수를 방지하고 코드에 내장된 문서화를 제공하며, React 컴포넌트나 Redux 리듀서 같은 곳에서 필요한 변수 타입을 편집기에서 보여줍니다. 모든 Redux 애플리케이션에 TypeScript 사용을 적극 권장합니다.

주의

예제 앱은 완성된 프로덕션 준비 프로젝트가 아닙니다. 목표는 Redux API와 일반적인 사용 패턴을 배우고, 제한된 예시를 통해 올바른 방향을 제시하는 것입니다. 또한 초기에 구축한 일부 내용은 더 나은 방법을 보여주기 위해 나중에 업데이트될 예정입니다. 사용된 모든 개념을 확인하려면 전체 튜토리얼을 꼭 읽어주세요.

프로젝트 설정

이 튜토리얼을 위해 React와 Redux가 미리 설정되어 있고 기본 스타일이 적용되었으며, 앱에서 실제 API 요청을 작성할 수 있도록 가짜 REST API가 포함된 사전 구성된 스타터 프로젝트를 만들었습니다. 여러분은 실제 애플리케이션 코드를 작성하는 기반으로 이 프로젝트를 사용하게 될 것입니다.

시작하려면 이 CodeSandbox를 열고 포크하세요:

이 GitHub 저장소에서 동일한 프로젝트를 클론할 수도 있습니다. 프로젝트는 패키지 관리자로 Yarn 4를 사용하도록 구성되어 있지만, 원하는 패키지 관리자(NPM, PNPM, Bun)를 사용해도 됩니다. 패키지를 설치한 후 yarn dev 명령어로 로컬 개발 서버를 시작할 수 있습니다.

만약 우리가 만들 예정인 최종 버전을 미리 보고 싶다면 tutorial-steps-ts 브랜치를 확인하거나 CodeSandbox에서 최종 버전을 살펴보세요.

Tania Rascia에게 감사드립니다. 그녀의 React와 함께 Redux 사용하기 튜토리얼이 이 페이지의 예시에 영감을 주었습니다. 또한 스타일링을 위해 그녀의 Primitive UI CSS 스타터를 사용했습니다.

새 Redux + React 프로젝트 생성하기

이 튜토리얼을 마친 후에는 자신만의 프로젝트를 시도해보고 싶을 것입니다. 새 Redux + React 프로젝트를 만드는 가장 빠른 방법으로 Vite 및 Next.js용 Redux 템플릿 사용을 권장합니다. 이 템플릿에는 Redux Toolkit과 React-Redux가 이미 구성되어 있으며, Part 1에서 본 "카운터" 앱 예제를 사용합니다. 이를 통해 Redux 패키지를 추가하거나 스토어를 설정할 필요 없이 실제 애플리케이션 코드 작성으로 바로 넘어갈 수 있습니다.

초기 프로젝트 살펴보기

초기 프로젝트에 포함된 내용을 간단히 살펴보겠습니다:

  • /public: 기본 CSS 스타일 및 아이콘 등의 정적 파일

  • /src

    • main.tsx: 애플리케이션 진입점 파일, <App> 컴포넌트를 렌더링합니다. 이 예제에서는 페이지 로드 시 가짜 REST API도 설정합니다.
    • App.tsx: 메인 애플리케이션 컴포넌트. 상단 네비게이션 바를 렌더링하고 다른 콘텐츠에 대한 클라이언트 측 라우팅을 처리합니다.
    • index.css: 전체 애플리케이션 스타일
    • /api
      • client.ts: HTTP GET 및 POST 요청을 할 수 있는 작은 fetch 래퍼 클라이언트
      • server.ts: 데이터를 위한 가짜 REST API 제공. 나중에 앱이 이 가짜 엔드포인트에서 데이터를 가져옵니다.
    • /app
      • Navbar.tsx: 상단 헤더 및 네비게이션 콘텐츠 렌더링

지금 앱을 로드하면 헤더와 환영 메시지가 표시되지만 기능은 아직 없습니다.

이제 시작해 봅시다!

Redux 스토어 설정하기

현재 프로젝트는 비어 있으므로 Redux 조각에 대한 일회성 설정부터 시작해야 합니다.

Redux 패키지 추가

package.json을 보면 Redux 사용에 필요한 두 패키지가 이미 설치되어 있음을 확인할 수 있습니다:

  • @reduxjs/toolkit: 모던 Redux 패키지, 앱 구축에 사용할 모든 Redux 함수 포함

  • react-redux: React 컴포넌트가 Redux 스토어와 통신할 수 있게 하는 함수들

처음부터 프로젝트를 설정하는 경우 먼저 이 패키지들을 프로젝트에 직접 추가하세요.

스토어 생성

첫 번째 단계는 실제 Redux 스토어를 생성하는 것입니다. Redux 원칙 중 하나는 전체 애플리케이션에 대해 단 하나의 스토어 인스턴스만 있어야 한다는 것입니다.

일반적으로 Redux 스토어 인스턴스를 자체 파일에서 생성하고 내보냅니다. 애플리케이션의 실제 폴더 구조는 사용자에 따라 다르지만, 애플리케이션 전체 설정 및 구성을 src/app/ 폴더에 두는 것이 표준입니다.

src/app/store.ts 파일을 추가하고 스토어를 생성하는 것부터 시작하겠습니다.

Redux Toolkit에는 configureStore라는 메서드가 포함되어 있습니다. 이 함수는 새로운 Redux 스토어 인스턴스를 생성합니다. 스토어 동작을 변경하기 위해 전달할 수 있는 여러 옵션이 있습니다. 또한 일반적인 실수를 확인하고, 상태 내용과 액션 기록을 볼 수 있도록 Redux DevTools 확장을 활성화하는 등 가장 일반적이고 유용한 구성 설정을 자동으로 적용합니다.

src/app/store.ts
import { configureStore } from '@reduxjs/toolkit'
import type { Action } from '@reduxjs/toolkit'

interface CounterState {
value: number
}

// An example slice reducer function that shows how a Redux reducer works inside.
// We'll replace this soon with real app logic.
function counterReducer(state: CounterState = { value: 0 }, action: Action) {
switch (action.type) {
// Handle actions here
default: {
return state
}
}
}

export const store = configureStore({
// Pass in the root reducer setup as the `reducer` argument
reducer: {
// Declare that `state.counter` will be updated by the `counterReducer` function
counter: counterReducer
}
})

configureStore에는 항상 reducer 옵션이 필요합니다. 일반적으로 애플리케이션의 다양한 부분을 위한 개별 "슬라이스 리듀서"를 포함하는 객체여야 합니다. (필요한 경우 루트 리듀서 함수를 별도로 생성하여 reducer 인자로 전달할 수도 있습니다.)

이 첫 번째 단계에서는 설정이 어떻게 보이는지 보여주기 위해 counter 슬라이스에 대한 모의(mock) 슬라이스 리듀서 함수를 전달합니다. 곧 실제로 구축할 애플리케이션을 위한 진짜 슬라이스 리듀서로 교체할 예정입니다.

Next.js와 함께 설정하기

Next.js를 사용하는 경우 설정 과정에 몇 가지 추가 단계가 필요합니다. Next.js와 함께 Redux를 설정하는 방법에 대한 자세한 내용은 Next.js와 함께 설정하기 페이지를 참조하세요.

스토어 제공하기

Redux 자체는 순수 JavaScript 라이브러리이며 어떤 UI 레이어와도 함께 작동할 수 있습니다. 이 애플리케이션에서는 React를 사용하므로 React 컴포넌트가 Redux 스토어와 상호작용할 수 있도록 해야 합니다.

이를 위해 React-Redux 라이브러리를 사용하고 Redux 스토어를 <Provider> 컴포넌트에 전달해야 합니다. 이는 React의 Context API를 활용하여 애플리케이션의 모든 React 컴포넌트가 Redux 스토어에 접근할 수 있도록 합니다.

다른 애플리케이션 코드 파일에서 Redux 스토어를 직접 가져오려고 해서는 안 됩니다! 스토어 파일은 하나뿐이므로 스토어를 직접 가져오면 실수로 순환 임포트 문제(파일 A가 B를, B가 C를, C가 A를 임포트하는 경우)를 일으켜 추적하기 어려운 버그로 이어질 수 있습니다. 또한 컴포넌트와 Redux 로직에 대한 테스트 작성이 가능해야 하는데, 이러한 테스트에는 자체적인 Redux 스토어 인스턴스 생성이 필요합니다. Context를 통해 컴포넌트에 스토어를 제공하면 이러한 유연성을 유지하면서 임포트 문제를 피할 수 있습니다.

이를 위해 store를 진입점 파일인 main.tsx로 임포트하고 <App> 컴포넌트를 스토어가 포함된 <Provider>로 감쌉니다:

src/main.tsx
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'

import App from './App'
import { store } from './app/store'

// skip mock API setup

const root = createRoot(document.getElementById('root')!)

root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)

Redux 상태 검사하기

이제 스토어가 있으므로 Redux DevTools 확장 프로그램을 사용해 현재 Redux 상태를 확인할 수 있습니다.

브라우저의 DevTools 보기(예: 페이지 아무 곳이나 마우스 오른쪽 버튼으로 클릭하고 "검사(Inspect)" 선택)를 열고 "Redux" 탭을 클릭하면 디스패치된 액션 기록과 현재 상태 값을 볼 수 있습니다:

Redux DevTools: 초기 앱 상태

현재 상태 값은 다음과 같은 객체 형태여야 합니다:

{
counter: {
value: 0
}
}

이 형태는 configureStore에 전달한 reducer 옵션에 의해 정의됩니다: 객체이며 counter라는 필드가 있고, counter 필드에 대한 슬라이스 리듀서는 상태로 {value} 같은 객체를 반환합니다.

스토어 타입 내보내기

TypeScript를 사용 중이므로 "Redux 상태의 타입"과 "Redux 스토어 dispatch 함수의 타입"을 위한 TS 타입을 자주 참조하게 됩니다.

이러한 타입들을 store.ts 파일에서 내보내야 합니다. TS의 typeof 연산자를 사용해 Redux 스토어 정의를 기반으로 타입을 추론하도록 요청하여 타입을 정의합니다:

src/app/store.ts
import { configureStore } from '@reduxjs/toolkit'

// omit counter slice setup

export const store = configureStore({
reducer: {
counter: counterReducer
}
})

// Infer the type of `store`
export type AppStore = typeof store
// Infer the `AppDispatch` type from the store itself
export type AppDispatch = typeof store.dispatch
// Same for the `RootState` type
export type RootState = ReturnType<typeof store.getState>

에디터에서 RootState 타입 위로 마우스를 가져가면 type RootState = { counter: CounterState; }가 표시될 것입니다. 이 타입은 스토어 정의에서 자동으로 파생되었으므로 향후 reducer 설정의 모든 변경 사항이 자동으로 RootState 타입에 반영됩니다. 이렇게 하면 한 번만 정의해도 항상 정확하게 유지됩니다.

타이핑된 훅 내보내기

컴포넌트에서 React-Redux의 useSelectoruseDispatch 훅을 광범위하게 사용할 예정입니다. 이 훅들은 사용할 때마다 RootStateAppDispatch 타입을 참조해야 합니다.

사용을 단순화하고 타입을 반복하지 않으려면 미리 타입이 지정된(pre-typed) 버전의 훅을 설정하여 올바른 타입이 내장된 상태로 사용할 수 있습니다.

React-Redux 9.1에는 올바른 타입을 적용하는 .withTypes() 메서드가 포함되어 있습니다. 이러한 미리 타입된 훅을 내보낸 후 애플리케이션 전체에서 사용할 수 있습니다:

src/app/hooks.ts
// This file serves as a central hub for re-exporting pre-typed Redux hooks.
import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()

이로써 설정 과정이 완료되었습니다. 이제 앱 구축을 시작해 보겠습니다!

메인 게시물 피드

소셜 미디어 피드 앱의 주요 기능은 게시물 목록입니다. 점차적으로 여러 기능을 추가할 예정이지만, 첫 번째 목표는 화면에 게시물 항목 목록만 표시하는 것입니다.

게시물 슬라이스 생성

첫 번째 단계는 게시물 데이터를 담을 새로운 Redux "슬라이스"를 생성하는 것입니다.

"슬라이스"는 앱의 단일 기능을 위한 Redux 리듀서 로직과 액션의 집합입니다. 일반적으로 단일 파일에 함께 정의됩니다. 이 이름은 루트 Redux 상태 객체를 여러 상태 "조각"으로 분할하는 개념에서 유래했습니다.

게시물 데이터가 Redux 스토어에 추가되면 해당 데이터를 화면에 표시할 React 컴포넌트를 생성할 수 있습니다.

src 내부에 새로운 features 폴더를 생성하고, features 안에 posts 폴더를 만든 후 postsSlice.ts 파일을 추가합니다.

Redux Toolkit의 createSlice 함수를 사용하여 게시물 데이터를 처리할 수 있는 리듀서 함수를 생성할 것입니다. 리듀서 함수는 앱 시작 시 Redux 스토어에 값이 로드되도록 초기 데이터를 포함해야 합니다.

우선 UI 추가를 시작할 수 있도록 가짜 게시물 객체가 포함된 배열을 생성하겠습니다.

createSlice를 임포트하고 초기 게시물 배열을 정의한 후 createSlice에 전달하며, createSlice가 생성한 게시물 리듀서 함수를 내보냅니다:

features/posts/postsSlice.ts
import { createSlice } from '@reduxjs/toolkit'

// Define a TS type for the data we'll be using
export interface Post {
id: string
title: string
content: string
}

// Create an initial state value for the reducer, with that type
const initialState: Post[] = [
{ id: '1', title: 'First Post!', content: 'Hello!' },
{ id: '2', title: 'Second Post', content: 'More text' }
]

// Create the slice and pass in the initial state
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {}
})

// Export the generated reducer function
export default postsSlice.reducer

새로운 슬라이스를 생성할 때마다 해당 리듀서 함수를 Redux 스토어에 추가해야 합니다. 이미 생성된 Redux 스토어가 있지만 현재는 내부에 데이터가 없습니다. app/store.ts를 열고 postsReducer 함수를 임포트한 후 counter 관련 코드를 모두 제거하고 configureStore 호출을 업데이트하여 postsReducerposts라는 리듀서 필드로 전달되도록 합니다:

app/store.ts
import { configureStore } from '@reduxjs/toolkit'

// Removed the `counterReducer` function, `CounterState` type, and `Action` import

import postsReducer from '@/features/posts/postsSlice'

export const store = configureStore({
reducer: {
posts: postsReducer
}
})

이를 통해 Redux에게 최상위 상태 객체 내에 posts 필드를 포함시키고, 액션이 디스패치될 때 state.posts의 모든 데이터가 postsReducer 함수에 의해 업데이트되도록 지시합니다.

Redux DevTools Extension을 열고 현재 상태 내용을 확인하여 작동 여부를 확인할 수 있습니다:

초기 게시물 상태

게시물 목록 표시

이제 스토어에 게시물 데이터가 있으므로 게시물 목록을 표시하는 React 컴포넌트를 생성할 수 있습니다. 피드 게시물 기능과 관련된 모든 코드는 posts 폴더에 있어야 하므로 해당 폴더에 PostsList.tsx 파일을 생성합니다. (참고: TypeScript로 작성되고 JSX 구문을 사용하는 React 컴포넌트이므로 TypeScript가 올바르게 컴파일할 수 있도록 .tsx 파일 확장자가 필요합니다)

게시물 목록을 렌더링하려면 어딘가에서 데이터를 가져와야 합니다. React 컴포넌트는 React-Redux 라이브러리의 useSelector 훅을 사용하여 Redux 스토어에서 데이터를 읽을 수 있습니다. 작성한 "셀렉터 함수"는 전체 Redux state 객체를 매개변수로 받으며, 컴포넌트가 스토어에서 필요한 특정 데이터를 반환해야 합니다.

TypeScript를 사용 중이므로 모든 컴포넌트는 항상 미리 타입이 지정된 useAppSelector 훅을 사용해야 합니다. 이 훅은 src/app/hooks.ts에 추가한 것으로, 이미 올바른 RootState 타입이 포함되어 있습니다.

초기 PostsList 컴포넌트는 Redux 스토어에서 state.posts 값을 읽은 다음, 게시물 배열을 순회하며 화면에 각 게시물을 표시합니다:

features/posts/PostsList.tsx
import { useAppSelector } from '@/app/hooks'

export const PostsList = () => {
// Select the `state.posts` value from the store into the component
const posts = useAppSelector(state => state.posts)

const renderedPosts = posts.map(post => (
<article className="post-excerpt" key={post.id}>
<h3>{post.title}</h3>
<p className="post-content">{post.content.substring(0, 100)}</p>
</article>
))

return (
<section className="posts-list">
<h2>Posts</h2>
{renderedPosts}
</section>
)
}

이제 App.tsx에서 라우팅을 업데이트하여 환영 메시지 대신 PostsList 컴포넌트를 표시해야 합니다. PostsList 컴포넌트를 App.tsx로 가져온 다음, 환영 텍스트를 <PostsList />로 교체하세요. 곧 메인 페이지에 다른 내용을 추가할 예정이므로 React Fragment로 감싸줍니다:

App.tsx
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'

import { Navbar } from './components/Navbar'
import { PostsList } from './features/posts/PostsList'

function App() {
return (
<Router>
<Navbar />
<div className="App">
<Routes>
<Route
path="/"
element={
<>
<PostsList />
</>
}
></Route>
</Routes>
</div>
</Router>
)
}

export default App

이 작업을 완료하면 앱의 메인 페이지가 다음과 같이 표시됩니다:

초기 게시물 목록

진전이 있습니다! Redux 스토어에 데이터를 추가하고 React 컴포넌트에서 화면에 표시했습니다.

새 게시물 추가

다른 사람이 작성한 게시물을 보는 것도 좋지만, 우리도 직접 게시물을 작성하고 저장할 수 있으면 좋겠습니다. "새 게시물 추가" 양식을 만들어 게시물을 작성하고 저장할 수 있도록 합시다.

먼저 빈 양식을 생성해 페이지에 추가하겠습니다. 그런 다음 양식을 Redux 스토어에 연결하여 "게시물 저장" 버튼을 클릭할 때 새 게시물이 추가되도록 합니다.

새 게시물 양식 추가

posts 폴더에 AddPostForm.tsx를 생성합니다. 게시물 제목을 위한 텍스트 입력 필드와 본문을 위한 텍스트 영역을 추가합니다:

features/posts/AddPostForm.tsx
import React from 'react'

// TS types for the input fields
// See: https://epicreact.dev/how-to-type-a-react-form-on-submit-handler/
interface AddPostFormFields extends HTMLFormControlsCollection {
postTitle: HTMLInputElement
postContent: HTMLTextAreaElement
}
interface AddPostFormElements extends HTMLFormElement {
readonly elements: AddPostFormFields
}

export const AddPostForm = () => {
const handleSubmit = (e: React.FormEvent<AddPostFormElements>) => {
// Prevent server submission
e.preventDefault()

const { elements } = e.currentTarget
const title = elements.postTitle.value
const content = elements.postContent.value

console.log('Values: ', { title, content })

e.currentTarget.reset()
}

return (
<section>
<h2>Add a New Post</h2>
<form onSubmit={handleSubmit}>
<label htmlFor="postTitle">Post Title:</label>
<input type="text" id="postTitle" defaultValue="" required />
<label htmlFor="postContent">Content:</label>
<textarea
id="postContent"
name="postContent"
defaultValue=""
required
/>
<button>Save Post</button>
</form>
</section>
)
}

아직 Redux 관련 로직은 포함되어 있지 않습니다. 이 부분은 다음에 추가하겠습니다.

이 예시에서는 "비제어" 입력을 사용하고 있으며, 빈 입력 필드 제출을 방지하기 위해 HTML5 폼 유효성 검사를 활용합니다. 물론 폼에서 값을 읽는 방식은 React 사용 패턴에 관한 선호도 문제이므로 Redux와는 무관합니다.

해당 컴포넌트를 App.tsx로 가져오고, <PostsList /> 컴포넌트 바로 위에 추가합니다:

App.tsx
// omit outer `<App>` definition
<Route
path="/"
element={
<>
<AddPostForm />
<PostsList />
</>
}
></Route>

이제 헤더 바로 아래에 양식이 표시되는 것을 확인할 수 있습니다.

게시물 항목 저장

이제 게시물 슬라이스를 업데이트하여 새 게시물 항목을 Redux 스토어에 추가해 보겠습니다.

게시물 슬라이스는 게시물 데이터의 모든 업데이트를 처리합니다. createSlice 호출 내부에는 reducers라는 객체가 있습니다. 현재는 비어 있습니다. 여기에 게시물 추가 사례를 처리할 리듀서 함수를 추가해야 합니다.

reducers 내부에 postAdded라는 함수를 추가합니다. 이 함수는 두 가지 인수를 받습니다: 현재 state 값과 디스패치된 action 객체입니다. 게시물 슬라이스는 자신이 담당하는 데이터만 알고 있으므로, state 인수는 전체 Redux 상태 객체가 아닌 게시물 배열 자체가 됩니다.

action 객체는 새 게시물 항목을 action.payload 필드로 갖습니다. 리듀서 함수를 선언할 때는 해당 action.payload의 실제 타입이 무엇인지 TypeScript에 알려줘야 합니다. 이렇게 하면 인수를 전달할 때와 action.payload 내용에 접근할 때 올바르게 검사할 수 있습니다. 이를 위해 Redux Toolkit에서 PayloadAction 타입을 가져와 action 인수를 action: PayloadAction<ThePayloadTypeHere>으로 선언해야 합니다. 이 경우에는 action: PayloadAction<Post>가 됩니다.

실제 상태 업데이트는 새 게시물 객체를 state 배열에 추가하는 작업으로, 리듀서 내에서 state.push()를 통해 수행할 수 있습니다.

경고

기억하세요: Redux 리듀서 함수는 반드시 항상 새로운 상태 값을 불변(immutable)하게 생성해야 합니다! Array.push() 같은 변형 함수를 호출하거나 state.someField = someValue 처럼 객체 필드를 수정하는 것은 createSlice() 내부에서는 안전합니다. 왜냐하면 Immer 라이브러리를 사용해 내부적으로 이러한 변형을 안전한 불변 업데이트로 변환하기 때문입니다. 하지만 createSlice 외부에서는 어떤 데이터도 변형하려 시도하지 마세요!

postAdded 리듀서 함수를 작성하면 createSlice가 자동으로 동일한 이름의 "액션 생성자" 함수를 생성합니다. 이 액션 생성자를 내보내서 UI 컴포넌트에서 사용자가 "게시물 저장"을 클릭할 때 액션을 디스패치할 수 있습니다.

features/posts/postsSlice.ts
// Import the `PayloadAction` TS type
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

// omit initial state

const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
// Declare a "case reducer" named `postAdded`.
// The type of `action.payload` will be a `Post` object.
postAdded(state, action: PayloadAction<Post>) {
// "Mutate" the existing state array, which is
// safe to do here because `createSlice` uses Immer inside.
state.push(action.payload)
}
}
})

// Export the auto-generated action creator with the same name
export const { postAdded } = postsSlice.actions

export default postsSlice.reducer

용어상 postAdded"케이스 리듀서(case reducer)" 의 예시입니다. 슬라이스 내부에 위치한 리듀서 함수로, 디스패치된 특정 액션 유형을 처리합니다. 개념적으로는 switch 문 안의 case 처럼 동작합니다. 즉, "이 정확한 액션 유형을 보면 해당 로직을 실행하라"는 의미입니다:

function sliceReducer(state = initialState, action) {
switch (action.type) {
case 'posts/postAdded': {
// update logic here
}
}
}

"게시물 추가" 액션 디스패치하기

AddPostForm에는 텍스트 입력 필드와 "게시물 저장" 버튼이 있지만, 아직 버튼은 아무 동작도 하지 않습니다. 제출 핸들러를 업데이트하여 postAdded 액션 생성자를 디스패치하고 사용자가 작성한 제목과 내용을 포함한 새 게시물 객체를 전달해야 합니다.

게시물 객체에는 id 필드도 필요합니다. 현재 초기 테스트 게시물들은 가짜 ID 번호를 사용하고 있습니다. 다음 증가할 ID 번호를 계산하는 코드를 작성할 수도 있지만, 무작위 고유 ID를 생성하는 것이 더 나은 방법입니다. Redux Toolkit에는 이 용도로 사용할 수 있는 nanoid 함수가 있습니다.

정보

ID 생성과 액션 디스패치에 대해서는 4부: Redux 데이터 사용하기에서 더 자세히 다룰 예정입니다.

컴포넌트에서 액션을 디스패치하려면 스토어의 dispatch 함수에 접근해야 합니다. React-Redux의 useDispatch 훅을 호출해 이를 얻을 수 있습니다. TypeScript를 사용 중이므로 올바른 타입을 가진 useAppDispatch 훅을 실제로 가져와야 합니다. 또한 이 파일에 postAdded 액션 생성자를 가져와야 합니다.

컴포넌트에서 dispatch 함수를 사용할 수 있게 되면 클릭 핸들러에서 dispatch(postAdded())를 호출할 수 있습니다. 폼에서 제목과 내용 값을 가져오고, 새 ID를 생성한 다음, 이들을 결합해 새 게시물 객체를 만들어 postAdded()에 전달합니다.

features/posts/AddPostForm.tsx
import React from 'react'
import { nanoid } from '@reduxjs/toolkit'

import { useAppDispatch } from '@/app/hooks'

import { type Post, postAdded } from './postsSlice'

// omit form types

export const AddPostForm = () => {
// Get the `dispatch` method from the store
const dispatch = useAppDispatch()


const handleSubmit = (e: React.FormEvent<AddPostFormElements>) => {
// Prevent server submission
e.preventDefault()

const { elements } = e.currentTarget
const title = elements.postTitle.value
const content = elements.postContent.value

// Create the post object and dispatch the `postAdded` action
const newPost: Post = {
id: nanoid(),
title,
content
}
dispatch(postAdded(newPost))

e.currentTarget.reset()
}

return (
<section>
<h2>Add a New Post</h2>
<form onSubmit={handleSubmit}>
<label htmlFor="postTitle">Post Title:</label>
<input type="text" id="postTitle" defaultValue="" required />
<label htmlFor="postContent">Content:</label>
<textarea
id="postContent"
name="postContent"
defaultValue=""
required
/>
<button>Save Post</button>
</form>
</section>
)
}

이제 제목과 텍스트를 입력하고 "게시물 저장"을 클릭해 보세요. 게시물 목록에 해당 게시물에 대한 새 항목이 표시되어야 합니다.

축하합니다! 첫 번째 React + Redux 앱을 성공적으로 구축했습니다!

이는 완전한 Redux 데이터 흐름 사이클을 보여줍니다:

  • 게시물 목록이 useSelector로 스토어에서 초기 게시물 집합을 읽어 초기 UI를 렌더링했습니다

  • 새 게시물 항목에 대한 데이터를 포함한 postAdded 액션을 디스패치했습니다

  • 게시물 리듀서가 postAdded 액션을 인식하고 새 항목으로 게시물 배열을 업데이트했습니다

  • Redux 스토어가 UI에 데이터 변경 사항을 알렸습니다

  • 게시물 목록이 업데이트된 게시물 배열을 읽고 새 게시물을 표시하도록 스스로 다시 렌더링했습니다

이후 추가할 모든 새 기능은 여기서 본 것과 동일한 기본 패턴을 따릅니다: 상태 슬라이스 추가, 리듀서 함수 작성, 액션 디스패치, Redux 스토어의 데이터를 기반으로 UI 렌더링.

Redux DevTools 확장 프로그램을 사용해 디스패치한 액션을 확인하고, 해당 액션에 대한 응답으로 Redux 상태가 어떻게 업데이트되었는지 확인할 수 있습니다. 액션 목록에서 "posts/postAdded" 항목을 클릭하면 "Action" 탭이 다음과 같이 표시됩니다:

postAdded 액션 내용

"Diff" 탭에는 state.posts에 인덱스 2 위치에 새 항목이 추가되었음이 표시되어야 합니다.

기억하세요, Redux 스토어는 애플리케이션에서 '전역'으로 간주되는 데이터만 포함해야 합니다! 이 경우 입력 필드의 최신 값은 AddPostForm 컴포넌트만 알면 됩니다. "제어되는" 입력으로 폼을 구축하더라도, 임시 데이터를 Redux 스토어에 보관하기보다는 React 컴포넌트 상태에 유지하는 것이 바람직합니다. 사용자가 폼 작성을 완료하면 사용자 입력을 기반으로 최종 값을 스토어에 업데이트하기 위해 Redux 액션을 디스패치합니다.

학습 내용 요약

Redux 앱의 기본 구성 요소(스토어, 리듀서가 포함된 슬라이스, 액션 디스패치 UI)를 설정했습니다. 현재까지의 애플리케이션 모습은 다음과 같습니다:

이번 섹션에서 배운 내용을 요약해 보겠습니다:

요약
  • Redux 앱은 단일 store를 가지며, <Provider> 컴포넌트를 통해 React 컴포넌트에 전달됩니다
  • Redux 상태는 "리듀서 함수"에 의해 업데이트됩니다:
    • 리듀서는 항상 불변 방식으로 새 상태를 계산하며, 기존 상태 값을 복사한 후 새 데이터로 복사본을 수정합니다
    • Redux Toolkit의 createSlice 함수는 "슬라이스 리듀서" 함수를 생성해주며, 안전한 불변 업데이트로 변환되는 "변경" 코드 작성을 가능하게 합니다
    • 이러한 슬라이스 리듀서 함수들은 configureStorereducer 필드에 추가되며, Redux 스토어 내부의 데이터 및 상태 필드 이름을 정의합니다
  • React 컴포넌트는 useSelector 훅으로 스토어에서 데이터를 읽습니다
    • 셀렉터 함수는 전체 state 객체를 받고 값을 반환해야 합니다
    • 셀렉터는 Redux 스토어가 업데이트될 때마다 재실행되며, 반환된 데이터가 변경되면 컴포넌트가 다시 렌더링됩니다
  • React 컴포넌트는 useDispatch 훅을 사용해 스토어를 업데이트하는 액션을 디스패치합니다
    • createSlice는 슬라이스에 추가한 각 리듀서에 대한 액션 생성자 함수를 생성합니다
    • 컴포넌트에서 dispatch(someActionCreator())를 호출해 액션을 디스패치합니다
    • 리듀서가 실행되어 해당 액션과 관련 있는지 확인하고, 적절하다면 새 상태를 반환합니다
    • 폼 입력 값과 같은 임시 데이터는 React 컴포넌트 상태나 일반 HTML 입력 필드로 유지해야 합니다. 사용자가 폼 작성을 완료하면 스토어를 업데이트하기 위해 Redux 액션을 디스패치하세요
  • TypeScript를 사용하는 경우, 초기 앱 설정에서 스토어를 기반으로 RootStateAppDispatch에 대한 TS 타입을 정의하고, React-Redux의 useSelectoruseDispatch 훅의 사전 정의된(pre-typed) 버전을 내보내야 합니다

다음 단계

이제 Redux의 기본 데이터 흐름을 알게 되었으니, 4부: Redux 데이터 사용하기로 이동해 애플리케이션에 추가 기능을 구현하고, 스토어에 이미 존재하는 데이터를 활용하는 실제 예시를 살펴보세요.