-
21. App 컴포넌트를 useReducer 로 구현하기 - 리액트 입문 | 벨로퍼트Front-end/React.js 2020. 7. 1. 22:57반응형
이번에는, App 컴포넌트에 있던 상태 업데이트 로직들을 useState 가 아닌 useReducer 를 사용하여 구현해보겠습니다. 우선, App 에서 사용 할 초기 상태를 컴포넌트 바깥으로 분리해주고, App 내부의 로직들을 모두 제거해주세요. 우리가 앞으로 차근차근 구현 할 것입니다.
먼저, reducer 함수의 틀만 만들어주고, useReducer 를 컴포넌트에서 사용해보세요.
/src/App.js :
import React, { useRef, useReducer, useMemo, useCallback } from 'react'; import UserList from './UserList'; import CreateUser from './CreateUser'; function countActiveUsers(users) { ... } const initialState = { inputs: { username: '', email: '' }, userInfo: [ ... ] }; function reducer(state, action) { return state; } function App() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> <CreateUser /> <UserList users={[]} /> <div>활성사용자 수 : 0</div> </> ); } export default App;
그 다음엔, state 에서 필요한 값들을 비구조화 할당 문법을 사용하여 추출하여 각 컴포넌트에게 전달해주세요.
/src/App.js :
// ... function App() { const [state, dispatch] = useReducer(reducer, initialState); const { inputs, userInfo } = state; const { username, email } = state.inputs; return ( <> <CreateUser username={username} email={email} /> <UserList users={users} /> <div>활성사용자 수 : 0</div> </> ); } export default App;
onChange 구현
// ... function reducer(state, action) { switch (action.type) { case 'CHANGE_INPUT': return { ...state, inputs: { ...state.inputs, [action.name]: action.value } }; default: return state; } } function App() { // ... const onChange = useCallback(e => { const { name, value } = e.target; dispatch({ type: 'CHANGE_INPUT', name, value }); }, []); return ( <> <CreateUser username={username} email={email} onChange={onChange} /> <UserList users={userInfo} /> <div>활성사용자 수 : 0</div> </> ); }
# onChange 함수에 dispatch을 사용해서 type: 'CHANGE_INPUT' 과 현재 타겟(input)의 name 과 현재 타겟(input)의 value를 가진 action 객체를 전달합니다.
# reducer 함수에서 action의 type이 'CHANGE_INPUT' 일 경우
1. 새로운 상태를 만들 때에는 불변성을 지켜주어야 하기 때문에 spread 연산자를 사용해서 기존에 자신이 갖고 있는 state를 넣어 준다.
2. inputs 역시 불변성을 지켜주어야 하기 때문에 기존에 자신이 갖고 있는 state의 input 값을 넣어준다. 그 다음 action 객체가 가진 name의 값을 action 객체가 가진 value로 넣어준다.
onCreate 구현
// ... function reducer(state, action) { switch (action.type) { // ... case 'CREATE_USER': return { inputs: initialState.inputs, users: state.userInfo.concat(action.user) }; default: return state; } } function App() { // ... const nextId = useRef(userInfo.length + 1); const onCreate = useCallback(() => { dispatch({ type: 'CREATE_USER', user: { id: nextId.current, username, email } }); nextId.current += 1; }, [username, email]); return ( <> <CreateUser username={username} email={email} onChange={onChange} onCreate={onCreate} /> <UserList users={userInfo} /> <div>활성사용자 수 : 0</div> </> ); }
# onCreate 함수에 dispatch을 사용해서 type: 'CREATE_USER' 과 user: {새로 생성되는 id, state.inputs에서 추출한 username, state.inputs에서 추출한 email} 을 가진 action 객체를 전달합니다.
# useCallback 함수를 썼기 때문에 deps에 username과 email 을 넣어서 가장 최신 값을 참조해야 한다.
# reducer 함수에서 action의 type이 'CREATE_USER' 일 경우
1. inputs는 ininitialState.inputs의 값을 넣어 초기값으로 변경한다.
2. userInfo는 state 객체의 userInfo에 concat 함수를 사용해서 action 객체의 user를 추가한다.
onToggle, onRemove 구현
// ... function reducer(state, action) { switch (action.type) { // ... case 'TOGGLE_USER': return { ...state, userInfo: state.userInfo.map(user => user.id === action.id ? { ...user, active: !user.active } : user ) }; case 'REMOVE_USER': return { ...state, users: state.userInfo.filter(user => user.id !== action.id) }; default: return state; } } function App() { // ... const onToggle = useCallback(id => { dispatch({ type: 'TOGGLE_USER', id }); }, []); const onRemove = useCallback(id => { dispatch({ type: 'REMOVE_USER', id }); }, []); return ( <> <CreateUser username={username} email={email} onChange={onChange} onCreate={onCreate} /> <UserList users={userInfo} onToggle={onToggle} onRemove={onRemove} /> <div>활성사용자 수 : 0</div> </> ); }
# onToggle, onRemove 함수에 id를 파라미터로 전달하고 dispatch을 사용해서 type: 'TOGGLE_USER' 또는 type: 'REMOVE_USER' 과 파라미터로 전달받은 id 을 가진 action 객체를 전달합니다.
# reducer 함수에서 action의 type이 'TOGGLE_USER' 일 경우
1. 새로운 상태를 만들 때에는 불변성을 지켜주어야 하기 때문에 기존에 자신이 갖고 있는 state를 넣어 준다.
2. userInfo는 state 객체의 userInfo에 map 함수를 사용해서 user를 파라미터로 전달하고 user 객체의 id가
- action 객체의 id와 같을 경우 : 기존의 user 값을 넣어주고 user의 active 값을 반전한다.
- action 객체의 id와 다를 경우 : 기존의 user 객체를 유지한다.
# reducer 함수에서 action의 type이 'REMOVE_USER' 일 경우
1. 새로운 상태를 만들 때에는 불변성을 지켜주어야 하기 때문에 기존에 자신이 갖고 있는 state를 넣어 준다.
2. userInfo는 state 객체의 userInfo에 filter 함수를 사용해서 user를 파라미터로 전달하고 user 객체의 id가
- action 객체의 id와 같을 경우 : 기존의 user 객체에서 없앤다.
- action 객체의 id와 다를 경우 : 기존의 user 객체에 유지한다.
활성 사용자 수 구현
// ... function App() { // ... const count = useMemo(() => countActiveUsers(userInfo), [userInfo]); return ( <> <CreateUser username={username} email={email} onChange={onChange} onCreate={onCreate} /> <UserList users={userInfo} onToggle={onToggle} onRemove={onRemove} /> <div>활성사용자 수 : {count}</div> </> ); }
useMemo() 함수를 사용해서 바뀌는 코드가 없습니다.
useReducer vs useState
자 이제 궁금해지는 점이 한가지 있을 것입니다. 어떨 때 useReducer 를 쓰고 어떨 때 useState 를 써야 할까요? 일단, 여기에 있어서는 정해진 답은 없습니다. 상황에 따라 불편할때도 있고 편할 때도 있습니다.
예를 들어서 컴포넌트에서 관리하는 값이 딱 하나고, 그 값이 단순한 숫자, 문자열 또는 boolean 값이라면 확실히 useState 로 관리하는게 편할 것입니다.
const [value, setValue] = useState(true);
하지만, 만약에 컴포넌트에서 관리하는 값이 여러개가 되어서 상태의 구조가 복잡해진다면 useReducer로 관리하는 것이 편해질 수도 있습니다.
이에 대한 결정은, 앞으로 여러분들이 useState, useReducer 를 자주 사용해보시고 맘에드는 방식을 선택하세요.
저의 경우에는 setter 를 한 함수에서 여러번 사용해야 하는 일이 발생한다면
setUsers(users => users.concat(user)); setInputs({ username: '', email: '' });
그 때부터 useReducer 를 쓸까? 에 대한 고민을 시작합니다.
useReducer 를 썼을때 편해질 것 같으면 useReducer 를 쓰고, 딱히 그럴것같지 않으면 useState 를 유지하면 되지요.
반응형'Front-end > React.js' 카테고리의 다른 글
23. Context API 를 사용한 전역 값 관리 - 리액트 입문 | 벨로퍼트 (0) 2020.07.05 22. 커스텀 Hooks 만들기 - 리액트 입문 | 벨로퍼트 (0) 2020.07.03 20. useReducer 이해하기 - 리액트 입문 | 벨로퍼트 (0) 2020.07.01 19. React.memo 를 사용한 컴포넌트 리렌더링 방지 - 리액트 입문 | 벨로퍼트 (0) 2020.06.30 18. useCallback 을 사용하여 함수 재사용하기 - 리액트 입문 | 벨로퍼트 (0) 2020.06.30