[패스트캠퍼스 수강 후기] 프론트엔드 인강 100% 환급 챌린지 28회차 미션 - 30강 Custom Hook, context API

5 minute read

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>
            &nbsp;
            <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