
Hook은 React 16.8에 새로 추가된 기능으로, class를 사용하지 않고도 상태 관리와 다양한 React의 기능들을 사용할 수 있게 해준다.
React는 처음에 클래스 컴포넌트를 사용했는데 몇 가지 불편한 점들이 있었다. 먼저 클래스 문법으로 인해 코드가 필요 이상으로 길어지고 복잡해져서 이해하기 어려웠다. 특히 this 키워드의 사용은 높은 러닝 커브가 되었다. 또한 자주 사용되는 로직이 있어도 컴포넌트 사이에서 재사용하기가 쉽지 않았다. 게다가 하나의 기능을 구현하는데 필요한 코드가 여러 곳에 흩어져 있어서 유지 보수할 때도 어려움이 많았다.
Hook이 등장하면서 이러한 문제들이 해결되었다. 아래 코드를 보면 같은 기능을 구현하더라도 Hook을 사용했을 때 얼마나 코드가 간단해지는지 확인할 수 있다.
// ✅ class 사용할 때
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return <div>{this.state.count}</div>;
}
}
// ✅ Hook 사용할 때
function Counter() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}
이처럼 Hook을 사용하면 원하는 기능을 만들어 여러 컴포넌트에서 쉽게 재사용할 수 있고, 관련된 로직을 한곳에 모아서 관리할 수 있게 되었다. 이러한 변화로 React 개발이 더 쉽고 직관적이며 효율적으로 바뀌었다.
반복문, 조건문, 중첩된 함수 내에서 Hook을 호출하면 안 된다.
// ⭕️ 올바른 사용
function Conuter() {
const [count, setCount] = useState(0);
useEffect(() => {
// 원하는 조건은 effect 내부에서 처리
if (count > 0) {
// ...
}
});
}
// ✅ 함수형 컴포넌트
function Component() {
const [state, setState] = useState();
return <div>{state}</div>;
}
// ✅ Custom Hook
function useCustomSize() {
const [size, setSize] = useState(getSize());
// ...
return size;
// ❌ 일반 함수
function generalFunction() {
const [state] = useState(); // 불가능
}
함수형 컴포넌트에서 상태를 관리하기 위한 가장 기본적인 Hook이다. 컴포넌트에서 변경 가능한 데이터를 다룰 때 사용하며, 상태가 변경되면 컴포넌트가 다시 렌더링 된다. 즉, 값이 변경되었을 때 컴포넌트 리렌더링 시킨다.
const Counter = () => {
const [count, setCount] = useState(0); // 초기값 설정: 0
/* 💡 count
1) 상태(state) 값 자체를 담는 변수.
2) 현재는 0이라는 초기값을 가지고 있다.
3) 이 값을 직접 수정하는 것은 불가능. (count = 1 ❌)
*/
/* 💡 setCount
1) count 값을 변경할 수 있는 함수.
2) React에게 상태를 업데이트하라고 알려주는 역할.
3) 이 함수를 통해서만 count 값을 변경 가능.
*/
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
// 📍 직접 값 설정
setCount(5); // 이전 값과 무관하게 상태를 특정 값으로 설정할 때
// 📍 함수형 업데이트
setCount(prevCount => prevCount + 1); // 이전 상태를 기반으로 계산이 필요할 때
직접 값을 설정하는 방식으로 상태를 두 번 업데이트하면 마지막 호출만 적용되어 한 번만 증가하지만, 함수형 업데이트를 사용하면 이전 상태를 정확히 참조하여 원하는 만큼 증가시킬 수 있다.
const [user, setUser] = useState({ name: '별이', age: 100, role: 'admin' });
// ⭕️ 올바른 방법: 새로운 객체 생성
setUser(prev => ({ ...prev, age: prev.age + 1, role: 'user' }));
// ❌ 잘못된 방법: 직접 수정
user.age = user.age + 1;
user.role = 'user';
함수형 컴포넌트에서 특정 DOM에 직접 접근하고 조작할 수 있게 해주는 Hook이다. useRef가 반환하는 ref 객체의 .current 값은 우리가 원하는 DOM을 가리키게 된다.
const Exam = () => {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus(); // DOM에 직접 접근하여 포커스
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleFocus}>focus</button>
</div>
);
}
useState와 useRef 모두 컴포넌트 내부에서 변수를 관리할 수 있지만, 동작 방식에 중요한 차이가 있다.
🔎 일반 변수: 값을 저장할 수는 있지만, 리렌더링 되면 초깃값으로 돌아감
🔎 useRef: 값을 저장할 수 있고, 리렌더링 되어도 값이 유지됨 + 값이 변경되어도 리렌더링 안됨
🔎 useState: 값을 저장할 수 있고, 리렌더링 되어도 값이 유지됨 + 값이 변경되면 리렌더링 됨
세 가지 모두 "값을 저장"할 수는 있지만, 그 값이 유지되는 방식과 리렌더링 여부가 다르다.
const Exam = () => {
const [count, setCount] = useState(0); // 값이 변경되면 리렌더링
const countRef = useRef(0); // 값이 변경되어도 리렌더링 없음
const handleClick = () => {
setCount(count + 1); // 화면 업데이트 발생
countRef.current += 1; // 화면 업데이트 발생하지 않음
};
return (
<div>
<p>State: {count}</p>
<p>Ref: {countRef.current}</p>
<button onClick={handleClick}>증가</button>
</div>
);
}
컴포넌트의 생명주기와 관련된 부수 효과(Side Effect)를 처리하기 위한 Hook으로, 컴포넌트가 렌더링 될 때 특정 작업을 수행할 수 있다. 클래스형 컴포넌트의 componentDidMount, componentDidupdate, componentWillUnmount가 합쳐진 것이라고 한다.
const Timer = () => {
const [count, setCount] = useState(0);
useEffect(() => {
// 마운트 시 실행될 코드
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
// 클린업 함수: 언마운트 시 실행
return () => clearInterval(timer);
}, []); // 의존성 배열이 비어있으면 마운트/언마운트 시에만 실행
}
의존성 배열을 어떻게 설정하느냐에 따라 effect가 실행되는 시점이 달라진다.
// 📍 배열 생략 - 매 렌더링마다 실행
useEffect(() => {
document.title = `Count: ${count}`;
}); // 컴포넌트가 리렌더링될 때마다 실행
// 📍 빈 배열 - 마운트/언마운트 시에만 실행
useEffect(() => {
const subscription = api.subscribe(); // 구독 설정
return () => subscription.unsubscribe(); // 구독 해제 (클린업 함수)
}, []); // 컴포넌트가 처음 나타날 때 한 번만 실행
// 📍 의존성 포함 - 특정 값이 변경될 때만 실행
useEffect(() => {
console.log('count가 변경됨:', count);
}, [count]); // count 값이 변경될 때만 실행
useEffect 내에서 반환하는 함수를 클린업 함수라고 하며, 이는 컴포넌트가 언마운트 되거나 다음 effect가 실행되기 전에 호출된다. 클린업 함수는 메모리 누수를 방지하고 불필요한 동작을 정리하는 중요한 역할을 한다.
useEffect(() => {
// 이벤트 리스너 등록
const handleScroll = () => {
console.log(window.scrollY);
};
window.addEventListener('scroll', handleScroll);
// 클린업 함수: 이벤트 리스너 제거
// 이것을 하지 않으면 컴포넌트가 언마운트된 후에도
// 이벤트 리스너가 남아있어 메모리 누수가 발생할 수 있다.
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
함수형 컴포넌트 내부에서 발생하는 무거운 연산을 최적화하기 위한 Hook이다. 결괏값을 메모이제이션(캐싱)하여 특정 값의 변경이 있을 때만 계산을 수행하고, 그렇지 않은 경우에는 이전에 저장된 결괏값을 재사용한다. 이는 특히 복잡한 계산이나 데이터 처리가 필요한 상황에서 성능 향상에 도움이 된다.
const ProductList = ({ products, minPrice, maxPrice }) => {
const filteredProducts = useMemo(() => {
// 가격 범위에 맞는 상품만 필터링
return products.filter(product =>
product.price >= minPrice &&
product.price <= maxPrice
);
}, [products, minPrice, maxPrice]); // 가격 범위나 상품 목록이 변경될 때만 재계산
return (
{filteredProducts.map(product => (
<li key={product.id}>
{product.name} - {product.price}원
</li>
))}
);
}
메모리를 사용하여 값을 저장하는 방식이기 때문에 무분별한 사용은 오히려 성능을 저하시킬 수 있다. 그래서 다음과 같은 경우에 사용하는 것이 좋다.
const ShoppingCart = ({ items }) => {
// ⭕️ 복잡한 계산이 필요한 경우 (할인율 계산, 총액 계산 등)
const totalPrice = useMemo(() => {
return items.reduce((sum, item) => {
const discount = item.quantity >= 2 ? 0.1 : 0; // 2개 이상 구매시 10% 할인
return sum + (item.price * item.quantity * (1 - discount));
}, 0);
}, [items]);
// ❌ 간단한 계산의 경우 불필요 (메모리만 차지하게 됨)
const itemCount = useMemo(() => {
return items.length;
}, [items]);
}
메모이제이션은 이전에 계산한 값을 다시 사용하는 것이다. 컴포넌트가 리렌더링될 때마다 새로운 객체가 생성되는 것을 방지할 수 있다. 특히 자식 컴포넌트에 객체를 Props로 전달할 때 유용하다.
const SearchFilter = () => {
// ❌ 렌더링될 때마다 새로운 객체가 계속 생성됨
const filters = {
category: '전체',
minPrice: 1000,
maxPrice: 50000,
inStock: true
};
// ⭕️ useMomo로 감싸서 항상 같은 객체를 재사용
const memoFilters = useMemo() => ({
category: '전체',
minPrice: 1000,
maxPrice: 50000,
inStock: true
}), []); // 빈 배열이므로 컴포넌트가 처음 만들어질 때 한 번만 생성
}
☝🏻 계산 비용이 높은 연산에만 사용 (복잡한 계산, 큰 데이터 처리)
✌🏻 참조 동일성이 중요한 경우에만 사용 (자식 컴포넌트의 props로 전달될 때)
🤟🏻 같은 입력값에 대해 항상 같은 결과를 반환하는 순수 계산인 경우에만 사용
특정 함수를 메모이제이션하기 위한 Hook이다. 컴포넌트가 리렌더링될 때마다 함수가 새로 생성되는 것을 방지하고, 동일한 함수 인스턴스를 재사용할 수 있게 해준다.
const TodoList = () => {
const [todos, setTodos] = useState([]);
// useCallback을 사용하여 함수를 메모이제이션
const addTodoMemo = useCallback((text) => {
setTodos(prev => [...prev, { id: Date.now(), text }]);
}, []);
};
useCallback도 useMemo처럼 함수를 저장하는 방식이기에 특별한 경우에만 사용해야 한다. 다음과 같은 경우에 사용하는 것이 좋다.
// 📍 자식 컴포넌트에 props로 전달되는 콜백 함수
const handleClick = useCallback(() => {
console.log('버튼 클릭!');
}, []);
// 📍 useEffect의 의존성 배열에 포함되는 함수
const fetchData = useCallback(async () => {
const res = await api.getData();
// ... 데이터 처리
},[/* 의존성 */]);
useEffect(() => {
fetchData();
}, [fetchData]);
☝🏻 모든 함수에 useCallback을 사용하는 것은 오히려 성능을 저하시킬 수 있다.
✌🏻 함수가 자주 변경되어야 하는 경우에는 useCallback 사용을 피하는 것이 좋다.
🤟🏻 의존성 배열을 올바르게 설정하지 않으면 버그가 발생할 수 있다.
서로 비슷하지만 useMemo는 값을 재사용하고, useCallback은 함수를 재사용한다. 두 Hook 모두 메모이제이션을 위해 사용되지만 그 목적과 용도가 다르다.
// 📍 useMemo - 계산된 값을 메모이제이션
const value = useMemo(() => sum(a, b), [a, b]);
// sum(a, b)의 결과값을 저장하고 재사용
// 📍 useCallback - 함수 자체를 메모이제이션
const handler = useCallback(() => {
console.log(a, b);
}, [a, b]);
// 함수 자체를 저장하고 재사용
// 💡 두 코드는 동일한 역할을 함
useMemo(() => fn, deps);
useCallback(fn, deps);
기술적으로 동일하지만 코드의 의도를 명확히 전달하기 위해 각각의 용도에 맞게 값을 메모이제이션할 때는 useMemo를, 함수를 메모이제이션할 때는 UseCallback을 사용하는 것이 좋다.