본문 바로가기

Custom Hook

1. Custom Hook

React에서 제공하는 useState, useEffect 등의 유용한 훅들을 이미 살펴보았습니다. 그러나 개발 과정에서 반복적으로 사용되는 로직이 자주 발생합니다. 예를 들어, 데이터 가져오기, 폼 처리, 또는 애니메이션 로직 등이 있습니다.

JavaScript에서는 이러한 중복 로직을 함수로 추출하여 재사용했습니다. React는 이 개념을 한 단계 발전시켜, 상태 관리와 생명주기 관련 로직을 포함할 수 있는 Custom Hook으로 만들어 분리하고 재사용할 수 있게 해줍니다.

Custom Hook은 로직의 재사용성을 높이고 컴포넌트의 복잡성을 줄이는 데 매우 유용합니다. 이를 통해 관심사를 분리하고 코드를 더 모듈화할 수 있습니다. 또한, Custom Hook을 사용하면 테스트하기 쉬운 코드를 작성할 수 있으며, 프로젝트 전반에 걸쳐 일관된 패턴을 유지하는 데 도움이 됩니다.

이제 Custom Hook을 만들고 사용하는 방법에 대해 자세히 알아보겠습니다.

1.1 Custom Hook 규칙

자신만의 Custom Hook을 만들 때 지켜야 할 작명 규칙이 있습니다. 첫 번째, Hook의 이름은 반드시 use로 시작해야 합니다. 이는 React가 해당 함수를 Hook으로 인식하고 적절히 처리할 수 있게 해줍니다. 두 번째, 'use' 뒤에 오는 첫 글자는 대문자여야 합니다.

위 규칙을 따르면 코드를 읽는 다른 개발자들이 해당 함수가 Hook임을 쉽게 알아볼 수 있습니다.

Hook을 사용하지 않는 함수는 Hook으로 만들 필요가 없습니다. 함수 내에서 useState나 useEffect 등의 훅을 호출하지 않는다면, 그 함수는 Hook으로 간주되지 않으므로 이름 앞에 'use'를 붙이지 않는 것이 좋습니다.

2. Custom Hook 사용 예시

이제 간단한 가계부 애플리케이션을 만들어 Custom Hook의 사용법을 알아보겠습니다. 먼저 Custom Hook을 사용하지 않은 버전을 살펴본 후, Custom Hook을 적용하여 코드를 개선해 보겠습니다.

2.1 Custom Hook 사용 전

App.css

.app-container {
  max-width: 600px;
  padding: 20px;
}

form {
  margin: 20px 0;
}

label {
  margin-bottom: 10px;
  display: flex;
  flex-direction: column;
}

input {
  margin-top: 5px;
  padding: 5px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

button {
  width: 100%;
  padding: 10px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

table {
  width: 100%;
  border-collapse: collapse;
}

th, td {
  border: 1px solid #ddd;
  padding: 8px;
}

th {
  background-color: #f2f2f2;
  font-weight: bold;
}

App.jsx

import React, { useState } from 'react';
import './App.css';

function App() {
    const [item, setItem] = useState('');
    const [amount, setAmount] = useState('');
    const [balance, setBalance] = useState('');
    const [list, setList] = useState([]);

    function handleItemChange(e) {
        setItem(e.target.value);
    }

    function handleAmountChange(e) {
        setAmount(e.target.value);
    }

    function handleBalanceChange(e) {
        setBalance(e.target.value);
    }

    function submit(e) {
        e.preventDefault();
        if (item && amount && balance) {
            setList([...list, { item, amount, balance }]);
            setItem('');
            setAmount('');
            setBalance('');
        }
    }

    return (
        <div className="app-container">
            <h1>가계부</h1>
            <form onSubmit={submit}>
                <label htmlFor="item">
                    항목
                    <input type="text" id="item" value={item} onChange={handleItemChange} />
                </label>
                <label htmlFor="amount">
                    지출 금액
                    <input type="number" id="amount" value={amount} onChange={handleAmountChange} />
                </label>
                <label htmlFor="balance">
                    잔액
                    <input type="number" id="balance" value={balance} onChange={handleBalanceChange} />
                </label>
                <button type="submit">추가</button>
            </form>
            <div>
                <table>
                    <thead>
                    <tr>
                        <th>항목</th>
                        <th>지출 금액</th>
                        <th>잔액</th>
                    </tr>
                    </thead>
                    <tbody>
                        {list.map((item, index) => (
                            <tr key={index}>
                                <td>{item.item}</td>
                                <td>{item.amount}</td>
                                <td>{item.balance}</td>
                            </tr>
                        ))}
                    </tbody>
                </table>
            </div>
        </div>
    )
}

export default App;

이 코드에서는 항목, 지출 금액, 잔액을 입력받아 저장하는 item, amount, balance state를 생성합니다. 각 입력창에서 입력한 값을 통해 상태를 변경할 수 있도록 해당하는 상태값과 상태 변경 함수를 입력창의 value와 onChange 속성에 연결하였습니다.

입력 전 화면입력 및 추가 후 화면
가계부 입력 및 추가 전 화면가계부 입력 및 추가 후 화면

추가 버튼을 누르면 submit 함수가 실행됩니다. 항목, 지출 금액, 잔액을 모두 입력하였다면 list 상태 배열에 새로운 객체를 추가합니다. 이 객체는 현재 입력된 item, amount, balance 값을 포함합니다. 객체 추가 후에는 setItem, setAmount, setBalance 함수를 사용하여 각 입력 필드를 빈 문자열로 초기화합니다.

list 상태 배열에 새로운 객체가 추가되면, 이는 자동으로 표 목록에 반영됩니다. 렌더링 부분에서 map 함수를 사용하여 list 배열의 각 항목을 표의 행으로 변환합니다.

이 코드에서는 각 입력 필드(item, amount, balance)마다 별도의 state와 onChange 핸들러를 만들어 사용하고 있습니다. 이로 인해 코드가 반복되고 있으며, 컴포넌트의 복잡성이 증가하고 있습니다. 이러한 반복적인 로직을 Custom Hook으로 만들어 재사용성을 높이고 코드를 간결하게 만들어 보겠습니다.

2.2 Custom Hook 생성 및 사용

hook/useInput.jsx

import { useState } from 'react'

export default function useInput(initialValue) {
    const [value, setValue] = useState(initialValue);

    function onChange(e) {
        setValue(e.target.value);
    }

    function reset() {
        setValue(initialValue);
    }

    return { value, onChange, reset };
}

useInput이라는 Custom Hook을 만들었습니다. 이 Hook은 초기값(initialValue)을 매개변수로 받아 useState를 통해 상태를 생성합니다. onChange 함수는 입력 요소의 값이 변경될 때 호출되며, 새로운 값으로 상태를 업데이트합니다. reset 함수는 상태를 초기값으로 되돌립니다. value(현재 상태값), onChange 함수, reset 함수를 객체 형태로 반환합니다.

이 Custom Hook을 사용하면 각 입력 필드마다 별도의 useState와 핸들러 함수를 작성할 필요 없이, 재사용 가능한 로직을 간편하게 활용할 수 있습니다. 이를 통해 코드의 중복을 줄이고 관리를 용이하게 할 수 있습니다.

App.jsx

import React, { useState } from 'react';
import './App.css';
import useInput from './hook/useInput'

function App() {
    const item = useInput('');
    const amount = useInput('');
    const balance = useInput('');
    const [list, setList] = useState([]);

    function submit(e) {
        e.preventDefault();
        if (item.value && amount.value && balance.value) {
            setList([...list, {
                item: item.value,
                amount: amount.value,
                balance: balance.value
            }]);
            item.reset();
            amount.reset();
            balance.reset();
        }
    }

    return (
        <div className="app-container">
            <h1>가계부</h1>
            <form onSubmit={submit}>
                <label htmlFor="item">
                    항목
                    <input type="text" id="item" {...item} />
                </label>
                <label htmlFor="amount">
                    지출 금액
                    <input type="number" id="amount" {...amount} />
                </label>
                <label htmlFor="balance">
                    잔액
                    <input type="number" id="balance" {...balance} />
                </label>
                <button type="submit">추가</button>
            </form>
            <div>
                <table>
                    <thead>
                        <tr>
                            <th>항목</th>
                            <th>지출 금액</th>
                            <th>잔액</th>
                        </tr>
                    </thead>
                    <tbody>
                        {list.map((item, index) => (
                            <tr key={index}>
                                <td>{item.item}</td>
                                <td>{item.amount}</td>
                                <td>{item.balance}</td>
                            </tr>
                        ))}
                    </tbody>
                </table>
            </div>
        </div>
    );
}

export default App;

useInput Custom Hook을 사용하여 item, amount, balance 입력 필드를 관리합니다. 각 useInput 호출은 빈 문자열('')을 초기값으로 사용합니다.

<input {...item} />와 같이 스프레드 연산자를 사용하여 item 객체의 속성을 input 요소에 전달합니다. 이는 <input value={item.value} onChange={item.onChange} />와 동일한 효과를 냅니다. reset 함수는 input 요소의 표준 속성이 아니므로 HTML 요소에 직접 적용되지 않지만, 컴포넌트 내에서 필요할 때 호출할 수 있습니다. submit 함수에서 각 입력 필드의 reset 함수를 호출하여 초기화합니다.

2.3 Custom Hook 사용 전후 비교

Custom Hook 사용 전에는 각 입력 필드마다 별도의 state와 onChange 핸들러를 정의해야 했습니다. 이로 인해 코드의 반복이 많았고, 컴포넌트의 복잡성이 높았습니다.

Custom Hook을 생성하고 사용하여 입력 필드 관리 로직이 useInput Hook으로 추상화되었습니다. 코드가 더 간결해졌고, 재사용성이 향상되었습니다. 또 컴포넌트의 복잡성이 줄어들었습니다.

이러한 변화로 인해 코드의 가독성이 향상되었고, 향후 유지보수가 더 쉬워졌습니다. 또한, useInput Hook은 다른 컴포넌트에서도 쉽게 재사용할 수 있어 개발 효율성이 크게 향상되었습니다.

이처럼 Custom Hook은 React 애플리케이션의 로직을 효과적으로 구조화하고 재사용할 수 있게 해줍니다.