컴포넌트에 reducer를 추가할 수 있는 React Hook
const [state, dispatch] = 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와 action을 인자로 받고 다음 state를 반환해야함. State와 action은 어떤 타입이든 가능함.initialArg: 초기 state가 계산되는 값. 모든 타입의 값이 될 수 있음. 이 값으로부터 초기 state를 계산하는 방법은 다음 init 인수에 따라 달라짐.init(optional): 초기 state를 반환해야 하는 initializer 함수. 지정하지 않으면 초기 state가 initialArg로 설정됨. 그렇지 않으면 초기 state는 init(initialArg)를 호출한 결과로 설정됨.useReducer는 정확히 두 개의 값이 있는 배열을 반환함:
init(initialArg) 또는 initialArg(init이 없는 경우)로 설정됨.dispatch 함수.useReducer는 Hook이므로 컴포넌트의 최상위 수준이나 자체 Hook에서만 호출할 수 있음. 루프나 조건 내부에서는 호출할 수 없음. 필요하다면 새 컴포넌트를 추출하고 state를 그 안으로 옮겨야 함.useReducer가 반환하는 dispatch 함수를 사용하면 state를 다른 값으로 업데이트하고 렌더링을 다시 트리거할 수 있음. dispatch 함수에는 action을 유일한 인수로 전달해야함:
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...
React는 현재 state와 함께 제공한 reducer 함수를 호출한 결과와 dispatch에 전달한 action으로 다음 state를 설정함.
action: 사용자가 수행한 작업. 모든 타입의 값이 될 수 있음. action은 일반적으로 이를 식별하는 type property가 있는 객체이며, 선택적으로 추가 정보가 있는 다른 property을 포함할 수 있음.반환 값이 없음.
dispatch 함수는 다음 렌더링에 대한 state 변수만 업데이트함. dispatch 함수를 호출한 후 state 변수를 읽으면 호출 전 화면에 표시되었던 이전 값을 그대로 가져옴.
Object.is 비교에 의해 결정된 새 값이 현재 state와 동일하다면, React는 컴포넌트와 그 자식들을 다시 렌더링하는 것을 건너뜀(최적화). React가 결과를 무시하기 전에 컴포넌트를 호출해야 할 수도 있지만 코드에는 영향을 미치지 않음.
React는 state 업데이트를 일괄 처리함. 모든 이벤트 핸들러가 실행되고, set 함수를 호출한 후에 화면을 업데이트함. 이렇게 하면 단일 이벤트 중에 여러 번 다시 렌더링되는 것을 방지할 수 있음. 드물지만 DOM에 액세스하기 위해 React가 화면을 더 일찍 업데이트하도록 강제해야 하는 경우, flushSync를 사용할 수 있음.
컴포넌트의 최상위 수준에서 useReducer를 호출하여 reducer를 통해 state를 관리:
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
useReducer는 정확히 두 개의 항목이 있는 배열을 반환함:
dispatch 함수.화면의 내용을 업데이트하려면 사용자가 수행한 작업, 즉 action을 나타내는 객체로 dispatch를 호출:
function handleClick() {
dispatch({ type: 'incremented_age' });
}
React는 현재 state와 action을 reducer 함수에 전달함. Reducer는 다음 state를 계산해서 반환함. React는 그 다음 state를 저장하고, 컴포넌트를 렌더링하고, UI를 업데이트함.
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 함수는 다음과 같이 선언됨:
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);
}
Action은 어떤 형태든 가질 수 있음. 일반적으로 action을 식별하는 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
});
}
// ...
Action type 이름은 컴포넌트에 로컬로 지정됨. 각 action은 데이터의 여러 변경을 초래하더라도 하나의 상호작용을 설명함. State의 형태는 임의적이지만, 일반적으로 객체나 배열이 됨.
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 }; }
React는 초기 state를 한 번 저장하고 다음 렌더링에서 이를 무시함.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...
createInitialState(username)의 결과는 초기 렌더링에만 사용되지만, 여전히 모든 렌더링에서 이 함수를 호출하게 됨. 이는 큰 배열을 만들거나 값비싼 계산을 수행하는 경우 낭비가 될 수 있음.
이 문제를 해결하려면 이 함수를 useReducer의 세 번째 인수로, initializer 함수로써 전달할 수 있음:
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...
함수를 호출한 결과인 createInitialState()가 아니라 함수 자체인 createInitialState를 전달하고 있다는 점에 유의할 것. 이렇게 하면 초기화 후 초기 state가 다시 생성되지 않음.
위의 예에서 createInitialState는 username 인수를 받음. Initializer가 초기 state를 계산하는 데 아무런 정보가 필요하지 않은 경우, useReducer의 두 번째 인수로 null을 전달할 수 있음.
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 }
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
};
}
// ...
}
}
새 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 필드만 포함되고 다른 항목은 포함되지 않음.
State가 예기치 않게 undefined가 된다면 case 중 하나에서 state를 반환하는 것을 잊었거나 action type이 case 문과 일치하지 않는 것일 수 있음. 이유를 찾으려면 switch 외부에서 오류를 발생시킬 것:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}
TypeScript와 같은 정적 유형 검사기를 사용하여 이러한 실수를 포착할 수도 있음.
Too many re-renders. React limits the number of renders to prevent an infinite loop. 과 같은 오류가 표시될 수 있음. 일반적으로 이것은 렌더링 '중'에 무조건 action을 dispatch 한다는 것을 의미하며, 컴포넌트는 렌더링, dispatch(렌더링 발생), 렌더링, dispatch(렌더링 발생)... 의 루프에 진입하게 됨. 이는 이벤트 핸들러를 지정할 때 실수로 인해 발생하는 경우가 많습음:
// 🚩 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 함수 호출을 찾아볼 것.
Strict Mode에서 React는 reducer와 initializer 함수를 두 번 호출함. 이 때문에 코드가 망가지는 것은 아님.
이 개발 환경 전용 동작은 컴포넌트를 순수하게 유지하는 데 도움이 됨. React는 호출 중 하나의 결과를 사용하고 다른 결과는 무시함. 컴포넌트, initializer, 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가 reducer 함수를 두 번 호출하기 때문에 todo가 두 번 추가된 것을 확인할 수 있으므로 실수가 있음을 알 수 있음. 이 예제에서는 배열을 변경하는 대신 배열을 교체하여 실수를 수정할 수 있음:
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에서 두 번 호출하면 실수를 찾는 데 도움이 됨. 컴포넌트, initializer, reducer 함수만 순수할 필요가 있음. 이벤트 핸들러는 순수할 필요가 없으므로 React는 이벤트 핸들러를 두 번 호출하지 않음.
참고: 컴포넌트를 순수하게 유지하기