Redux | Redux Essentials, Part 2: Redux App Structure

woongstaa·2022년 4월 26일
0

Redux

목록 보기
2/3
post-thumbnail

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

어플리케이션 요소

  • 이번 파트에서는 어떻게 리덕스가 작동하는지 알아봅시다
  • 아래와 같이 앱이 구성되어 있습니다
    • /src
      • index.js 어플리케이션의 시작점
      • App.js 리액트 컴포넌트 최상위 레벨
      • /app
        • store.js 리덕스 스토어 인스턴스를 생성하는 곳
      • /features
        • /counter
          • Counter.js 카운트 기능이 있는 UI 컴포넌트
          • counterSlice.js 카운터 기능의 리덕스 로직

Redux Store 생성

app/store.js를 열면 아래와 같이 구성되어 있다.

// app/store.js

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

export default configureStore({
  reducer: {
    counter: counterReducer
  }
})
  • Redux storeRedux Toolkit이라는 라이브러리의 configureStore으로 생성되었습니다
  • configureStorereducer를 인자로 전달해야합니다
  • 해당 어플리케이션은 다양한 기능들로 구성되어 있어 각각의 기능들은 reducer 함수를 가지고 있어야합니다.
  • configureStore을 호출할 때 객체 속에 다른 reducer를 전달해야 합니다.
    • key value는 최종 state 값에 맞게 정합니다.
  • features/counter/counterSlice.js라는 파일은 counter 로직에 관한 reducer 함수를 내보내고, storecounterReducer 함수를 불러올 수 있습니다.
  • {counter: counterReducer}라는 객체를 전달한다는 의미는 리덕스 state 객체의 state.counter가 필요하다는 의미이며, counterReducer 함수가 state.counteraction이 보내졌을 때 업데이트 여부와 방법을 결정 할 수 있도록 합니다.
  • Redux는 middleware와 enhancers로 불리는 플러그인으로 커스터마이즈해서 사용할 수도 있습니다.
  • configureStore은 좋은 개발자 경험을 위해 기본적으로 여러개의 middleware를 store에 추가하며, 또한 Redux DevTools Extension이 컨텐츠 검사를 할 수 있게 store에 설정합니다.

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/commentsSlice'

export default configureStore({
  reducer: {
    users: usersReducer,
    posts: postsReducer,
    comments: commentsReducer
  }
})
  • 예시 속 state.users, state.post, state.comments는 리덕스에서 분리된 각각의 조각 단위 상태입니다.
  • userReducer가 state.users 조각에 의존적으로 업데이트 되기 때문에 우리는 이것을 slice reducer 함수라고 부릅니다.

Reducers와 State 구조

리덕스 스토어는 생성되었을때 하나의 루트 리듀서 함수를 필요로 합니다. 만약 다양한 종류의 리듀서 슬라이스들이 존재한다면 어떻게 하나의 루트 리듀서를 대체할 수 있을까요? 그리고 어떻게 리덕스 스토어의 상태 값들을 규정할 수 있을까요? 만약 우리가 슬라이스 리듀서를 일일히 호출한다면 아래와 같을 것입니다.

function rootReducer(state = {}, action) {
  return {
    users: usersReducer(state.users, action),
    posts: postsReducer(state.posts, action),
    comments: commentsReducer(state.comments, action)
  }
}

위 예시는 각각의 슬라이스 리듀서를 개별로 호출할 수 있으며, 각각의 리덕스 상태 속 슬라이스로 전달 할 수 있습니다. 그리곤 최종적으로 새로운 리덕스 상태 객체를 각각의 밸류로 반환할 것 입니다. Redux는 위와 같은 과정을 자동적으로 처리해주는 combineReducers라는 함수를 가지고 있습니다. 해당 함수는 모든 slice reducers를 인자로 받으며 action을 dispatch 했을때 각각의 reducer를 호출하여 반환합니다. 각각의 slice reducer으로부터의 결과는 하나의 객체로 합쳐질 것입니다. 위 예시를 combineReducer를 사용해 아래와 같이 표현할 수 있습니다

const rootReducer = combineReducers({
  users: usersReducer,
  posts: postsReducer,
  comments: commentsReducer
})

우리가 slice reducer 객체를 configureStore로 보낼 때, configureStore는 하나의 root reducer를 생성하기 위해 combineReducers를 전달할 것입니다. 우리가 이전에 살펴봤듯이, reducer함수를 reducer 인자를 통해 바로 전달 할 수도 있습니다

const store = configureStore({
  reducer: rootReducer
})

Slice Reducers와 Actions 생성

features/counter/counterSlice.js 에서 counterReducer에 대해 알아봤으니 파일 속에 어떻게 구성되있는지 하나하나 살펴보도록 합시다

// features/counter/counterSlice.js

import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state => {
			// 리덕스 툴킷은 reducers에서 "mutating(변형)"을 사용할 수 있게 합니다.
			// "초기 상태"의 변화를 감지하고 새로운 불변성을 가진 상태를 생성하는
			// immer 라이브러리를 사용하기 때문에 실제로 상태를 변형시키는 것은 아닙니다.

      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가 전달한 Redux action type에 대해 알고 있습니다.

  • {type: “counter/increment”}
  • {type: “counter/decrement”}
  • {type: “counter/incrementByAmount”}

우리는 actions 객체가 type필드로 구성된 순수한 객체인 것을 알고 있습니다. type필드는 항상 string 값이며 통상적으로 action을 생성하고 반환하는 action creator 함수를 가지고 있습니다. 그래서 어디에 action 객체, type strings, action creator가 정의될까요?

우리는 매번 직접 정의했어야 했을겁니다. 아주 지루한 일이겠죠. 게다가 Redux에서 가장 중요한 것은 reducer 함수와 새로운 state를 계산하기 위한 로직입니다.

Redux Toolkit은 그 지루한 일을 해결해주는 createSlice라는 함수를 가지고 있습니다. 우리가 해야 할 일은 단지 slice의 이름을 정의하고, reducer 함수 내의 객체에 그 이름을 넣는 것입니다. 그렇게 한다면 대응하는 action을 자동적으로 생성할 것입니다. 이름을 정의할 때 보통 앞부분에는 각각의 action type을 뒷부분에는 reducer 함수의 key값을 넣습니다. 위 예시를 보면 action type인 counter와 reducer 함수 내의 action type인 increment를 합쳐 {type: “counter/increment”}로 사용한 것을 확인할 수 있습니다.

그리고 이름 필드, 즉 createSlice는 reducer에 initial state를 전달하기 때문에 처음 호출될 때 state값이 있어야합니다. 이러한 경우를 대비해 0으로 시작하는 value 필드를 제공합니다.

우리는 위 reducer 함수들이 각각 할당된 버튼을 눌려 일치하는 action type에 응답하는 것을 알 수 있습니다.

createSlice는 자동적으로 우리가 작성한 reducer함수 내에서 같은 이름을 가진 action creator들을 생성합니다. 우리는 아래와 같이 입력해 어떠한 값이 반환되는지도 확인 할 수 있습니다.

console.log(counterSlice.actions.increment())
// {type: "counter/increment"}

아래와 같이 입력해 모든 action type들이 어떻게 응답하는지도 확인할 수 있습니다

const newState = counterSlice.reducer(
  { value: 10 },
  counterSlice.actions.increment()
)
console.log(newState)
// {value: 11}

Reducers의 규칙

이전에도 서술했듯 reducer는 반드시 아래의 규칙을 따라야한다

  • state와 action인자에 기반해 새로운 state value를 계산해야만 한다
  • 기존의 state을 변환시키면 안된다. 대신 기존 state를 복사한 후 그 값을 변환시켜 불변성 업데이트를 시켜야만 한다
  • 비동기 로직을 사용하거나 side effect를 일으키는 로직은 사용하면 안된다

왜 이런 규칙이 중요할까? 여기엔 좀 다른 이유들도 있다.

  • Redux의 목표는 모든 코드가 예측가능하게 만드는 것이다. 함수의 output이 오직 input인자로만 계산된다면 코드를 이해하기 쉽고, 테스트하기도 쉬울 것이다.
  • 만약 함수가 외부 변수에 의존한다면, 혹은 무작위성을 띄고 행동한다면 어떠한 일이 일어날지 전혀 알 수 없다.
  • 만약 하나의 함수가 인자로 받은 값을 포함해 다른 값들까지 변화시킨다면, 전체 어플리케이션의 작동을 예측할 수 없게 만드는 것일 겁니다. 예를 들어 “내 상태를 업데이트 했는데 왜 UI가 업데이트 되지 않는거야?” 같은 버그가 일어날 것 입니다.
  • 그렇기 때문에 Redux DevTool은 우리가 얼마나 reducer의 규칙을 잘 지키냐에 따라 생산력이 결정된다고 할 수 있습니다.

immutable updates(불변성을 유지한 업데이트)는 정말 중요한 개념이고 더 얘기할 가치가 있습니다.

Reducers와 Immutable Updates

앞서 우리는 mutationimmutability에 관해 얘기했습니다.

리덕스에서는 reducer절대 기존/현재의 state 값을 직접 변경시키는 것을 허용하지 않습니다!

// 금지!! - 기본적으로 아래 작업은 state값을 직접 변경시킬 것 입니다
state.value = 123

Redux에서 state값을 변경시키지 않아야 하는 이유는

  • UI가 최신의 value를 보여주지 않는 버그가 발생합니다
  • state가 왜? 어떻게? 업데이트 되는지 이해하기 힘들어집니다
  • 테스트 코드를 작성하기 힘들어집니다
  • time-travel debugging을 활용하기 힘들어집니다
  • Redux를 위한 의도된 패턴에 위배됩니다

그렇다면 우리는 어떻게 기존/현재의 state를 건들이지 않고 업데이트 할 수 있을까요?

// copy를 사용했기 때문에 안전하게 state를 변경시킬 수 있습니다.
return {
	...state,
	value: 123
}

우리는 이미 JavaScript의 spread operator(전개연산자)를 사용해 원본 값을 복사해 immtable updates 하는 법을 알고 있습니다. 하지만 위 절차가 까다롭다고 생각 할 수도 있습니다.

일일이 immutable updates 로직을 작성하는 것은 어렵고, 사용자가 실수로 상태를 변경하는 것은 가장 흔한 실수이기도 합니다.

그런 실수와 어려움을 없애기 위해 Redux Toolkit의 createSlice는 immutable updates를 쉬운 방법으로 사용할 수 있게 도와줍니다.

createSlice는 Immer라고 불리는 라이브러리를 내부적으로 사용합니다. Immer 라이브러리는 제공된 데이터를 래핑한 후, 래핑한 코드를 변경하는 Proxy라고 불리는 특이한 JavaScript도구를 사용합니다. 하지만 Immer는 우리가 시도한 모든 변화를 추적해 만든 리스트를 이용해 안전하게 불변성을 유지한 업데이트된 값을 반환합니다. 만약 일일이 모든 불변성을 유지한 업데이트 로직을 작성한다면 아래와 같을겁니다.

function handwrittenReducer(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
}

훨씬 읽기 편해졌습니다!

하지만 여기엔 반드시 기억해야할 아주 중요한점이 있습니다.

  • state를 변경시키는 로직을 작성해야 할때는 Redux Toolkit의 createSlicecreateReducerImmer가 내장되어있기 때문에 반드시 이 둘만을 사용해야합니다!
  • 만약 Immer없이 값을 변경시키는 reducer를 사용했다면, state가 변경될 것이고 버그를 일으킬 것입니다!

이 포인트를 염두에 두고 이전에 봤던 counter slice로 돌아가 reducer를 살펴봅시다.

// features/counter/counterSlice.js

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state => {
			// 리덕스 툴킷은 reducers에서 "mutating(변형)"을 사용할 수 있게 합니다.
			// "초기 상태"의 변화를 감지하고 새로운 불변성을 가진 상태를 생성하는
			// immer 라이브러리를 사용하기 때문에 실제로 상태를 변형시키는 것은 아닙니다.

      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})

우리는 increment reducer가 state.value를 항상 1씩 더할 것을 알 수 있습니다. Immer가 우리가 초기 state 객체에 변화를 준 것을 알기 때문에, 우리는 일련의 복잡한 로직들을 반환할 필요가 없습니다. 같은 방법으로 decrement reducer는 1씩 빠질겁니다.

위에서 확인한 두 reducer에서는 우리는 action 객체에서 봤던 코드가 필요하지 않습니다. action 객체는 언제든지 전달될 것이지만 우리가 필요하지 않기 때문에 우리는 reducers를 위한 인자로 action을 선언하는 것을 생략할 수 있습니다.

반면 incrementByAmount reducer는 counter value에 얼마나 더해질지를 아는 것이 필요합니다. 그래서 우리는 reducer에 state와 action인자를 선언했습니다. 이런 경우엔 텍스트 박스에 입력한 양이 action.payload 필드에 입력되고 있으므로 state.value에 추가하는 로직을 구성할 수 있습니다.

Thunks를 이용해 비동기 로직 작성하기

counter 앱에서 모든 로직은 동기적으로 작동합니다. action은 dispatch되며 store은 reducer를 실행하면서 새로운 state를 계산하고, 그리곤 전달한 함수에서 로직이 끝이나게 됩니다. 하지만 JavaScript언어는 비동기적으로 코드를 작성하는 다양한 방법이 있습니다. 일반적으로 API에서 데이터를 받아올때 비동기 로직을 사용합니다. 그렇기 때문에 우리는 Redux에 비동기 로직을 넣을 장소가 필요합니다.

Thunk는 비동기 로직을 포함시킬 수 있는 특별한 종류의 Redux 함수입니다. Thunks를 사용하기 위해 두 가지 함수를 필요로 합니다.

  • Thunk 함수 내부엔 dispatchgetState를 인자로 받는 함수
  • Thunk 함수를 생성하고 반환하는 외부 생성자 함수

아래 함수는 counterSlice에서 가져온 Thunk action creator 예시입니다.

// 아래 함수는 thunk라고 불리는 함수이며 비동기적인 로직이 작동되게 허용해줍니다.
// `dispatch(incrementAsync(10))`는 일반적인 action처럼 전달될 수 있습니다
// 이것이 thunk에서 사용하는 첫번째 인자이며
// 비동기 코드는 실행되고 이후 다른 action들이 전달될 것입니다
export const incrementAsync = amount => dispatch => {
  setTimeout(() => {
    dispatch(incrementByAmount(amount))
  }, 1000)
}

우리는 같은 방법을 Redux action creator를 이용해 아래와 같이 사용할 수 있습니다

store.dispatch(incrementAsync(5))

그러나 thunk를 사용하기 위해선 redux-thunk라는 미들웨어가 Redux store가 생성됐을때 추가되어야 합니다. 다행히 Redux Toolkit의 configureStore 함수가 이미 우리를 위해 자동적으로 그 기능을 가지고 있습니다.

우리가 AJAX 통신을 서버와 할 때, 우리는 thunk를 작성해야합니다. 아래에 약간 길지만 어떻게 정의했는지 확인 할 수 있습니다.

// the outside "thunk creator" function
const fetchUserById = userId => {
  // the inside "thunk function"
  return async (dispatch, getState) => {
    try {
      // make an async call in the thunk
      const user = await userAPI.fetchById(userId)
      // dispatch an action when we get the response back
      dispatch(userLoaded(user))
    } catch (err) {
      // If something went wrong, handle it here
    }
  }
}

더 자세한 내용은 Part 5에서 다룰 예정입니다.

React Counter Component

앞서 우리는 독립적인 React 컴포넌트를 확인했습니다. React + Redux 앱은 컴포넌트와 비슷하지만 약간의 차이점이 있습니다.

// 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}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
      </div>
      {/* omit additional rendering output here */}
    </div>
  )
}

순수 React로 구성된 Counter라는 함수형 컴포넌트였다면, useState라는 훅을 사용해 데이터를 보관했을겁니다.

그러나 이 컴포넌트는 실제 현재의 counter value를 state로 가지고 있지 않은 것처럼 보입니다. count라는 변수가 있습니다. 하지만 useState 훅을 사용하지는 않았습ㄴ디ㅏ.

React는 useStateuseEffect같은 기본 훅이 있지만, 다른 라이브러리들은 그 라이브러리에 맞는 커스텀 훅을 만들 수 있습니다.

React-Redux 라이브러리는 Redux store과 React component가 상호작용 할 수 있는 커스텀 훅을 가지고 있습니다.

useSelector로 데이터 읽기

우선 useSelector 훅은 Redux store에 존재하는 state중 필요한 것만 가져옵니다.

앞서, 우리는 state를 인자로 받고 state의 일부 값을 반환하는 selector라는 함수를 사용했었습니다

// 아래 selector라고 불리는 함수는 state에서 값을 선택 할 수 있게 도와줍니다
// selector는 slice 파일을 사용하는 것 대신 inline으로 정의할 수 있습니다
// 예시: `useSelector((state) => state.counter.value)`

export const selectCount = state => state.counter.value

Redux store에 접근 할 수 있다면, 현재 counter 값을 아래와 같이 재할당 할 수 있습니다.

const count = selectCount(store.getState())
console.log(count)
// 0

컴포넌트들은 Redux store와 직접적으로 소통할 수 없기 때문에 우리는 컴포넌트 파일에서 불러올 수 없었습니다. 하지만 useSelector는 가능하게 도와줍니다. 만약 selector 함수를 전달한다면, someSelector(store.getState())를 호출해 결과를 반환 할 수 있을겁니다.

그래서 우리는 아래와 같이 현재 store counter 값을 가져올 수 있습니다

const counter = useSelector(selectCount)

위 예시처럼 이미 내보낸 selector만 사용할 필요는 없습니다. 예를 들어 우리는 useSelector를 사용해 selector를 inline으로 사용할 수 도 있습니다.

const conutPlusTwo = useSelector(state => state.counter.value + 2)

Action이 Redux Store에 전달이 되면 언제든지 업데이트 될 것이고, useSelector는 우리의 selector 함수를 재실행시킬겁니다. 만약 selector가 지난번과 다른 값을 반환한다면 useSelector는 우리의 컴포넌트가 새로운 값을 가지고 다시 렌더링이 되게 할 것입니다.

useDispatch를 사용해 Action 전달하기

비슷하게 우리가 Redux sotre에 접근하려면, action creator를 이용해 action을 전달해야한다는 것을 알고 있습니다. store.dispatch(increment())처럼요. 우리는 그것만으론 store에 접근할 수 없기 때문에 우리는 dispatch 메서드가 필요합니다.

useDispatch 훅은 그런 우리들을 위해 dispatch 메서드를 제공해줍니다.

const dispatch = useDispatch()

이걸로 인해 우리는 버튼을 클릭하는 것과 같은 일련의 행동들로 action을 전달할 수 있게 됩니다.

// features/counter/Counter.js

<button
  className={styles.button}
  aria-label="Increment value"
  onClick={() => dispatch(increment())}
>
  +
</button>

Component State and Forms

지금쯤 아마도 궁금할 것입니다. “내가 어플리케이션의 상태를 관리할 때 항상 Redux store를 사용해야할까?”

답은 NO입니다. 앱에서 다양한 컴포넌트에서 사용되는 전역 상태라면 Redux store에서 관리해야겠지만, 하나의 컴포넌트에서 사용되는 상태라면 해당 컴포넌트에서 관리하면 됩니다.

아래는 그 예시입니다.

// 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에서 사용하는 incrementAmount를 Redux store에서 관리 할 수도 있을겁니다. 하지만 어떤 메리트도 존재하지 않습니다. 이 값이 필요한 곳은 오직 이 컴포넌트이기 때문입니다.

그렇기 때문에 useState훅을 사용해 해당 값을 컴포넌트에 보관하는 것이 좋을 것입니다.

비슷하게 우리가 만약 isDropdownOpen같은 boolean타입의 값을 호출한다면, 어떤 컴포넌트도 그 값을 알 필요가 없으며 이 컴포넌트 내에서 지역적으로 관리될 것입니다.

React + Redux에서는 전역 state는 Redux store에서 관리하고, 지역 state는 해당 컴포넌트에서 관리합니다

만약 state를 어디에서 관리해야할지 애매하다면, 여기에 분류하는 기준이 몇가지 있습니다.

  • 다른 컴포넌트가 이 데이터 값을 알아야 하는가?
  • 이 데이터를 기반으로 파생되는 데이터를 생성 할 수 있어야 하는가?
  • 여러 컴포넌트들에서 같은 데이터를 중복 사용하고 있는가?
  • 특정 시점으로 값을 복원시킬 수 있어야 하는가? (time travel debugging, 시간 여행 디버깅)
  • 데이터를 캐싱하기 원하는가(현재 상태를 재요청하는 대신 빠르게 적용시키려는가)?
  • UI 컴포넌트를 hot-reloading하는 동안 데이터를 일관성있게 유지하고 싶은가?

위 기준은 Redux를 사용할때 일반적으로 고려할 수 있는 좋은 예시들입니다. 대부분의 form state는 Redux에 보관되지 않아야합니다. 대신 컴포넌트의 데이터를 편집할 때만 컴포넌트에 상태를 보관한 후, 작업이 완료됐을때 Redux action에 보내지도록 store에 업데이트 합니다.

다음으로 넘어가기 전에 counterSlice.js에 있던 incrementAsync thunk를 기억하고 있나요? 우리는 이것을 다른 action 생성자를 전달하는 것과 같은 방식으로 이 컴포넌트에서 사용합니다. 이 컴포넌트는 우리가 일반 action을 감시하거나 비동기 로직이 시작 되는 것을 신경쓰지 않습니다. 오직 버튼을 눌렸을때 뭔가 보낸다는 것만 알고 있습니다.

Providing the Store

우리는 컴포넌트들이 useSelectoruseDispatch 훅을 통해 Redux store와 소통하는 것을 살펴봤습니다. 하지만 왜 우리가 store를 불러와야하는지, 어떻게 이러한 훅들이 Redux store와 소통하는지는 알지 못합니다.

지금부터 우리는 어플리케이션의 다른 부분을 보게 될 것입니다. 이 어플리케이션의 시작점으로 되돌아가 어떻게 마지막 퍼즐 조각이 맞춰지는지 봅시다.

// 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 serviceWorker from './serviceWorker'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

우리는 항상 <App> 컴포넌트를 렌더링 시키기 위해서 React에게 ReactDOM.render(<App />)을 요청해 호출해야만 합니다. useSelector 같은 훅이 정상적으로 작동하기 위해서 우리는 <Provider>라고하는 컴포넌트를 사용해 Redux store를 백그라운드에서 전달해야 합니다.

우리는 이미 app/store.js에 store를 생성하였고, 우리는 이 곳에서 불러올 수 있습니다. 그리고 나서 <Provider> 컴포넌트를 전체 <App>을 감싸주게 설정합니다. 그리곤 우리의 store를 전달해줍니다. <Provider store={store}>처럼요.

이제 어떤 React 컴포넌트에서도 <Provider>를 부여했기 때문에 useSelector 혹은 useDispatch를 호출해 Redux store에 접근 할 수 있을 것 입니다.

0개의 댓글