useReducer
는 컴포넌트에 리듀서(reducer)를 추가해주는 리액트 훅이에요.
const [state, dispatch] = useReducer(reducer, initialArg, init?)
Reference | 레퍼런스
useReducer(reducer, initialArg, init?)
리듀서로 상태를 관리하기 위해서는 최상위 컴포넌트에서 useReducer
를 호출하세요.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
Parameters | 파라미터
reducer
: 상태를 업데이트하는 방법을 지정하는 리듀서 함수. 이 함수는 순수 함수여야하며, 인자로 상태와 동작을 받아야하고, 다음 상태를 반환해야해요. 상태와 동작은 모든 타입이 가능해요.initialArg
: 초기 상태가 계산된 값. 모든 타입을 가질 수 있어요. 이 값에서 초기 함수가 계산되는 방법은 다음에 소개될init
인자에 의존해요.init
(선택적) : 초기 상태를 반환하는 초기화 함수. 만약 이 함수가 지정되지 않으면, 초기함수는initialArg
로 정해져요. 그렇지 않다면 초기 함수는init(initialArg)
를 호출하여 실행한 결과값으로 정해져요.
Returns | 반환값
useReducer
는 정확히 두 개의 값을 가지는 배열을 반환해요.
- 현재 상태. 최초 렌더링 동안
init(initialArg)
또는 (만약init
함수를 인자로 받지 않았다면)initialArg
로 정해져요. - 다른 값으로 상태를 업데이트하고 리렌더링을 발생시키는
dispatch
함수.
Caveats | 주의사항
useReducer
는 훅이기 때문에 최상위 컴포넌트 또는 직접 만든 훅에서만 호출할 수 있어요. 반복문이나 조건문 안에서는 호출할 수 없어요. 만약 그럴 필요가 있다면, 컴포넌트를 추출하고 그 안으로 상태를 옮기세요.- 엄격한 모드(Strict Mode)에서 리액트는 의도하지 않은 불순물을 찾는 것을 돕기 위해 리듀서와 초기화 함수를 두 번 호출해요. 개발 모드에서만 이렇게 동작하고 실제 프로덕션 환경에는 영향을 미치지 않아요. 만약 리듀서와 초기화 함수가 순수함수라면 이 동작이 로직에 영향을 미치지 않을 거예요. 둘 중 하나의 결과값는 무시해요.
dispatch
function | dispatch
함수
useReducer
에서 반환되는 dispatch
함수는 다른 값으로 상태를 업데이트시키고 리렌더링을 발생시켜요. dispatch
함수는 하나의 인자인 액션을 받아요.
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...
}
리액트는 현재 state
와 함께 넘겨진 reducer
함수와 dispatch
로 전달한 액션의 결과를 다음 상태로 지정해요.
Parameters | 파라미터
action
: 사용자에 의해 수행될 액션. 타입은 상관 없어요. 컨벤션에 따르면, 액션은 일반적으로 액션을 식별하는type
속성과 선택적으로 추가적인 정보를 갖고 있는 다른 속성들로 구성된 객체에요.
Returns | 반환값
dispatch
함수는 반환값이 없어요.
Caveats | 주의사항
dispatch
함수는 오직 다음렌더링을 위해 상태 변수를 업데이트해요. 만약dispatch
함수를 호출한 후에 상태 변수를 읽는다면, 호출하기 전에 화면에 띄워져 있던 기존의 값을 받을 거예요.- 만약 넘겨준 새로운 값이
Object.js
로 비교해보았을 때 현재state
와 동일하다고 판명되면 리액트는 해당 컴포넌트와 그 자식 컴포넌트의 리렌더링을 건너뛰어요. 이는 최적화의 방법이에요. 리액트는 결과는 무시하기 전에 컴포넌트를 호출할 필요가 있지만 코드에는 영향을 미치지 않아요. - 리액트는 상태 업데이트를 일괄적으로 처리해요. 모든 이벤트 핸들러가 동작한 후에 화면을 업데이트하고 상태의
set
함수를 호출해요. 이는 단일 이벤트가 진행되는 동안 리렌더링이 여러번 되는 것을 방지해요. 거의 일어날 확률은 없지만, DOM에 접근해야하는 상황처럼 만약 리액트가 화면을 더 빠르게 업데이트를 하도록 만들어야하는 상황이라면flushSync
를 사용할 수 있어요.
Usage | 용법
Adding a reducer to a component | 컴포넌트에 리듀서 추가하기
리듀서로 상태를 관리하기 위해서는 최상위 컴포넌트에서 useReducer
를 호출하세요.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
useReducer
는 정확히 두 아이템을 가진 배열을 반환해요.
- 제공한 초기 상태(
{age: 42}
)로 설정된 이 상태 변수의 현재 상태(state
) - 상호작용의 응답으로 상태를 바꾸게 만들어주는 디스패치 함수(
dispatch
)
화면에 보여줄 것을 업데이트 하려면 사용자가 해야하는 일인 액션 을 표현한 객체를 가진 dispatch
를 호출하세요.
function handleClick() {
dispatch({ type: 'incremented_age' });
}
리액트는 리듀서 함수(reducer
)에 현재 상태와 액션을 전달해요. 지정된 리듀서는 다음 상태를 계산하고 반환해요. 리액트는 다음 상태를 저장하고, 저장한 상태로 컴포넌트를 리렌더한 후, UI를 업데이트해요.
useReducer
는 useState
와 굉장히 비슷하지만 이벤트 핸들러의 상태 업데이트 로직을 컴포넌트 외부의 단일함수로 이동시켜줘요. useState
와 useReducer
를 선택하는 방법에 대해 더 알아보세요.
Writing the reducer function | 리듀서 함수 작성하기
리듀서 함수는 아래와 같이 선언돼요.
function reducer(state, action) {
// ...
}
그리고 나서 다음 상태를 계산하고 반환하는 코드로 내부를 채우면 돼요. 컨벤션에 따르면 switch
구문을 사용하는 것이 일반적이에요. switch
의 각 case
에서 다음 상태를 계산하고 반환하세요.
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
속성을 가진 객체를 전달하는 것이 일반적이에요. 리듀서가 다음 상태를 계산하기 위해 필요한 필수적인 최소 정보를 포함해야해요.
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
});
}
// ...
액션의 타입 이름은 컴포넌트 내부에서 사용되는 지역적인 이름이에요. 설령 데이터의 여러가지 변화를 만든다 하더라도 각 액션은 단일 상호작용을 묘사해요. 상태의 형태는 임의적이지만 일반적으로는 객체나 배열이 많이 사용돼요.
더 알아보고 싶으면 상태 로직을 리듀서로 추출하기 문서를 읽어보세요.
함정
상태는 읽기만 가능(read-only)해요. 상태에서 어떤 객체나 배열도 수정하지 마세요.
function reducer(state, action) { switch (action.type) { case 'incremented_age': { // 🚩 아래와 같이 상태에서 객체를 변경하지 마세요. state.age = state.age + 1; return state; } // ... } }
대신, 언제나 리듀서를 통해 새 객체를 반환하세요.
function reducer(state, action) { switch (action.type) { case 'incremented_age': { // ✅ 대신, 새 객체를 반환하세요. return { ...state, age: state.age + 1 }; } // ... } }
더 알아보고 싶다면 상태에서 객체 업데이트하기 문서와 상태에서 배열 업데이트하기 문서를 읽어보세요.
기본적인 useReducer 예시
1. 폼 (객체)
이 예시에서 리듀서는 name
와 age
라는 두 개의 필드를 가진 상태 객체를 관리해요.
2. 투두 리스트 (배열)
이 예시에서 리듀서는 할 일의 배열읠 관리해요. 배열은 뮤테이션 없이 업데이트 되어야해요.
3. Immer를 사용하여 업데이트 로직을 간결하게 작성하기
만약 뮤테이션 없이 배열과 객체를 업데이트하는 것이 지루하다면, Immer와 같이 반복적인 코드를 줄여주는 라이브러리를 사용할 수 있어요. Immer는 객체를 변경하려고 한 것처럼 코드를 간결하게 작성하도록 해줘요. 하지만 실제로는 물변적인 업데이트를 수행해요.
Avoiding recreating the initial 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
를 전달한다는 것을 명심하세요. 이렇게 하면 초기화 이후에 초기 상태는 재생성되지 않아요.
위의 예시에서 createInitialState
는 username
이라는 인자를 받아요. 만약 초기화함수가 초기 상태를 계산하는데 아무런 정보도 필요하지 않다면 useReducer
의 두 번째 인자로 null
을 넘겨주면 돼요.
초기화 함수를 전달하는 것과 초기 상태를 직접 전달하는 것의 차이
Troubleshooting | 트러블슈팅
I've dispatched an action, but logging gives me the old state value | 액션을 디스패치했는데 로깅함수가 기존의 상태 값을 보여줘요.
dispatch
함수를 호출하는 것은 작동중인 코드 안에서 상태를 변경하지 않아요.
function handleClick() {
console.log(state.age); // 42
dispatch({ type: 'incremented_age' }); // 43으로 리렌더링 되도록 요청
console.log(state.age); // 여전히 42가 출력
setTimeout(() => {
console.log(state.age); // 다시 42가 출력
}, 5000);
}
그 이유는 상태는 스냅샷처럼 동작하기 때문이에요. 상태를 업데이트 하는 것은 새로운 상태 값을 가진 다른 렌더링을 요청해요. 하지만 이미 작동하고 있는 이벤드 핸들러 안에 있는 state
라는 자바스크립트 변수에 영향을 미치지는 않아요.
만약 다음 상태 값을 추측할 필요가 있다면, 리듀서를 직접 호출하여 수동으로 값을 계산할 수 있어요.
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }
I've dispatched an action, but the screen doesn't update | 액션을 디스패치했는데 화면이 업데이트 되지 않아요
리액트는 Object.js
로 비교해보았을 때 만약 다음 상태가 이전의 상태와 동일하면 업데이트를 무시해요. 이는 보통 상태에서 직접적으로 객체나 배열을 변경할 때 발생해요.
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩틀린 코드 : 존재하는 객체 안에서 변경
state.age++;
return state;
}
case 'changed_name': {
// 🚩 틀린 코드 : 존재하는 객체 안에서 변경
state.name = action.nextName;
return state;
}
// ...
}
}
존재하는 state
객체를 변경하고 반환했기 때문에 리액트는 업데이트를 무시했어요. 이를 고치기 위해서는변경하는 대신 언제나 상태에서 객체를 업데이트하거나 상태에서 배열을 업데이트 해야해요.
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ 올바른 코드: 새 객체 생성하기
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ 올바른 코드: 새 객체 생성하기
return {
...state,
name: action.nextName
};
}
// ...
}
}
A part of my reducer state becomes undefined after dispatching | 디스패치를 한 이후 일부 리듀서 상태가 undefined가 돼요
새 상태를 반환할 때 모든 case
브랜치가 존재하는 모든 필드를 복사하는 것을 잊지 마세요.
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // 이 과정을 잊지 마세요.
age: state.age + 1
};
}
// ...
}
}
위에서 작성된 ...state
가 없다면 반환된 다음 상태는 age
필드 외에는 아무것도 갖고 있지 않아요.
My entire reducer state becomes undefined after dispatching | 디스패치를 한 이후 모든 리듀서 상태가 undefined가 돼요
만약 상태가 예상치 못하게 undefined
가 되었다면 케이스 중 하나에서 return
구문을 잊어버렸거나 액션 타입이 case
구문과 맞지 않는 거예요. 이유를 찾으려면 switch
밖에서 에러를 던지세요.
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}
이러한 실수를 잡으려면 타입스크립트(TypeScript)와 같은 정적 타입 체커를 사용할 수도 있어요.
I’m getting an error: “Too many re-renders” | "너무 많은 리렌더링이 발생해요."라는 에러가 떠요.
너무 많은 리렌더링이 발생해요. 리액트는 무한 반복을 방지하기 위해 렌더링의 횟수를 제한해요.
라는 에러를 봤을 거예요. 일반적으로 이 메시지는 렌더링을 하는 동안 액션을 무조건 디스패칭하고 있어서 컴포넌트가 렌더링, (렌더링을 유발하는) 디스패치, 렌더링, (렌더링을 유발하는) 디스패치를 계속 수행하고 있다는 것을 의미해요. 이 에러는 이벤트 핸들러를 지정하면서 실수를 했기 때문에 발생하는 경우가 많아요.
// 🚩틀린 코드 : 렌더링 동안 핸들러를 호출해요.
return <button onClick={handleClick()}>Click me</button>
// ✅ 올바른 코드 : 이벤트 핸들러를 전달해요.
return <button onClick={handleClick}>Click me</button>
// ✅ 올바른 코드 : 인라인 함수를 전달해요.
return <button onClick={(e) => handleClick(e)}>Click me</button>
만약 이 에러의 원인을 찾을 수 없다면 콘솔에서 에러 옆에 있는 화살표를 클릭하고 에러를 유발하는 구체적인 dispatch
함수 호출을 찾기 위해 자바스크립트 스택을 살펴보세요.
My reducer or initializer function runs twice | 리듀서 함수가 두 번 실행돼요.
엄격한 모드에서 리액트는 어떤 함수를 두번씩 실행해요. 이는 코드를 깨뜨리지 않아요.
이 개발 모드에서만 실행되는 동작은 컴포넌트를 순수하게 만들어줘요. 리액트는 이 호출 중 하나의 결과만을 사용하고 다른 호출의 결과는 무시해요. 컴포넌트, 초기화 함수 그리고 리듀서 함수가 순수한 한, 이 실행이 로직에 영향을 미치지 않아요. 하지만 의도치 않게 이들이 순수하지 않다면 이 실행이 실수를 알아채도록 도와줘요.
예를 들어, 이 불순한 리듀서 함수는 상태에서 배열을 변경해요.
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 실수 : 상태 변경하기
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}
리액트는 리듀서 함수를 두 번 호출하기 때문에 할 일(todo)가 두번 추가되는 것을 확인할 수 있고 실수가 있다는 것을 알게 돼요. 이 예시에서 배열을 변경하는 대신 대체하는 것으로 실수를 해결할 수 있어요.
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ 올바른 코드 : 새 상태로 대체하기
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}
이제 이 리듀서 함수는 순수하지만 추가적으로 이 함수를 호출하는 것이 작동에서 차이를 만들진 않아요. 이것이 바로 리액트가 함수를 두 번 호출하는 것이 실수를 찾는 것을 도와주는 이유예요. 컴포넌트, 초기화 함수 그리고 리듀서 함수만 순수하면 돼요. 이벤트 핸들러는 순수할 필요가 없기 때문에 리액트는 절대 이벤트 핸들러를 두 번 호출하지 않아요.
더 알고싶다면 컴포넌트를 순수하게 유지하기 문서를 읽어보세요.
'리액트 공식문서 | React Docs > Reference > react@18.2.0' 카테고리의 다른 글
[Hooks] useState | useState 훅 (1) | 2024.02.06 |
---|---|
[Hooks] useRef | useRef 훅 (0) | 2024.02.04 |
[Hooks] useOptimistic | useOptimistic 훅 (0) | 2024.02.03 |
[Hooks] useMemo | useMemo 훅 (0) | 2024.02.01 |
[Hooks] useLayoutEffect | useLayoutEffect 훅 (1) | 2024.01.25 |