우리는 멋진 개발자들이라서 중복 코드를 보면 고치고 싶다.
우리가 만든 기능이 여러 컴포넌트에 쓰일 때,
복사-붙여넣기가 대신 마치 리액트 훅처럼 깔끔하게 쓸 수 있다면 좋겠다고 생각할 것이다.
리액트에서는 내가 원하는 인터페이스와 기능을 담은 나만의 커스텀 훅을 직접 만들 수 있다.
const[eat,setFood] = useEat("🍕")
함수명은 반드시 use 로 시작해야 한다.
그 안에 어떤 기능을 넣을지, 어떤 매개변수와 어떤 값을 반환할지는 전적으로 개발자가 자유롭게 설계할 수 있다.
만약 use 키워드가 빠진다면 React 는 이 함수를 Hook으로 인식하지 못하기 때문에, 에러가 발생했을 때도 Hook 관련 경고나 안내 메시지를 제대로 제공할 수 없다.
Custom Hook 내부에서 useState, useEffect 같은 다른 훅도 자유롭게 사용할 수 있다.
그리고 이 커스텀 훅을 사용하는 컴포넌트는 내부에서 어떤 훅들을 쓰는지 몰라도 된다.
import { useState } from 'react';
function App() {
const [inputValue, setInputValue] = useState('');
const handleChange = (e) => {
setInputValue(e.target.value);
};
const handleSubmit = () => {
alert(inputValue);
setInputValue('');
};
return (
<div>
<h1>useInput</h1>
<input value={inputValue} onChange={handleChange} />
<button onClick={handleSubmit}>확인</button>
</div>
);
}
export default App;
컴포넌트가 처음 렌더링될 때 inputValue 라는 state 를 빈 문자열로 만들고, setInputValue 라는 setter 함수로 값을 변경할 수 있게 한다. 사용자가 input 창에 글자를 입력할 때마다 onChange 이벤트가 발생하고, handleChange 함수가 실행되면서 e.target.value 를 그대로 setInputValue 로 넣어준다. 그래서 입력하는 순간마다 state 가 업데이트되고, 이 값이 다시 input 의 value 로 반영되면서 화면에 보여진다.
버튼을 클릭하면 handleSubmit 함수가 실행된다. 이 함수에서는 현재 입력된 state 값을 alert 로 한번 보여주고, 그 다음에 다시 setInputValue('') 로 state 를 초기화해 input 을 빈 문자열로 리셋한다. 결국 입력 → state 변경 → 화면 반영 → 버튼 클릭으로 확인 → 초기화 라는 흐름을 보여준다.
여러 컴포넌트나 여러 input 태그에 쓰인다면 중복된 코드가 여기저기 생길 것이다.
그러나 훅으로 만들면 단 한줄로 사용할 수 있게 된다.
이제 인풋처리 기능을 추출해서 훅으로 만들어보자.
// useInput.js
import { useState } from 'react';
export function useInput(initialValue, submitAction) {
const [inputValue, setInputValue] = useState(initialValue);
const handleChange = (e) => {
setInputValue(e.target.value);
};
const handleSubmit = () => {
submitAction(inputValue);
setInputValue("");
};
return [inputValue, handleChange, handleSubmit];
}
---------------------------------------------------------------
// App.js
import { useInput } from './useInput';
function DisplayMessage(message) {
alert(message);
}
function App() {
const [inputValue, handleChange, handleSubmit] =
useInput("id를 입력해주세요", DisplayMessage);
const [inputValue2, handleChange2, handleSubmit2] =
useInput("pw를 입력해주세요", DisplayMessage);
return (
<div>
<h1>useInput</h1>
<input value={inputValue} onChange={handleChange} />
<input value={inputValue2} onChange={handleChange2} />
<button onClick={handleSubmit}>ID 확인</button>
<button onClick={handleSubmit2}>PW 확인</button>
</div>
);
}
export default App;
좀더 유용한 예제를 만나보자.
아래 코드는 API 요청 코드를 커스텀 훅으로 분리한 것이다.
컴포넌트마다 fetch → json → setState 를 매번 반복하지 않고,
useFetch 훅 내부에 공통 로직을 모아둔 뒤
컴포넌트에서는 단순히 fetchUrl('users') 처럼 원하는 타입만 바꿔 호출하면 된다.
초기 렌더링 시에는 initialType 으로 한 번 자동 요청되며,
요청 결과는 data 로 받아 출력할 수 있다.
// useFetch.js
import { useEffect, useState } from 'react';
export function useFetch(baseUrl, initialType) {
const [data, setData] = useState(null);
const fetchUrl = (type) => {
fetch(baseUrl + '/' + type)
.then((res) => res.json())
.then((res) => setData(res));
};
useEffect(() => {
fetchUrl(initialType);
}, []);
return {
data,
fetchUrl,
};
}
---------------------------------------------------------------
// App.js
import { useFetch } from './useFetch';
const baseUrl = 'https://jsonplaceholder.typicode.com';
function App() {
const { data, fetchUrl } = useFetch(baseUrl, 'users');
return (
<div>
<h1>useFetch</h1>
<button onClick={() => fetchUrl('users')}>Users</button>
<button onClick={() => fetchUrl('posts')}>Posts</button>
<button onClick={() => fetchUrl('todos')}>Todos</button>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
export default App;
마지막은 재미로 만들어보았다.
// useCoffee.js
import { useState } from "react";
export function useCoffee(defaultMenu) {
const [coffee, setCoffee] = useState(defaultMenu);
const order = (menu) => {
setCoffee(menu);
};
const refill = () => {
setCoffee(coffee + " ☕");
};
return { coffee, order, refill };
}
---------------------------------------------------
// App.jsx
import { useCoffee } from "./useCoffee";
export default function App() {
const { coffee, order, refill } = useCoffee("아메리카노");
return (
<div>
<h2>오늘 내 커피 : {coffee}</h2>
<button onClick={() => order("라떼")}>
라떼로 바꾸기
</button>
<button onClick={refill}>
리필하기
</button>
</div>
);
}