Redux | Redux Essentials, Part 3: Basic Redux Data Flow

이진웅·2022년 4월 29일
0

Redux

목록 보기
3/3
post-thumbnail

공식문서를 공부하기 위해 번역한 내용이기 때문에 의역이나 오역이 존재할 수 있습니다.

들어가며

빠르게 초기 폴더구조를 살펴봅시다.

  • /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

Main Posts Feed

이 어플리케이션의 가장 주요 기능은 포스트들의 리스트를 나타내는 것이다. 우리는 이 기능을 위해 몇 가지의 기능들을 더 추가할 것 입니다. 하지만 시작하기 앞서, 우리의 첫 목표는 화면에 포스트 목록을 보여주는 것입니다.

Posts Slice 만들기

포스트의 데이터를 관리하기 위해 새로운 리덕스 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 함수에 의해 업데이트될 것임을 알려줍니다.

리덕스 개발자도구를 이용해 현재 상태를 확인할 수 있습니다.

https://redux.js.org/assets/images/example-initial-posts-f00f1a94398a31484c97fd7916633bad.png

Posts List 보여주기

현재 우리는 스토어에 포스트 데이터들이 담겨있고, 포스트 리스트를 보여줄 수 있는 리액트 컴포넌트를 만들 수 있습니다. 모든 포스트 피드 관련된 코드들은 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 />를 추가하면, 메인페이지가 아래와 같이 렌더링 될 것 입니다.

https://redux.js.org/assets/images/working_post_list-f2f507f15b46413695ee314d2e32f7ff.png

새로운 Posts 추가하기

현재 작성된 포스트 목록은 보기 좋지만 포스트를 추가할 수는 없습니다. 포스트를 새로 작성하고 저장할 수 있는 “Add New Post”를 만들어 봅시다

우리는 우선 빈 양식을 만들 것이고 페이지에 추가할겁니다. 그리곤 Save Post 버튼을 눌리면 새로운 포스트가 추가 될 수 있게 리덕스 스토어에 연결시킬겁니다.

새로운 Posts 양식 추가하기

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

이제 헤더 아래에 추가된 폼이 보이게 될 것입니다.

Post 목록 저장하기

이제 리덕스 스토어에 새로운 포스트 목록을 추기하기 위해 우리 포스트의 슬라이스를 업데이트 해봅시다.

포스트 슬라이스는 포스트 데이터에 관한 모든 업데이트를 다룹니다. createSlice에 빈 reducers가 있었습니다. 이제 포스트를 추가할 수 있는 리듀서 함수로 채워줍시다.

reducerspostAdded라는 현재 state값과 디스패치될 action객체를 파라미터로 받는 함수를 추가합시다. 포스트 슬라이스는 관련이 있는 데이터에 대해서만 알고있기 때문에 state인자는 전체 리덕스 state 객체가 아닌 포스트의 배열이 해당됩니다.

action객체는 action.payload를 가진 새로운 포스트 목록을 가지게 될 것입니다. 그리곤 새로운 포스트 객체를 state 배열에 넣게 될 것 입니다.

postAdded 리듀서 함수를 작성할 때, createSlice는 자동적으로 같은 이름을 가진 액션 생성자 함수를 생성할 것입니다. 우리는 액션 생성자 내보내고 UI 컴포넌트를 통해 유저가 “Save Post”버튼을 클릭할 때 actiondispatch할 수 있습니다.

// 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 밖에선 어떠한 값도 변화시키지마세요!

Dispatching the "Post Added" Action

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 어플리케이션을 만들어 보았습니다!

Redux Data Flow Cyle

  1. 포스트 목록은 useSelector를 통해 스토어에서 초기 목록 값을 읽고 UI를 렌더링 합니다.
  2. 새로운 데이터 목록을 담은 postAdded액션을 전달합니다.
  3. 포스트의 리듀서는 postAdded를 확인하고, 새로운 데이터 목록으로 업데이트 합니다.
  4. 리덕스 스토어는 UI에게 데이터 상태 값이 바뀌었다고 알려줍니다.
  5. 포스트 목록은 업데이트된 포스트 배열을 읽은 뒤 그 값으로 새로 렌더링 됩니다.

이후 다른 추가 기능도 slice 생성, 리듀서 함수 작성, 액션 디스패치, 리덕스 스토어의 데이터에 기반한 UI랜더링과 같은 기본 패턴을 이용해 구현할 수 있을 겁니다.

AddPostForm 컴포넌트에는 유저가 타이핑한 제목과 내용을 추적하는 useState 훅이 있다는 것을 주의해야합니다. 리덕스 스토어에는 오직 전역적으로 필요한 데이터만 포함되어야 합니다! AddPostForm은 제목과 내용의 최신 인풋 값이 필요하기 때문에 리덕스 스토어에 임시적으로 그 값을 보관하기 보단 해당 컴포넌트에서 관리해야합니다. 유저가 폼을 작성완료 했을 때 우리는 리덕스 스토어에 업데이트 된 값을 전달할 것입니다.

Summary

  • 리덕스의 state는 “reducer functions”으로 업데이트 한다.
    • 리듀서는 항상 기존 상태 값을 복사하고 복사본을 수정하여 새 상태로 만들어 불변성을 유지하게 작동합니다.
    • 리덕스 툴킷의 createSlice 함수는 “slice reducer”함수를 생성한 뒤 상태 값을 안전한 불변성 업데이트를 통해 변화시킬 수 있게 해줍니다.
    • 이 “slice reducer”함수는 configureStorereducer 필드에 추가되며 리덕스 스토어 내부에 상태 값 이름이 정의됩니다.
  • 리액트 컴포넌트는 useSelector 훅을 이용해 스토어의 데이터를 읽는다.
    • 셀렉터 함수는 전체 state 객체를 받으며, 그 값을 반환해야합니다.
    • 셀렉터는 리덕스 스토어가 업데이트 될 때마다 재실행 될 것이며, 만약 변화된 값이 리턴 된다면 컴포넌트는 재렌더링 될 것 입니다.
  • 리액트 컴포넌트는 useDispatch 훅을 이용해 스토어에 actions을 전달해 업데이트 시킨다.
    • createSlice 는 우리가 추가한 슬라이스의 각각의 리듀서를 위한 액션 생성자 함수를 만들 것입니다.
    • dispatch(someActionCreator())를 컴포넌트에 불러와 액션을 전달합니다.
    • 리듀서는 전달받은 액션이 관계 있다고 생각되면 일치하는 상태 값의 새로운 상태를 반환할 것 입니다.
    • 폼의 인풋 값 같은 일시적인 데이터 들은 리액트 컴포넌트 단위 상태로 관리되며, 유저가 작성이 완료 됐을 때 그 값을 액션을 통해 전달해 스토어의 값을 업데이트 시킵니다.

Reference

https://redux.js.org/tutorials/essentials/part-3-data-flow

https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/master/?from-embed - 실습 전

https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/checkpoint-1-postAdded/?from-embed - 실습 후 결과물

0개의 댓글