React Hooks 학습 - useEffect & useRef
1. useRef 훅
기본 개념
useRef는 렌더링과 무관하게 값을 유지하고, DOM 요소에 직접 접근할 수 있게 해주는 훅입니다.
문법
const 변수명 = useRef<타입>(초기값);
특징
- 리렌더링을 유발하지 않음 - 값이 변경되어도 컴포넌트가 다시 렌더링되지 않음
- 값이 유지됨 - 컴포넌트가 리렌더링되어도 값이 사라지지 않음
.current프로퍼티로 접근 - 실제 값은.current에 저장됨
2. useRef의 두 가지 사용법
2.1 DOM 요소에 접근 (우리 코드의 경우)
const boardRef = useRef<HTMLDivElement>(null);
return <div ref={boardRef}>보드</div>;
동작 원리:
useRef<HTMLDivElement>(null)- div 요소를 참조할 ref 생성ref={boardRef}- 실제 DOM div 요소와 연결boardRef.current- 실제 DOM 요소에 접근 가능
예시 - 우리 프로젝트:
export const Board = ({ gameId }: BoardProps) => {
const boardRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (boardRef.current) {
// boardRef.current는 실제 <div class="board"> DOM 요소
boardRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
}, []);
return (
<div className='board' ref={boardRef}>
{/* 보드 내용 */}
</div>
);
};
타입 설명:
HTMLDivElement- div 요소 타입HTMLInputElement- input 요소 타입HTMLButtonElement- button 요소 타입- 등등...
2.2 값 저장 (useState와 차이점)
// useState - 값이 변경되면 리렌더링 발생
const [count, setCount] = useState(0);
// useRef - 값이 변경되어도 리렌더링 없음
const countRef = useRef(0);
비교 예시:
function Example() {
const [stateCount, setStateCount] = useState(0);
const refCount = useRef(0);
const handleStateClick = () => {
setStateCount(stateCount + 1); // ✅ 리렌더링 발생!
console.log('State 업데이트');
};
const handleRefClick = () => {
refCount.current += 1; // ❌ 리렌더링 없음
console.log('Ref 업데이트:', refCount.current);
};
return (
<div>
<p>State: {stateCount}</p> {/* 화면에 반영됨 */}
<p>Ref: {refCount.current}</p> {/* 화면에 반영 안됨 */}
<button onClick={handleStateClick}>State 증가</button>
<button onClick={handleRefClick}>Ref 증가</button>
</div>
);
}
언제 useRef를 쓸까?
- 이전 값을 기억해야 하지만 화면에 표시할 필요 없을 때
- 타이머 ID 저장 (setInterval, setTimeout)
- 스크롤 위치 추적
- 포커스 관리
3. useEffect 훅
기본 개념
useEffect는 컴포넌트의 생명주기에 따라 부수 효과(side effect)를 실행하는 훅입니다.
부수 효과(Side Effect)란?
- 데이터 가져오기 (API 호출)
- DOM 조작 (스크롤, 포커스)
- 타이머 설정
- 이벤트 리스너 등록
- 로그 출력
문법
useEffect(() => {
// 실행할 코드
return () => {
// 정리(cleanup) 함수 (선택사항)
};
}, [의존성 배열]);
4. useEffect의 실행 시점
4.1 의존성 배열 없음 - 매번 실행
useEffect(() => {
console.log('렌더링될 때마다 실행');
});
- 컴포넌트가 렌더링될 때마다 실행
- 거의 사용 안 함 (성능 문제)
4.2 빈 배열 [] - 마운트 시 한 번만 (우리 코드의 경우)
useEffect(() => {
console.log('컴포넌트가 처음 나타날 때 한 번만 실행');
}, []);
마운트(mount)란?
- 컴포넌트가 화면에 처음 나타나는 것
언제 쓸까?
- 초기 데이터 로딩
- 스크롤 위치 설정 (우리 코드)
- 초기 설정 작업
우리 프로젝트 예시:
// Board 컴포넌트가 처음 화면에 나타날 때 한 번만 실행
useEffect(() => {
if (boardRef.current) {
boardRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
}, []); // 빈 배열 = 마운트 시 한 번만
실행 흐름:
1. 게임 시작 버튼 클릭
2. Board 컴포넌트 생성 (마운트)
3. JSX 렌더링 (<div ref={boardRef}>...)
4. useEffect 실행 → 스크롤 이동4.3 특정 값 변경 시 - 의존성 배열에 값 넣기
useEffect(() => {
console.log('count가 변경될 때마다 실행');
}, [count]);
예시:
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState(null);
useEffect(() => {
// userId가 변경될 때마다 사용자 정보 새로 불러오기
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]); // userId가 변경되면 실행
return <div>{user?.name}</div>;
}
5. Cleanup 함수 (정리)
개념
컴포넌트가 언마운트(사라질 때) 또는 다음 effect 실행 전에 실행되는 함수
문법
useEffect(() => {
// 설정
const timer = setInterval(() => {
console.log('1초마다 실행');
}, 1000);
return () => {
// 정리
clearInterval(timer); // 타이머 정리
};
}, []);
언제 필요할까?
- 타이머 정리 (
clearInterval,clearTimeout) - 이벤트 리스너 제거
- 구독 취소
- 연결 해제
예시 - 이벤트 리스너:
useEffect(() => {
const handleResize = () => {
console.log('창 크기 변경:', window.innerWidth);
};
// 이벤트 리스너 등록
window.addEventListener('resize', handleResize);
// cleanup: 컴포넌트 사라질 때 이벤트 리스너 제거
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
왜 필요할까?
- 메모리 누수(memory leak) 방지
- 불필요한 작업 중단
- 여러 번 등록되는 것 방지
6. 우리 코드 상세 분석
export const Board = ({ gameId }: BoardProps) => {
// 1. ref 생성 - div 요소를 참조할 수 있는 변수
const boardRef = useRef<HTMLDivElement>(null);
// 2. Effect 설정 - Board가 마운트될 때 실행
useEffect(() => {
// 3. boardRef.current가 존재하는지 확인
// (렌더링 전에는 null이므로 체크 필요)
if (boardRef.current) {
// 4. DOM 메서드 호출 - 스크롤 이동
boardRef.current.scrollIntoView({
behavior: 'smooth', // 부드러운 애니메이션
block: 'center', // 화면 중앙에 위치
});
}
}, []); // 5. 빈 배열 - 마운트 시 한 번만 실행
return (
// 6. ref 연결 - boardRef와 실제 DOM 연결
<div className='board' ref={boardRef}>
{/* 보드 내용 */}
</div>
);
};
실행 순서:
[1] Board 컴포넌트 함수 실행
↓
[2] boardRef 생성 (초기값: { current: null })
↓
[3] JSX 반환 (<div ref={boardRef}>)
↓
[4] React가 실제 DOM 생성
↓
[5] boardRef.current에 DOM 요소 연결
↓
[6] useEffect 실행
↓
[7] boardRef.current 존재 확인
↓
[8] scrollIntoView() 호출 → 스크롤 이동!7. scrollIntoView 메서드
문법
element.scrollIntoView(options);
옵션
{
behavior: 'auto' | 'smooth', // 스크롤 애니메이션
block: 'start' | 'center' | 'end' | 'nearest', // 세로 정렬
inline: 'start' | 'center' | 'end' | 'nearest', // 가로 정렬
}
예시:
// 즉시 이동
element.scrollIntoView();
// 부드럽게 이동, 상단 정렬
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
// 부드럽게 이동, 중앙 정렬 (우리 코드)
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 즉시 이동, 하단 정렬
element.scrollIntoView({ behavior: 'auto', block: 'end' });
8. 실전 예제
예제 1: 입력창 자동 포커스
function SearchBox() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// 컴포넌트 마운트 시 입력창에 포커스
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder='검색...' />;
}
예제 2: 이전 값 기억하기
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef(0);
useEffect(() => {
// 렌더링 후 이전 값 저장
prevCountRef.current = count;
}, [count]);
return (
<div>
<p>현재: {count}</p>
<p>이전: {prevCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
예제 3: 타이머
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timerId = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
// cleanup: 컴포넌트 사라질 때 타이머 정리
return () => clearInterval(timerId);
}, []);
return <div>{seconds}초 경과</div>;
}
예제 4: API 호출
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 마운트 시 사용자 목록 불러오기
fetch('/api/users')
.then((res) => res.json())
.then((data) => {
setUsers(data);
setLoading(false);
});
}, []); // 한 번만 실행
if (loading) return <div>로딩 중...</div>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
9. 주의사항
9.1 useRef는 리렌더링을 유발하지 않음
// ❌ 잘못된 사용 - 화면에 반영 안됨
const countRef = useRef(0);
countRef.current += 1;
return <div>{countRef.current}</div>; // 화면 업데이트 안됨
// ✅ 올바른 사용 - 화면에 반영됨
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>; // 화면 업데이트됨
9.2 useEffect에서 async/await 직접 사용 불가
// ❌ 잘못된 사용
useEffect(async () => {
const data = await fetch('/api/data');
}, []);
// ✅ 올바른 사용
useEffect(() => {
const fetchData = async () => {
const data = await fetch('/api/data');
};
fetchData();
}, []);
9.3 의존성 배열 빠뜨리지 않기
// ❌ 경고 발생
useEffect(() => {
console.log(count); // count 사용
}, []); // count가 의존성 배열에 없음!
// ✅ 올바른 사용
useEffect(() => {
console.log(count);
}, [count]); // count 추가
10. 정리
| 특징 | useRef | useEffect |
|---|---|---|
| 용도 | DOM 접근, 값 저장 | 부수 효과 실행 |
| 리렌더링 | 유발 안 함 | 유발 안 함 (setState는 유발) |
| 값 접근 | .current |
- |
| 실행 시점 | 즉시 | 렌더링 후 |
| cleanup | 없음 | 있음 (return) |
useRef 사용 시기:
- ✅ DOM 요소에 접근할 때
- ✅ 리렌더링 없이 값을 유지할 때
- ✅ 이전 값을 기억할 때
useEffect 사용 시기:
- ✅ 컴포넌트 마운트 시 초기 작업
- ✅ 데이터 로딩 (API 호출)
- ✅ 특정 값 변경에 반응
- ✅ 이벤트 리스너 등록/해제
- ✅ 타이머 설정/정리
우리 코드에서:
useRef→ Board div 요소에 접근useEffect→ Board 마운트 시 스크롤 이동- 빈 배열
[]→ 한 번만 실행 scrollIntoView()→ 부드럽게 중앙으로 이동
11. 연습 문제
문제 1
버튼 클릭 시 입력창에 포커스를 주는 코드를 작성하세요.
정답 보기
function FocusExample() {
const inputRef = useRef<HTMLInputElement>(null);
const handleFocus = () => {
inputRef.current?.focus();
};
return (
<>
<input ref={inputRef} />
<button onClick={handleFocus}>포커스</button>
</>
);
}
문제 2
3초 후 자동으로 메시지가 사라지는 알림을 만드세요.
정답 보기
function Notification() {
const [visible, setVisible] = useState(true);
useEffect(() => {
const timer = setTimeout(() => {
setVisible(false);
}, 3000);
return () => clearTimeout(timer);
}, []);
if (!visible) return null;
return <div>알림: 3초 후 사라집니다</div>;
}
문제 3
창 크기가 변경될 때마다 현재 너비를 표시하세요.
정답 보기
function WindowSize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <div>창 너비: {width}px</div>;
}
'STUDY > [ React ]' 카테고리의 다른 글
| react new start (0) | 2025.11.22 |
|---|---|
| forwardRef 사용법 - 장기 프로젝트 (0) | 2025.11.22 |
| useEffect (0) | 2025.11.22 |
| CSS opacity와 brightness 비교 + filter 속성 (0) | 2025.11.20 |
| CSS 가상 요소 , Clip Path (0) | 2025.11.20 |