필수 내장 Hooks 마스터: useEffect와 useRef 완벽 가이드
useEffect로 부수 효과(Side Effect) 관리하기
리액트 컴포넌트는 오직 '입력(Props/State)을 받아 UI(JSX)를 반환하는' 순수한 역할에 집중해야 합니다. 하지만 실제 앱에서는 화면을 그리는 일 외에 서버에서 데이터 가져오기(Fetch), 브라우저 타이머 세팅, DOM 직접 조작 같은 부수 효과(Side Effect)가 반드시 필요합니다.
useEffect는 렌더링이 화면에 반영된 직후에 비동기적으로 이러한 작업들을 수행할 수 있게 해주는 훅입니다.컴포넌트 생명주기(Lifecycle)와 useEffect
1. Mount (생성)
브라우저에 실제 HTML(DOM)이 그려진 직후에
useEffect가 딱 한 번 실행됩니다. 주로 백엔드 API 데이터를 불러오거나 이벤트를 등록할 때 쓰입니다.2. Update (업데이트)
상태(State)가 바뀌어 화면이 다시 그려지면, 이전 상태의 찌꺼기를 지우기 위해 Cleanup 함수가 먼저 실행되고, 그다음 새로운 Setup 함수가 실행됩니다.
3. Unmount (소멸)
다른 페이지로 이동하거나 조건부 렌더링에 의해 컴포넌트가 사라질 때, 메모리 누수를 막기 위해 마지막으로 Cleanup 함수가 한 번 실행됩니다.
의존성 배열(Dependency Array) 완벽 이해
useEffect(() => { ... }): 렌더링될 때마다 매번 실행됩니다. (거의 안 씀)useEffect(() => { ... }, []): 빈 배열. 컴포넌트가 처음 화면에 나타날 때(Mount) 단 한 번만 실행됩니다. API 초기 호출에 주로 씁니다.useEffect(() => { ... }, [상태]): 배열 안의 상태가 변경될 때마다 실행됩니다.
또한 컴포넌트가 사라질 때(Unmount) 리소스를 해제(Cleanup)하려면
return으로 정리 함수를 반환하면 됩니다.useRef의 두 가지 핵심 용도
useRef는 리액트 컴포넌트 생애주기 동안 유지되는 변경 가능한 객체(.current)를 생성합니다. useState와 비슷해 보이지만 가장 큰 차이점은 값이 변경되어도 컴포넌트가 리렌더링되지 않는다는 점입니다.
- DOM 요소 직접 선택하기
특정 input에 포커스를 주거나, 스크롤 위치를 계산하는 등 바닐라 JS의getElementById처럼 실제 DOM 노드에 직접 접근해야 할 때 사용합니다. - 리렌더링을 유발하지 않는 변수 저장
타이머 ID, 이전 상태값 기억 등 값이 바뀌어도 굳이 화면을 다시 그릴 필요가 없는 내부 데이터를 보관할 때 사용합니다.
Timer.jsx
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// 1. Mount 시점에 1초마다 동작하는 타이머 세팅
console.log("타이머 시작!");
const intervalId = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// 2. Cleanup 함수 반환
// 컴포넌트가 화면에서 사라질 때(Unmount) 반드시 타이머를 해제해야 메모리 누수를 막습니다.
return () => {
console.log("타이머 정리됨");
clearInterval(intervalId);
};
}, []); // 빈 배열이므로 처음 한 번만 실행됨
return <p style={{ fontSize: '1.2rem', fontWeight: 'bold', color: '#0f172a' }}>경과 시간: {seconds}초</p>;
}
export default function App() {
const [show, setShow] = useState(true);
return (
<div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
<h3 style={{ marginTop: 0 }}>타이머 마운트/언마운트 예제</h3>
<button
onClick={() => setShow(!show)}
style={{ padding: '8px 16px', background: show ? '#ef4444' : '#10b981', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', marginBottom: '15px' }}
>
{show ? '타이머 숨기기 (Unmount)' : '타이머 켜기 (Mount)'}
</button>
<div style={{ minHeight: '50px', padding: '15px', background: '#fff', borderRadius: '6px', border: '1px solid #e2e8f0' }}>
{show ? <Timer /> : <p style={{ color: '#64748b' }}>타이머가 화면에서 사라졌습니다.</p>}
</div>
</div>
);
}RefExample.jsx
import React, { useRef, useState } from 'react';
export default function RefExample() {
// 1. DOM에 접근하기 위한 Ref
const inputRef = useRef(null);
// 2. 리렌더링 없이 값을 유지하기 위한 Ref
const clickCountRef = useRef(0);
const [renderTrigger, setRenderTrigger] = useState(0);
const focusInput = () => {
// current 프로퍼티가 실제 DOM 요소(input)를 가리킵니다.
inputRef.current.focus();
};
const handleSecretClick = () => {
// 값이 증가해도 화면은 다시 그려지지 않습니다.
clickCountRef.current += 1;
console.log(`몰래 ${clickCountRef.current}번 클릭됨!`);
};
return (
<div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
<h3 style={{ marginTop: 0 }}>DOM 포커스 예제</h3>
{/* ref 속성을 통해 요소 연결 */}
<input
type="text"
ref={inputRef}
placeholder="여기에 포커스됩니다"
style={{ padding: '8px', marginRight: '10px', borderRadius: '4px', border: '1px solid #cbd5e1' }}
/>
<button
onClick={focusInput}
style={{ padding: '8px 16px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
>
포커스 이동!
</button>
<hr style={{ margin: '20px 0', borderColor: '#e2e8f0' }} />
<h3>변수 유지 예제</h3>
<div style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}>
<button
onClick={handleSecretClick}
style={{ padding: '8px 16px', background: '#64748b', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
>
화면 변화 없는 버튼
</button>
<button
onClick={() => setRenderTrigger(renderTrigger + 1)}
style={{ padding: '8px 16px', background: '#f59e0b', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
>
화면 강제 업데이트
</button>
</div>
<p style={{ fontWeight: 'bold', color: '#475569' }}>은밀한 클릭 횟수: <span style={{ color: '#ec4899', fontSize: '1.2rem' }}>{clickCountRef.current}</span></p>
</div>
);
}