ReactReact 기초강의 10개 중 8번째난이도 기초

커스텀 훅 — 로직 재사용과 추상화

발행 읽는 시간 약 1 분

  • React
  • 커스텀훅
  • useLocalStorage
  • useDebounce
  • 재사용

강의 소개

반복되는 로직을 커스텀 훅으로 추출해 재사용하는 방법을 배웁니다. useLocalStorage, useDebounce, useFetch 등 실용적인 커스텀 훅을 직접 만들어봅니다.

커스텀 훅이란?#

use로 시작하는 함수로, React 훅을 내부에서 사용하는 재사용 가능한 로직 단위입니다.


useLocalStorage#

TSX
import { useState } from "react";

function useLocalStorage<T>(key: string, initialValue: T) {
    const [stored, setStored] = useState<T>(() => {
        try {
            const item = localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch {
            return initialValue;
        }
    });

    function setValue(value: T | ((prev: T) => T)) {
        const newValue = value instanceof Function ? value(stored) : value;
        setStored(newValue);
        localStorage.setItem(key, JSON.stringify(newValue));
    }

    function removeValue() {
        setStored(initialValue);
        localStorage.removeItem(key);
    }

    return [stored, setValue, removeValue] as const;
}

// 사용
function Settings() {
    const [theme, setTheme] = useLocalStorage("theme", "light");
    const [language, setLanguage] = useLocalStorage("lang", "ko");

    return (
        <div>
            <button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>
                테마: {theme}
            </button>
        </div>
    );
}

useDebounce#

입력값이 멈춘 후 일정 시간이 지나야 값이 업데이트됩니다.

TSX
import { useState, useEffect } from "react";

function useDebounce<T>(value: T, delay: number = 500): T {
    const [debounced, setDebounced] = useState(value);

    useEffect(() => {
        const timer = setTimeout(() => setDebounced(value), delay);
        return () => clearTimeout(timer);
    }, [value, delay]);

    return debounced;
}

// 사용: 검색 API를 타이핑마다 호출하지 않고 멈추면 호출
function SearchBox() {
    const [query, setQuery] = useState("");
    const debouncedQuery = useDebounce(query, 300);

    useEffect(() => {
        if (debouncedQuery) {
            fetch(`/api/search?q=${debouncedQuery}`);
        }
    }, [debouncedQuery]);

    return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

useFetch#

TSX
interface FetchState<T> {
    data: T | null;
    loading: boolean;
    error: Error | null;
}

function useFetch<T>(url: string) {
    const [state, setState] = useState<FetchState<T>>({
        data: null, loading: true, error: null,
    });

    useEffect(() => {
        let cancelled = false;

        setState({ data: null, loading: true, error: null });

        fetch(url)
            .then(r => {
                if (!r.ok) throw new Error(`HTTP ${r.status}`);
                return r.json() as Promise<T>;
            })
            .then(data => { if (!cancelled) setState({ data, loading: false, error: null }); })
            .catch(error => { if (!cancelled) setState({ data: null, loading: false, error }); });

        return () => { cancelled = true; };
    }, [url]);

    return state;
}

// 사용
function UserList() {
    const { data: users, loading, error } = useFetch<User[]>("/api/users");

    if (loading) return <Spinner />;
    if (error) return <ErrorMessage error={error} />;
    return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

useToggle#

TSX
function useToggle(initial = false) {
    const [value, setValue] = useState(initial);
    const toggle = useCallback(() => setValue(v => !v), []);
    const setTrue = useCallback(() => setValue(true), []);
    const setFalse = useCallback(() => setValue(false), []);
    return [value, toggle, setTrue, setFalse] as const;
}

// 사용
function Modal() {
    const [isOpen, toggleModal, openModal, closeModal] = useToggle();

    return (
        <>
            <button onClick={openModal}>열기</button>
            {isOpen && (
                <div className="modal">
                    <button onClick={closeModal}>닫기</button>
                </div>
            )}
        </>
    );
}

useWindowSize#

TSX
function useWindowSize() {
    const [size, setSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight,
    });

    useEffect(() => {
        const handler = () => setSize({
            width: window.innerWidth,
            height: window.innerHeight,
        });
        window.addEventListener("resize", handler);
        return () => window.removeEventListener("resize", handler);
    }, []);

    return size;
}

// 사용
function ResponsiveComponent() {
    const { width } = useWindowSize();
    return <div>{width < 768 ? <MobileView /> : <DesktopView />}</div>;
}

커스텀 훅 설계 원칙#

Diagram · Mermaid

정리#

커스텀 훅을 만들 때:

  1. 컴포넌트 여러 곳에서 같은 로직이 반복되면 훅으로 추출
  2. use 접두사 필수 (React 훅 규칙 적용)
  3. 상태와 로직만 반환, UI는 컴포넌트가 담당

다음 편에서는 React Router — 싱글페이지 앱에서 페이지 라우팅을 구현하는 방법을 배웁니다.

Follow · Ask · Work

이 글이 도움이 됐다면

짧은 작업 기록과 데모는 인스타그램에 올립니다. 만들고 싶은 것이 있다면 프로젝트 의뢰나 협업 제안도 편하게 보내 주세요.

글쓴이

파란여우

직접 만든 AI 앱·제품의 개발·배포를 공개하는 실전형 테크 크리에이터

파란여우 소개 Instagram @bluefox._.hi

이어서 보기

만들고 싶은 것이 있다면 프로젝트 의뢰로 알려 주세요.