공식문서를 공부하기 위해 번역한 내용이기 때문에 의역이나 오역이 존재할 수 있습니다.
빠르게 초기 폴더구조를 살펴봅시다.
/public
: the HTML host page template and other static files like icons/src
index.js
: 어플리케이션의 엔트리포인트(진입 지점). React-Redux 컴포넌트인 <Provider>
와 메인 컴포넌트인 <App>
을 렌더링한다.App.js
: 앱의 메인 컴포넌트. 클라이언트 사이드 렌더링으로 top navbar를 렌더링한다.index.css
/api
client.js
: 간단한 GET, POST 요청이 있는 AJAX 요청 클라이언트server.js
: 임의로 서버역할을 하는 파일/app
Navbar.js
: top header and nav content 컴포넌트store.js
: creates the Redux store instance이 어플리케이션의 가장 주요 기능은 포스트들의 리스트를 나타내는 것이다. 우리는 이 기능을 위해 몇 가지의 기능들을 더 추가할 것 입니다. 하지만 시작하기 앞서, 우리의 첫 목표는 화면에 포스트 목록을 보여주는 것입니다.
포스트의 데이터를 관리하기 위해 새로운 리덕스 slice를 만드는 것이 첫 단계입니다. 한 번 리덕스 스토어에 데이터를 만들면, 페이지에 데이터를 보여주기위한 리액트 컴포넌트를 만들 수 있습니다.
src
폴더에 features
폴더를 생성 한 후 그 속에 posts
폴더도 생성해 줍니다. 그리고 postSlice.js
파일을 생성해줍니다.
우리는 포스트 데이터를 관리하기 위한 리듀서 함수를 생성하기 위해 리덕스 툴킷의 createSlice
를 사용 할 것입니다. 앱이 시작될 때 리덕스 스토어에 해당 값이 로드되도록 리듀서 함수는 초기 데이터가 필요합니다.
지금부터 우리는 UI를 추가할 수 있게 해줄 가짜 포스트 객체를 배열안에 만들어 줍니다.
createSlice
불러와 초기 포스트 배열로 정의해 createSlice
에 전달합니다. 그리곤 생성한createSlice
의 포스트의 리듀서 함수를 내보내줍니다.
// features/posts/postsSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = [
{ id: '1', title: 'First Post!', content: 'Hello!' },
{ id: '2', title: 'Second Post', content: 'More text' }
]
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {}
})
export default postsSlice.reducer
매번 새로운 슬라이스를 생성 할 때, 우리는 슬라이스에 맞는 리듀서 함수를 리덕스 스토어에 추가해야합니다. 우리는 이미 리덕스 스토어를 생성했습니다만 지금 당장은 어떤 데이터도 없습니다. app/store.js
를 열어 postReducer
를 불러온 뒤 configureStore
를 업데이트하여 postReducer
가 리듀서 내의 posts
라는 값으로 연결시켜줍니다.
// app/store.js
import { configureStore } from '@reduxjs/toolkit'
import postsReducer from '../features/posts/postsSlice'
export default configureStore({
reducer: {
posts: postsReducer
}
})
이것은 리덕스에게 우리의 최상위 상태 객체 내부에 posts
라는 이름의 필드를 갖기를 원하고 state.posts
에 대한 모든 데이터는 액션이 전달될 때 postsReducer
함수에 의해 업데이트될 것임을 알려줍니다.
리덕스 개발자도구를 이용해 현재 상태를 확인할 수 있습니다.
현재 우리는 스토어에 포스트 데이터들이 담겨있고, 포스트 리스트를 보여줄 수 있는 리액트 컴포넌트를 만들 수 있습니다. 모든 포스트 피드 관련된 코드들은 posts
폴더에 있어야합니다. 그렇기 때문에 PostList.js
파일을 그 곳에 새로 생성해주도록 합시다.
포스트 목록을 렌더링 할 것이라면, 우리는 어딘가에서 데이터를 받아와야 합니다. 리액트 컴포넌트는 리액트-리덕스 라이브러리의 훅인 useSelector
로 리덕스 스토어의 데이터를 읽을 수 있습니다. “셀렉터 함수"는 전체 리덕스 state
객체를 매개변수로 사용해 호출되며, 이 구성요소가 스토어에서 필요로 하는 특정 데이터를 반환해야합니다.
우리의 초기 PostList
컴포넌트는 리덕스 스토어로부터 state.posts
값을 읽을 것이고, posts 배열을 순회하며 각각의 내용을 스크린에 보여줄 것 입니다.
// features/posts/PostsList.js
import React from 'react'
import { useSelector } from 'react-redux'
export const PostsList = () => {
const posts = useSelector(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>
)
}
우리는 이제 PostList
컴포넌트를 보여주기 위해 App.js
에서 라우팅을 업데이트해야합니다. PostList
컴포넌트를 App.js
로 불러와 넣어줍니다. 그리곤 Fragment
로 감싸줍니다.
// App.js
import React from 'react'
import {
BrowserRouter as Router,
Switch,
Route,
Redirect
} from 'react-router-dom'
import { Navbar } from './app/Navbar'
import { PostsList } from './features/posts/PostsList'
function App() {
return (
<Router>
<Navbar />
<div className="App">
<Switch>
<Route
exact
path="/"
render={() => (
<React.Fragment>
<PostsList />
</React.Fragment>
)}
/>
<Redirect to="/" />
</Switch>
</div>
</Router>
)
}
export default App
<PostList />
를 추가하면, 메인페이지가 아래와 같이 렌더링 될 것 입니다.
현재 작성된 포스트 목록은 보기 좋지만 포스트를 추가할 수는 없습니다. 포스트를 새로 작성하고 저장할 수 있는 “Add New Post”를 만들어 봅시다
우리는 우선 빈 양식을 만들 것이고 페이지에 추가할겁니다. 그리곤 Save Post 버튼을 눌리면 새로운 포스트가 추가 될 수 있게 리덕스 스토어에 연결시킬겁니다.
posts
폴더에 AddPostForm.js
를 생성합시다. 포스트 제목을 위한 텍스트 영역과 포스트 내용을 위한 텍스트 영역을 추가합시다.
// features/posts/AddPostForm.js
import React, { useState } from 'react'
export const AddPostForm = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const onTitleChanged = e => setTitle(e.target.value)
const onContentChanged = e => setContent(e.target.value)
return (
<section>
<h2>Add a New Post</h2>
<form>
<label htmlFor="postTitle">Post Title:</label>
<input
type="text"
id="postTitle"
name="postTitle"
value={title}
onChange={onTitleChanged}
/>
<label htmlFor="postContent">Content:</label>
<textarea
id="postContent"
name="postContent"
value={content}
onChange={onContentChanged}
/>
<button type="button">Save Post</button>
</form>
</section>
)
}
App.js
에 불러와 <PostList />
컴포넌트 위에 위치시킵시다.
// App.js
<Route
exact
path="/"
render={() => (
<React.Fragment>
<AddPostForm />
<PostsList />
</React.Fragment>
)}
/>
이제 헤더 아래에 추가된 폼이 보이게 될 것입니다.
이제 리덕스 스토어에 새로운 포스트 목록을 추기하기 위해 우리 포스트의 슬라이스를 업데이트 해봅시다.
포스트 슬라이스는 포스트 데이터에 관한 모든 업데이트를 다룹니다. createSlice
에 빈 reducers
가 있었습니다. 이제 포스트를 추가할 수 있는 리듀서 함수로 채워줍시다.
reducers
에 postAdded
라는 현재 state
값과 디스패치될 action
객체를 파라미터로 받는 함수를 추가합시다. 포스트 슬라이스는 관련이 있는 데이터에 대해서만 알고있기 때문에 state
인자는 전체 리덕스 state
객체가 아닌 포스트의 배열이 해당됩니다.
action
객체는 action.payload
를 가진 새로운 포스트 목록을 가지게 될 것입니다. 그리곤 새로운 포스트 객체를 state
배열에 넣게 될 것 입니다.
postAdded
리듀서 함수를 작성할 때, createSlice
는 자동적으로 같은 이름을 가진 액션 생성자 함수를 생성할 것입니다. 우리는 액션 생성자 내보내고 UI 컴포넌트를 통해 유저가 “Save Post”버튼을 클릭할 때 action
을 dispatch
할 수 있습니다.
// features/posts/postsSlice.js
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded(state, action) {
state.push(action.payload)
}
}
})
export const { postAdded } = postsSlice.actions
export default postsSlice.reducer
⛔ 경고
리듀서 함수는 반드시 새로운 상태 값을 카피를 통해 불변성을 유지하면서 생성해야합니다.Array.push()
혹은state.someField = someValue
같은 것을createSlice
내부에서 사용하면 안전합니다.Immer
라이브러리 덕분에 안전하게 상태 값을 변화시킬 수 있기 때문입니다. 하지만createSlice
밖에선 어떠한 값도 변화시키지마세요!
AddPostForm
에는 텍스트 인풋창과 “Save Post” 버튼이 있습니다만 버튼은 아직 어떤 것도 할 수 없습니다. 우리는 ”postAdded
액션 생성자”와 “새로운 포스트 객체”를 보낼 클릭핸들러를 추가해야합니다.
포스트 객체는 id
필드가 필요합니다. 지금은 우리의 초기 포스트 값의 ID 필드는 임의로 값을 할당했습니다. 우리는 다음 번의 ID가 할당 될 때 초기값에서 늘어나게 할 코드를 입력할 것이지만, 랜덤한 값을 생성하는 것이 더 나을 것입니다. 리덕스 툴킷은 그런 것을 처리해줄 nanoid
함수를 가지고 있습니다.
컴포넌트에서 action을 전달하기 위해 스토어의 dispatch
함수가 필요합니다. 우리는 useDispatch
라는 훅을 이용해 해결할 것입니다. 뿐만 아니라 postAdded
라는 액션 생성자도 불러옵시다.
컴포넌트에서 dispatch
함수를 사용할 수 있게된다면, 우리는 dispatch(postAdded())
를 클릭 핸들러에 할당 할 수 있을겁니다. 우리는 제목과 내용에 관한 상태를 관리하기 위해 useState
훅을 사용할 수 있습니다. 이렇게 생성된 제목과 내용, 새롭게 생성될 ID를 PostAdded()
로 전달해 새로운 포스트 객체에 넣어줍니다.
// features/posts/AddPostForm
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { nanoid } from '@reduxjs/toolkit'
import { postAdded } from './postsSlice'
export const AddPostForm = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const dispatch = useDispatch()
const onTitleChanged = e => setTitle(e.target.value)
const onContentChanged = e => setContent(e.target.value)
const onSavePostClicked = () => {
if (title && content) {
dispatch(
postAdded({
id: nanoid(),
title,
content
})
)
setTitle('')
setContent('')
}
}
return (
<section>
<h2>Add a New Post</h2>
<form>
{/* omit form inputs */}
<button type="button" onClick={onSavePostClicked}>
Save Post
</button>
</form>
</section>
)
}
이제 제목과 내용을 입력한 후 Save Post 버튼을 눌리면 새로운 포스트가 목록에 추가될 것 입니다.
이렇게 첫 React + Redux 어플리케이션을 만들어 보았습니다!
useSelector
를 통해 스토어에서 초기 목록 값을 읽고 UI를 렌더링 합니다.postAdded
액션을 전달합니다.postAdded
를 확인하고, 새로운 데이터 목록으로 업데이트 합니다.이후 다른 추가 기능도 slice 생성, 리듀서 함수 작성, 액션 디스패치, 리덕스 스토어의 데이터에 기반한 UI랜더링과 같은 기본 패턴을 이용해 구현할 수 있을 겁니다.
AddPostForm
컴포넌트에는 유저가 타이핑한 제목과 내용을 추적하는 useState
훅이 있다는 것을 주의해야합니다. 리덕스 스토어에는 오직 전역적으로 필요한 데이터만 포함되어야 합니다! AddPostForm
은 제목과 내용의 최신 인풋 값이 필요하기 때문에 리덕스 스토어에 임시적으로 그 값을 보관하기 보단 해당 컴포넌트에서 관리해야합니다. 유저가 폼을 작성완료 했을 때 우리는 리덕스 스토어에 업데이트 된 값을 전달할 것입니다.
createSlice
함수는 “slice reducer”함수를 생성한 뒤 상태 값을 안전한 불변성 업데이트를 통해 변화시킬 수 있게 해줍니다.configureStore
속 reducer
필드에 추가되며 리덕스 스토어 내부에 상태 값 이름이 정의됩니다.useSelector
훅을 이용해 스토어의 데이터를 읽는다.state
객체를 받으며, 그 값을 반환해야합니다.useDispatch
훅을 이용해 스토어에 actions을 전달해 업데이트 시킨다.createSlice
는 우리가 추가한 슬라이스의 각각의 리듀서를 위한 액션 생성자 함수를 만들 것입니다.dispatch(someActionCreator())
를 컴포넌트에 불러와 액션을 전달합니다.https://redux.js.org/tutorials/essentials/part-3-data-flow
https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/master/?from-embed - 실습 전