리액트(React)를 처음 배울 때도, 한동안 안 쓰다가 다시 잡을 때도, 실무에서 막히는 순간에도 항상 필요한 것이 있습니다. 빠르게 찾아볼 수 있는 핵심 문법 레퍼런스입니다. 리액트 문법 정리는 공식 문서처럼 방대하지 않으면서도, 실제 프로젝트에서 80% 이상의 상황을 커버하는 핵심 패턴들을 한 곳에 담아야 제 역할을 합니다. 오늘은 JSX 기본 규칙부터 Hook 사용법, 조건부 렌더링, 이벤트 처리, 커스텀 Hook, Context API까지 실무 코드 예시와 함께 완전히 정리해 드립니다. 북마크 해두고 필요할 때마다 꺼내 쓰는 리액트 치트시트로 활용해 보세요.
목차
- JSX 핵심 문법 규칙 — 리액트의 언어를 이해한다
- 컴포넌트 정의와 Props — 리액트의 기본 단위
- 핵심 Hook 완전 정리 — useState·useEffect·useRef·useMemo·useCallback
- 조건부 렌더링과 리스트 렌더링 — 실무 필수 패턴
- 이벤트 처리와 폼 관리 — 사용자 입력 다루기
- Context API와 커스텀 Hook — 상태 공유와 로직 재사용
1. JSX 핵심 문법 규칙 — 리액트의 언어를 이해한다
JSX(JavaScript XML)는 자바스크립트 안에서 HTML과 유사한 문법으로 UI를 표현하는 리액트의 핵심 문법입니다. 브라우저는 JSX를 직접 이해하지 못하므로 Babel이 이를 React.createElement() 호출로 변환합니다. JSX는 HTML과 비슷하지만 다른 규칙이 있어 처음 배울 때 자주 실수하는 지점이 있습니다.
규칙 1. 반드시 하나의 루트 엘리먼트로 감싸야 한다
JSX는 항상 하나의 부모 요소로 감싸야 합니다. 여러 요소를 나란히 반환하면 에러가 발생합니다. 불필요한 div 추가를 피하려면 빈 태그(<>...</>) 또는 React.Fragment를 사용합니다.
jsx
// ❌ 잘못된 예시 — 여러 루트 요소
function Bad() {
return (
<h1>제목</h1>
<p>내용</p>
);
}
// ✅ 올바른 예시 — Fragment로 감싸기
function Good() {
return (
<>
<h1>제목</h1>
<p>내용</p>
</>
);
}
규칙 2. 모든 태그는 반드시 닫아야 한다
HTML에서는 <br>, <img>, <input> 같은 태그를 닫지 않아도 되지만, JSX에서는 반드시 자기 닫는 태그(/>를 포함)로 작성해야 합니다.
jsx
// ❌ 잘못된 예시
<img src="photo.jpg">
<input type="text">
<br>
// ✅ 올바른 예시
<img src="photo.jpg" />
<input type="text" />
<br />
규칙 3. 속성명은 camelCase로 작성한다
HTML의 class는 JSX에서 className, for는 htmlFor, onclick은 onClick으로 작성합니다. HTML 속성명이 자바스크립트 예약어와 충돌하거나 관례가 다르기 때문입니다.
jsx
// ❌ 잘못된 예시
<div class="container" onclick={handleClick}>
<label for="email">이메일</label>
</div>
// ✅ 올바른 예시
<div className="container" onClick={handleClick}>
<label htmlFor="email">이메일</label>
</div>
규칙 4. 자바스크립트 표현식은 중괄호 {}로 감싼다
JSX 안에서 자바스크립트 변수·함수 호출·삼항 연산자 등의 표현식을 사용할 때는 중괄호로 감쌉니다. 단, if문·for문·switch문 같은 구문(Statement)은 직접 사용할 수 없습니다.
jsx
const name = "김리액트";
const isLoggedIn = true;
function Greeting() {
return (
<div>
{/* 변수 사용 */}
<h1>안녕하세요, {name}님!</h1>
{/* 함수 호출 */}
<p>현재 시간: {new Date().toLocaleTimeString()}</p>
{/* 삼항 연산자 */}
<span>{isLoggedIn ? "로그인 중" : "로그아웃"}</span>
</div>
);
}
규칙 5. 인라인 스타일은 객체로 전달한다
JSX에서 인라인 스타일은 문자열이 아닌 자바스크립트 객체로 전달하며, CSS 속성명은 camelCase로 작성합니다.
jsx
// ❌ HTML 방식 — JSX에서 작동 안 함
<div style="color: red; font-size: 16px;">텍스트</div>
// ✅ 올바른 JSX 방식
<div style={{ color: "red", fontSize: "16px" }}>텍스트</div>
// ✅ 스타일 객체 분리 (권장)
const titleStyle = {
color: "#333",
fontSize: "24px",
fontWeight: "bold",
};
<h1 style={titleStyle}>제목</h1>
2. 컴포넌트 정의와 Props — 리액트의 기본 단위
리액트에서 UI를 구성하는 기본 단위는 컴포넌트(Component) 입니다. 현재 리액트 실무에서는 함수형 컴포넌트가 표준이며, 클래스 컴포넌트는 레거시 코드베이스에서만 마주칩니다.
함수형 컴포넌트 정의 3가지 방법
jsx
// 방법 1. function 선언식 (호이스팅 가능, 가독성 좋음)
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
// 방법 2. 화살표 함수 + const (파일 내 재선언 방지)
const Card = ({ title, children }) => {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
};
// 방법 3. 화살표 함수 즉시 반환 (단일 JSX 반환 시 간결)
const Badge = ({ count }) => <span className="badge">{count}</span>;
Props 전달과 구조 분해 할당
Props는 부모 컴포넌트가 자식 컴포넌트에 데이터를 전달하는 단방향 채널입니다. 실무에서는 거의 항상 구조 분해 할당(Destructuring)으로 받아 사용합니다.
jsx
// 부모 컴포넌트
function Parent() {
return (
<UserCard
name="김리액트"
age={28}
isActive={true}
hobbies={["코딩", "독서"]}
onDelete={() => console.log("삭제")}
/>
);
}
// 자식 컴포넌트 — Props 구조 분해 할당
function UserCard({ name, age, isActive, hobbies, onDelete }) {
return (
<div>
<h3>{name} ({age}세)</h3>
<span>{isActive ? "활성" : "비활성"}</span>
<ul>
{hobbies.map((hobby, idx) => (
<li key={idx}>{hobby}</li>
))}
</ul>
<button onClick={onDelete}>삭제</button>
</div>
);
}
Props 기본값 설정 — defaultProps vs 기본 매개변수
jsx
// 방법 1. ES6 기본 매개변수 (권장 — 타입스크립트 친화적)
function Button({ label = "클릭", size = "md", disabled = false }) {
return (
<button
className={`btn btn-${size}`}
disabled={disabled}
>
{label}
</button>
);
}
// 방법 2. defaultProps (구버전 방식, 클래스 컴포넌트 잔재)
Button.defaultProps = {
label: "클릭",
size: "md",
disabled: false,
};
children Props — 컴포넌트 합성의 핵심
children은 컴포넌트 태그 사이에 넣은 내용을 전달받는 특수 prop입니다. 레이아웃 컴포넌트, 모달, 카드 컴포넌트 등에서 핵심적으로 활용됩니다.
jsx
// 레이아웃 컴포넌트
function Modal({ title, children, onClose }) {
return (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">
<h2>{title}</h2>
<button onClick={onClose}>✕</button>
</div>
<div className="modal-body">
{children} {/* 부모가 넣어준 내용 */}
</div>
</div>
</div>
);
}
// 사용 예시
function App() {
return (
<Modal title="알림" onClose={() => {}}>
<p>저장이 완료됐습니다.</p>
<button>확인</button>
</Modal>
);
}
3. 핵심 Hook 완전 정리 — useState·useEffect·useRef·useMemo·useCallback
Hook은 함수형 컴포넌트에서 상태·사이드 이펙트·최적화를 다루는 리액트의 핵심 API입니다. Hook 규칙은 단 두 가지입니다. 반드시 컴포넌트 최상단에서 호출할 것, 반드시 리액트 함수(컴포넌트 또는 커스텀 Hook) 안에서만 호출할 것입니다.
useState — 상태 관리의 기본
useState는 컴포넌트 내부 상태를 선언하고 업데이트하는 가장 기본적인 Hook입니다.
jsx
import { useState } from "react";
function Counter() {
// [현재 상태값, 상태 업데이트 함수] = useState(초기값)
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: "", email: "" });
// 단순 값 업데이트
const increment = () => setCount(count + 1);
// 이전 상태 기반 업데이트 (함수형 업데이트 — 비동기 안전)
const safeIncrement = () => setCount((prev) => prev + 1);
// 객체 상태 업데이트 — 반드시 스프레드로 나머지 유지
const updateName = (name) => {
setUser((prev) => ({ ...prev, name }));
};
return (
<div>
<p>카운트: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={safeIncrement}>안전한 +1</button>
</div>
);
}
useEffect — 사이드 이펙트 처리
useEffect는 API 호출·구독·타이머·DOM 조작 같은 사이드 이펙트를 처리합니다. 두 번째 인자인 의존성 배열이 핵심입니다.
jsx
import { useState, useEffect } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// 패턴 1. 마운트 시 1회 실행 (빈 의존성 배열)
useEffect(() => {
console.log("컴포넌트 마운트");
return () => console.log("컴포넌트 언마운트"); // 클린업
}, []);
// 패턴 2. 특정 값 변경 시 실행 (의존성 배열에 값 지정)
useEffect(() => {
if (!userId) return;
setLoading(true);
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => {
setUser(data);
setLoading(false);
});
}, [userId]); // userId 바뀔 때마다 재실행
// 패턴 3. 렌더링마다 실행 (의존성 배열 생략 — 거의 안 씀)
useEffect(() => {
document.title = `사용자: ${user?.name}`;
});
if (loading) return <p>로딩 중...</p>;
return <div>{user?.name}</div>;
}
useRef — DOM 접근과 렌더링 무관 값 저장
useRef는 두 가지 용도로 사용됩니다. DOM 요소에 직접 접근하거나, 렌더링을 트리거하지 않고 값을 저장할 때 사용합니다.
jsx
import { useRef, useEffect } from "react";
function InputFocus() {
const inputRef = useRef(null); // DOM 접근용
const renderCount = useRef(0); // 렌더링 횟수 추적 (리렌더 없이)
const timerRef = useRef(null); // 타이머 ID 저장
useEffect(() => {
inputRef.current.focus(); // 마운트 시 포커스
renderCount.current += 1;
console.log(`렌더링 횟수: ${renderCount.current}`);
});
const startTimer = () => {
timerRef.current = setInterval(() => {
console.log("타이머 실행 중");
}, 1000);
};
const stopTimer = () => {
clearInterval(timerRef.current); // 저장된 ID로 타이머 정리
};
return (
<div>
<input ref={inputRef} type="text" placeholder="자동 포커스" />
<button onClick={startTimer}>시작</button>
<button onClick={stopTimer}>중지</button>
</div>
);
}
useMemo와 useCallback — 성능 최적화 콤보
useMemo는 계산 비용이 큰 값을, useCallback은 함수를 메모이제이션합니다. 의존성이 바뀌지 않으면 이전 값·함수를 재사용하여 불필요한 재계산과 리렌더링을 방지합니다.
jsx
import { useState, useMemo, useCallback } from "react";
function ProductList({ products }) {
const [search, setSearch] = useState("");
const [sortOrder, setSortOrder] = useState("asc");
// useMemo — 무거운 필터링·정렬 연산을 메모이제이션
const filteredProducts = useMemo(() => {
console.log("필터링 실행"); // 의존성 바뀔 때만 실행
return products
.filter((p) => p.name.includes(search))
.sort((a, b) =>
sortOrder === "asc" ? a.price - b.price : b.price - a.price
);
}, [products, search, sortOrder]);
// useCallback — 자식 컴포넌트에 전달하는 함수 메모이제이션
const handleDelete = useCallback((id) => {
console.log(`삭제: ${id}`);
// 삭제 로직
}, []); // 의존성 없으면 항상 같은 함수 참조 반환
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="검색"
/>
<button onClick={() => setSortOrder((o) => o === "asc" ? "desc" : "asc")}>
정렬 전환
</button>
{filteredProducts.map((p) => (
<ProductItem key={p.id} product={p} onDelete={handleDelete} />
))}
</div>
);
}
useMemo vs useCallback 한 줄 정리:
useMemo(() => 계산값, [deps])→ 값 메모이제이션useCallback(() => 함수, [deps])→ 함수 메모이제이션
useReducer — 복잡한 상태 관리
상태 로직이 복잡하거나 다음 상태가 이전 상태에 의존하는 경우 useState 대신 useReducer를 사용합니다.
jsx
import { useReducer } from "react";
// 리듀서 함수 — 순수 함수여야 함
function cartReducer(state, action) {
switch (action.type) {
case "ADD_ITEM":
return { ...state, items: [...state.items, action.payload] };
case "REMOVE_ITEM":
return {
...state,
items: state.items.filter((item) => item.id !== action.payload),
};
case "CLEAR_CART":
return { ...state, items: [] };
default:
return state;
}
}
function Cart() {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const addItem = (item) => dispatch({ type: "ADD_ITEM", payload: item });
const removeItem = (id) => dispatch({ type: "REMOVE_ITEM", payload: id });
const clearCart = () => dispatch({ type: "CLEAR_CART" });
return (
<div>
<p>아이템 수: {state.items.length}</p>
<button onClick={() => addItem({ id: 1, name: "상품A" })}>추가</button>
<button onClick={clearCart}>비우기</button>
</div>
);
}
4. 조건부 렌더링과 리스트 렌더링 — 실무 필수 패턴
리액트에서 UI를 동적으로 보여주는 두 가지 핵심 패턴입니다. 이 두 가지를 자유자재로 다루면 대부분의 UI를 구현할 수 있습니다.
조건부 렌더링 5가지 패턴
jsx
function Dashboard({ user, isLoading, hasError, notifications }) {
// 패턴 1. if문으로 조기 반환 (Early Return) — 가장 명확
if (isLoading) return <Spinner />;
if (hasError) return <ErrorMessage />;
if (!user) return null; // 아무것도 렌더링하지 않음
return (
<div>
{/* 패턴 2. 삼항 연산자 — 둘 중 하나 선택 */}
{user.isAdmin ? <AdminPanel /> : <UserPanel />}
{/* 패턴 3. && 단락 평가 — 조건 만족 시만 렌더링 */}
{notifications.length > 0 && (
<NotificationBadge count={notifications.length} />
)}
{/* 패턴 4. || 단락 평가 — 기본값 처리 */}
<p>{user.nickname || "닉네임 없음"}</p>
{/* 패턴 5. 옵셔널 체이닝 — 중첩 객체 안전 접근 */}
<p>{user?.profile?.bio ?? "소개 없음"}</p>
</div>
);
}
리스트 렌더링과 key props
map()으로 배열을 JSX 배열로 변환하는 것이 리스트 렌더링의 핵심입니다. key prop은 리액트가 리스트 아이템을 추적하는 데 사용하므로 반드시 고유값을 지정해야 합니다.
jsx
function TodoList({ todos }) {
return (
<ul>
{/* ✅ id를 key로 사용 — 가장 안전 */}
{todos.map((todo) => (
<TodoItem
key={todo.id} // 고유 id 사용
todo={todo}
/>
))}
</ul>
);
}
// ❌ 인덱스를 key로 사용 — 아이템 추가·삭제·재정렬 시 버그 발생
{todos.map((todo, index) => (
<li key={index}>{todo.text}</li> // 절대 이렇게 하지 말 것
))}
// ✅ 고유 id가 없을 때 — crypto.randomUUID() 활용
const todosWithId = todos.map((todo) => ({
...todo,
id: crypto.randomUUID(),
}));
복합 조건부 + 리스트 렌더링 실무 패턴
jsx
function ProductGrid({ products, isLoading, error }) {
// 로딩 상태
if (isLoading) {
return (
<div className="grid">
{Array.from({ length: 6 }).map((_, i) => (
<SkeletonCard key={i} /> // 스켈레톤 UI
))}
</div>
);
}
// 에러 상태
if (error) return <ErrorMessage message={error.message} />;
// 빈 상태
if (products.length === 0) {
return <EmptyState message="상품이 없습니다" />;
}
// 정상 렌더링
return (
<div className="grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
5. 이벤트 처리와 폼 관리 — 사용자 입력 다루기
리액트의 이벤트 처리는 HTML과 비슷하지만 camelCase 이벤트명, 함수 참조 전달 등의 차이가 있습니다. 폼 관리는 실무에서 가장 자주 구현하는 UI 중 하나입니다.
이벤트 처리 핵심 패턴
jsx
function EventExamples() {
// ❌ 잘못된 패턴 — () 없이 함수 참조 전달해야 함
// <button onClick={handleClick()}> ← 렌더링 시 즉시 실행됨
// ✅ 올바른 패턴 — 함수 참조 전달
const handleClick = () => console.log("클릭");
// 인수 전달이 필요할 때 — 화살표 함수로 래핑
const handleDelete = (id) => console.log(`삭제: ${id}`);
// 이벤트 객체 접근
const handleChange = (e) => {
console.log(e.target.value); // 입력값
console.log(e.target.name); // input name 속성
};
// 이벤트 기본 동작 방지
const handleSubmit = (e) => {
e.preventDefault(); // 폼 새로고침 방지
console.log("제출");
};
// 이벤트 버블링 방지
const handleCardClick = (e) => {
e.stopPropagation(); // 부모로 이벤트 전파 차단
};
return (
<div onClick={() => console.log("부모 클릭")}>
<button onClick={handleClick}>클릭</button>
<button onClick={() => handleDelete(42)}>삭제</button>
<button onClick={handleCardClick}>버블링 차단</button>
</div>
);
}
제어 컴포넌트(Controlled Component)로 폼 관리
리액트에서 권장하는 폼 관리 방식은 useState로 폼 상태를 직접 관리하는 제어 컴포넌트입니다.
jsx
import { useState } from "react";
function SignupForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
role: "user",
agree: false,
});
const [errors, setErrors] = useState({});
// 단일 핸들러로 모든 input 처리
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData((prev) => ({
...prev,
[name]: type === "checkbox" ? checked : value,
}));
};
const validate = () => {
const newErrors = {};
if (!formData.name) newErrors.name = "이름을 입력해 주세요";
if (!formData.email.includes("@")) newErrors.email = "올바른 이메일 형식이 아닙니다";
if (formData.password.length < 8) newErrors.password = "비밀번호는 8자 이상이어야 합니다";
return newErrors;
};
const handleSubmit = (e) => {
e.preventDefault();
const newErrors = validate();
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
console.log("제출 데이터:", formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="이름"
/>
{errors.name && <span className="error">{errors.name}</span>}
</div>
<div>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="이메일"
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<select name="role" value={formData.role} onChange={handleChange}>
<option value="user">일반 사용자</option>
<option value="admin">관리자</option>
</select>
</div>
<div>
<label>
<input
name="agree"
type="checkbox"
checked={formData.agree}
onChange={handleChange}
/>
이용약관 동의
</label>
</div>
<button type="submit" disabled={!formData.agree}>
가입하기
</button>
</form>
);
}
6. Context API와 커스텀 Hook — 상태 공유와 로직 재사용
리액트에서 컴포넌트 트리 전체에 데이터를 공유하거나, 반복되는 로직을 재사용할 때 사용하는 고급 패턴입니다.
Context API — Props Drilling 없는 전역 상태
Context는 컴포넌트 트리 전체에 데이터를 공유하는 방법으로, 로그인 사용자 정보·테마·언어 설정처럼 앱 전반에서 필요한 데이터에 적합합니다.
jsx
import { createContext, useContext, useState } from "react";
// 1단계: Context 생성
const AuthContext = createContext(null);
// 2단계: Provider 컴포넌트 — 값을 공급
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (userData) => setUser(userData);
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// 3단계: 커스텀 Hook으로 사용 편의성 향상
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth는 AuthProvider 안에서만 사용 가능합니다");
}
return context;
}
// 4단계: 어디서든 사용
function Header() {
const { user, logout } = useAuth();
return (
<header>
{user ? (
<>
<span>{user.name}님 환영합니다</span>
<button onClick={logout}>로그아웃</button>
</>
) : (
<a href="/login">로그인</a>
)}
</header>
);
}
// 5단계: App 최상단에 Provider 감싸기
function App() {
return (
<AuthProvider>
<Header />
<Main />
</AuthProvider>
);
}
커스텀 Hook — 로직 재사용의 핵심
커스텀 Hook은 use로 시작하는 함수로, 여러 컴포넌트에서 반복되는 로직을 추출하여 재사용합니다. 상태·이벤트·사이드 이펙트를 포함할 수 있습니다.
jsx
// 커스텀 Hook 1. API 데이터 페칭
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController(); // 요청 취소용
fetch(url, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error("네트워크 오류");
return res.json();
})
.then(setData)
.catch((err) => {
if (err.name !== "AbortError") setError(err);
})
.finally(() => setLoading(false));
return () => controller.abort(); // 언마운트 시 요청 취소
}, [url]);
return { data, loading, error };
}
// 커스텀 Hook 2. 로컬스토리지 동기화
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
const setStoredValue = (newValue) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
};
return [value, setStoredValue];
}
// 커스텀 Hook 3. 디바운스
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 사용 예시 — 커스텀 Hook 조합
function SearchPage() {
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 400);
const { data, loading, error } = useFetch(
`/api/products?q=${debouncedSearch}`
);
const [recentSearches, setRecentSearches] = useLocalStorage(
"recentSearches",
[]
);
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="검색어 입력"
/>
{loading && <Spinner />}
{error && <ErrorMessage />}
{data?.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
결론
리액트 문법 정리의 핵심은 JSX 규칙·컴포넌트·Hook·조건부 렌더링·이벤트 처리·Context라는 여섯 가지 축을 완전히 자기 것으로 만드는 것입니다. 각 문법을 개별로 외우는 것보다 실제 프로젝트에서 하나씩 적용해 보는 것이 훨씬 빠른 습득 방법입니다. 특히 커스텀 Hook은 처음에는 낯설지만, 한 번 만들어보면 코드 재사용성과 가독성이 얼마나 높아지는지 체감하게 됩니다. 오늘 바로 useFetch, useLocalStorage, useDebounce 세 가지 커스텀 Hook을 직접 만들어 자신의 프로젝트에 적용해 보세요.
답글 남기기