리액트에 훅이 추가(리액트 16.8 버전)되기 이전에는 클래스 컴포넌트에서만 상태를 가질 수 있었다. componentDidMount, componentDidUpdate와 같이 하나의 생명주기 함수에서만 상태 업데이트에 따른 로직을 실행시킬 수 있었다.
리액트 훅이 추가되면서 함수 컴포넌트에서도 클래스 컴포넌트와 같이 컴포넌트의 생명주기에 맞춰 로직을 실행할 수 있게 되었다.
리액트 함수 컴포넌트에서 상태(status)를 관리하기 위해 useState 훅을 활용할 수 있다.
useState 훅을 사용하면 함수형 컴포넌트에서도 상태를 유지하고, 이 상태값이 업데이트 될 때마다 컴포넌트가 자동으로 리렌더링 된다.
React의 useState는 다음 두 가지를 반환한다.
const [state, setState] = useState(initialValue);
React는 함수형 컴포넌트가 한 번 렌더링될 때마다 고정된 state 스냅샷을 사용한다.
즉, setState()를 호출해도 그 자리에서 state가 즉시 상태를 바꾸지 않는다.
예시를 통해 이해해보자.
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count); // 여전히 0, 바로 바뀌지 않음
}
이 비동기적 특성 때문에, 이전 상태를 기준으로 새 상태를 계산해야 하는 경우
(prevState) => newState 형태를 써야 정확한 결과를 얻는다.
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// 결과적으로 count가 +3이 됨
function useState<S>(
initialState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>];
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);
SetStateAction에는 useState로 관리할 상태 타입인 S 또는 이전 상태 값을 받아 새로운 상태를 반환하는 함수인 (prevState: S) => S)가 들어갈 수 있다.이처럼 함수형 업데이트를 사용하면 실제로 비동기적으로 동작하는 상태 업데이트를 이전 상태값을 ‘동기적으로 참조’해서 계산할 수 있다.
useState의 사용팁: 하나의 컴포넌트 안에 useState를 여러 개 쓸 수 있다. 그런데 useState가 많다는건 컴포넌트의 역할이 크다는 뜻이다. 즉, 의존되는 컴포넌트가 많은 것이기 때문에 수정할 필요가 있을 수 있다.
useEffect는 렌더링 이후 리액트 함수 컴포넌트에 어떤 일을 수행해야 하는지 알려주기 위해 등록하는 함수이다. DependencyList라는 의존성 배열을 사용한다.
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
type DependencyList = readonly unknown[];
type EffectCallback = () => void | Destructor;
두번째 인자인 deps는 옵셔널하게 제공되고 effect가 수행되기 위한 조건을 나열한다.
deps가 변경되었는지를 얕은 비교로만 판단하기 때문에, 실제 객체 값이 바뀌지 않았더라도 객체의 참조 값이 변경되면 콜백 함수가 실행된다.
type SomeObject = {
name: string;
id: string;
}
interface LabelProps {
value: SomeObject;
}
const Label: React.FC<LabelProps> = ({ value }) => {
useEffect(() => {
// value.name과 value.id를 사용해서 작업한다
}, [value]);
}
→ 부모에서 받은 인자를 직접 deps로 작성한 경우, 원치 않는 렌더링이 발생할 수 있다. 이를 방지하기 위해 실제 사용되는 값을 useEffect의 deps로 사용해야 한다.
const Label: React.FC<LabelProps> = ({ value }) => {
useEffect(() => {
// value.name과 value.id 대신 name, id를 직접 사용한다
}, [value.name]);
}
이처럼 useEffect는 Destructor를 반환하는데 이것은 마운트가 해제될 때 실행하는 함수이다.
useEffect도 마찬가지로 비동기로 동작하기 때문에 선언을 먼저 해도 레이아웃 배치와 화면 렌더링이 모두 완료된 후에 실행된다.
그런데 만약 name을 지정하는 setName이 오랜 시간이 걸린 후에 실행된다면?
→ 사용자는 빈 이름을 오랫동안 보고 있어야 할 것이다.
이런 문제를 해결하기 위해 useLayoutEffect 훅을 사용할 수 있다.
useLayoutEffect는 화면이 렌더링 되기 전에 콜백 함수를 실행한다.
useEffect는 비동기로 동작해서 화면 업데이트를 방해하지 않지만, 동기적인 방식이 필요하다면 useLayoutEffect를 사용하면 된다.
useEffect의 사용팁: useEffect내의 deps를 올바르게 명시하지 않아서 발생하는 사이드 이펙을 해결하는 것이 더 많은 비용이 발생할 수 있다. 따라서 사용하는 방식을 올바르게 익혀두도록 하자. (useEffect 사용을 지양하는 방법도 있음)
useRef로 관리되는 변수는 값이 바뀌어도 값을 기억하고 있어 재렌더링이 발생하지 않는다.
const ref = useRef(initialValue);
ref.current 안의 값은 렌더링 사이에서도 사라지지 않고 유지된다.| 구분 | useState | useRef |
|---|---|---|
| 값 변경 시 렌더링? | 다시 렌더링됨 | 렌더링 안 됨 |
| 언제 읽을 수 있나? | 다음 렌더 후에 | 즉시 읽을 수 있음 |
| 주 사용 목적 | 화면 상태(UI 값) | 렌더링과 무관한 값, DOM, 타이머 등 |
useRef는 useState와 다르게 렌더링과 무관한 값을 기억해야 할 때 사용된다.
ex)
const prevValue = useRef<number>();
useEffect(() => {
prevValue.current = count; // 이전 값 저장
}, [count]);
const timeoutId = useRef<number>();
timeoutId.current = window.setTimeout(...);
const renderCount = useRef(0);
renderCount.current += 1;
console.log("렌더 횟수:", renderCount.current);
React는 기본적으로 “선언적 UI” 방식이다.
직접 DOM을 조작하는 대신, 상태(state)가 바뀌면 React가 알아서 DOM을 업데이트한다.
그런데 특정 상황에서는 React의 자동 업데이트보다 더 직접적인 제어가 필요할 때가 있다.
이럴 때 “실제 DOM 노드에 접근” 해야 한다.
<input /> 요소에 foucs를 설정하거나 특정 컴포넌트의 위치로 스크롤 하는 등의 DOM요소에 직접 접근해야 할 때 사용할 수 있다.
function SearchInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// 컴포넌트가 마운트되면 자동 포커스
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="검색어 입력" />;
}
<input> 엘리먼트를 렌더링inputRef.current에 저장inputRef.current.focus() 호출→ React가 대신 해줄 수 없는, “DOM API 호출” (focus, scroll, measure 등) 을 직접 수행할 수 있게 된다.
| 상황 | 사용하는 이유 |
|---|---|
| 입력창 포커스 | 자동 focus(), blur() 제어 |
| 스크롤 제어 | 특정 위치로 scrollIntoView() |
| 요소 크기/좌표 측정 | getBoundingClientRect() 로 위치 계산 |
| 캔버스/비디오 제어 | <canvas>, <video> 의 내부 API 직접 제어 |
| 외부 라이브러리 연동 | chart.js, mapbox 등 React 외부 DOM이 필요한 라이브러리 접근 |
일반적인 경우에는 DOM에 직접 접근하는 것은 권장하지 않는다.