React hooks를 설명하기 앞서 리액트 생애주기(React Lifecycle)를 알아야 한다.
리액트 생애주기(React Lifecycle)는 리액트 컴포넌트가 생성되고, 업데이트되고, 삭제되는 일련의 과정을 의미한다. 이 생애주기는 클래스형 컴포넌트와 함수형 컴포넌트에 따라 다르다. 리액트 함수형 컴포넌트가 등장하기 이전엔 클래스형 컴포넌트만 사용했는데, 클래스형 컴포넌트에서는 컴포넌트의 생애주기를 다양한 메소드로 제어할 수 있었다.
주요 생애주기 메소드는 다음과 같다.
Mounting은 사전적의미로 오르다, 설치하다의 의미를 갖고있다.
constructor: 컴포넌트가 생성될 때 호출되며 초기 상태를 설정할 수 있다.
static getDerivedStateFromProps: 부모 컴포넌트로부터 받은 props를 기반으로 상태를 업데이트할 때 사용된다.
render: 컴포넌트의 UI를 렌더링한다.
componentDidMount: 컴포넌트가 DOM에 마운트된 후 호출된다. 데이터 요청과 같은 부수 효과를 수행할 때 사용된다.
static getDerivedStateFromProps: 새로운 props가 들어올 때 호출된다.
shouldComponentUpdate: 컴포넌트가 다시 렌더링될지 결정하는 메소드.
render: 컴포넌트의 UI를 렌더링한다.
getSnapshotBeforeUpdate: DOM이 업데이트되기 직전에 호출되어 변경 전의 스냅샷을 캡처한다.
componentDidUpdate: 컴포넌트가 업데이트된 후 호출된다. 데이터 요청이나 DOM 업데이트와 같은 작업을 수행할 수 있다.
componentWillUnmount: 컴포넌트가 DOM에서 제거되기 전에 호출된다. 타이머 정리, 구독 해제 등 정리 작업을 수행한다.
함수형 컴포넌트는 이러한 생애주기 메소드를 사용할 수 없지만, 다양한 React Hooks를 통해 생애주기와 유사한 기능을 제공한다. Hooks는 useState, useEffect, useMemo, useCallback, useReducer 등이 있다.
useEffect는 컴포넌트가 렌더링된 후에 실행되며 부수 효과(side effects)를 다루기 위해 사용된다. 즉 의존성 배열 안에 값이 바뀌면, useEffect안의 동작을 자동으로 실행하기 위해 사용한다.
Side effects(부수 효과)
함수나 컴포넌트가 자신의 주요 목적 외에 수행하는 모든 작업을 말한다. React 컴포넌트의 맥락에서, 주요 목적은 UI를 렌더링하는 것이다. 그 외의 작업들이 side effects에 해당한다.
useEffect의 의존성 배열 자리에 값을 비워두면 컴포넌트가 랜더링될때마다 useEffect 안에 구문이 실행된다.
useEffect(() => {
document.title = `You clicked ${count} times`;
},);
useEffect의 의존성 배열 자리에 빈 배열을 쓰면, 컴포넌트가 최초 랜더링될때만 useEffect 안에 구문이 실행된다.
useEffect(() => {
document.title = `You clicked ${count} times`;
},);
useEffect의 의존성 배열 자리에 원하는 '값'을 넣어두면 해당 값이 변경될때마다 useEffect 안에 구문이 실행된다. 아래 예시에서는 count가 변경될 때마다 문서의 제목을 업데이트하는 side effect를 수행한다.
import React, { useState, useEffect } from 'react';
function ExampleComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// 부수 효과: 문서의 제목을 업데이트
document.title = `You clicked ${count} times`;
// 정리 함수: 컴포넌트 언마운트(제거 직전) 시 실행
return () => cleanup();
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect는 정리(cleanup) 함수를 반환할 수 있으며, 이는 컴포넌트가 언마운트될 때 실행된다.
useMemo(() => computeExpensiveValue(a, b), [a, b])
의존성 배열의 '값'이 바뀌면 실행하도록 해야할 때 사용한다. useMemo는 메모리에 값을 캐싱해두는 것이기 때문에 남용하게되면 메모리에 저장해야할 값이 많아져서 컴퓨터의 퍼포먼스가 낮아지게 됨으로 조심해야 한다. 되도록이면 꼭 필요할 때만 사용해야 한다.
아래 예시에서는 a나 b가 변경될 때만 재계산되며, 그 외의 경우에는 이전에 계산된 값을 재사용한다.
import React, { useMemo } from 'react';
function ExpensiveComputation({ a, b }) {
const result = useMemo(() => {
console.log('Computing...');
return a * b * 1000000000; // 복잡한 계산을 가정
}, [a, b]);
return <div>Result: {result}</div>;
}
useCallback(() => doSomething(a, b), [a, b])
useCallback은 함수를 메모이제이션한다.
메모이제이션(Memoization)
메모이제이션은 비용이 많이 드는 함수 호출의 '결과를 저장'하고, 동일한 입력이 다시 발생할 때 저장된 결과를 반환하는 기술이다.
의존성 배열의 값이 변경되지 않는 한 동일한 함수 인스턴스를 재사용한다. 예시에서 incrementCount 함수는 컴포넌트가 리렌더링되어도 재생성되지 않는다. useCallback은 주로 자식 컴포넌트에 props로 함수를 전달할 때 사용되어, 불필요한 렌더링을 방지한다.
import React, { useState, useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const incrementCount = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);
return (
<div>
<ChildComponent onClick={incrementCount} />
<p>Count: {count}</p>
</div>
);
}
function ChildComponent({ onClick }) {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Increment</button>;
}
| 특징 | useEffect | useMemo | useCallback |
|---|---|---|---|
| 목적 | 부수 효과 처리 (side effects) | 값의 메모이제이션 (복잡한 계산 결과 저장) | 함수의 메모이제이션 (불필요한 함수 재생성 방지) |
| 실행 시점 | 렌더링 후, 의존성 배열의 값이 변경될 때 실행 (비동기적) | 렌더링 중, 의존성 배열의 값이 변경될 때 계산 (동기적) | 렌더링 중, 의존성 배열의 값이 변경될 때 함수 생성 (동기적) |
| 정리 함수(clean-up) | 정리 함수를 반환할 수 있음 | 정리 함수 없음 | 정리 함수 없음 |
| 주로 사용 | 데이터 fetching, 타이머 설정, 구독 설정 등 | 복잡한 계산 결과 저장 및 성능 최적화 | 불필요한 자식 컴포넌트 리렌더링 방지 |
두 훅은 의존성 변경 시 계산/함수 재생성, 그 외에는 이전 값 재사용한다는 공통점이 있지만 중요한 차이가 있다. useMemo는 반환된 값을 직접 사용하지만, useCallback은 반환된 함수를 호출하여 사용한다.