본문 바로가기

React.js

React(리액트): 커스텀 Hook 만들어 사용하기

반응형

이번에는 Custom Hook을 만드는 방법에 대해서 알아보자.

 

const onChange = (e) => {
  const {name, value } = e.target;
  setInputs({...inputs, [name]: value});
}

컴포넌트를 만들다 보면 가끔씩 반복되는 로직들이 발생한다. 다음과 같이 input을 관리하는 코드는 꽤나 많이 작성하게

될 수 있는 코드다. input을 관리하려면 'e.target' 안에 있는 name과 value를 읽어서 이를 참조해 어떤 새로운 상태를 

설정해야 하기 때문이다. 이러한 상황에서는 우리가 custom Hook을 만들어 사용할 수 있다. 즉, 우리만의 Hook을 

만든다는 소리이다. 

 

리액트에 내장되어있는 useEffect, useState, useReducer와 같은 Hook을 사용해서 원하는 기능을 구현하고 

컴포넌트에서 사용하고 싶은 값들을 반환해주면 된다. 반환해주는 값은 우리 마음대로 설정할 수 있다.

 

input 상태를 관리하는 커스텀 Hook을 작성해보자.

 

useInputs.js

import {useState, useCallback } from 'react';

function useInputs(initialForm) {
    const [form, setFrom] = useState(initialForm);
    const onChange = useCallback(e => {
        const {name,value} = e.target;
        setFrom(form => ({...form, [name]:value}));
    }, []);
    const reset = useCallback(() => setFrom(initialForm), [initialForm]);
    
    return [form, onChange, reset]
}

export default useInputs;

useInputs라는 함수를 만들어주고 이 함수에서 사용할 파라미터는 initialForm이다. 즉 해당 inputForm에서 관리할 

초기값이다.

useState를 사용해 form이라는 새로운 상태를 선언하게 되는데 그 상태의 초기값은 파라미터로 가져온 initialForm이다.

그리고 onChange 함수도 만들어주었다. 이 함수에서는 setForm을 사용해 form을 업데이트해준다. deps 배열에는 

의존하는 다른 상태가 없기 때문에 비워주면 된다. 

추가적으로 reset이라는 함수도 만들어 주었는데 이 함수의 역할을 form을 초기화시킨다. 초기값으로 받아온 initialForm으로 form 업데이트해줌으로써 초기화 기능을 수행한다. 

만든 것들을 바깥으로 내보내 주어야 하는데 객체 형태로 내보내 줘도 되고, 배열 형태로 내보내 줘도 된다. 여기서는 배열 형태로 내보내 주었다.

 

useInputs를 사용하게 될 때는 함수에서 관리할 form에 대하여 초기값을 파라미터로 받아온 다음, 이 Hook이 반환하는 

onChange를 사용해서 input의 Change 이벤트를 관리하면 되고 상태 같은 경우는 form에서 조회하면 되고, 초기화하고

싶다면 reset을 호출하면 된다.

 

App.js

import "./App.css";
import UserList from './UserList';
import {useRef, useReducer, useMemo, useCallback } from 'react';
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: 'paboke22',
      email: 'paboke22@gmail.com',
      active: true
  },
  {
      id:2,
      username: 'tester',
      email: 'tester@example.com',
      active: false,
  },
  {
      id: 3,
      username: 'guest',
      email: 'guest@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;

App 컴포넌트의 코드는 다음과 같다. reducer에서 'CHANGE_INPUT' case를 관리해줄 필요가 없어 지워주었고 

initialState에서 inputs 객체도 더 이상 필요가 없다. 

 

App.js

const [form,onChange,reset] = useInputs({
    username: '',
    email: '',
  })
const {username, email} = form;

useInput을 다음처럼 사용해주었고 username과 email을 공백으로 초기화하게 넘겨준다. username과 email을 form에서

추출해준다.

 

const onCreate = useCallback(() => {
    dispatch({
      type:'CREATE_USER',
      user:{
        id: nextId.current,
        username,
        email,
      }
    });
    nextId.current += 1;
    reset();
  },[username, email, reset])

reset은 onCreate에서 호출해준다. 

 

우리가 만들어본 것은 커스텀 Hook인데, 커스텀 Hook을 만들 때는 'use'라는 키워드로 시작해서 그 뒤에는 우리가 구현할 기능에 대한 어떤 단어를 넣어서 함수를 만들어주는 것이 암묵적인 규칙이다. 

 

useInputs.js

import {useCallback, useReducer } 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);
    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;

앞에서 useState를 사용해 상태관리를 해줬던 useInputs 컴포넌트를 useReducer를 사용해 상태관리하도록 바꾼 

코드이다.

반응형