21. 커스텀 Hooks 만들기
- TOC {:toc}
이 글은 벨로퍼트와 함께하는 모던 React중 21. 커스텀 Hooks 만들기의 내용을 복습하기위해 핵심 내용을 요약 정리한 글입니다.
- 제가 필요한 부분 위주로 확인하면서 정리하고 있어 글에 덜 작성된 부분이 있을 수 있습니다.
- 글 작성 후 원문의 내용이 수정되거나 내용을 이해하기 위한 개인적인 설명이나 해석이 있을 수 있습니다. 되도록 원문을 참고해주시길 바랍니다.
- 잘못된 부분이 있다면 댓글이나 그 외 편하신 방법으로 알려주시면 감사하겠습니다.
컴포넌트를 만들다보면, 로직이 반복되는 경우가 자주 있다. 예를 들어 input 을 관리하는 코드는 매 번 비슷한 코드가 반복된다. 이번에는 커스텀 Hooks 를 만들어서 반복되는 로직을 쉽게 재사용하는 방법을 알아보자.
우선 useInputs.js라는 파일을 만든다.
- 커스텀 Hooks 를 만들 때는 보통
use
라는 키워드로 시작하는 파일을 만들고 그 안에 함수를 작성한다. - 만든 커스텀 Hooks들은
src
디렉터리에hooks
라는 디렉터리를 만들어 따로 관리하는 경우가 많다.
커스텀 Hooks 를 만드는 방법은 굉장히 간단하다. 만든 커스텀 Hooks 파일 안에서 useState
, useEffect
, useReducer
, useCallback
과 같은 Hooks 를 사용하여 원하는 기능을 구현해주고, 컴포넌트에서 사용하고 싶은 값들을 반환해주면 된다.
/* useInputs.js */
import { useState, useCallback } from 'react';
function useInputs(initialForm) {
// useState를 이용해 form을 관리
// 이전의 inputs, setInputs와 동일한 역할이다.
const [form, setForm] = useState(initialForm);
// onChange 구현
// 이전과 동일하게 target의 name과 value를 받아와서 form을 업데이트 한다.
const onChange = useCallback(e => {
const { name, value } = e.target;
setForm(form => ({ ...form, [name]: value }));
}, []);
// reset 함수. form의 값을 초기값으로 돌린다.
const reset = useCallback(() => setForm(initialForm), [initialForm]);
return [form, onChange, reset];
}
export default useInputs;
useInputs.js를 useReducer
를 이용해서 만들수도 있다.
/* useInputs.js */
import { useReducer, useCallback } from 'react';
// reducer 구현
function reducer(state, action) {
switch (action.type) {
// onChange용 action
case 'CHANGE':
return {
...state,
[action.name]: action.value
};
// 값 초기화를 위한 action
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;
이제 useInputs
Hook 을 App.js 에서 사용해보자.
먼저 useReducer
쪽에서 사용하는 inputs
를 없애고 이에 관련된 작업을 useInputs
로 대체해주어야 한다. 새로운 항목을 추가 할 때 input
값을 초기화해야 하므로 데이터 등록 후 reset()
을 호출한다.
/* App.js */
import React, { useRef, useReducer, useMemo, useCallback } from 'react';
// ...
// useInputs를 import
import useInputs from './hooks/useInputs';
function countActiveUsers(users) { /* ... */ }
const initialState = {
// inputs 제거
/*
* inputs: {
* username: '',
* email: ''
* },
*/
users: [ /* ... */ ]
};
function reducer(state, action) {
switch (action.type) {
// CHANGE_INPUT 제거
/*
* case 'CHANGE_INPUT':
* return {
* ...state,
* inputs: {
* ...state.inputs,
* [action.name]: action.value
* }
* };
* default:
* return state;
*/
case 'CREATE_USER':
return {
// initialState에서 inputs를 제거했으므로 아래의 코드를 삭제한다.
// inputs 초기화는 useInputs에서 가져온 reset 함수로 대체한다.
// inputs: initialState.inputs,
users: state.users.concat(action.user)
};
case 'TOGGLE_USER':
return { /* ... */ };
case 'REMOVE_USER':
return { /* ... */ };
default:
return state;
}
}
function App() {
// useInputs에서 inputs, onChange, reset을 받아온다.
// inputs는 username, email로 구조 분해 할당해서 받아온다.
const [{ username, email }, onChange, reset] = useInputs({
username: '',
email: ''
});
// ...
// initialState에서 inputs를 제거했으므로 아래의 코드를 삭제한다.
// username, email은 위의 useInputs에서 받아온다.
// const { username, email } = state.inputs;
// onChange 제거
/*
* const onChange = useCallback(e => {
* const { name, value } = e.target;
* dispatch({
* type: 'CHANGE_INPUT',
* name,
* value
* });
* }, []);
*/
const onCreate = useCallback(() => {
dispatch({ /* ... */ });
reset(); // 생성을 끝내면 입력 form을 초기화한다.
nextId.current += 1;
}, [username, email, reset]);
const onToggle = useCallback(id => { /* ... */ }, []);
const onRemove = useCallback(id => { /* ... */ }, []);
// ...
return ( /* ... */ );
}
export default App;
이렇게 커스텀 Hook 을 만들어 컴포넌트의 로직을 분리시키고 필요 할 때 쉽게 재사용 할 수도 있다.
참고
읽어보고 정리
useInput.js에서 궁금한게 있습니다.
setForm(form => ({ …form, [name]: value })); 이 코드에서… form => 저는 이것을 arrow function으로 생각해서 {{…form, [name]…로 생각했는데, 왜 괄호를 (로 감싸는건가요?
위 싸이트에 답이 있네요(저도 왜그런가 몰라서 찾아봤어요..)
// 객체 리터럴 표현을 반환하기 위해서는 함수 본문(body)을 괄호 속에 넣음: params => ({foo: bar})
괄호를 안쓰면 인터프리터에서 함수 선언부로 인식해서 그렇습니다. 객체 반환으로 명시하기위해선 괄호를 써야합니다.
원래 함수형 업데이트를 할 때는 값을 리턴 해줘야 하는 건가요?
-
draft