[패스트캠퍼스 수강 후기] 프론트엔드 인강 100% 환급 챌린지 28회차 미션 - 30강 Custom Hook, context API
Custom Hook
컴포넌트를 만들다보면 반복적인 로직들이 발견된다. 예를들어 아래와 같이 인풋을 관리하는 코드는 꽤나 자주 발생하는 코드다. 이런 경우 우리는 Custom Hook을 만들어 사용할 수 있다. 그럼 직접 만들어보자. useInputs.js라는 커스텀 훅 파일을 새로 만든다.
useInputs.js (useState활용)
import { useState, useCallback } from 'react';
function useInputs(initialForm){
const [form, setForm] = useState(initialForm);
const onChange = useCallback(e => {
const {name, value} = e.target;
setForm(form => ({...form, [name]: value}));
}, []);
const reset = useCallback(() => setForm(initialForm), [initialForm]);
return[form, onChange, reset];
};
export default useInputs;
커스컴훅을 만들때는 이렇게 use라는 이름과 기능(Inputs)이렇게 섞어서 함수를 만든다. 우리는 이번 훅을 만들때 useState를 사용해서 만들었다. useState말고 useReducer로도 만들 수 있다.
useInputs.js (useReducer활용)
import { useReducer, useCallback } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'CHANGE':
return {
...state,
[action.name]: action.value
};
case 'RESET':
return Object.keys(state).reduce((acc, current) => {
acc[current] = '';
return acc;
}, {});
default:
return state;
}
}
function useInputs(initialForm) {
const [form, dispatch] = useReducer(reducer, initialForm);
// change
const onChange = useCallback(e => {
const { name, value } = e.target;
dispatch({ type: 'CHANGE', name, value });
}, []);
const reset = useCallback(() => dispatch({ type: 'RESET' }), []);
return [form, onChange, reset];
}
export default useInputs;
useReducer를 활용하면 위와 같이 작성할 수 있다. 이제 App.js를 아래와 같이 수정해보자.
App.js
import React, { useRef, useReducer, useMemo, useCallback } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';
import useInputs from './useInputs';
function countActiveUsers(users){
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
const initialState = {
users: [
{
id: 1,
username: 'bmyu',
email: 'bomee88@naver.com',
active: true,
},
{
id: 2,
username: 'tester',
email: 'tester@example.com',
active: false,
},
{
id: 3,
username: 'sample',
email: 'sample@example.com',
active: false,
}
]
}
function reducer(state, action){
switch (action.type){
case 'CREATE_USER':
return {
inputs: initialState.inputs,
users: state.users.concat(action.user)
}
case 'TOGGLE_USER':
return {
...state,
users: state.users.map(user =>
user.id === action.id
? { ...user, active: !user.active }
: user
)
};
case 'REMOVE_USER':
return {
...state,
users: state.users.filter(user => user.id !== action.id)
}
default:
throw new Error('Unhandled action');
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const [form, onChange, reset] = useInputs({
username: '',
email: '',
});
const { username, email } = form;
const nextId = useRef(4);
const { users } = state;
const onCreate = useCallback(() => {
dispatch({
type: 'CREATE_USER',
user: {
id: nextId.current,
username,
email,
}
});
nextId.current += 1;
reset();
}, [username, email, reset]);
const onToggle = useCallback (id => {
dispatch({
type: 'TOGGLE_USER',
id
})
},[]);
const onRemove = useCallback (id => {
dispatch({
type: 'REMOVE_USER',
id
})
},[]);
const count = useMemo(() => countActiveUsers(users),[users])
return(
<>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList
users={users}
onToggle={onToggle}
onRemove={onRemove}
/>
<div>활성 사용자 수 : {count}</div>
</>
)
}
export default App;
이렇게 수정해주고 보면 기존과 똑같이 작동하는 것을 볼 수 있다. 이렇게 자주쓰는 항목은 use+기능이름으로 커스텀훅을 만들어서 활용할 수 있다는 점 알아두도록 하자.
context API를 사용한 전역 값 관리
리액트를 개발하다보면 여러개의 컴포넌트가 만들어지는데 어떤 props값을 여러번 전달해야하는 상황이 생기곤 한다. 그런데 그때 contextAPI를 활용하면 props를 여러번 전달하지 않고 원하는 곳에 바로 전달할 수 있다. 한번 코드로 알아보도록 하자. ContextSample이라는 컴포넌트를 새로 만든다.
ContextSample.js
import React, { createContext, useContext } from 'react';
function Child({ text }) {
return <div>안녕하세요? {text}</div>
}
function Parent({ text }){
return <Child text={text} />
}
function GrandParent({ text }){
return <Parent text={text} />
}
function ContextSample(){
return <GrandParent text="GOOD" />
}
export default ContextSample;
이렇게 나오는 화면이 있다고 하자. 위의 코드를 보면 ContextSample 안에서의 GrandParent의 text값으로 GOOD을 반환하고 또 GrandParent에서는 Parent로, Parent에서는 Child로 계속 이관되는 것을 볼 수 있다. 이것을 createContext와 useContext를 사용하면 아래와 같이 코드를 쓸 수 있다.
import React, { createContext, useContext } from 'react';
const MyContext = createContext('defaultValue'); //2. MyContext의 값이 아까 1.의 value로 설정됨.
function Child() {
const text = useContext(MyContext); //3. 아까 2.의 내용을 변수에 넣어줌.
return <div>안녕하세요? {text}</div> //4. 변수 text에 담긴걸 보여줌
}
function Parent(){
return <Child />
}
function GrandParent(){
return <Parent />
}
function ContextSample(){
return (
<MyContext.Provider
//1. MyContext를 통해 Provider를 설정해줌. value로 보여줄 텍스트값을 넣어줌.
value="GOOD">
<GrandParent />
</MyContext.Provider>
)
}
export default ContextSample;
이렇게 Context를 만들어서 다른 곳에서도 계속 사용할 수 있다는 장점이 있다.
그리고 이 MyContext의 value값은 변하게 할수도 있다. 아래의 예시를 봐보자.
import React, { createContext, useContext, useState } from 'react';
const MyContext = createContext('defaultValue');
function Child() {
const text = useContext(MyContext);
return <div>안녕하세요? {text}</div>
}
function Parent(){
return <Child />
}
function GrandParent(){
return <Parent />
}
function ContextSample(){
const [value, setValue] = useState(true);
return (
<MyContext.Provider value={value ? 'GOOD' : 'BAD'}>
<GrandParent />
<button onClick={() => setValue(!value)}>Click Me</button>
</MyContext.Provider>
)
}
export default ContextSample;
이렇게하면 버튼을 클릭할때마다 GOOD이 BAD로, BAD가 GOOD으로 바뀌는 것을 볼 수 있다. 상태가 바뀔때마다 값을 각 function에 전달하는것이 아니라 바로 전달할 수 있게 된다. 이제 한번 App.js에서 우리가 만든 컴포넌트를 활용해보도록 하자.
UserDispatch Context 만들기
우리가 기존에 만들어놓은 구조 중 App.js의 함수인 onToggle과 onRemove는 두개의 상태를 User컴포넌트에 보내기 위해서 UserList를 거치고 있다. 우리는 이것을 UserDispatch라는 Context를 만들어서 개선해볼것이다.
App.js
import React, { useRef, useReducer, useMemo, useCallback, createContext } from 'react'; //createContext추가
import UserList from './UserList';
import CreateUser from './CreateUser';
import useInputs from './useInputs';
function countActiveUsers(users){
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
const initialState = {
users: [
{
id: 1,
username: 'bmyu',
email: 'bomee88@naver.com',
active: true,
},
{
id: 2,
username: 'tester',
email: 'tester@example.com',
active: false,
},
{
id: 3,
username: 'sample',
email: 'sample@example.com',
active: false,
}
]
}
function reducer(state, action){
switch (action.type){
case 'CREATE_USER':
return {
inputs: initialState.inputs,
users: state.users.concat(action.user)
}
case 'TOGGLE_USER':
return {
...state,
users: state.users.map(user =>
user.id === action.id
? { ...user, active: !user.active }
: user
)
};
case 'REMOVE_USER':
return {
...state,
users: state.users.filter(user => user.id !== action.id)
}
default:
throw new Error('Unhandled action');
}
}
export const UserDispatch = createContext(null); //export를 사용해서 전역으로 내보낼 수 있게 해줌. 받아보고 싶은 곳에서 import로 불러와서 사용하면 됨. (UserList에서 볼 수 있음.)
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const [form, onChange, reset] = useInputs({
username: '',
email: '',
});
const { username, email } = form;
const nextId = useRef(4);
const { users } = state;
const onCreate = useCallback(() => {
dispatch({
type: 'CREATE_USER',
user: {
id: nextId.current,
username,
email,
}
});
nextId.current += 1;
reset();
}, [username, email, reset]);
const count = useMemo(() => countActiveUsers(users),[users])
return(
<UserDispatch.Provider value={dispatch}>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} />
<div>활성 사용자 수 : {count}</div>
</UserDispatch.Provider>
)
}
export default App;
기존 코드에서 주석으로 단 부분을 추가했다. 아래의 UserList에서 받아오는 부분을 수정해보도록 하자.
UserList.js
import React, { useContext } from 'react'; //useContext를 불러와줌.
import { UserDispatch } from './App'; //아까 위에서 만든 export로 보냈던 UserDispatch를 받아와줌.
const User = React.memo(function User({ user }){
const {username, email, id, active} = user;
const dispatch = useContext(UserDispatch); //useContext를 활용하여, dispatch로 연동.
return(
<div>
<b
style=
onClick={() => dispatch({
type: 'TOGGLE_USER',
id
})}
>
{username}
</b>
<span>({email})</span>
<button onClick={() => dispatch({
type: 'REMOVE_USER',
id
})}>삭제</button>
</div>
);
});
function UserList({ users}){
return(
<div>
{
users.map(
(user) => (
<User
user={user}
key={user.id}
/>
)
)
}
</div>
)
}
export default React.memo(UserList);
이렇게 바꿔볼 수 있다. 매우 좋은거 같은데 과제로 내주신 내용이 내수준엔 무리여서 답안을 참고했다.
오늘은 여기까지..
시청 영상 30강 23~25까지
프론트엔드 개발 올인원 패키지 with React Online. 👉 https://bit.ly/31Cf1hp
Comments