[React] Hook 상태관리와 성능최적화
React는 선언적 UI 개발을 위한 강력한 도구를 제공하며, 그 중 핵심적인 역할을 하는 것이 바로 Hook입니다.
Hook은 상태와 사이드 이펙트를 관리하는 기능을 제공하며, 함수형 컴포넌트에서 이러한 기능을 사용할 수 있게 해줍니다.
1. Hook의 규칙
React에서 Hook을 사용할 때는 몇 가지 규칙이 있습니다. 이 규칙들을 잘 이해하고 따르면, React 애플리케이션을 더욱 효율적으로 작성할 수 있습니다.
1.1. 최상위 레벨에서만 호출
Hook은 컴포넌트 함수의 최상위 레벨에서만 호출해야 합니다. 조건문이나 반복문, 중첩된 함수 안에서는 호출할 수 없습니다. 이 규칙을 지키는 이유는 React가 Hook의 호출 순서를 추적하기 위함입니다.
1.2. 리액트 함수 컴포넌트에서만 사용
Hook은 리액트 함수 컴포넌트 내에서만 호출 가능합니다. 클래스형 컴포넌트에서는 사용할 수 없으며, 리액트 함수형 컴포넌트에서만 사용할 수 있습니다.
1.3. 항상 같은 순서로 호출
Hook은 컴포넌트가 렌더링될 때마다 항상 같은 순서로 호출되어야 합니다. 렌더링 순서가 변경되면, React는 Hook의 상태를 추적할 수 없게 되어 오류가 발생할 수 있습니다.
2. 기본적인 Hook
2.1. useState()
useState
는 함수형 컴포넌트에서 상태를 관리하기 위해 사용되는 Hook입니다.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
useState
는 두 가지 값을 반환합니다: 상태값과 상태를 업데이트할 수 있는 함수입니다.
2.2. useEffect()
useEffect
는 컴포넌트가 렌더링된 후 특정 작업을 실행할 수 있도록 해주는 Hook입니다. 예를 들어, API 호출이나 구독 설정 등을 useEffect
안에서 처리할 수 있습니다.
import { useEffect, useState } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData();
}, []); // 빈 배열을 넣으면 마운트 시에만 실행됨
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
};
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
useEffect
는 첫 번째 인자로 이펙트 함수를 받고, 두 번째 인자로 의존성 배열을 받습니다.- 의존성 배열이 비어 있으면,
useEffect
는 컴포넌트가 처음 마운트될 때만 실행됩니다. - 의존성 배열에 값이 포함되면, 해당 값이 변경될 때마다
useEffect
가 다시 실행됩니다.
2.3. useMemo()
& useCallback()
useMemo
와 useCallback
은 성능 최적화를 위해 사용되는 Hook입니다. useMemo
는 연산 결과를 기억하고, useCallback
은 함수를 기억합니다.
useMemo()
import { useMemo } from 'react';
function ExpensiveComputation({ num }) {
const computedValue = useMemo(() => {
return expensiveCalculation(num);
}, [num]); // num이 변경될 때만 계산
return <div>{computedValue}</div>;
}
function expensiveCalculation(num) {
console.log('Calculating...');
return num * 2;
}
useMemo
는 특정 연산이 불필요하게 다시 실행되지 않도록 하기 위해 사용됩니다.- 의존성 배열의 값이 변경될 때만 연산이 다시 실행됩니다.
useCallback()
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1);
}, [count]); // count가 변경될 때만 새로운 함수가 생성됨
return <Child increment={increment} />;
}
useCallback
은 함수의 재생성을 방지하여 성능을 최적화하는 데 사용됩니다.- 주로 자식 컴포넌트에 함수를 전달할 때, 자식이 렌더링 될 때마다 함수가 재생성되는 것을 방지합니다.
2.4. useRef()
useRef
는 DOM 엘리먼트나 값에 대한 참조를 저장할 수 있게 해주는 Hook입니다. 컴포넌트가 리렌더링되어도 참조된 값은 유지됩니다.
import { useRef } from 'react';
function FocusInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}
useRef
는 렌더링 사이클에 영향을 주지 않고 값을 유지하며, DOM 참조를 통해 컴포넌트에 접근할 수 있습니다.
3. 고급 Hook 사용
3.1. useReducer()
useReducer
는 useState
보다 복잡한 상태 관리가 필요할 때 사용됩니다. 예를 들어, 여러 개의 값이나 복잡한 상태 로직을 처리할 때 유용합니다.
import { 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 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
useReducer
는 복잡한 상태 로직을 분리하여 더 관리하기 쉬운 코드로 만들 수 있습니다.
3.2. useContext()
useContext
는 전역 상태 관리에 유용합니다. 여러 컴포넌트에서 공유하는 값을 쉽게 관리할 수 있습니다.
import React, { useContext, useState } from 'react';
// Context 생성
const ThemeContext = React.createContext('light');
function ThemedComponent() {
const theme = useContext(ThemeContext);
return <div>The current theme is {theme}</div>;
}
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle Theme</button>
<ThemedComponent />
</ThemeContext.Provider>
);
}
useContext
를 사용하면 전역 상태를 쉽게 공유할 수 있습니다.
3.3. useLayoutEffect()
useLayoutEffect
는 useEffect
와 유사하지만, 렌더링 후 레이아웃을 수정하기 전에 실행되는 Hook입니다.
import { useLayoutEffect, useState, useRef } from 'react';
function LayoutEffectComponent() {
const [width, setWidth] = useState(0);
const divRef = useRef(null);
useLayoutEffect(() => {
setWidth(divRef.current.getBoundingClientRect().width);
}, []); // 마운트 시에만 실행됨
return <div ref={divRef}>Width: {width}px</div>;
}
useLayoutEffect
는 DOM의 크기나 위치를 측정하고 조정해야 할 때 사용됩니다.