티스토리 뷰
이번 파트에서는 createSlice를 사용해서 reducer로직의 slice를 store에 추가하는 방법과 useSelectore, UseDispatch 사용밥법에 대해서 알아보겠습니다.
프로젝트 설정
react와 redux가 설정되어 있고, 기본적인 css와 API 요청을 가능하게하는 fake REST API가 있는 프로젝트 예제입니다. 이를 토대로 배워보도록 하겠습니다.
코드는 CodeSandbox를 통해 확인할 수 있고, fork를 해서 진행 할 수 있습니다. 또는 github 저장소에서 clone을 해도 됩니다.
codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/master/?from-embed
github.com/reduxjs/redux-essentials-example-app
완성된 모습은 아래 링크를 통해 확인 할 수 있습니다. github의 경우 tutorial-steps 브랜치에서 확인 할 수 있습니다
codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/tutorial-steps
새로운 Redux + React 프로젝트를 만들고 싶다면?
이번 튜토리얼을 마치면 자신의 프로젝트를 생성하고 싶을 것 입니다. 새로운 Redux + React 프로젝트를 생성하는 가장 빠른 방법은 Create-React-App에서 제공하는 Redux 템플릿을 사용하는 것이 좋습니다. 이전 장에서 배웠던 카운터 예제가 작성되어 있는 코드입니다.
프로젝트 확인
첫부분에 말했던 프로젝트의 구조를 간단히 살펴 보겠습니다.
- /src
- index.js: 애플리케이션의 진입점 파일입니다. React-Redux <Provider>컴포넌트와 메인 <App>컴포넌트를 렌더링합니다.
- App.js: 주요 응용 프로그램 구성 요소입니다. top navbar를 렌더링하고 다른 콘텐츠에 대한 client-side 라우팅을 처리합니다.
- index.css: 애플리케이션에 사용되는 css 파일입니다.
- /api
- client.js: GET 및 POST 요청을 할 수있게 하는 AJAX 요청 파일 입니다.
- server.js: 데이터에 대한 fake REST API를 제공합니다. 우리 앱은 나중에 fake 엔드 포인트에서 데이터를 가져옵니다.
- /app
- Navbar.js: 상단 헤더 및 nav 콘텐츠를 렌더링합니다.
- store.js: Redux 스토어 인스턴스 생성합니다
지금 앱을 보면 헤더와 welcome메세지를 볼 수 있습니다. Redux DevTools Extension에서 초기 Redux state를 보면 비어있는 것을 확인 할 수 있습니다.
메인 게시물 피드
이 앱에서 만들 주요기능은 게시물 목록입니다. 첫번째 목표는 화면에 게시물 목록을 표시하는 것입니다.
게시물 slice 만들기
첫번째 단계는 게시물에 대한 데이터를 가지고 있는 redux slice를 만드는 것 입니다. redux store에 해당 데이터가 있으면 페이지에 해당 데이터를 표시하는 react 컴포넌트를 만들 수 있습니다.
src 폴더 안에 features 폴더를 생성하고, 그 안에 post 폴더를 생성한 후 postsSlice.js 파일을 생성합니다.
redux toolkit의 createSlice함수를 사용하여 게시물 데이터를 처리하는 방법을 아는 reducer를 만들겠습니다. reducer에는 앱이 시작될 때 redux store에 해당 값이 로드 되도록 초기 데이터가 있어야 합니다.
지금은 화면에 UI가 생길 수 있도록 내부에 fake 데이터로 게시물 객체가 있는 배열을 만들겠습니다. createSlice를 import하고, 초기 posts 배열을 정의한 후 createSlice에 인자로 전달합니다. 그 후 createSlice로 생성된 posts reducer를 export 합니다
// 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
새로운 slice를 만들 때마다 redux store에 reducer를 추가 해야 합니다. 현재 redux store가 있지만 내부 데이터가 없습니다. app/store.js 파일에서 postsReducer를 import한 다음 configureStore를 가져와서 posts라는 이름의 postsReducer를 업데이트 합니다.
// app/store.js
import { configureStore } from '@reduxjs/toolkit'
import postsReducer from '../features/posts/postsSlice'
export default configureStore({
reducer: {
posts: postsReducer
}
})
이 코드는 redux의 최상위 state에 posts라는 필드가 있고, state.posts의 모든 데이터는 action이 전달 될 때 postsReducer에 의해서 업데이트 되는 것을 뜻합니다.
여기까지 한 후 Redux DevTools Extension에서 현재 state를 확인 해 봅시다.
게시물 목록 노출
이제 store에 몇 가지 게시물 데이터가 있으므로 게시물 목록을 표시하는 react 콤포넌트를 만들겠습니다. 피드 게시물 기능과 관련된 모든 코드는 posts 폴더에 있어야하므로 여기에 PostsList.js 파일을 만드세요.
게시물 목록을 렌더링하려면 데이터를 가져와야합니다. react 컴포넌트는 react-redux 라이브러리의 hooks 중 useSelector를 사용하여 redux store에서 데이터를 읽을 수 있습니다. "selector function"은 전체 redux state를 매개 변수로 사용하여 호출되며 컴포넌트가 필요한 특정 데이터를 store에서 반환해야합니다.
초기 PostList 컴포넌트는 Redux store에서 state.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>{post.content.substring(0, 100)}</p>
</article>
))
return (
<section>
<h2>Posts</h2>
{renderedPosts}
</section>
)
}
다음으로 "welcome"메시지 대신 PostList 컴포넌트가 나타나도록 App.js 컴포넌트의 라우팅을 수정합니다. App.js 파일에 PostsList파일을 import하고, welcome 메시지를 <PostsList />로 변경합니다. 다른 컴포넌트도 추가할 예정이므로 React 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
이렇게 작성하면 아래와 같은 화면을 확인 할 수 있습니다.
새로운 포스트 추가하기
게시물을 작성하고 저장할 수 있는 새로운 게시물 추가 form을 만들어 보겠습니다. 먼저 빈 form을 만들어서 페이지에 추가합니다. 그 후 form을 redux store에 연결하여 "게시물 저장"버튼을 클릭 할 때 새로운 게시물이 추가되도록합니다.
새 게시물 양식 추가
posts폴더에 AddPostForm.js 파일을 생성합니다. 게시물 제목에 대한 text input과 게시물 본문에 대한 text area를 추가합니다
// 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파일에 컴포넌트를 import 한 후 <PostsList /> 컴포넌트 위에 작성해서 헤더 아래에 form이 나타나게 합니다.
//App.js
<Route
exact
path="/"
render={() => (
<React.Fragment>
<AddPostForm />
<PostsList />
</React.Fragment>
)}
/>
게시물 저장하기
Redux store에 새로운 게시물을 추가하기 위해 posts slice를 수정합니다.
posts slice는 게시물 데이터에 대한 모든 업데이트를 처리합니다. createSlice 내부에는 reducers라는 객체가 있습니다. 지금은 비어있기 때문에 게시물을 추가시킬 수 있는 reducer function을 추가해야합니다.
reducer 안에 postAdded라는 function을 추가하면 현재 state 값과 전달 되는 action을 인수로 받습니다. posts slice는 자신이 담당하는 데이터에 대해서만 알고있기 때문에 state는 전체 Redux state가 아니라 posts state이어야합니다. 여기에서는 posts 배열이 들어갑니다.
action 객체의 action.payload 필드를 통해 새로운 post 객체를 전달할 것이고, 이 객체를 post 배열에 추가할 것입니다.
postAdded reducer를 작성할 때 createSlice는 동일한 이름의 action 생성기를 자동으로 만들어 줍니다. action 생성기를 export하고 UI 컴포넌트에서 사용하여 사용자가 "게시물 저장" 버튼을 클릭하면 action을 전달할 수 있습니다.
// 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
"Post Added" action 전달하기
AddPostForm에는 text input과 "게시물 저장"버튼이 있지만 이벤트를 등록하지 않았습니다. postAdded action 생성자를 dispatch해서 사용자가 작성한 제목과 콘텐츠가 포함 된 객체를 전달할 수 있게 해야합니다.
게시물 객체는 id 필드를 가지고 있어야합니다. 현재 초기 테스트 게시물은 fake 데이터로 임의로 id를 지정해 주었습니다. 다음에 들어갈 id가 무엇인지 알아내는 코드를 작성할 수 있지만, 고유의 id를 생성하는 것이 더 좋습니다. Redux toolkit에는 이런 기능을 하는 nanoid 함수가 있습니다
ID 생성과 action dispatch에 대한 자세히 설명은 다음장에서 하겠습니다
컴포넌트에서 action을 전달하려면 store의 dispatch 함수를 가지고 와야합니다. react-redux에서 useDispatch hook를 통해서 가지고 올 수 있습니다. 또한 postAdded action 생성자를 이 파일에 import해야합니다.
컴포넌트에서 dispatch 함수를 사용할 수 있게되면 dispatch( postAdded() )를 호출 할 수 있습니다. React 컴포넌트 useState hook에서 제목 및 콘텐츠를 가져온 후 새 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>
)
}
이제 제목과 텍스트를 입력하고 "게시물 저장"을 클릭하세요. 해당 게시물이 게시물 목록에 표시되어야 합니다.
첫 번째로 작동하는 react + redux 앱을 만들었습니다
이번에 배운 내용을 통해 redux의 데이터가 어떻게 움직이는지 알 수 있었습니다.
- 게시물 목록은 useSelector를 사용하여 store의 초기 게시물 목록을 읽고 초기 UI를 렌더링 합니다
- 새로운 게시물 데이터 객체를 postAdded action을 통해 dispatch 합니다.
- posts reducer는 postAdded action을 확인하고, 새로운 게시물 데이터를 가지고 posts배열을 업데이트합니다.
- redux store는 UI에 일부 데이터가 변경 되었다고 알립니다.
- 게시물 목록은 업데이트 된 게시물 배열 데이터를 읽고 새로운 게시물을 표시하기 위해 다시 렌더링합니다.
요약
- redux state는 reduxer에 의해서 업데이트됩니다.
- reducers는 기존의 state값을 복사하고 새 데이터로 복사본을 수정한 후 새로운 state로 반환합니다
- redu toolkit에서 createSlice는 slice reducer를 생성하고 immer 라이브러리를 통해 불변성을 지키면서 업데이트 할 수 있습니다.
- slice reducer는 configureStore 안에 reducer 필드로 추가되고, redux store 안에 데이터 및 state 필드 이름을 정의합니다.
- react 컴포넌트는 useSelector hook을 사용하여 store의 데이터를 읽습니다
- selector 함수는 전체 state를 받고 값을 반환해야합니다
- selecorts는 redux store가 업데이트 될 때마다 다시 실행되며, 반환하는 데어터가 변경되면 컴포넌트가 다시 렌더링 됩니다
- react 컴포넌트는 useDispatch hook을 사용해서 actions을 전달하고 store를 업데이트 합니다
- createSlice는 reducer에 있는 action들에 대한 action 생성자 함수를 생성합니다
- 컴포넌트에서 action을 전달할 때는 dispatch(someActionCreator())를 호출합니다
- reducer가 실행 될 때 해당하는 action이 있는지 확인하고, 있을 경우 새로운 state를 반환합니다
- form input 값과 같은 임시 데이터는 react 컴포넌트 안에 state로 생성하는 것이 좋습니다. 사용자가 form을 모두 작성한 후에 store를 업데이트하는 action을 전달합니다.
다음 장에서는 몇가지 기능을 더 추가하고 이미 store에 있는 데이터가 어떻게 동작하는지에 대해 알아보겠습니다
'React' 카테고리의 다른 글
react 컴포넌트에 websocket 적용하기 (0) | 2021.02.09 |
---|---|
redux 강의 기본 핵심 튜토리얼 5탄 - redux data 사용하기 (0) | 2020.11.16 |
redux 강의 기본 핵심 튜토리얼 3탄 - redux app 구조 (0) | 2020.11.03 |
redux 강의 기본 핵심 튜토리얼 2탄 - redux를 사용하기 위해 알아야하는 용어 및 개념 (0) | 2020.10.29 |
redux 강의 기본 핵심 튜토리얼 1탄 - redux란 무엇일까? (0) | 2020.10.29 |