[Redux] Basic Redux Data Flow (번역, 정리)

chaevivi·2023년 11월 30일
0
post-thumbnail

리덕스의 데이터 흐름

💡 이 챕터에서 배우는 내용

  • createSlice로 리덕스 스토어에 리듀서 로직의 "슬라이스"를 추가하는 방법
  • useSelector 훅으로 컴포넌트의 리덕스 데이터 읽기
  • useDispatch 훅으로 컴포넌트의 액션을 디스패치 하기


1. 메인 포스트 피드

소셜 미디어 피드 앱을 만들면서 리덕스 데이터의 흐름을 알아볼 것입니다.


1.1. 포스트 슬라이스 생성

  • 첫 번째 단계는 포스트 데이터를 가지고 있는 새로운 리덕스 "슬라이스"를 생성하는 것입니다.
  • 리덕스 스토어에 해당 데이터를 가지고 있으면, 페이지에 해당 데이터를 보여주기 위한 리액트 컴포넌트를 만들 수 있습니다.

  • src > features > posts > postsSlice.js 파일 생성

  • 리듀서 함수를 만들기 위해 리덕스 툴킷의 createSlice 함수를 사용합니다.
    리듀서 함수에는 일부 초기 데이터가 포함되어야 앱이 시작했을 때 리덕스 스토어가 해당 값을 불러올 수 있습니다.

  • createSlice를 임포트하고 초기 포스트 배열을 정의하고 해당 배열을 createSlice에 전달하고 createSlice가 생성될 수 있도록 포스트 리듀서 함수를 내보냅니다.

    // features/posts/postsSlice.js
    
    import { createSlice } from '@reduxjs/toolkit'
    
    const initialStte = [
      { 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 함수를 임포트하고 postReducerposts라는 이름의 리듀서 필드에 전달되기 위해 configureStore에 대한 호출을 업데이트합니다.

    // app/store.js
    
    import { configureStore } from '@reduxjs/toolkit'
    
    import postsReudcer from '../features/posts/postsSlice'
    
    export default configureStore({
      reducer: {
        posts: postsReducer
      }
    })
    • 이는 리덕스에게 posts 필드를 가지려면 최상위 레벨의 상태 객체를 원하고 액션이 디스패치될 때 state.posts의 모든 데이터는 postsReducer 함수에 의해 업데이트될 것이라는 것을 알려줍니다.
    • 리덕스 데브툴 익스텐션의 현재 상태 내용을 보면 해당 작업들을 확인할 수 있습니다.

1.2. 포스트 리스트 표시

  • 이제 스토어에 어느 정도 포스트 데이터가 있으므로 포스트 리스트를 보여주는 리액트 컴포넌트를 만들 수 있습니다. 피드 포스트 기능과 관련된 모든 코드는 posts 폴더에 있어야 하고 PostsList.js라는 새로운 파일을 생성합니다.

  • 포스트 리스트를 렌더링할 때 어딘가에서 데이터를 가져와야 합니다.

    • 리액트 컴포넌트는 리액트-리덕스 라이브러리의 useSelector 훅을 사용하여 리덕스 스토어에서 데이터를 읽을 수 있습니다.
    • 작성한 "선택자 함수"는 전체 리덕스의 상태 객체를 매개변수로 사용하여 호출될 것이고, 컴포넌트가 스토어로부터 필요한 구체적인 데이터를 반환해야 합니다.
  • 초기의 PostsList 컴포넌트는 리덕스 스토어의 state.post 값을 읽고 포스트 배열을 반복해서 각각을 화면에 보여줍니다.

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

  • 그 다음 App.js 에서 라우팅을 업데이트해서 PostsList 컴포넌트가 나오도록 합니다.

    • App.js에서 PostsList 컴포넌트를 임포트하고 <PostsList /> 컴포넌트를 추가합니다.
    • React Fragment로 감싸줍니다. 왜냐하면 나중에 메인 페이지에서 다른 것들을 추가할 것이기 때문입니다.
    // App.js
    
    import React from 'react'
    import {
      BrowserRouter as Router,
      Switch,
      Rout,
      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

이렇게 추가하고 나면 앱의 메인페이지는 아래처럼 보입니다.


1.3. 새로운 포스트 추가

  • 포스트를 작성하고 저장할 수 있는 "Add New Post" 폼을 만들어 봅시다. 처음에는 빈 폼을 만들고 페이지에 추가할 것입니다. 그리고 폼을 리덕스 스토어에 연결해 "Save Post" 버튼이 클릭될 때마다 새로운 포스트들이 추가되도록 할 것입니다.

(1) 새로운 포스트 폼 추가

  • 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에 임포트하고 <PostsList /> 컴포넌트 위에 추가합니다.

    // App.js
    
    <Route 
      exact
      path="/"
      render={() => (
        <React.Fragment>
          <AddPostForm />
          <PostsList />
        </React.Fragment>
      )}
    />
  • 페이지 헤더의 바로 밑에서 해당 폼을 볼 수 있습니다.


(2) 포스트 항목 저장

  • 이제 리덕스 스토어에 새 포스트 항목을 추가하여 포스트 슬라이스를 업데이트 해봅시다.

  • 포스트 슬라이스는 포스트 데이터에 대한 모든 업데이트를 처리해야 합니다. createSlice 호출 부분에 reudcers라고 불리는 객체가 있습니다. 지금은 비어 있지만 포스트 추가를 위해 리듀서 함수를 추가해야 합니다.

    • reudcer 안에 2개의 인자(현재 state 값과 디스패치된 action 객체)를 받는 postAdded라는 이름의 함수를 추가합니다. 포스트 슬라이스는 자신이 맡은 데이터에 대해서만 알고있기 때문에 state 인자는 전체 리덕스 상태 객체가 아니라 그 자체로 포스트 배열이 될 것입니다.
    • action 객체는 action.payload 필드로 새 포스트 항목을 가집니다. 그리고 state 배열에 새 포스트 객체를 넣을 수 있습니다.
  • postAdded 리듀서 함수를 적을 때 createSlice는 자동으로 같은 이름의 "액션 생성자" 함수를 생성합니다. 액션 생성자는 내보낼 수도 있고, 사용자가 "Save Post" 버튼을 누르면 액션이 디스패치될 수 있도록 UI 컴포넌트에서 사용할 수도 있습니다.

    // 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()를 호출하거나 createSlice()안에 state.someField = someValue 같은 객체 필드를 수정하는 것이 안전합니다. 왜냐하면 이머 라이브러리를 사용해서 해당 변동사항을 내부적으로 안전한 불변하는 업데이트로 바꾸기 때문입니다.

(3) "Post Added" 액션 디스패치

  • AddPostForm은 텍스트 입력값과 아직 아무 일도 안하는 "Save Post" 버튼을 가지고 있습니다. postAdded 액션 생성자를 디스패치하고 사용자가 쓴 제목과 내용을 포함하는 새포스트 객체를 넘겨 주기 위한 클릭 핸들러를 추가해야 합니다.
    또한 포스트 객체는 id 필드가 필요합니다. 지금은 초기의 테스트 포스트 아이디로 가짜 숫자를 사용하고 있습니다. 리덕스 툴킷은 고유의 랜덤 아이디를 생성할 수 있는 nanoid 함수를 가지고 있습니다.

  • 컴포넌트로 액션을 디스패치 하기 위해 스토어의 dispach 함수에 접근해야 합니다. 리액트 리덕스로부터 useDispatch훅을 호출하여 접근할 수 있습니다. 또한 이 파일에 postAdded 액션 생성자를 임포트해야 합니다.

  • 컴포넌트에 사용할 수 있는 dispatch 함수를 가져오면 클릭 핸들러에서 dispatch(postAdded())를 호출할 수 있습니다. 리액트 컴포넌트의 useState 훅으로 제목과 내용 값을 가져올 수 있고, 새로운 아이디를 생성할 수 있고, 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>
            {/* 폼 입력 생략 */}
            <button type="button" onClick={onSavePostClicked}>
              Save Post
            </button>
          </form>
        </section>
      )
    }
  • 이제 제목과 텍스트를 입력하고 "Save Post"를 클릭하면 포스트 리스트의 포스트에 새로운 아이템이 추가되는 것을 볼 수 있습니다.


위의 예제는 완전한 리덕스 데이터 흐름 사이클을 보여줍니다.

  • 포스트 리스트가 useSelector로 스토어로부터 초기 포스트 데이터를 읽고 초기 UI를 렌더링합니다.
  • 새 포스트 항목으로 데이터를 포함하는 postAdded 액션을 디스패치합니다.
  • 포스트 리듀서는 postAdded 액션을 보고 새로운 항목으로 포스트 배열을 업데이트합니다.
  • 리덕스 스토어는 UI에게 몇몇 데이터가 변경되었다고 알려줍니다.
  • 포스트 리스트는 업데이트된 포스트 배열을 읽고, 새 포스트를 보여주기 위해 리렌더링합니다.

  • 디스패치한 액션을 보기 위해 리덕스 데브툴스 익스텐션을 체크하고 액션의 응답으로 리덕스 상태가 어떻게 업데이트되는지 볼 수 있습니다. 액션 리스트의 "posts/postAdded"를 클릭하면 "Action" 탭은 아래와 같이 보입니다.

  • AddPostForm 컴포넌트는 사용자가 타이핑한 제목과 내용 값을 추적하기 위해 안에 리액트 useState 훅을 가지고 있다는 것에 유의하세요.
    리덕스 스토어는 애플리케이션에서 "전역" 데이터만 가지고 있습니다. 이때는 AddPostForm만 입력값 필드의 최신 값에 대해 알아야 합니다. 그래서 리덕스 스토어에서 임시의 데이터를 가지고 있으려하기 보다는 리액트 컴포넌트 상태의 데이터를 가지고 있으려고 합니다. 사용자의 입력값을 기반으로 한 최종 값을 스토어에 업데이트하기 위해 사용자가 폼을 다 작성했을 때 리덕스 액션을 디스패치 합니다.


2. 정리

  • 리덕스 상태는 "리듀서 함수"에 의해 업데이트됩니다.
    • 리듀서는 기존의 상태값을 복사하고 복사한 값을 새로운 데이터로 수정함으로써 항상 불변한 새로운 상태값을 계산합니다.
    • 리덕스 툴킷의 createSlice 함수는 "슬라이스 리듀서"를 생성하고 안전하고 불변한 업데이트로 바꿔주는 "변하는(mutating)" 코드를 작성할 수 있도록 해줍니다.
    • 슬라이스 리듀서 함수가 configureStorereducer 필드에 추가되면 리덕스 스토어 안에서 데이터 상태 필드 이름들을 정의합니다.
  • 리액트 컴포넌트들은 useSelector 훅을 이용해 스토어의 데이터를 읽을 수 있습니다.
    • 선택자 함수는 전제 state 객체를 받고 값을 반환해야 합니다.
    • 선택자들은 리덕스 스토어가 업데이트될 때마다 재시작하고 그들이 반환한 데이터가 변경되면 컴포넌트가 리렌더링됩니다.
  • 리액트 컴포넌트들은 스토어를 업데이트하기 위해 useDispatch 훅을 이용해 액션을 디스패치합니다.
    • createSlice는 슬라이스에 추가된 리듀서들의 액션 생성자 함수를 생성합니다.
    • 액션을 디스패치하기 위해 dispatch(someActionCreator())을 컴포넌트 안에 호출합니다.
    • 그러면 리듀서들이 실행되고 해당 액션들이 관련있는지 확인하고 관련 있다면 새 상태값을 반환합니다.
    • 폼 입력값과 같은 임시의 데이터는 리액트 컴포넌트 상태로 유지되어야 합니다. 사용자가 폼을 다 작성하면 스토어를 업데이트하기 위해 리덕스 액션을 디스패치합니다.



출처
🔗 공식 문서: https://ko.redux.js.org/tutorials/essentials/part-3-data-flow
🔗 Github: https://github.com/chaevivin/Front-end_study/blob/main/Redux/Redux_Data_Flow.md
더 자세하게 정리되어 있고, 번역된 문서를 보고싶다면 Github에서 제가 작성한 문서를 확인하세요.

profile
직접 만드는 게 좋은 프론트엔드 개발자

0개의 댓글