[React/리액트] useReducer 를 사용하여 상태 업데이트 로직 분리하기
상태 관리 Hook 함수
- useState: 설정하고 싶은 새로운 상태를 직접 지정하여 상태를 업데이트
- useReducer: 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리할 수 있다
useState 와 다르게 상태 업데이트 로직을 바깥에 작성할 수 있고, 다른 파일에 작성 후 불러와서 사용할 수도 있다.
현재 컴포넌트가 아닌 다른 곳에 state 를 저장하고 싶을 때 유용하다.
reducer
reducer
는 현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수이다. (상태 업데이트 함수)
반환해주는 상태는 곧 컴포넌트가 지닐 새로운 상태가 된다. 새로운 상태를 만들 때는 불변성을 지켜주어야 한다.
function reducer(state, action) {
// 새로운 상태를 만드는 로직
// const nextState = ...
return nextState;
}
function reducer(state, action) {
switch(action.type) {
case 'INCREMENT': // action type 이 INCREMENT 인 경우 기존상태 +1
return state + 1;
case 'DECREMENT': // action type 이 DECREMENT 인 경우 기존상태 -1
return state - 1;
default:
return state;
}
};
여기서 action
은 업데이트를 위한 정보로 객체의 형태는 자유이며 주로 type 값을 지니고 있다.
이 객체를 기반으로 참조하여 업데이트 한다.
useReducer 사용법
첫번째 파라미터는 reducer 함수이고, 두번째 파라미터는 초기상태이다.
const [state, dispatch] = useReducer(reducer, initialState);
- state: 앞으로 컴포넌트에서 사용할 수 있는 상태를 가리킨다.
- dispatch: 액션을 발생시키는 함수
- initialState: 객체. 초기 정보를 담고 있는 useReducer 에게 전달하는 객체
dispatch 함수의 경우 다음과 같이 사용한다.
dispatch({ type: 'INCREMENT' })
useReducer vs useState
어떨 때 useReducer 를 쓰고 어떨 때 useState 를 써야하는지에는 정해진 답은 없다.
- useState: 컴포넌트에서 관리하는 값이 하나이고, 그 값이 단순한 숫자, 문자열, boolean 값일 때
- useReducer: 컴포넌트에서 관리하는 값이 여러개가 되어서 상태의 구조가 복잡할 때
setUsers, setInputs ... 등등 set 함수를 이용해서 관리하는 컴포넌트들이 늘어날때 useReducer로 관리하는 것을 고려해보면 좋을 것 같다.
예제
1. useReducer 를 사용하기에 앞서 먼저 reducer 함수를 생성해준다.
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
throw new Error('Unhandled action');
}
}
2. 초기상태를 0으로 한 useReducer 함수를 만들어준다.
const [number, dispatch] = useReducer(reducer, 0);
const onIncrease = () => {
setNumber(prevNumber => + prevNumber + 1);
}
const onDecrease = () => {
setNumber(prevNumber => prevNumber - 1);
}
이전에 useState를 이용해서 set 함수로 증가와 감소를 위와 같이 작성해주었따면, useReducer 함수에서는 dispatch 를 이용해서 작성해준다.
const [number, dispatch] = useReducer(reducer, 0);
const onIncrease = () => {
dispatch({
type: 'INCREMENT'
})
}
const onDecrease = () => {
dispatch({
type: 'DECREMENT'
})
}
useState → useReducer 로 구현하기
1. App 컴포넌트에서 사용할 초기 상태(initialState)를 App 컴포넌트 밖에 선언해준다.
- inputs
: inputs 로 받아왔던 useState 내부에 입력되어 있던 객체들
- users
: users 로 받아왔던 useState 내부에 배열들
const initialState = {
inputs: {
username: '',
email: ''
},
users: [
{
id: 1,
username: 'velopert',
email: 'public.velopert@gmail.com',
active: true,
},
{
id: 2,
username: 'tester',
email: 'tester@example.com',
active: false,
},
{
id: 3,
username: 'liz',
email: 'liz@example.com',
active: false,
}
],
}
2. reducer 함수를 생성한다.
function reducer(state, action) {
return state;
}
3. useReducer를 사용해서 앞에서 작성한 값들을 받아온다.
첫번째 파라미터로 reducer 함수, 두번째 파라미터로 초기상태를 받아온다.
const [state, dispatch] = useReducer(reducer, initialState);
4. initialState 내부의 inpusts, users 값들을 비구조화 할당을 통해서 추출한 뒤, 컴포넌트에 props 로 전달하게 한다.
const { users } = state;
const { username, email } = state.inputs;
5. onChange 를 구현해서 이벤트가 발생했을 때 실행될 dispatch 액션을 설정한다.
useCallback 함수를 이용해서 컴포넌트가 처음 렌더링 될 때만 만들고, 그 다음부터는 재사용을 한다.
name과 value 값을 e.target 이벤트가 발생하는 DOM으로부터 추출한다.
dispatch 액션을 지정해준다.
const onChange = useCallback(e => {
const { name, value } = e.target;
dispatch({
type: 'CHANGE_INPUT',
name,
value
})
}, [])
그리고 CreateUser 에 전달해준다.
return (
<>
<CreateUser username={username} email={email} onChange={onChange} />
<UserList users={users} />
<div>활성 사용자 수: 0</div>
</>
);
6. reducer 구현하기
onChange 에는 type, name, value 값이 있는데, 만약에 type이 'change_input' 이라면 현재 자신이 지니고 있는 특정값도 바뀌어야 한다. (initialState 내부값 변화)
switch 문을 이용하여 값에 따라 다른 값을 반환할 수 있도록 지정한다.
type 이 'change_input' 이라면 spread 문법을 이용하여 ...state 기존의 자신이 들고있는 상태를 복사해주고, 입력받은 inputs 값을 덮어쓸 수 있도록 한다.
function reducer(state, action) {
switch (action.type) {
case 'CHANGE_INPUT':
return {
...state,
inputs: {
...state.inputs,
[action.name]: action.value
}
};
default:
throw new Error('Unhandled actoin');
// 1. 지금처럼 throw new Error 를 통해서 설정하지 않은 액션에 대해서 받았을 때 확인할 수 있도록 하는 방법
// 2. return state; 그냥 현재 상태를 업데이트 하도록 진행하는 방법
}
}
7. onCreate 구현하기
마찬가지로 useCallback 함수를 이용해서 생성된 뒤 재사용할 수 있도록 한다.
dispatch를 통해 type 은 'create_user' 라는 액션을 발생시킨다.
user 내부에 id 값을 넣어줄 것인데 useRef 로 관리해주어야 한다.
username, email 도 넣어주고 deps 배열에 함수에서 기존 상태를 의존하고 있는 것이 있으므로 username과 email을 넣어준다.
const onCreate = useCallback(() => {
dispatch({
type: "CREATE_USER",
user: {
id: 1,
username,
email,
},
});
}, [username, email]);
8. nextId 값 useRef 로 관리하기
기존에 id 가 3개가 등록되어 있으므로 다음에 새로 등록되는 아이디의 기본값은 4이다.
const nextId = useRef(4);
useRef
1. DOM을 선택하는 용도
2. 컴포넌트 안에서 조회 및 수정을 할 수 있는 변수를 관리
- useRef 로 관리되는 변수는 값이 바뀌어도 컴포넌트가 리렌더링 되지 않는다.
- useRef( ) 파라미터로 넣어주는 값이 .current 값이 된다.
아까 onCreate 에서 생성해주었던 id 에 nextId.current 로 작성해주고, id 를 생성한 뒤에는 nextId.current 에 1을 더해준다.
const onCreate = useCallback(() => {
dispatch({
type: "CREATE_USER",
user: {
id: nextId.current,
username,
email,
},
});
nextId.current += 1; // user정보를 생성하고, nextId 값을 1 증가시켜놓음
}, [username, email]);
9. reducer 에서 case 로 type 'create_user' 가 왔을 때 실행될 함수 지정해주기
case 'CREATE_USER':
return {
inputs: initialState.inputs, // 초기값으로 설정
users: state.users.concat(action.user) // users 에 action.user 배열합침
}
10. reducer 함수에 onToggle/onRemove case 작성
- reducer 함수에 'toggle_user' case 작성
case 'TOGGLE_USER':
return {
...state, // 기존상태 복사
users: state.users.map(user => // user 에 대하여 비교
user.id === action.id // id 가 action 을 통해 받아온 id 와 일치하는지
? { ...user, active: !user.active } // 같다면 active 값을 반전시켜준다. (true-false)
: user // 같지 않다면 기존의 user 객체 유지
)
}
- 'remove_user' case 작성
case 'REMOVE_USER':
return {
...state,
users: state.users.filter(user => user.id !== action.id) // user.id 와 action.id 를 비교
} // 일치하지 않으면 유지, 일치하면 배열에서 제거
11. onToggle/onRemove 구현하기
const onToggle = useCallback((id) => {
dispatch({
type: "TOGGLE_USER",
id,
});
}, []);
const onRemove = useCallback((id) => {
dispatch({
type: "REMOVE_USER",
id,
});
}, []);
UserList 에 연결해주기
12. 활성사용자수 구현하기 (useMemo 사용)
const count = useMemo(() => countActiveUsers(users), [users])
return 활성사용자 수 값에 {count} 연결해주기