useReducer
는 컴포넌트에 reducer를 추가할 수 있는 React 훅입니다.const [state, dispatch] = useReducer(reducer, initialArg, init?)
참조
useReducer(reducer, initialArg, init?)
컴포넌트의 최상위 레벨에서
useReducer
를 호출하여 reducer를 통해 state를 관리하세요.import { useReducer } from 'react'; function reducer(state, action) { // ... } function MyComponent() { const [state, dispatch] = useReducer(reducer, { age: 42 }); // ...
매개변수
reducer
: state가 업데이트되는 방식을 지정하는 reducer 함수입니다. 순수 함수여야 하며, state와 액션을 인자로 받아야 하고, 다음 state를 반환해야 합니다. state와 액션은 어떤 유형이든 가능합니다.
initialArg
: 초기 state가 계산되는 값입니다. 모든 유형의 값일 수 있습니다. 이 값에서 초기 state를 계산하는 방법은 다음init
인자에 따라 달라집니다.
- 선택적
init
: 초기 state 계산 방법을 지정하는 초기화 함수입니다. 이것을 지정하지 않으면 초기 state는initialArg
로 설정됩니다. 그렇지 않으면 초기 state는init(initialArg)
를 호출한 결과로 설정됩니다.
반환값
useReducer
는 정확히 두 개의 값을 가진 배열을 반환합니다:- 현재 state. 첫 번째 렌더링 중에는
init(initialArg)
또는 (init
이 없는 경우)initialArg
로 설정됩니다.
주의사항
useReducer
는 훅이므로 컴포넌트의 최상위 레벨 또는 자체 훅에서만 호출할 수 있습니다. 반복문이나 조건문 내부에서는 호출할 수 없습니다. 필요하다면 새 컴포넌트를 추출하고 state를 그 안으로 옮기세요.
- Strict Mode에서 React는 의도치 않은 불순물을 찾기 위해 reducer와 초기화 함수를 두 번 호출합니다. 이는 개발 전용 동작이며 상용 환경에서는 영향을 미치지 않습니다. reducer와 초기화 함수가 순수하다면(그래야 합니다) 컴포넌트의 로직에 영향을 미치지 않습니다. 호출 중 하나의 결과는 무시됩니다.
dispatch
function
useReducer
가 반환하는 dispatch
함수를 사용하면 state를 다른 값으로 업데이트하고 다시 렌더링을 촉발할 수 있습니다. dispatch
함수에 유일한 인수로 액션을 전달해야 합니다:const [state, dispatch] = useReducer(reducer, { age: 42 }); function handleClick() { dispatch({ type: 'incremented_age' }); // ...
React는
reducer
함수에 현재 state
와 dispatch
한 액션을 전달하고, 그 결과를 다음 state로 설정합니다.매개변수
action
: 사용자가 수행한 작업입니다. 어떤 데이터 유형이든 올 수 있습니다. 관용적으로 액션은 보통 이를 식별하는 type 속성이 있는 객체이며, 선택적으로 추가 정보가 있는 다른 속성을 포함할 수 있습니다.
반환값
dispatch
함수에는 반환값이 없습니다.주의사항
dispatch
함수는 다음 렌더링에 대한 state 변수만 업데이트합니다. 만약dispatch
함수를 호출한 후 state 변수를 읽으면, 호출 전 화면에 있던 이전 값이 계속 표시됩니다.
- 만약 여러분이 제공한 새 값이
Object.is
로 비교했을 때 현재state
와 동일하다면, React는 컴포넌트와 그 자식들을 다시 렌더링하는 것을 건너뜁니다. 이것은 최적화입니다. React는 결과를 무시하기 전에 여전히 컴포넌트를 호출하게 될 수도 있지만, 코드에 영향을 미치지는 않습니다.
- React는 state 업데이트를 일괄 처리합니다. 모든 이벤트 핸들러가 실행되고
set
함수를 호출한 후에 화면을 업데이트합니다. 이렇게 하면 단일 이벤트 중에 여러 번 다시 렌더링되는 것을 방지할 수 있습니다. 드물지만 DOM에 접근하기 위해 React가 화면을 더 일찍 업데이트하도록 강제해야 하는 경우,flushSync
를 사용할 수 있습니다.
사용법
컴포넌트에 reducer 추가하기
컴포넌트의 최상위 레벨에서
useReducer
를 호출하여 reducer로 state를 관리하세요.import { useReducer } from 'react'; function reducer(state, action) { // ... } function MyComponent() { const [state, dispatch] = useReducer(reducer, { age: 42 }); // ...
useReducer
는 정확히 두 개의 항목이 있는 배열을 반환합니다:- 이 state 변수의 현재 state. 처음에 제공한 초기 state로 설정됨.
- 상호작용에 반응하여 이를 변경할 수 있는
dispatch
함수.
화면에 표시되는 내용을 업데이트하려면 사용자가 수행한 작업을 나타내는 객체, 즉,액션을 사용하여
dispatch
를 호출합니다:function handleClick() { dispatch({ type: 'incremented_age' }); }
React는 현재 state와 액션을 reducer 함수에 전달합니다. Reducer는 다음 state를 계산하고 반환합니다. React는 다음 state를 저장하고, 컴포넌트를 렌더링하고, UI를 업데이트합니다.
App.js
import { useReducer } from 'react'; function reducer(state, action) { if (action.type === 'incremented_age') { return { age: state.age + 1 }; } throw Error('Unknown action.'); } export default function Counter() { const [state, dispatch] = useReducer(reducer, { age: 42 }); return ( <> <button onClick={() => { dispatch({ type: 'incremented_age' }) }}> Increment age </button> <p>Hello! You are {state.age}.</p> </> ); }
useReducer
는 useState
와 매우 유사하지만 이벤트 핸들러의 state 업데이트 로직을 컴포넌트 외부의 단일 함수로 옮길 수 있습니다. 더 자세한 내용은 useState
와 useReducer
중 하나를 선택하는 방법에서 확인하세요.Reducer 함수 작성하기
Reducer 함수는 다음과 같이 선언됩니다:
function reducer(state, action) { // ... }
그런 다음 다음 state를 계산하고 반환할 코드를 입력해야 합니다. 관례상
switch
문으로 작성하는 것이 일반적입니다. switch
의 각 case
에 대해 다음 state를 계산하고 반환해야 합니다.function reducer(state, action) { switch (action.type) { case 'incremented_age': { return { name: state.name, age: state.age + 1 }; } case 'changed_name': { return { name: action.nextName, age: state.age }; } } throw Error('Unknown action: ' + action.type); }
액션은 어떤 형태든 가질 수 있습니다. 관례상 액션을 식별하는
type
프로퍼티가 있는 객체를 전달하는 것이 일반적입니다. 여기에는 reducer가 다음 state를 계산하는 데 필요한 최소한의 필수 정보가 포함되어야 합니다.function Form() { const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 }); function handleButtonClick() { dispatch({ type: 'incremented_age' }); } function handleInputChange(e) { dispatch({ type: 'changed_name', nextName: e.target.value }); } // ...
액션 유형 이름은 컴포넌트에 로컬로 지정됩니다. 각 액션은 아무리 많은 데이터를 변경하게 되더라도 오직 하나의 상호작용만을 기술합니다. state의 모양은 임의적이지만 일반적으로 객체나 배열이 될 것입니다.
자세한 내용은 state 로직을 reducer로 추출하기를 읽어보세요.
Pitfall | 함정
state는 읽기 전용입니다. state의 객체나 배열을 수정하지 마세요:
function reducer(state, action) { switch (action.type) { case 'incremented_age': { // 🚩 Don't mutate an object in state like this: state.age = state.age + 1; return state; }
대신, reducer로부터 새로운 객체를 반환하세요:
function reducer(state, action) { switch (action.type) { case 'incremented_age': { // ✅ Instead, return a new object return { ...state, age: state.age + 1 }; }
자세한 내용은 state 객체 업데이트하기 및 state배열 업데이트하기를 참고하세요.
초기 state 재생성 방지하기
React는 초기 state를 한 번 저장하고 다음의 모든 렌더링에서 이를 무시합니다.
function createInitialState(username) { // ... } function TodoList({ username }) { const [state, dispatch] = useReducer(reducer, createInitialState(username)); // ...
createInitialState(username)
의 결과는 초기 렌더링에만 사용되지만, 이후의 모든 렌더링에서도 여전히 이 함수를 호출하게 됩니다. 이는 큰 배열을 만들거나 값비싼 계산을 수행하는 경우 낭비가 될 수 있습니다.이 문제를 해결하려면
useReducer
세번 째 인수에 초기화 함수를 전달할 수 있습니다.function createInitialState(username) { // ... } function TodoList({ username }) { const [state, dispatch] = useReducer(reducer, username, createInitialState); // ...
함수를 호출한 결과인
createInitialState()
가 아니라 함수 자체인 createInitialState
를 전달하고 있다는 점에 유의하세요. 이렇게 하면 초기화 후에는 초기 state가 다시 생성되지 않습니다.위의 예에서
createInitialState
는 username
인수를 받습니다. 초기화 함수가 초기 state를 계산하는 데 아무런 정보가 필요하지 않은 경우, useReducer
의 두 번째 인수로 null
을 전달할 수 있습니다.초기화 함수를 전달하는 것과 초기 state를 직접 전달하는 것의 차이점
1. 초기화 함수 전달하기
const [state, dispatch] = useReducer( reducer, username, createInitialState );
2. 초기 state 직접 전달하기
const [state, dispatch] = useReducer( reducer, createInitialState(username) );
문제 해결
action을 dispatch했지만 로깅하면 이전 state값이 표시됩니다
dispatch
함수를 호출해도 실행 중인 코드의 state는 변경되지 않습니다:function handleClick() { console.log(state.age); // 42 dispatch({ type: 'incremented_age' }); // Request a re-render with 43 console.log(state.age); // Still 42! setTimeout(() => { console.log(state.age); // Also 42! }, 5000); }
state는 스냅샷처럼 동작하기 때문입니다. state를 업데이트하면 새 state 값으로 다른 렌더링을 요청하지만 이미 실행 중인 이벤트 핸들러의
state
JavaScript 변수에는 영향을 미치지 않습니다.다음 state 값을 추측해야 하는 경우 reducer를 직접 호출하여 수동으로 계산할 수 있습니다:
const action = { type: 'incremented_age' }; dispatch(action); const nextState = reducer(state, action); console.log(state); // { age: 42 } console.log(nextState); // { age: 43 }
action을 dispatch 했지만 화면은 업데이트 되지 않습니다
Object.is
비교 결과 다음 state가 이전 state와 같다면 React는 업데이트를 무시합니다. 이는 보통 객체나 배열 state를 직접 변경(변이)할 때 발생합니다:function reducer(state, action) { switch (action.type) { case 'incremented_age': { // 🚩 Wrong: mutating existing object state.age++; return state; } case 'changed_name': { // 🚩 Wrong: mutating existing object state.name = action.nextName; return state; } // ... } }
기존
state
객체를 변경하고 반환했기 때문에 React가 업데이트를 무시했습니다. 이 문제를 해결하려면 변이를 시키는것이 아닌, 항상 객체 state 업데이트 및 배열 state 업데이트를 해야 합니다.function reducer(state, action) { switch (action.type) { case 'incremented_age': { // ✅ Correct: creating a new object return { ...state, age: state.age + 1 }; } case 'changed_name': { // ✅ Correct: creating a new object return { ...state, name: action.nextName }; } // ... } }
dispatch하면 reducer state의 일부분이 undefined가 됩니다
새 state를 반환할 때 모든
case
브랜치가 기존 필드를 모두 복사하는지 확인하세요:function reducer(state, action) { switch (action.type) { case 'incremented_age': { return { ...state, // Don't forget this! age: state.age + 1 }; } // ...
위의
...state
가 없으면 반환된 다음 state에는 age
필드만 포함되고 다른 항목은 포함되지 않습니다.dispatch하면 모든 reducer state가 undefined가 됩니다
state가 예기치 않게
undefined
가 된 경우, 케이스 중 하나에서 state를 반환하는 것을 잊었거나 액션 유형이 case
문 중 어느 것과도 일치하지 않을 수 있습니다. 이유를 찾으려면 switch
외부에서 오류를 발생시키세요:function reducer(state, action) { switch (action.type) { case 'incremented_age': { // ... } case 'edited_name': { // ... } } throw Error('Unknown action: ' + action.type); }
TypeScript와 같은 정적 유형 검사기를 사용하여 이러한 실수를 포착할 수도 있습니다.
“리렌더링이 너무 많습니다” 라는 오류가 발생합니다
다음과 같은 오류가 발생할 수 있습니다:
리렌더링이 너무 많습니다. 무한 루프를 방지하기 위해 React가 렌더링 횟수를 제한합니다.
일반적으로 이는 매 렌더링시 무조건적으로 액션을 디스패치하고 있음을 의미하는데, 따라서 컴포넌트는 렌더링, 디스패치(렌더링을 유발), 렌더링, 디스패치(렌더링을 유발) 등의 루프에 진입하게 되는 것입니다. 이벤트 핸들러를 지정하는 과정에서 실수로 인해 발생하는 경우가 많습니다:// 🚩 Wrong: calls the handler during render return <button onClick={handleClick()}>Click me</button> // ✅ Correct: passes down the event handler return <button onClick={handleClick}>Click me</button> // ✅ Correct: passes down an inline function return <button onClick={(e) => handleClick(e)}>Click me</button>
이 오류의 원인을 찾을 수 없는 경우, 콘솔에서 오류 옆에 있는 화살표를 클릭하여 JavaScript 스택을 살펴보고, 오류의 원인이 되는 특정
dispatch
함수 호출을 찾아보세요.reducer 또는 초기화 함수가 두 번 실행됩니다
Strict Mode에서 React는 reducer 및 초기화 함수 함수를 두 번 호출합니다. 이로 인해 코드가 깨지지 않아야 합니다.
이 개발 전용 동작은 컴포넌트를 순수하게 유지하는 데 도움이 됩니다. React는 두 호출 중 하나의 결과만 사용하고 다른 호출 결과는 무시합니다. 컴포넌트, 초기화 함수, reducer함수가 모두 순수하다면 로직에 영향을 미치지 않습니다. 의도치 않게 이 중 일부가 불순한 경우 해당 실수를 알아내어 수정하는 데 도움이 될 것입니다.
예를 들어, 다음의 불순한 reducer 함수는 state의 배열을 변이합니다:
function reducer(state, action) { switch (action.type) { case 'added_todo': { // 🚩 Mistake: mutating state state.todos.push({ id: nextId++, text: action.text }); return state; } // ... } }
React는 ruducer 함수를 두 번 호출하기 때문에 할 일이 두 번 추가되었으며, 이로부터 실수가 있음을 파악할 수 있습니다. 이 예제에서는 배열을 변이하는 대신 교체하여 실수를 수정할 수 있습니다:
function reducer(state, action) { switch (action.type) { case 'added_todo': { // ✅ Correct: replacing with new state return { ...state, todos: [ ...state.todos, { id: nextId++, text: action.text } ] }; } // ... } }
이제 이 reducer 함수는 순수 함수이므로 한 번 더 호출해도 동작에 차이가 없습니다. 그렇기 때문에 React가 두 번 호출하면 실수를 찾는 데 도움이 됩니다. 컴포넌트, 초기화 함수, 리듀서 함수는 반드시 순수해야 합니다. 이벤트 핸들러는 순수할 필요가 없으며, React는 이벤트 핸들러를 두 번 호출하지 않습니다.