본문으로 건너뛰기

Redux 핵심 원리, 파트 5: UI와 React

비공식 베타 번역

이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →

학습 내용
  • Redux 스토어가 UI와 어떻게 동작하는지
  • React와 함께 Redux를 사용하는 방법

소개

파트 4: 스토어에서 Redux 스토어 생성, 액션 디스패치, 현재 상태 읽는 방법을 살펴보았습니다. 또한 스토어 내부 작동 방식, 엔핸서와 미들웨어로 스토어에 추가 기능을 구현하는 방법, 액션 디스패치 시 앱 내부 상황을 확인할 수 있는 Redux DevTools 추가 방법도 다뤘습니다.

이번 섹션에서는 할 일 앱에 사용자 인터페이스를 추가하겠습니다. Redux가 UI 레이어와 전반적으로 어떻게 동작하는지 살펴보고, 특히 Redux가 React와 함께 어떻게 작동하는지 구체적으로 다룰 것입니다.

주의

이 페이지와 "핵심 원리" 튜토리얼 전체는 최신 React-Redux 훅 API 사용법을 다룹니다. 이전 방식의 connect API도 여전히 동작하지만, 현재 우리는 모든 Redux 사용자가 훅 API를 사용하기를 권장합니다.

또한 이 튜토리얼의 다른 페이지들은 의도적으로 이전 스타일의 Redux 로직 패턴을 보여줍니다. 이는 Redux의 원리와 개념을 설명하기 위해 현대적인 Redux Toolkit 패턴보다 더 많은 코드가 필요한 방식입니다. 오늘날 Redux로 앱을 구축하는 올바른 접근 방식은 Redux Toolkit을 사용하는 것입니다.

실제 애플리케이션을 위한 Redux Toolkit 및 React-Redux 훅을 사용한 "Redux를 올바르게 사용하는 방법"의 전체 예시는 "Redux Essentials" 튜토리얼을 참조하세요.

Redux와 UI 통합하기

Redux는 독립적인 JS 라이브러리입니다. 앞서 살펴본 것처럼, 사용자 인터페이스가 설정되지 않았더라도 Redux 스토어를 생성하고 사용할 수 있습니다. 이는 Redux를 어떤 UI 프레임워크와도 함께 사용할 수 있음(심지어 UI 프레임워크 없이도)을 의미하며, 클라이언트와 서버 모두에서 사용 가능합니다. React, Vue, Angular, Ember, jQuery 또는 바닐라 자바스크립트로 Redux 앱을 작성할 수 있습니다.

그러나 Redux는 특히 React와 잘 동작하도록 설계되었습니다. React는 UI를 상태의 함수로 설명할 수 있게 하며, Redux는 상태를 관리하고 액션에 응답하여 상태를 업데이트합니다.

이러한 이유로 이 튜토리얼에서는 할 일 앱을 구축하며 React를 사용하고, React와 Redux를 함께 사용하는 기본 사항을 다룰 것입니다.

본론에 들어가기 전에, 일반적으로 Redux가 UI 레이어와 어떻게 상호작용하는지 간단히 살펴보겠습니다.

Redux와 UI 통합 기본 사항

어떤 UI 레이어와 함께 Redux를 사용하든지 일관된 단계가 필요합니다:

  1. Redux 스토어 생성

  2. 업데이트 구독

  3. 구독 콜백 내부에서:

    1. 현재 스토어 상태 가져오기
    2. 해당 UI 부분에 필요한 데이터 추출
    3. 데이터로 UI 업데이트
  4. 필요한 경우 초기 상태로 UI 렌더링

  5. Redux 액션을 디스패치하여 UI 입력에 응답

1부에서 본 카운터 앱 예제로 돌아가 이 단계들이 어떻게 적용되는지 살펴보겠습니다:

// 1) Create a new Redux store with the `createStore` function
const store = Redux.createStore(counterReducer)

// 2) Subscribe to redraw whenever the data changes in the future
store.subscribe(render)

// Our "user interface" is some text in a single HTML element
const valueEl = document.getElementById('value')

// 3) When the subscription callback runs:
function render() {
// 3.1) Get the current store state
const state = store.getState()
// 3.2) Extract the data you want
const newValue = state.value.toString()

// 3.3) Update the UI with the new value
valueEl.innerHTML = newValue
}

// 4) Display the UI with the initial store state
render()

// 5) Dispatch actions based on UI inputs
document.getElementById('increment').addEventListener('click', function () {
store.dispatch({ type: 'counter/incremented' })
})

어떤 UI 레이어를 사용하든 Redux는 모든 UI와 동일한 방식으로 동작합니다. 실제 구현은 성능 최적화를 위해 일반적으로 조금 더 복잡하지만, 매번 동일한 단계를 따릅니다.

Redux는 별도의 라이브러리이므로, 특정 UI 프레임워크와 함께 Redux를 사용할 수 있도록 도와주는 다양한 "바인딩" 라이브러리가 존재합니다. 이러한 UI 바인딩 라이브러리는 스토어 구독 및 상태 변경 시 UI를 효율적으로 업데이트하는 세부 사항을 처리하므로, 해당 코드를 직접 작성할 필요가 없습니다.

React와 함께 Redux 사용하기

공식 React-Redux UI 바인딩 라이브러리는 Redux 코어와 별도의 패키지입니다. 추가로 설치해야 합니다:

npm install react-redux

이 튜토리얼에서는 React와 Redux를 함께 사용하는 데 필요한 가장 중요한 패턴과 예시를 다루며, 할 일 앱의 일부로 실제 작동 방식을 살펴보겠습니다.

정보

React와 Redux를 함께 사용하는 완전한 가이드와 React-Redux API 문서는 **공식 React-Redux 문서**를 참조하세요.

컴포넌트 트리 설계

요구사항에 기반해 상태 구조를 설계한 것처럼, 애플리케이션의 전체 UI 컴포넌트 집합과 상호 관계도 설계할 수 있습니다.

앱의 비즈니스 요구사항 목록을 바탕으로 최소한 다음 컴포넌트들이 필요합니다:

  • <App>: 나머지 모든 요소를 렌더링하는 루트 컴포넌트
    • <Header>: "새 할 일" 텍스트 입력 필드와 "모든 할 일 완료" 체크박스 포함
    • <TodoList>: 필터링 결과에 기반한 현재 표시할 할 일 항목 목록
      • <TodoListItem>: 할 일의 완료 상태를 토글할 수 있는 체크박스와 색상 카테고리 선택기가 있는 개별 할 일 항목
    • <Footer>: 활성 할 일 개수 표시 및 완료 상태와 색상 카테고리에 기반한 목록 필터링 컨트롤

이 기본 컴포넌트 구조 외에도 여러 방식으로 컴포넌트를 분할할 수 있습니다. 예를 들어 <Footer> 컴포넌트는 하나의 큰 컴포넌트일 수도 있고, <CompletedTodos>, <StatusFilter>, <ColorFilters> 같은 작은 컴포넌트들로 구성될 수도 있습니다. 분할하는 단일한 정답은 없으며 상황에 따라 큰 컴포넌트로 작성하거나 여러 작은 컴포넌트로 나누는 것이 더 나을 수 있습니다.

지금은 이해를 쉽게 하기 위해 이 작은 컴포넌트 목록으로 시작하겠습니다. React를 이미 알고 있다고 가정하므로 이 컴포넌트들의 레이아웃 코드 작성 세부사항은 건너뛰고 React 컴포넌트에서 실제로 React-Redux 라이브러리를 사용하는 방법에 집중하겠습니다.

다음은 Redux 관련 로직을 추가하기 전의 초기 React UI입니다:

useSelector로 스토어에서 상태 읽기

할 일 항목 목록을 표시할 수 있어야 합니다. 스토어에서 할 일 목록을 읽고 항목들을 반복 처리하며 각 할 일 항목에 대해 <TodoListItem> 컴포넌트를 표시하는 <TodoList> 컴포넌트부터 만들어 보겠습니다.

React 함수 컴포넌트에서 React 상태 값에 접근할 수 있게 해주는 useState 같은 React 훅에 익숙할 것입니다. React는 또한 커스텀 훅 작성도 허용하는데, 이는 React의 내장 훅 위에 자체 동작을 추가하는 재사용 가능한 훅을 추출합니다.

다른 많은 라이브러리처럼 React-Redux도 자체 커스텀 훅을 포함하고 있어 컴포넌트에서 사용할 수 있습니다. React-Redux 훅은 React 컴포넌트가 상태를 읽고 액션을 디스패치하여 Redux 스토어와 통신할 수 있게 해줍니다.

첫 번째로 살펴볼 React-Redux 훅은 useSelector으로, React 컴포넌트가 Redux 스토어에서 데이터를 읽을 수 있게 합니다.

useSelector는 단일 함수를 인자로 받으며, 이 함수를 셀렉터(selector) 함수라고 부릅니다. 셀렉터는 전체 Redux 스토어 상태를 인자로 받아 상태에서 일부 값을 읽고 그 결과를 반환하는 함수입니다.

예를 들어, 우리의 할 일 애플리케이션에서 Redux 상태는 할 일 항목 배열을 state.todos로 관리합니다. 해당 todos 배열을 반환하는 간단한 셀렉터 함수를 작성할 수 있습니다:

const selectTodos = state => state.todos

또는 현재 "완료됨"으로 표시된 할 일 항목이 몇 개인지 확인하고 싶을 수도 있습니다:

const selectTotalCompletedTodos = state => {
const completedTodos = state.todos.filter(todo => todo.completed)
return completedTodos.length
}

따라서 셀렉터는 Redux 스토어 상태에서 값을 반환할 수 있을 뿐만 아니라, 해당 상태를 기반으로 파생된 값도 반환할 수 있습니다.

할 일 배열을 <TodoList> 컴포넌트로 불러오겠습니다. 먼저 react-redux 라이브러리에서 useSelector 훅을 임포트한 후, 셀렉터 함수를 인자로 전달하여 호출합니다:

src/features/todos/TodoList.js
import React from 'react'
import { useSelector } from 'react-redux'
import TodoListItem from './TodoListItem'

const selectTodos = state => state.todos

const TodoList = () => {
const todos = useSelector(selectTodos)

// since `todos` is an array, we can loop over it
const renderedListItems = todos.map(todo => {
return <TodoListItem key={todo.id} todo={todo} />
})

return <ul className="todo-list">{renderedListItems}</ul>
}

export default TodoList

<TodoList> 컴포넌트가 처음 렌더링될 때, useSelector 훅은 selectTodos를 호출하고 전체 Redux 상태 객체를 전달합니다. 셀렉터가 반환하는 값은 훅을 통해 컴포넌트로 반환됩니다. 따라서 컴포넌트 내 const todos는 Redux 스토어 상태 내부의 state.todos 배열과 동일한 값을 가지게 됩니다.

그런데 {type: 'todos/todoAdded'} 같은 액션을 디스패치하면 어떻게 될까요? Redux 상태는 리듀서에 의해 업데이트되지만, 컴포넌트는 새로운 할 일 목록으로 재렌더링하기 위해 변경 사항을 인지해야 합니다.

스토어 변경을 수신하기 위해 store.subscribe()를 호출할 수 있으므로, 모든 컴포넌트에서 스토어를 구독하는 코드를 작성해볼 수 있습니다. 하지만 이는 금방 반복적이고 관리하기 어려워질 것입니다.

다행히 useSelector가 자동으로 Redux 스토어를 구독해 줍니다! 따라서 액션이 디스패치될 때마다 셀렉터 함수를 즉시 다시 호출합니다. 셀렉터가 반환한 값이 이전 결과와 달라지면, useSelector는 새로운 데이터로 컴포넌트를 재렌더링합니다. 우리는 컴포넌트에서 useSelector()를 한 번 호출하기만 하면 되며, 나머지 작업은 자동으로 처리됩니다.

하지만 여기서 반드시 기억해야 할 중요한 점이 있습니다:

주의

useSelector는 엄격한 === 참조 비교로 결과를 비교하므로, 셀렉터 결과가 새로운 참조일 때마다 컴포넌트가 재렌더링됩니다! 즉 셀렉터에서 새로운 참조를 생성해 반환하면, 데이터가 실제로 변경되지 않았더라도 액션이 디스패치될 때마다 컴포넌트가 재렌더링될 수 있습니다.

예를 들어, 다음 셀렉터를 useSelector에 전달하면 array.map()이 항상 새로운 배열 참조를 반환하기 때문에 컴포넌트가 항상 재렌더링됩니다:

// Bad: always returning a new reference
const selectTodoDescriptions = state => {
// This creates a new array reference!
return state.todos.map(todo => todo.text)
}

이 문제를 해결하는 한 가지 방법은 이 섹션 후반부에 설명하겠습니다. 또한 7부: 표준 Redux 패턴에서 "메모이즈된(memoized)" 셀렉터 함수로 성능을 개선하고 불필요한 재렌더링을 방지하는 방법도 다루겠습니다.

또한 셀렉터 함수를 별도 변수로 작성할 필요는 없습니다. 다음과 같이 useSelector 호출 내부에 직접 셀렉터 함수를 작성할 수 있습니다:

const todos = useSelector(state => state.todos)

useDispatch로 액션 디스패치하기

이제 Redux 스토어에서 컴포넌트로 데이터를 읽어오는 방법을 알게 되었습니다. 그렇다면 컴포넌트에서 스토어로 액션을 디스패치하려면 어떻게 해야 할까요? React 외부에서는 store.dispatch(action)을 호출할 수 있지만, 컴포넌트 파일 내에서는 스토어에 접근할 수 없습니다. 따라서 컴포넌트 내부에서 dispatch 함수 자체에 접근할 방법이 필요합니다.

React-Redux의 useDispatch은 스토어의 dispatch 메서드를 반환합니다. (실제 구현은 return store.dispatch입니다.)

따라서 액션을 디스패치해야 하는 컴포넌트에서 const dispatch = useDispatch()를 호출한 후, 필요에 따라 dispatch(someAction)을 호출할 수 있습니다.

이제 <Header> 컴포넌트에서 이 방법을 적용해봅시다. 사용자가 새로운 할 일 항목을 입력할 수 있도록 하고, 입력된 텍스트를 포함한 {type: 'todos/todoAdded'} 액션을 디스패치해야 합니다.

"제어된 입력"을 사용하는 일반적인 React 폼 컴포넌트를 작성하겠습니다. 사용자가 폼에 텍스트를 입력하고 특별히 Enter 키를 누를 때 해당 액션을 디스패치합니다.

src/features/header/Header.js
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'

const Header = () => {
const [text, setText] = useState('')
const dispatch = useDispatch()

const handleChange = e => setText(e.target.value)

const handleKeyDown = e => {
const trimmedText = e.target.value.trim()
// If the user pressed the Enter key:
if (e.key === 'Enter' && trimmedText) {
// Dispatch the "todo added" action with this text
dispatch({ type: 'todos/todoAdded', payload: trimmedText })
// And clear out the text input
setText('')
}
}

return (
<input
type="text"
placeholder="What needs to be done?"
autoFocus={true}
value={text}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
)
}

export default Header

Provider로 스토어 전달하기

이제 컴포넌트들은 스토어에서 상태를 읽고 액션을 디스패치할 수 있습니다. 그러나 아직 빠진 부분이 있습니다. React-Redux 훅은 어떻게 올바른 Redux 스토어를 찾을까요? 훅은 자바스크립트 함수이므로, store.js에서 스토어를 자동으로 가져올 수 없습니다.

대신 컴포넌트에서 사용할 스토어를 명시적으로 React-Redux에 알려야 합니다. 전체 <App><Provider> 컴포넌트로 감싸고, Redux 스토어를 <Provider>의 prop으로 전달함으로써 이를 해결합니다. 이 작업을 한 번 수행하면 애플리케이션의 모든 컴포넌트가 필요한 경우 Redux 스토어에 접근할 수 있습니다.

메인 index.js 파일에 이를 추가해봅시다:

src/index.js
import React from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'

import App from './App'
import store from './store'

const root = createRoot(document.getElementById('root'))

root.render(
// Render a `<Provider>` around the entire `<App>`,
// and pass the Redux store to it as a prop
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)

이로써 React와 함께 React-Redux를 사용하는 핵심 부분을 다룹니다:

  • React 컴포넌트에서 useSelector 훅을 호출하여 데이터 읽기

  • React 컴포넌트에서 useDispatch 훅을 호출하여 액션 디스패치

  • 전체 <App> 컴포넌트를 <Provider store={store}>로 감싸서 다른 컴포넌트들이 스토어와 통신할 수 있도록 설정

이제 실제로 앱과 상호작용할 수 있어야 합니다! 지금까지 구현된 작동 UI는 다음과 같습니다:

이제 할 일 앱에서 함께 사용할 수 있는 몇 가지 추가 패턴을 살펴보겠습니다.

React-Redux 패턴

전역 상태, 컴포넌트 상태 및 폼

지금쯤 "앱의 모든 상태를 항상 Redux 스토어에 넣어야 하나요?"라는 의문이 들 수 있습니다.

대답은 아니오입니다. 앱 전체에서 필요한 전역 상태는 Redux 스토어에 넣어야 하지만, 한 곳에서만 필요한 상태는 컴포넌트 상태로 유지해야 합니다.

앞서 작성한 <Header> 컴포넌트가 좋은 예시입니다. 입력 필드의 onChange 핸들러에서 액션을 디스패치하고 리듀서에 텍스트 문자열을 저장함으로써 현재 입력 텍스트를 Redux 스토어에 보관할 수 있습니다. 하지만 이렇게 해도 아무런 이점이 없습니다. 해당 텍스트 문자열은 <Header> 컴포넌트에서만 사용되기 때문입니다.

따라서 이 값을 <Header> 컴포넌트의 useState 훅에 보관하는 것이 합리적입니다.

마찬가지로 isDropdownOpen이라는 불리언 플래그가 있다면, 앱의 다른 컴포넌트들은 이 값을 신경 쓰지 않을 것이므로 로컬 상태로 유지해야 합니다.

React + Redux 앱에서는 전역 상태는 Redux 스토어에, 로컬 상태는 React 컴포넌트에 유지해야 합니다.

어디에 둘지 확실하지 않다면, 어떤 종류의 데이터를 Redux에 넣어야 하는지 판단하는 일반적인 규칙을 참고하세요:

  • 애플리케이션의 다른 부분이 이 데이터를 필요로 하나요?
  • 이 원본 데이터를 기반으로 파생된 데이터를 생성해야 하나요?
  • 동일한 데이터가 여러 컴포넌트를 구동하는 데 사용되나요?
  • 특정 시점으로 상태를 복원할 수 있는 기능(예: 시간 여행 디버깅)이 유용한가요?
  • 데이터를 캐시하고 싶나요(예: 재요청하지 않고 이미 상태에 있는 데이터 사용)?
  • UI 컴포넌트를 핫 리로딩할 때 이 데이터의 일관성을 유지하고 싶나요(컴포넌트가 교체될 때 내부 상태를 잃을 수 있음)?

이것은 또한 Redux에서 폼을 어떻게 생각해야 하는지에 대한 좋은 예시입니다. 대부분의 폼 상태는 Redux에 보관하지 않는 것이 좋습니다. 대신 편집 중인 데이터는 폼 컴포넌트에 두고, 사용자가 완료했을 때 Redux 액션을 디스패치하여 스토어를 업데이트하세요.

컴포넌트에서 여러 선택자 사용하기

현재는 <TodoList> 컴포넌트만 스토어에서 데이터를 읽고 있습니다. <Footer> 컴포넌트도 일부 데이터를 읽기 시작하면 어떻게 될지 살펴보겠습니다.

<Footer> 컴포넌트는 세 가지 정보를 알아야 합니다:

  • 완료된 할 일 항목 수

  • 현재 "상태" 필터 값

  • 현재 선택된 "색상" 카테고리 필터 목록

이 값들을 컴포넌트에서 어떻게 읽을 수 있을까요?

하나의 컴포넌트 내에서 useSelector를 여러 번 호출할 수 있습니다. 사실 이 방법이 권장됩니다. useSelector를 호출할 때마다 가능한 가장 작은 양의 상태만 반환해야 합니다.

앞서 완료된 할 일 항목 수를 세는 선택자를 작성하는 방법을 이미 살펴보았습니다. 필터 값의 경우 상태 필터 값과 색상 필터 값 모두 state.filters 슬라이스에 있습니다. 이 컴포넌트가 둘 다 필요하므로 전체 state.filters 객체를 선택할 수 있습니다.

앞서 언급했듯이, 모든 입력 처리를 <Footer>에 직접 넣거나 <StatusFilter> 같은 개별 컴포넌트로 분리할 수 있습니다. 설명을 간결하게 하기 위해 입력 처리 작성의 정확한 세부 사항은 건너뛰고, 일부 데이터와 변경 핸들러 콜백을 props로 받는 별도의 작은 컴포넌트가 있다고 가정하겠습니다.

이 가정을 바탕으로 컴포넌트의 React-Redux 관련 부분은 다음과 같을 수 있습니다:

src/features/footer/Footer.js
import React from 'react'
import { useSelector } from 'react-redux'

import { availableColors, capitalize } from '../filters/colors'
import { StatusFilters } from '../filters/filtersSlice'

// Omit other footer components

const Footer = () => {
const todosRemaining = useSelector(state => {
const uncompletedTodos = state.todos.filter(todo => !todo.completed)
return uncompletedTodos.length
})

const { status, colors } = useSelector(state => state.filters)

// omit placeholder change handlers

return (
<footer className="footer">
<div className="actions">
<h5>Actions</h5>
<button className="button">Mark All Completed</button>
<button className="button">Clear Completed</button>
</div>

<RemainingTodos count={todosRemaining} />
<StatusFilter value={status} onChange={onStatusChange} />
<ColorFilters value={colors} onChange={onColorChange} />
</footer>
)
}

export default Footer

ID로 목록 항목에서 데이터 선택하기

현재 <TodoList>는 전체 state.todos 배열을 읽고 실제 할 일 객체를 각 <TodoListItem> 컴포넌트에 prop으로 전달합니다.

이 방법은 동작하지만 성능 문제가 발생할 수 있습니다.

  • 하나의 할 일 객체를 변경하면 할 일과 state.todos 배열 모두의 복사본을 생성하게 되며, 각 복사본은 메모리에서 새로운 참조입니다

  • useSelector는 결과로 새로운 참조를 발견하면 컴포넌트를 강제로 다시 렌더링합니다

  • 따라서 할 일 객체 하나가 업데이트될 때마다(예: 완료 상태 토글을 위해 클릭) 전체 <TodoList> 부모 컴포넌트가 다시 렌더링됩니다

  • 그런 다음 React는 기본적으로 모든 자식 컴포넌트를 재귀적으로 다시 렌더링하기 때문에, 실제로 변경되지 않은 대부분의 <TodoListItem> 컴포넌트들도 모두 다시 렌더링됩니다!

컴포넌트를 다시 렌더링하는 것은 나쁘지 않습니다. React가 DOM을 업데이트해야 하는지 확인하는 방법이기 때문입니다. 하지만 실제로 아무것도 변경되지 않았을 때 많은 컴포넌트를 다시 렌더링하면 목록이 너무 클 경우 성능이 저하될 수 있습니다.

이 문제를 해결하는 방법은 여러 가지입니다. 한 가지 옵션은 모든 <TodoListItem> 컴포넌트를 React.memo()로 래핑하여 실제로 props가 변경되었을 때만 다시 렌더링되도록 하는 것입니다. 이는 성능 개선을 위해 자주 사용하는 좋은 방법이지만, 자식 컴포넌트가 실제로 변경되기 전까지 항상 동일한 props를 받아야 한다는 점이 필요합니다. 각 <TodoListItem> 컴포넌트가 할 일 항목을 prop으로 받고 있으므로 실제로 변경된 prop을 받아 다시 렌더링해야 하는 항목은 하나뿐입니다.

다른 옵션은 <TodoList> 컴포넌트가 스토어에서 할 일 ID 배열만 읽고, 이 ID들을 자식 <TodoListItem> 컴포넌트에 props로 전달하는 것입니다. 그러면 각 <TodoListItem>은 해당 ID를 사용해 필요한 올바른 할 일 객체를 찾을 수 있습니다.

이 방법을 시도해 보겠습니다.

src/features/todos/TodoList.js
import React from 'react'
import { useSelector } from 'react-redux'
import TodoListItem from './TodoListItem'

const selectTodoIds = state => state.todos.map(todo => todo.id)

const TodoList = () => {
const todoIds = useSelector(selectTodoIds)

const renderedListItems = todoIds.map(todoId => {
return <TodoListItem key={todoId} id={todoId} />
})

return <ul className="todo-list">{renderedListItems}</ul>
}

이번에는 <TodoList>에서 스토어로부터 할 일 ID 배열만 선택하여 각 todoIdid prop으로 자식 <TodoListItem>에 전달합니다.

그런 다음 <TodoListItem>에서 이 ID 값을 사용해 할 일 항목을 읽을 수 있습니다. 또한 할 일 ID를 기반으로 "toggled" 액션을 dispatch하도록 <TodoListItem>을 업데이트할 수 있습니다.

src/features/todos/TodoListItem.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'

import { availableColors, capitalize } from '../filters/colors'

const selectTodoById = (state, todoId) => {
return state.todos.find(todo => todo.id === todoId)
}

// Destructure `props.id`, since we only need the ID value
const TodoListItem = ({ id }) => {
// Call our `selectTodoById` with the state _and_ the ID value
const todo = useSelector(state => selectTodoById(state, id))
const { text, completed, color } = todo

const dispatch = useDispatch()

const handleCompletedChanged = () => {
dispatch({ type: 'todos/todoToggled', payload: todo.id })
}

// omit other change handlers
// omit other list item rendering logic and contents

return (
<li>
<div className="view">{/* omit other rendering output */}</div>
</li>
)
}

export default TodoListItem

그러나 여기에 문제가 있습니다. 앞서 셀렉터에서 새 배열 참조를 반환하면 컴포넌트가 매번 리렌더링된다고 말씀드렸는데, 현재 <TodoList>에서는 새 ID 배열을 반환하고 있습니다. 이 경우 할 일을 토글할 때 ID 배열의 내용은 동일해야 합니다. 할 일 항목을 추가하거나 삭제하지 않았기 때문에 동일한 할 일 항목을 표시하고 있기 때문입니다. 하지만 해당 ID를 포함하는 배열은 새 참조이므로 <TodoList>는 실제로 필요하지 않을 때도 리렌더링됩니다.

이에 대한 한 가지 해결책은 useSelector가 값 변경을 확인하는 방식을 변경하는 것입니다. useSelector는 두 번째 인자로 비교 함수를 받을 수 있습니다. 비교 함수는 이전 값과 새 값을 인자로 받아 동일하다고 판단되면 true를 반환합니다. 값이 동일하면 useSelector는 컴포넌트 리렌더링을 발생시키지 않습니다.

React-Redux에는 배열 내부 항목이 여전히 동일한지 확인하는 데 사용할 수 있는 shallowEqual 비교 함수가 있습니다. 이를 사용해 보겠습니다:

src/features/todos/TodoList.js
import React from 'react'
import { useSelector, shallowEqual } from 'react-redux'
import TodoListItem from './TodoListItem'

const selectTodoIds = state => state.todos.map(todo => todo.id)

const TodoList = () => {
const todoIds = useSelector(selectTodoIds, shallowEqual)

const renderedListItems = todoIds.map(todoId => {
return <TodoListItem key={todoId} id={todoId} />
})

return <ul className="todo-list">{renderedListItems}</ul>
}

이제 할 일 항목을 토글하면 ID 목록이 동일한 것으로 간주되어 <TodoList>는 리렌더링할 필요가 없습니다. 하나의 <TodoListItem>은 업데이트된 할 일 객체를 받아 리렌더링하지만, 나머지는 기존 할 일 객체를 유지하므로 전혀 리렌더링되지 않습니다.

앞서 언급했듯이, 컴포넌트 렌더링 성능을 개선하기 위해 "메모이제이션된 셀렉터"라는 특수한 셀렉터 함수를 사용할 수도 있으며, 다른 섹션에서 사용 방법을 살펴보겠습니다.

학습 내용 요약

이제 동작하는 할 일 앱이 완성되었습니다! 스토어를 생성하고 <Provider>를 사용해 React UI 계층에 스토어를 전달한 다음 React 컴포넌트에서 useSelectoruseDispatch를 사용해 스토어와 통신합니다.

정보

누락된 나머지 UI 기능을 직접 구현해 보세요! 추가해야 할 항목 목록은 다음과 같습니다:

  • <TodoListItem> 컴포넌트에서 useDispatch 훅을 사용해 색상 카테고리 변경 및 할 일 삭제 액션을 dispatch하세요
  • <Footer>에서 useDispatch 훅을 사용해 모든 할 일 완료 표시, 완료 항목 삭제, 필터 값 변경 액션을 dispatch하세요

필터 구현은 7부: 표준 Redux 패턴에서 다룰 예정입니다.

간결하게 하기 위해 생략했던 컴포넌트와 섹션을 포함해 현재 앱의 모습을 살펴보겠습니다:

요약
  • Redux 스토어는 모든 UI 계층과 함께 사용 가능
    • UI 코드는 항상 스토어를 구독하고 최신 상태를 가져와 스스로 다시 그림
  • React-Redux는 React용 공식 Redux UI 바인딩 라이브러리
    • React-Redux는 별도의 react-redux 패키지로 설치됨
  • useSelector 훅을 통해 React 컴포넌트가 스토어에서 데이터 읽기 가능
    • 셀렉터 함수는 전체 스토어 state를 인자로 받고 해당 상태를 기반으로 값을 반환
    • useSelector는 셀렉터 함수를 호출하고 셀렉터의 결과를 반환
    • useSelector는 스토어를 구독하며 액션이 dispatch될 때마다 셀렉터를 다시 실행
    • 셀렉터 결과가 변경될 때마다 useSelector는 새 데이터로 컴포넌트 리렌더링을 강제함
  • useDispatch 훅을 통해 React 컴포넌트가 스토어에 액션을 dispatch 가능
    • useDispatch는 실제 store.dispatch 함수를 반환
    • 컴포넌트 내부에서 필요할 때마다 dispatch(action) 호출 가능
  • <Provider> 컴포넌트는 다른 React 컴포넌트가 스토어를 사용할 수 있게 함
    • 전체 <App> 주위에 <Provider store={store}>를 렌더링

다음 단계

이제 UI가 작동하므로 Redux 앱이 서버와 통신하는 방법을 살펴볼 차례입니다. 6부: 비동기 로직에서는 타임아웃이나 HTTP 요청 같은 비동기 로직이 Redux 데이터 흐름에 어떻게 통합되는지 다루겠습니다.