React 커스텀 훅을 위한 실용적인 패턴
Lukas Schneider
DevOps Engineer · Leapcell

소개
현대 웹 개발이 빠르게 발전하는 환경에서 React는 풍부하고 상호작용적인 사용자 인터페이스를 만드는 선도적인 라이브러리로 자리매김했습니다. React의 강력함과 유연성에 크게 기여한 것은 함수형 컴포넌트에 상태 관련 로직과 부수 효과를 통합할 수 있게 해주는 훅(Hooks)의 도입입니다. React는 useState 및 useEffect와 같은 내장 훅 세트를 제공하지만, 진정한 마법은 종종 커스텀 훅을 만드는 데 있습니다. 이러한 커스텀 훅은 복잡한 컴포넌트 로직을 추출하고 재사용할 수 있게 하여, 더 깨끗하고 유지보수성이 높으며 매우 조합 가능한 코드베이스로 이어집니다.
이 글에서는 커스텀 React 훅을 구축하기 위한 일반적이고 매우 실용적인 패턴을 탐구하며, 널리 적용 가능한 두 가지 예시, 즉 useDebounce와 useLocalStorage를 통해 이러한 개념을 설명합니다.
커스텀 훅의 핵심 개념 이해
특정 예시를 살펴보기 전에, 커스텀 훅의 기본 원칙에 대한 공통된 이해를 확립해 봅시다.
커스텀 훅이란 무엇인가요?
커스텀 훅은 이름이 use로 시작하고 다른 훅을 호출할 수 있는 JavaScript 함수입니다. 이러한 훅은 다양한 컴포넌트에서 코드를 중복하지 않고 재사용할 수 있는 로직을 캡슐화합니다. use 접두사는 React 린터가 훅 규칙을 강제하도록 허용하는 규칙이며, 훅이 커스텀 훅 또는 함수형 컴포넌트의 최상위 수준에서만 호출되도록 보장합니다.
커스텀 훅의 이점:
- 로직 재사용성: 시각적이지 않은 로직 추출 및 공유
- 가독성 향상: 컴포넌트를 더 깨끗하게 만들고 렌더링에 집중
- 유지보수성 향상: 로직 중앙 집중화로 변경 사항 관리 용이
- 테스트 용이성 향상: 격리된 로직은 독립적으로 테스트하기 더 쉬움
훅 규칙:
- 훅은 최상위 수준에서만 호출하세요: 루프, 조건문 또는 중첩 함수 내에서 훅을 호출하지 마세요.
- 훅은 React 함수에서만 호출하세요: React 함수형 컴포넌트 또는 다른 커스텀 훅에서 훅을 호출하세요.
이러한 기본 개념을 바탕으로 두 가지 매우 유용한 커스텀 훅을 구축하는 방법을 살펴보겠습니다.
커스텀 훅 패턴 1: useDebounce로 상태 업데이트 디바운싱하기
상호작용적인 UI를 구축할 때 흔히 발생하는 과제는 검색창 입력이나 창 크기 조절과 같이 빈번한 사용자 입력을 처리하는 것입니다. 모든 키 입력마다 이벤트 핸들러나 API 호출을 실행하면 성능 문제가 발생하거나 불필요한 서버 부하가 발생할 수 있습니다. '디바운싱' 기법은 추가 트리거 없이 특정 시간이 경과할 때까지 함수의 실행을 지연시키는 방식으로 이 문제를 해결합니다.
useDebounce가 해결하는 문제
검색 입력 필드를 상상해 보세요. 사용자가 입력함에 따라 검색 결과를 가져오고 싶을 수 있습니다. 모든 onChange 이벤트에서 API 호출을 트리거하고 사용자가 빠르게 입력하면 많은 불필요한 요청을 하게 됩니다. 디바운싱은 사용자가 지정된 시간 동안 입력을 멈춘 후에만 검색 요청이 전송되도록 보장합니다.
useDebounce 구현
useDebounce 훅은 일반적으로 값과 지연 시간을 입력으로 받아 디바운스된 버전의 값을 반환합니다.
import { useState, useEffect } from 'react'; /** * 값을 디바운싱하는 커스텀 훅. * * @param {any} value 디바운싱할 값. * @param {number} delay 디바운스된 값을 업데이트하기 전의 밀리초 단위 지연 시간. * @returns {any} 디바운스된 값. */ function useDebounce(value, delay) { // 디바운스된 값을 저장하는 상태 const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { // 지정된 지연 시간 후에 디바운스된 값을 업데이트하는 시간 초과 설정 const handler = setTimeout(() => { setDebouncedValue(value); }, delay); // 시간 초과가 발생하기 전에 값이나 지연 시간이 변경되거나 // 컴포넌트가 마운트 해제되는 경우 시간 초과 정리 return () => { clearTimeout(handler); }; }, [value, delay]); // 값 또는 지연 시간이 변경된 경우에만 다시 실행 return debouncedValue; } export default useDebounce;
useDebounce 작동 방식
- 디바운스된 값을 위한
useState:debouncedValue는value의 안정적이고 디바운스된 버전을 보유합니다. 초기value로 초기화됩니다. - 디바운스 로직을 위한
useEffect:useEffect내에서delay밀리초 후에debouncedValue를 업데이트하기 위한setTimeout이 설정됩니다.- 정리 함수:
useEffect는clearTimeout을 호출하는 정리 함수를 반환합니다. 이것은 매우 중요합니다.value또는delay가 변경될 때마다 이전 시간 초과는 취소되고 새 시간 초과가 설정됩니다. 이는value가delay시간 동안 변경되지 않았을 때만setDebouncedValue가 실행되도록 보장합니다.
- 의존성 배열:
[value, delay]는 효과가value또는delay가 변경될 때만 다시 실행되도록 보장합니다.
useDebounce 적용
검색 컴포넌트에서 이 훅을 어떻게 사용할 수 있는지 살펴보겠습니다.
import React, { useState } from 'react'; import useDebounce from './useDebounce'; // 훅을 파일에 저장했다고 가정 function SearchInput() { const [searchTerm, setSearchTerm] = useState(''); // 디바운스된 검색어 사용 const debouncedSearchTerm = useDebounce(searchTerm, 500); // 500ms 디바운스 지연 // debouncedSearchTerm이 변경될 때 검색을 수행하는 효과 useEffect(() => { if (debouncedSearchTerm) { console.log('검색 수행 대상:', debouncedSearchTerm); // 실제 애플리케이션에서는 API 호출을 수행합니다. // 예: fetch(`/api/search?q=${debouncedSearchTerm}`).then(...) } else { console.log('검색어 지워짐.'); } }, [debouncedSearchTerm]); // debouncedSearchTerm이 변경될 때만 실행 const handleChange = (event) => { setSearchTerm(event.target.value); }; return ( <div> <input type="text" placeholder="검색하려면 입력하세요..." value={searchTerm} onChange={handleChange} style={{ padding: '8px', width: '300px' }} /> <p>현재 검색어: {searchTerm}</p> <p>디바운스된 검색어: {debouncedSearchTerm}</p> </div> ); } export default SearchInput;
이 예제에서 SearchInput은 내부 searchTerm을 즉시 업데이트하지만, 가상화된 검색 작업을 트리거하는 useEffect는 debouncedSearchTerm에만 반응하여 효율성을 보장합니다.
커스텀 훅 패턴 2: useLocalStorage로 상태 영속화하기
많은 웹 애플리케이션은 페이지 새로고침 간에 사용자 기본 설정 또는 데이터를 영속화해야 합니다. localStorage는 이 목적을 위한 편리한 브라우저 API입니다. 그러나 컴포넌트 내에서 localStorage와 직접 상호작용하면 반복적인 코드와 초기 상태 동기화 문제가 발생할 수 있습니다. useLocalStorage 훅은 이 프로세스를 간소화합니다.
useLocalStorage가 해결하는 문제
localStorage에서 값을 저장하고 검색하는 것은 종종 JSON.parse 및 JSON.stringify, 오류 처리, 컴포넌트의 초기 상태가 저장된 값을 올바르게 반영하도록 보장하는 등 상용구 코드를 포함합니다. useLocalStorage는 이 복잡성을 추상화하여 영속적인 상태 관리를 간단하게 만듭니다.
useLocalStorage 구현
이 훅은 localStorage 키와 초기 값을 받아들입니다. useState와 유사하게 현재 값과 이를 업데이트하는 함수를 반환합니다.
import { useState, useEffect } from 'react'; /** * localStorage에 상태를 영속화하는 커스텀 훅. * * @param {string} key localStorage에 값이 저장되는 키. * @param {any} initialValue localStorage에서 아무것도 찾을 수 없을 때 사용할 초기 값. * @returns {[any, (value: any) => void]} 현재 상태와 설정 함수가 포함된 튜플. */ function useLocalStorage(key, initialValue) { // 지연 초기화를 위해 `useState`에 함수형 업데이트 사용. // `localStorage.getItem`이 한 번만 호출되도록 보장합니다. const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); // 저장된 JSON 파싱 또는 없으면 initialValue 반환 return item ? JSON.parse(item) : initialValue; } catch (error) { // 오류 발생 시 (예: localStorage 사용 불가), // initialValue 반환 및 오류 로깅. console.error('localStorage에서 읽기 오류:', error); return initialValue; } }); // storedValue가 변경될 때마다 localStorage를 업데이트하기 위해 useEffect 사용 useEffect(() => { try { window.localStorage.setItem(key, JSON.stringify(storedValue)); } catch (error) { console.error('localStorage에 쓰기 오류:', error); } }, [key, storedValue]); // 키 또는 storedValue가 변경되면 효과 다시 실행 return [storedValue, setStoredValue]; } export default useLocalStorage;
useLocalStorage 작동 방식
useState를 사용한 지연 초기화:useState훅은 함수로 초기화됩니다. 이 함수는 컴포넌트의 초기 렌더링 중에만 한 번 실행됩니다.- 이 함수 내에서 제공된
key를 사용하여localStorage에서 항목을 검색하려고 시도합니다. JSON.parse를 사용하여 검색된 문자열을 파싱합니다. 항목을 찾지 못하거나 오류가 발생하면initialValue로 대체됩니다. 이렇게 하면 모든 렌더링에서localStorage에 접근하는 것을 방지합니다.
- 영속화를 위한
useEffect:- 이
useEffect훅은key또는storedValue가 변경될 때마다 실행됩니다. - 현재
storedValue를 가져와JSON.stringify를 사용하여 JSON 문자열로 변환한 후 지정된key아래localStorage에 저장합니다. - 견고한 작동을 위해 오류 처리가 포함되어 있습니다.
- 이
- 반환 값: 훅은
[storedValue, setStoredValue]를 반환하여useState의 API를 모방합니다.
useLocalStorage 적용
사용자가 다크 모드 기본 설정을 전환할 수 있는 컴포넌트를 고려해 보세요.
import React from 'react'; import useLocalStorage from './useLocalStorage'; // 훅을 저장했다고 가정 function ThemeSwitcher() { // `isDarkMode` 상태를 관리하기 위해 사용자 지정 훅 사용, // localStorage에 없으면 기본값은 false. const [isDarkMode, setIsDarkMode] = useLocalStorage('isDarkMode', false); const toggleDarkMode = () => { setIsDarkMode(!isDarkMode); // 일반적으로 body 또는 루트 요소에 CSS 클래스를 적용합니다. // document.body.classList.toggle('dark-mode', !isDarkMode); }; useEffect(() => { // 현재 다크 모드 상태에 따라 스타일 적용 document.body.style.backgroundColor = isDarkMode ? '#333' : '#FFF'; document.body.style.color = isDarkMode ? '#FFF' : '#333'; }, [isDarkMode]); return ( <div style={{ padding: '20px', border: '1px solid #ccc', margin: '20px' }}> <h2>테마 전환기</h2> <p>현재 테마: {isDarkMode ? '어두움' : '밝음'}</p> <button onClick={toggleDarkMode}> {isDarkMode ? '밝음' : '어두움'} 모드로 전환 </button> <p>페이지를 새로고침하여 상태가 영속되는 것을 확인하세요!</p> </div> ); } export default ThemeSwitcher;
useLocalStorage를 사용하면 useState를 사용하는 것처럼 간단하게 isDarkMode 상태를 관리할 수 있지만, 그 값은 브라우저 세션 간에 자동으로 유지됩니다.
결론
커스텀 React 훅은 애플리케이션 전반에 걸쳐 상태 관련 로직을 추상화, 재사용 및 구성하는 우아하고 강력한 메커니즘을 제공합니다. 디바운싱 제어 흐름 및 상태 영속화와 같은 핵심 패턴을 이해함으로써 개발자는 더 효율적이고 견고하며 유지보수 가능한 React 애플리케이션을 구축할 수 있습니다. useDebounce 및 useLocalStorage 예시는 이러한 패턴의 실용적인 적용을 보여주며 일반적인 개발 문제를 크게 단순화합니다. 커스텀 훅을 채택하면 더 깨끗한 컴포넌트, 향상된 재사용성 및 더 즐거운 개발자 경험으로 이어집니다.

