
1. 훅(Hooks)란 무엇일까?
React에서 훅(Hooks)은 함수형 컴포넌트에서 React의 기능(상태 관리, 생명 주기 관리 등)을 사용할 수 있게 해주는 도구입니다.
원래는 클래스형 컴포넌트에서만 상태를 다루거나 생명 주기를 관리할 수 있었는데, 훅 덕분에 함수형 컴포넌트에서도 이런 기능을 사용할 수 있게 되었습니다.
훅의 장점
- 더 간결한 코드 작성 가능.
- 재사용성과 가독성 향상.
- 클래스 없이 상태나 생명 주기 같은 React 기능을 사용 가능.
2. 자주 사용하는 기본 훅들
React에서 제공하는 몇 가지 기본 훅이 있습니다.
2.1. useState
컴포넌트에서 상태를 관리하기 위한 훅입니다.
- 상태(state)는 UI가 기억해야 할 값이라고 생각하면 됍니다.
예를 들어, 버튼을 클릭했을 때 숫자를 증가시키고 싶다면 숫자 상태를 저장해야겠죠?
import React, { useState } from "react";
function Counter() {
// useState(초기값): 상태와 상태를 업데이트하는 함수를 반환합니다.
const [count, setCount] = useState(0);
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
export default Counter;
어떻게 동작하냐면:
- useState(0)은 초기값 0을 가진 상태 count를 만듭니다.
- setCount를 호출하면 상태가 변경되고, React가 화면을 다시 렌더링합니다.
2.2. useEffect
컴포넌트가 렌더링되거나 상태가 변할 때 실행할 작업을 정의하는 훅입니다.
- 예를 들어, 컴포넌트가 처음 화면에 나타났을 때 데이터를 가져오거나, 컴포넌트가 사라질 때 정리(clean-up) 작업을 하고 싶을 때 사용합니다.
import React, { useState, useEffect } from "react";
function Timer() {
const [seconds, setSeconds] = useState(0);
// useEffect: 컴포넌트가 렌더링될 때마다 실행
useEffect(() => {
const interval = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
// 정리 작업(clean-up): 컴포넌트가 사라질 때 실행
return () => clearInterval(interval);
}, []); // []는 의존성 배열로, 특정 값이 변경될 때만 실행되게 함.
return <p>타이머: {seconds}초</p>;
}
export default Timer;
어떻게 동작하냐면:
- 컴포넌트가 처음 렌더링될 때 useEffect 내부 코드가 실행됍니다.
- 컴포넌트가 사라질 때(unmount), 정리 작업이 실행됍니다(return 함수).
2.3. useRef
DOM 요소나 값의 참조를 저장하기 위한 훅입니다.
- 상태와 달리 값이 바뀌어도 리렌더링되지 않아. 예를 들어, 입력 필드에 포커스를 주거나 특정 값을 저장해야 할 때 유용합니다.
import React, { useRef } from "react";
function InputFocus() {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus(); // inputRef가 연결된 input에 포커스
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleFocus}>포커스 주기</button>
</div>
);
}
export default InputFocus;
어떻게 동작하냐면:
- useRef는 inputRef라는 참조 객체를 만듭니다.
- ref 속성을 통해 input과 연결돼, 이후 inputRef.current로 실제 DOM 요소에 접근할 수 있습니다.
2.4 useContext: 전역 데이터 공유
useContext는 컴포넌트 트리에서 데이터를 자식 컴포넌트로 쉽게 전달할 수 있도록 해줍니다.
props를 계속 전달하지 않아도 됍니다!
import React, { createContext, useContext } from "react";
// 1. Context 생성
const ThemeContext = createContext();
function App() {
return (
// 2. Context Provider로 데이터 제공
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
// 3. Context 데이터 사용
const theme = useContext(ThemeContext);
return <button style={{ background: theme === "dark" ? "#333" : "#ccc", color: "#fff" }}>Theme: {theme}</button>;
}
export default App;
- ThemeContext.Provider를 통해 value="dark"라는 데이터를 제공.
- useContext(ThemeContext)를 사용해 현재 테마를 쉽게 가져올 수 있습니다.
2.5 useReducer: 복잡한 상태 로직 처리
useReducer는 useState보다 복잡한 상태 관리 로직을 처리할 때 유용합니다.
예를 들어, 여러 상태를 한 곳에서 관리하거나 상태 변경 로직이 복잡할 때 좋습니다.
import React, { useReducer } from "react";
// 초기 상태
const initialState = { count: 0 };
// 리듀서 함수: 상태를 업데이트하는 로직 정의
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
export default Counter;
dispatch({ type: "increment" })처럼 액션을 보내면 reducer 함수가 실행되고 상태가 업데이트됍니다.
2.6 useMemo: 계산 결과 캐싱
useMemo는 성능 최적화를 위해 특정 값이 변하지 않으면 이전에 계산한 결과를 재사용합니다.
비싼 계산(시간이 오래 걸리는 작업)을 반복하지 않도록 도와줍니다.
import React, { useState, useMemo } from "react";
function ExpensiveCalculation(num) {
console.log("Expensive calculation...");
return num * 2;
}
function App() {
const [count, setCount] = useState(0);
const [input, setInput] = useState("");
// useMemo로 계산 결과 캐싱
const doubled = useMemo(() => ExpensiveCalculation(count), [count]);
return (
<div>
<p>Doubled Count: {doubled}</p>
<button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</div>
);
}
export default App;
count가 바뀌지 않으면 ExpensiveCalculation이 다시 실행되지 않아, 성능이 향상됍니다.
2.7 useCallback: 함수 재생성 방지
useCallback은 특정 의존성이 변경될 때만 함수를 새로 생성합니다.
컴포넌트가 자주 리렌더링되는 경우 성능 최적화에 유용합니다.
import React, { useState, useCallback } from "react";
function Child({ onClick }) {
console.log("Child rendered");
return <button onClick={onClick}>Click me</button>;
}
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// useCallback으로 함수 재생성을 방지
const handleClick = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<Child onClick={handleClick} />
<input value={text} onChange={(e) => setText(e.target.value)} />
</div>
);
}
export default App;
- useCallback을 사용하면 Child 컴포넌트가 불필요하게 리렌더링되지 않습니다.
- 의존성 배열([])에 따라 함수가 재생성되지 않고 캐싱됍니다.
3. 훅의 규칙
React 훅은 몇 가지 규칙을 따라야 합니다
- 훅은 함수 컴포넌트 또는 커스텀 훅 내부에서만 사용해야 합니다.
- 예: useState를 컴포넌트 함수 밖에서 호출하면 안 됍니다.
- 훅은 항상 동일한 순서로 호출해야 합니다.
- 조건문이나 반복문 안에서 호출하면 안 됍니다.
4. 훅을 사용하는 이유
- 코드 재사용성이 좋아집니다.
- 클래스 컴포넌트에서는 로직을 재사용하기 어려웠지만, 훅은 커스텀 훅을 만들어 쉽게 로직을 재사용할 수 있습니다.
- 더 간결하고 읽기 쉬운 코드.
- 클래스 컴포넌트의 복잡한 생명 주기 메서드 대신, useEffect 하나로 간단히 처리 가능합니다.
5. 훅 주요 기능 및 간단 예시
useState | 컴포넌트에서 상태를 관리 (ex. 숫자, 문자열, 배열 등). |
useEffect | 컴포넌트의 생명주기(렌더링, 업데이트, 언마운트)에 따라 작업 수행. |
useRef | DOM 요소나 값의 참조를 저장, 값이 변경되어도 리렌더링되지 않음. |
useContext | 컴포넌트 트리에서 데이터를 전역적으로 쉽게 공유. |
useReducer | 복잡한 상태 로직을 처리하고, 여러 상태를 한 곳에서 관리. |
useMemo | 계산 결과를 캐싱해 불필요한 재계산 방지 (성능 최적화). |
useCallback | 함수의 재생성을 방지해 컴포넌트의 불필요한 리렌더링 방지. |
간단하게 생각해보자면은
- useState: 클릭 카운트 상태 관리
- useEffect: API 호출 및 컴포넌트가 사라질 때 정리 작업
- useRef: 입력 필드 포커스
- useContext: 다크 모드 상태 공유
- useReducer: 카운터 증감 로직 통합
- useMemo: 계산된 값 캐싱 (ex. 배열 정렬)
- useCallback: 클릭 이벤트 핸들러 캐싱