[Redux] Redux App Structure (번역, 정리)

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

리덕스 앱의 구조

💡 이 챕터에서 배우는 내용

  • 전형적인 React + Redux 앱의 구조
  • Redux DevTools 확장 프로그램에서 상태 변화를 보는 방법


1. Counter 예제

  • 버튼을 클릭하면 숫자가 증가하거나 감소하는 카운터 앱 예제로 React + Redux 애플리케이션의 중요 부분을 살펴볼 것입니다.

1.1. Counter App으로 Redux DevTools 살펴보기

  • 브라우저의 DevTools를 열고 "Redux" 탭을 선택합니다.
  • 그리고 오른쪽 상단 툴바에 있는 "State" 버튼을 클릭하면 아래와 같은 화면을 볼 수 있습니다.
  • 사진의 오른쪽을 보면 리덕스 스토어가 다음과 같은 앱 상태 값으로 시작하는 것을 볼 수 있습니다.
    {
      counter: {
        value: 0
      }
    }
  • DevTools는 앱을 사용할 때 스토어 상태가 어떻게 변하는지 보여줍니다.

  • 카운터 앱에서 + 버튼을 누르고 Redux DevTools의 "Diff" 탭을 보면 value 값이 1로 증가한 것을 볼 수 있습니다.
  • 여기서 두 가지 중요 사항을 알 수 있습니다.
    • + 버튼을 누르면 "counter/increment" 타입의 액션이 스토어에게 디스패치됩니다.
    • 액션이 디스패치되면 state.counter.value 필드는 0에서 1로 변화합니다.

  • 그 다음 아래 단계를 시행합니다.
    • + 버튼을 다시 누르면 값이 2가 됩니다.
    • - 버튼을 한 번 누르면 값이 1이 됩니다.
    • Add Amount 버튼을 누르면 값이 3이 됩니다.
    • 텍스트 박스의 숫자를 "2"에서 "3"으로 변경합니다.
    • Add Async 버튼을 누르면 프로그레스 바가 채워지고 몇 초 후에 값이 6이 됩니다.
  • Redux DevTools로 돌아가면 버튼을 클릭할 때마다 디스패치된 총 다섯 개의 액션들을 볼 수 있습니다. 왼쪽 리스트의 맨 마지막 "counter/incrementByAmount" 탭을 클릭하고, "Action" 탭을 클릭하면 아래와 같은 액션 객체를 볼 수 있습니다.
  • "Diff" 탭을 클릭하면 액션의 응답으로 state.counter.value 필드가 3에서 6으로 변경된 것을 볼 수 있습니다.

  • DevTools는 앱의 내부에서 무슨 일이 일어나는지와 상태값이 어떻게 변화하는지 볼 수 있습니다.
  • DevTools는 앱을 디버깅하도록 도와주는 더 많은 명령들과 옵션들을 가지고 있습니다.
    • 오른쪽의 "Trace" 탭을 클릭하면, 액션이 스토어에 도착했을 때 실행 중이던 소스 코드의 여러 부분들과 함께 자바스크립트 함수가 패널에서 추적들을 쌓는 것을 볼 수 있습니다.
    • 특히 한 줄을 하아라이트되어야 합니다. 이 라인은 <Counter> 컴포넌트로부터 액션을 디스패치하는 코드입니다.
    • 이는 어느 부분의 코드에서 특정 액션을 디스패치 했는지 추적하기 쉽게 해줍니다.


2. 앱의 동작 방식

  • 앱이 무슨 일을 하는 지 알아봤습니다. 이제 어떻게 동작하는 지 알아봅시다.

  • 애플리케이션을 구성하는 중요 파일들
    • /src
      • index.js: 앱의 시작 부분
      • App.js: 리액트 컴포넌트의 최상위 레벨
      • /app
        • store.js: 리덕스 스토어 인스턴스 생성
      • /features
        • /counter
          - Counter.js: 카운터 기능의 UI를 보여주는 리액트 컴포넌트
          - counterSlice.js: 카운터 기능의 리덕스 로직

  • 이제 리덕스 스토어가 어떻게 생성되는 지 알아봅시다.

2.1. 리덕스 스토어 생성

// app/store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export default configureStore({
  reducer: {
    counter: counterReducer
  }
})
  • configureStore
    • 리덕스 툴킷의 configureStore 함수를 사용하면 리덕스 스토어가 생성됩니다.
    • configureStore은 전달하려는 리듀서를 인자로 요청합니다.
    • 한 애플리케이션에는 여러 기능들이 있고, 각각의 기능들은 고유의 리듀서 함수를 가집니다. configureStore을 호출하면 객체의 모든 리듀서들을 전달할 수 있습니다. 객체의 키 이름은 최후의 상태 값의 키들로 정의됩니다.
  • counterReducer
    • counterReducer 함수는 카운터 로직의 리듀서 함수를 내보내는 features/counter/counterSlice 파일에서 임포트합니다.
    • counterReducer는 스토어를 생성하면 포함시킬 수 있습니다.
  • {counter: counterReducer}
    • 위 처럼 객체를 넘기면 리덕스 상태 객체의 state.counter을 원한다는 의미입니다.
    • 액션이 전달될 때마다 state.counter을 업데이트할 지 여부와 방법을 결정하는 역할을 counterReducer 함수가 담당하기를 원한다는 의미입니다.

(1) Redux Slices

  • slice는 리덕스 리듀서 로직과 앱의 하나의 기능을 담당하는 액션의 집합입니다.

  • 슬라이스라는 이름은 최상위 리덕스 상태 객체를 여러 개의 상태 "슬라이스"로 나누는 데에서 유래하였습니다.

  • 예를 들어, 블로그 앱의 스토어 코드를 봐봅시다.

    import { configureStore } from '@reduxjs/toolkit'
    import usersReducer from '../features/users/usersSlice'
    import postsReducer from '../features/posts/postsSlice'
    import commentsReducer from '../features/comments/commnetsSlice'
    
    export default configureStore({
      reducer: {
        users: usersReducer,
        posts: postsReducer,
        comments: commentsReducer
      }
    })
  • state.users, state.posts, state.comments는 리덕스 상태의 분리된 "슬라이스" 입니다.

  • usersReducerstate.users 슬라이스가 업데이트하는 것에 반응하기 때문에, usersReducer를 "슬라이스 리듀서" 함수라고 부릅니다.


2.2. 슬라이스 리듀서와 액션 생성

  • 위에서 counterReducer 함수가 features/counter/counterSlice.js에서 온다는 것을 배웠습니다. 지금부터 그 파일에 무엇이 있는지 자세히 알아봅시다.

// features/counter/counterSlice.js

import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter',
  initialValue: {
    value: 0
  },
  reducers: {
    increment: state => {
      // 리덕스 툴킷을 사용하면 "변형" 로직 작성 가능
      // 실제로 상태 변형 X
      // 왜냐하면 "초안 상태"의 변경을 감지하고 해당 변경을 기반으로
      // 새로운 불변의 상태를 생산하는 이머 라이브러리(Immer library)를 사용하기 때문
      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer
  • 이전에 UI에서 여러 종류의 버튼을 클릭하면 세 개의 다른 리덕스 액션 타입들을 디스패치하는 것을 살펴보았습니다.
    • {type: "counter/increment"}
    • {type: "counter/decrement"}
    • {type: "counter/incrementByAmount"}
  • 액션들은 타입 필드가 있는 순수 객체이고, 타입 필드는 항상 문자열입니다. 그리고 일반적으로 액션 객체를 생성하고 반환하는 함수인 "액션 생성자(action creator)"를 가지고 있습니다. 그렇다면 이 액션 객체와 타입 문자열들과 액션 생성자들은 어디에서 정의될까요?
  • 우리가 매번 액션 객체, 타입 문자열, 액션 생성자를 작성할 수 있지만 리덕스에서 중요한 것은 리듀서 함수와 새로운 상태를 계산하는 로직입니다.
  • 리덕스 툴킷(Redux Toolkit)은 createSlice라는 함수를 가지고 있는데, 이 함수는 액션 타입 문자열, 액션 생성자 함수, 액션 객체를 생성해줍니다. 우리는 이 슬라이스에 이름을 붙여주고 리듀서 함수를 가지고 있는 객체를 작성하면(counterSlice) 자동으로 상응하는 액션 코드를 생성해 줍니다.
    • name 옵션의 문자열은 각각의 액션 타입의 첫 번째 부분에 사용되고 각각의 리듀서 함수의 키 이름은 두 번째 부분에서 사용됩니다.
    • 그래서 "counter" 이름 + "increment" 리듀서 함수는 {type: "counter/increment"}의 액션 타입을 생성하였습니다.
  • name 필드 이외에도 createSlice는 처음 호출될 때 상태가 있도록 리듀서 초기 상태값을 전달해야 합니다. 위에서는 0부터 시작하는 value 필드를 가진 객체를 제공하였습니다.
  • 그 다음 3개의 리듀서 함수를 볼 수 있습니다. (increment, decrement, incrementByAmount) 이 함수들은 다양한 버튼들을 클릭할 때 디스패치되는 세 개의 액션 타입들과 상응합니다.
  • createSlice는 자동으로 우리가 작성한 리듀서 함수와 같은 이름의 액션 생성자를 생성합니다. 우리는 그 중 하나를 호출하고 어떤 값을 반환하는지 확인할 수 있습니다.
    console.log(counterSlice.action.increment())
    // {type: "counter/increment"}
  • 또한 createSlice는 모든 액션 타입들에 응답하는 방법을 아는 슬라이스 리듀서 함수 또한 생성합니다.
    const newState = counterSlice.reducer(
      { value: 10 },
      counterSlice.action.increment()
    )
    console.log(newState) 
    // {value: 11}

2.3. 리듀서 규칙

  • 리듀서는 특별한 규칙들을 따라야 합니다.
    • 리듀서는 stateaction 인자에 기반한 새로운 상태 값만 계산해야 합니다.
    • 이미 존재하는 state를 수정하면 안됩니다. 대신에 이미 존재하는 state를 복사하고 복사한 값을 변경함으로써 불변하는 업데이트를 만들어야 합니다.
    • 리덕스는 비동기 로직이나 다른 "부작용"을 수행해서는 안 됩니다.

  • 왜 위의 규칙들이 중요할까요?
    • 리덕스의 목표 중 하나는 코드를 실용적으로 만드는 것입니다. 함수의 출력값이 입력된 인자에 의해서만 계산될 때 코드가 어떻게 동작하는지 이해하고 테스트하기 쉬워집니다.
    • 반면에, 함수가 바깥의 변수에 의존하거나 랜덤하게 행동하면 함수를 동작할 때 어떤 일이 일어나는 지 알 수 없을 것입니다.
    • 만약 함수가 인자를 포함한 다른 값들을 수정한다면 이로 인해 애플리케이션이 예기치 않게 작동하는 방식이 변경될 수 있습니다. 이는 "상태값을 업데이트했지만 UI가 업데이트되지 않아!" 같은 버그의 일반적인 원인이 됩니다.
    • 리덕스 데브툴스의 몇몇 기능들은 리듀서가 위의 규칙들을 올바르게 따라가도록 합니다.
  • "불변 업데이트"에 대한 규칙을 특별히 중요합니다.

2.4. 리듀서와 불변 업데이트

  • 지금까지 "불변"(이미 존재하는 객체/배열 값 수정)과 "불변성"(값을 변하지 않는 것처럼 대하는 것)에 대해 이야기 하였습니다.
  • 리덕스에서 리듀서는 원래의/현재의 상태 값을 절대 변경할 수 없습니다.
    //  ❌ Illegal
    state.value = 123
    • 리덕스에서 상태를 변경하지 않아야 하는 이유
      • 최신 값을 표시하기 위해 UI가 적절히 업데이트 되지 않는 버그가 발생합니다.
      • 상태가 왜 그리고 어떻게 업데이트 되는지 이해하기 어려워집니다.
      • 테스트 코드를 작성하기 어려워집니다.
      • "시간 여행 디버깅(time-travel debugging)"을 올바르게 사용할 수 없습니다.
      • 리덕스의 의도된 바와 사용 패턴에 어긋납니다.

  • 원래의 값을 변경하지 못한다면 어떻게 업데이트된 상태값을 반환할 수 있을까요?
    - 리듀서는 원래 값의 복사본을 만들고 해당 복사본을 변경할 수 있습니다.
    // ✅ 안전한 방법
    return {
      ...state,
      value: 123
    }
    • 자바스크립트의 배열과 객체의 스프레드 연산자와 원래 값의 복사본을 반환하는 함수들을 사용하여 불변하는 업데이트를 작성할 수 있습니다.
  • 하지만 "직접 불변하는 업데이트를 작성하는 방법은 기억하고 제대로 사용하기 어렵다"라고 생각할 수 있습니다.
    • 불변 업데이트 로직을 직접 손으로 쓰는 것은 어렵습니다. 그리고 리듀서에서 실수로 상태를 변경하는 것은 리듀서 사용자들이 흔히 하는 실수입니다.
  • 그래서 리덕스 툴킷의 createSlice 함수는 더 쉽게 불변 업데이트를 작성할 수 있도록 도와줍니다.
    • createSlice는 Immer라고 불리는 라이브러리를 사용합니다.
    • Immer는 데이터를 래핑하기 위해 Proxy라고 불리는 특별한 자바스크립트 툴을 사용하고, 래핑된 데이터를 "변동"하는 코드를 작성할 수 있도록 해줍니다.
    • 하지만 Immer는 마치 직접 불변 업데이트 로직을 작성하는 것처럼, 만들려고 하는 모든 변동사항을 추적하고 안전하게 불변 업데이트 값을 반환하기 위해 변동 사항 리스트를 사용합니다.
    • 이 코드 대신에
      function handewrittenReducer(state, action) {
        return {
          ...state,
          first: {
            ...state.first,
            second: {
              ...state.first.second,
              [action.someId]: {
                ...state.first.second[action.someId],
                fourth: action.someValue
              }
            }
          }
        }
      }
    • 이렇게 작성할 수 있습니다.
      function reducerWithImmer(state, action) {
        state.first.second[action.someId].fourth = action.someValue
      }
    • 주의
      • 리덕스 툴킷의 createSlicecreateReducer에서만 "변동" 로직을 작성할 수 있습니다. 왜냐하면 내부에서 Immer를 사용하기 때문입니다. 리듀서에서 Immer 없이 변동 로직을 작성한다면 상태값을 변동시킬 것이고 결국 버그가 발생하게 됩니다.

  • counter slice의 실제 리듀서

    // features/counter/counterSlice.js
    
    export const counterSlice = createSlice({
      name: 'counter',
      initialState: {
        value: 0
      },
      reducers: {
        increment: state => {
          state.value += 1
        },
        decrement: state => {
          state.value -= 1
        },
        incrementByAmount: (state, action) => {
          state.value += action.payload
        }
      }
    })
    • increment
      • increment 리듀서는 state.value로 항상 1을 더합니다. 왜냐하면 Immer는 초기 state 객체의 변경 사항을 알고 있으므로 여기서 아무것도 반환할 필요가 없기 때문입니다.
    • decrement
      • increment 리듀서와 동일하게 decrement 리듀서는 1을 뺍니다.
    • 두 개의 리듀서 모두 실제 코드에서 우리가 action 객체를 볼 필요가 없습니다. 어쨋든 전달되지만 필요하지 않기 때문에 리듀서에 action을 파라미터로 선언하지 않아도 됩니다.
    • 반면에 incrementByAmount 리듀서에서 알아야 할 것이 있습니다. -> 카운터 값에 얼마나 추가되어야 하는지.
      • 그래서 stateaction 인자를 가진 리듀서를 선언합니다.
      • 이때는 텍스트 박스에 타이핑한 수가 action.payload 필드에 넣어져야 state.value 에 더할 수 있습니다.

2.5. Thunks로 비동기 로직 작성하기

  • 지금까지 애플리케이션의 모든 로직은 동기화 되었습니다. 액션은 디스패치 되었고, 스토어는 리듀서를 작동하고 새로운 상태값을 계산하고, 디스패치 함수는 종료합니다.
  • 하지만 자바스크립트 언어는 많은 비동기 코드를 작성하는 방법을 가지고 있고 앱들은 일반적으로 API로부터 데이터를 패칭해 오는 비동기 로직을 가지고 있습니다.
  • 우리는 리덕스 앱에서 비동기 로직을 놓아야 할 자리가 필요합니다.

  • thunk는 비동기 로직을 가지고 있는 리덕스 함수의 특별한 종류입니다.

  • Thunks는 2가지 함수를 사용하여 작성할 수 있습니다.

    • thunk 함수 안에서는 dispatchgetState를 인자로 받습니다.
    • 생성자 함수 바깥에서는 thunk 함수를 생성하고 반환합니다.
  • counterSlice 에서 내보낸 다음 함수는 thunk 액션 생성자 예제입니다.

    // features/counter/counterSlice.js
    
    // 아래의 함수는 thunk라고 불리고 비동기 로직을 수행
    // 일반 액션처럼 디스패치 가능: 'dispatch(incrementAsync(10))'
    // 첫 번째 인자로 'dispatch' 함수와 thunk 호출
    // 그 다음 비동기 코드 실행 -> 다른 액션들 디스패치
    export const incrementAsync = amount => dispatch => {
      setTimeout(() => {
        dispatch(incrementByAmount(amount))
      }, 1000)
    }
    • 전형적인 리덕스 액션 생성자에서 똑같은 방식으로 사용할 수 있습니다.
      store.dispatch(incrementAsnc(5))

  • 하지만 thunks를 사용하려면 redux-thunk 미들웨어(리덕스용 플러그인 유형)가 리덕스 스토어가 생성될 때 스토어에 추가되는 과정이 필요합니다. 다행히도 리덕스 툴킷의 configureStore 함수는 자동으로 이를 설정해 두었기 때문에 thunks를 그냥 사용할 수 있습니다.

  • 서버로부터 데이터를 패치하기 위해 AJAX를 호출할 때, thunk에 호출을 둘 수 있습니다. 어떻게 정의하는 지 알아보기 위해 예제를 살펴봅시다.

    // features/counter/counterSlice.js
    
    // "thunk 생성자" 함수 바깥
    const fetchUserById = userId => {
      // "thunk 함수" 내부
      return async (dispatch, getState) => {
        try {
          // thunk에서 비동기 호출
          const user = await userAPI.fetchById(userId)
          // 응답을 받을 때 액션 디스패치
          dispatch(userLoaded(user))
        } catch (err) {
          // 에러가 발생하면 여기에서 처리
        }
      }
    }

2.6. 리액트 카운터 컴포넌트

  • 이전에 독립형 리액트 컴포넌트인 <Counter>를 살펴보았습니다. React + Redux 앱은 <Counter> 컴포넌트와 비슷하지만 다른 점이 있습니다.

  • 먼저 Counter.js 컴포넌트 파일부터 살펴봅시다.

    // features/counter/Counter.js
    
    import React, { useState } from 'react'
    import { useSelector, useDispatch } from 'react-redux'
    import {
      decrement,
      increment,
      incrementByAmount,
      incrementAsync,
      selectCount
    } from './counterSlice'
    import styles from './Counter.module.css'
    
    export function Counter() {
      const count = useSelector(selectCount)
      const dispatch = useDispatch()
      const [incrementAmount, setIncrementAmount] = useState('2')
    
      return (
        <div>
            <div className={styles.row}>
              <button
                  className={styles.button}
                  arial-label="Increment value"
                  onClick={() => dispatch(increment())}
              >
                +
              </button>
              <span className={styles.value}>{count}</span>
              <button
                  className={styles.button}
                  arial-label="Decrement value"
                  onClick={() => dispatch(decrement())}
              >
                -
              </button>
          </div>
          {/* omit additional rendering output here */}
        </div>
      )
    }
    • 순수 리액트 예제와 같이 useState 훅으로 데이터를 저장하는 Counter 컴포넌트 함수가 있습니다.
    • 하지만 이 컴포넌트는 현재 카운터 값을 상태값으로 저장하는 것처럼 보이지 않습니다. count라고 불리는 변수가 있지만 ustState 훅에서 발생하지는 않습니다.
  • 리액트는 useStateuseEffect 같이 몇몇의 빌트인 훅을 가지고 있지만 다른 라이브러리들은 커스텀 로직을 빌드하기 위해 리액트 훅을 사용하는 자체적인 커스텀 훅을 만들 수 있습니다.


  • React-Redux 라이브러리는 리액트 컴포넌트가 리덕스 스토어와 상호작용하도록 하는 일련의 커스텀 훅들을 가지고 있습니다.

    • useSelector로 데이터 읽기

      • 첫 번째로, useSelector 훅은 컴포넌트가 리덕스 스토어 상태에서 필요한 데이터 조각을 추출할 수 있습니다.

        • 이전에 state를 인자로 취급하고 상태 값의 일부를 반환하는 "선택자" 함수에 대해 알아보았습니다.

        • counterSlice.js는 밑에서 이 선택자 함수를 가지고 있습니다.

          // features/counter/counterSlice.js
          
          // 아래의 함수는 선택자라고 불리는 값을 상태로부터 선택할 수 있도록 합니다.
          // 선택자는 슬라이스 파일 대신에 사용되는 곳인 인라인에서 정의될 수도 있습니다.
          // 예를 들어, 'useSelector((state) => state.counter.value)'
          export const selectCount = state => state.counter.value;
        • 리덕스 스토어에 접근할 때 현재의 카운터 값을 검색할 수 있습니다.

          const count = selectCount(store.getState())
          console.log(count)    // 0
        • 우리의 컴포넌트들은 리덕스 스토어에 직접 이야기 할 수 없기 때문에 컴포넌트 파일에서 임포트해 올 수 없습니다. 하지만 useSelector는 우리를 위해 뒤에서 리덕스 스토어에게 이야기 하는 일을 처리합니다. 만약 선택자 함수를 전달하면 우리를 위해 someSelector(store.getState())를 호출하고 결과값을 반환합니다.

        • 그래서 아래의 코드로 현재의 스토어 카운터를 받아올 수 있습니다.

          const count = useSelector(selectCount)
        • 또한 이미 내보낸 선택자만 사용하지 않아도 됩니다. 예를 들어, useSelector로 선택자 함수를 인라인 인자로 작성할 수 있습니다.

          const countPlusTwo = useSelector(state => state.counter.value)
        • 액션이 디스패치되고 리덕스 스토어가 업데이트될 때마다 useSelector는 선택자 함수를 재실행합니다. 만약 선택자가 지난번과 다른 값을 반환했다면 useSelector는 새로운 값으로 컴포넌트를 리렌더링합니다.

    • useDispatch로 액션 디스패치

      • 비슷하게 리덕스 스토어에 접근할 때 store.dispatch(increment()) 같이 액션 생성자를 사용하여 액션을 디스패치할 수 있습니다. 스토어 자체에 접근할 수 없기 때문에 dispatch 메서드에서만 접근할 수 있는 방법이 필요합니다.

        • useDispatch 훅은 dispatch 메서드에 접근하고 리덕스 스토어로부터 실제 dispatch 메서드를 제공합니다.

          const dispatch = useDispatch()
        • 거기에서 버튼을 누르는 것처럼 사용자가 무엇인가를 하면 액션을 디스패치할 수 있습니다.

          // features/counter/Counter.js
          
          <button
              className={styles.button}
              arial-label="Increment value"
              onClick={() => dispatch(increment())}
          >
            +
          </button>

2.7. 컴포넌트 상태와 폼

  • 지금쯤이면 "항상 내 앱의 모든 상태를 리덕스 스토어에 넣어야 하는가?"라는 의문이 생길 것입니다.

    • 답은 '아니다.'입니다. 앱 전반적으로 필요한 전역 상태는 리덕스 스토어에 있어야 합니다. 하지만 한 곳에서만 필요한 상태는 컴포넌트 상태에 있어야 합니다.

    • 예를 들어, 카운터에 값을 추가하기 위해 사용자가 다음 숫자를 입력해야 하는 텍스트 박스가 있다고 합시다.

      // features/counter/Counter.js
      
      const [incrementAmount, setIncrementAmount] = useState('2')
      
      //later
      return (
        <div className={styles.row}>
          <input
              className={styles.textbox}
              aria-label="Set increment amount"
              value={incrementAmount}
              onChange={e => setIncrementAmount(e.target.value)}
          />
          <button
              className={styles.button}
              onClick={() => dispatch(incrementByAmount(Number(incrementAmount) || 0))}
          >
            Add Amount
          </button>
          <button
              className={styles.asyncButton}
              onClick={() => dispatch(IncrementAsync(Number(incrementAmount) || 0))}
          >
              Add Async
          </button>
        </div>
      )
      • 현재 인풋 태그의 onChange 핸들러에서 액션을 디스패치하고 리듀서에 저장함으로써 숫자 문자열을 리덕스 스토어에 저장합니다. 하지만 이렇게 하면 어떠한 이익도 없습니다. <Counter> 컴포넌트 안에서 텍스트 문자열이 사용되는 곳은 여기밖에 없습니다.(당연히 이 예제의 다른 컴포넌트인 <App>이 있습니다. 하지만 많은 컴포넌트를 가진 큰 애플리케이션이라면, <Counter>에서만 입력값을 처리합니다.)
      • 그래서 <Counter> 컴포넌트의 useState 훅에서 해당 값을 유지하는 것이 더 합리적입니다.

  • 리액트 + 리덕스 앱에서 전역 상태는 리덕스 스토어로 들어가야 하고, 지역 상태는 리액트 컴포넌트 안에 있어야 합니다.
  • 어디에 두어야 할 지 모르겠다면 리덕스에 어떤 종류의 데이터를 넣어야 하는지 결정할 수 있는 아래의 일반적인 법칙을 참고하세요.
    • 애플리케이션의 다른 파트에서 이 데이터를 처리하는가?
    • 원래의 데이터를 기반으로 추가로 파생된 데이터를 만들 필요가 있는가?
    • 여러 컴포넌트를 작동하기 위해 똑같은 데이터를 사용해야 하는가?
    • 이 상태를 특정 시점으로 복원할 수 있다는 것이 가치가 있는가? (즉,시간 여행 디버깅)
    • 데이터를 캐시하고 싶은가? (즉, 다시 요청하는 대신에 이미 있는 상태를 사용하라.)
    • (교환될 때 내부 상태를 잃어버리는)UI 컴포넌트를 핫리로딩하는 동안에 이 데이터를 일관되게 유지하고 싶은가?
  • 이 법칙은 리덕스에서 일반적으로 폼에 대해 생각하는 방법에 대한 좋은 예이기도 합니다. 대부분의 폼 상태는 리덕스에서 유지되지 않아야 합니다. 대신에, 편집하는 동안 컴포넌트의 데이터를 유지하고, 사용자가 완료하면 스토어를 업데이트하기 위해 리덕스 액션을 디스패치해야 합니다.
  • 다음으로 넘어가기 전에 기억해야 할 것이 있습니다. counterSlice.jsincrementAsync thunk를 기억하고 계시나요? 다른 일반적인 액션 생성자를 디스패치하는 것과 똑같은 방법을 사용하고 있다는 것을 주의하세요. 이 컴포넌트는 일반 액션을 디스패치하는지 비동기 로직을 시작하는지 상관하지 않습니다. 이 컴포넌트는 우리가 버튼을 클릭할 때 어떤 것을 디스패치한다는 것만 알고 있습니다.

2.8. 스토어 제공

  • 지금까지 우리는 컴포넌트가 리덕스 스토어와 이야기 하기 위해 useSelectoruseDispatch 훅을 사용하는 것을 보았습니다. 하지만 스토어를 임포트하지 않는다면 훅들이 어떤 리덕스 스토어와 이야기해야 하는지 알 수 있을까요?

  • 우리는 카운터 애플리케이션의 모든 부분을 보았습니다.이제 이 애플리케이션의 처음 부분으로 돌아가 마지막 남은 퍼즐이 어떻게 맞춰지는 지 보아야 할 시간입니다.

    // index.js
    
    import React from 'react'
    import ReactDOM from 'react-dom'
    import './index.css'
    import App from './App'
    import store from './app/store'
    import { Provider } from 'react-redux'
    import * as serviceWorkerfrom './serviceWorker'
    
    ReactDOM.render(
      <Provider sotre={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    )
    • 리액트에게 루트 컴포넌트인 <App> 렌더링을 시작해달라고 말하기 위해 항상 ReactDOM.render(<App />)을 호출해야 합니다. useSelector 같은 훅들이 잘 동작하기 위해서는 리덕스 스토어를 뒤에서 전달하여 접근할 수 있도록 <Provider>라고 불리는 컴포넌트를 사용해야 합니다.
    • 스토어를 app/store.js에 이미 생성해 두었기 때문에 여기에서 임포트 해왔습니다. <Provider> 컴포넌트는 전제 <App> 주위에 두고 스토어에 전달합니다. (<Provider store={store}>)
    • 이제 useSelector이나 useDispatch를 호출하는 어떠한 리액트 컴포넌트라도 <Provider>에게 주었다고 리덕스 스토어와 이야기할 수 있을 것입니다.


3. 정리

  • 리덕스 툴킷의 configureStore API를 사용하여 리덕스 스토어를 생성할 수 있습니다.
    • configureStorereducer 함수를 기명 인자로 받아들입니다.
    • configureStore는 자동으로 스토어를 제일 좋은 기본으로 세팅합니다.
  • 리덕스 스토어는 일반적으로 "슬라이스"라고 불리는 파일로 조직되어 있습니다.
    • "슬라이스"는 리듀서 로직을 포함하고 액션은 리덕스 상태의 특정한 기능이나 섹션과 관련있습니다.
    • 리덕스 툴킷의 createSlice API는 액션 생성자와 개발자가 제공한 각각의 리듀서 함수의 액션 타입을 생성합니다.
  • 리덕스 리듀서는 특정한 법칙을 따라야 합니다.
    • stateaction 인자를 기반으로 한 새로운 상태 값만 계산해야 합니다.
    • 이미 존재하는 상태를 복사하여 불변의 업데이트를 만들어야 합니다.
    • 비동기 로직이나 다른 "부작용"을 포함할 수 없습니다.
    • 리덕스 툴킷의 createSlice API는 "돌연변이" 불변의 업데이트를 허용하기 위해 Immer를 사용합니다.
  • 비동기 로직은 일반적으로 "thunks"라고 불리는 특별한 함수에 의해 작성됩니다.
    • Thunks는 dispatchgetState를 인자로 받습니다.
    • 리덕스 툴킷은 기본적으로 redux-thunk 미들웨어를 실행합니다.
  • 리액트-리덕스는 리액트 컴포넌트가 리덕스 스토어와 상호작용할 수 있도록 합니다.
    • <Provider store={store}>로 앱을 감싸면 모든 컴포넌트에서 스토어를 사용할 수 있습니다.
    • 전역 상태는 리덕스 스토어로 들어가야 하고, 지역 상태는 리액트 컴포넌트 안에 있어야 합니다.



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

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

0개의 댓글