React에서 ref, useRef는 왜 필요한가? — DOM 직접 제어가 필요한 순간
React 이전: DOM을 직접 조작하던 시대
React가 나오기 전에는 화면을 바꾸려면 DOM을 직접 건드려야 했습니다.
// jQuery 시절 (2010년대 초)
document.getElementById('name').innerText = '홍길동';
document.getElementById('age').innerText = '25';
document.getElementById('list').innerHTML = '<li>항목1</li><li>항목2</li>';
작은 앱에서는 괜찮았지만, 앱이 커지면 문제가 터집니다.
- DOM 수정 코드가 수백 줄로 폭발
- “지금 화면에 뭐가 보이고 있지?” 추적 불가능
- 버그 발생 → 어디서 DOM을 잘못 건드렸는지 찾기 지옥
React의 해결책 (2013): “DOM은 내가 관리할게”
React는 이 혼란을 정리하기 위해 등장했습니다. 핵심 아이디어는 간단합니다.
개발자는 데이터(state)만 바꾸면 된다. DOM 업데이트는 React가 알아서 한다.
// 개발자는 데이터(state)만 바꾸면 됨
const [name, setName] = useState('홍길동');
// React가 알아서 DOM을 업데이트
return <div>{name}</div>;
setName('김철수');
// React: "name이 바뀌었으니 div를 업데이트해야지"
흐름: 개발자 → state 변경 → React → DOM 업데이트
개발자가 DOM을 직접 건드릴 필요가 없어졌습니다.
그런데 예외가 생김: “React야, 네가 못 하는 게 있어”
React가 아무리 좋아도, 처리 못 하는 영역이 있습니다.
| 케이스 | 왜 React가 못 하나 |
|---|---|
input.focus() |
포커스는 DOM API로만 가능 |
element.scrollIntoView() |
스크롤도 DOM API로만 가능 |
contentEditable |
브라우저가 직접 DOM을 수정, React 관여 불가 |
| 외부 라이브러리 연동 | 지도, 비디오 플레이어 등 DOM 직접 제어 필요 |
이런 경우를 위해 “React를 우회해서 DOM에 직접 접근하는 탈출구” 가 필요했고, 그게 바로 ref입니다.
ref의 역할: “React의 탈출구”
// React 방식 — state로 제어 (보통은 이걸 씀)
const [value, setValue] = useState('');
<input value={value} onChange={e => setValue(e.target.value)} />
// ref 방식 — DOM 직접 접근 (React가 못 하는 것만)
const inputRef = useRef(null);
<input ref={inputRef} />
inputRef.current.focus(); // 포커스 → React로 불가능
inputRef.current.scrollHeight; // 높이 측정 → React로 불가능
ref의 두 가지 사용 방식
1. useRef (객체 ref)
하나의 DOM 요소를 참조할 때 사용합니다.
const myRef = useRef<HTMLDivElement>(null);
<div ref={myRef}>내용</div>
// React가 DOM 생성 후 myRef.current = div요소 를 넣어줌
// 이후 myRef.current.focus() 같은 DOM 조작 가능
2. ref 콜백 (함수 ref)
여러 요소를 동적으로 참조할 때 유용합니다.
<div ref={(el) => {
// el = 실제 DOM 요소
// React가 이 div를 DOM에 넣는 순간 이 함수를 호출해줌
}} />
내부 동작 순서:
React 렌더링
→ 가상 DOM 생성
→ 실제 DOM에 <div> 생성
→ ref 콜백 호출: ref(div요소) ← 이 시점!
→ 화면에 표시
DOM에서 제거될 때는 ref(null)로 한 번 더 호출됩니다.
map에서 ref 콜백이 특히 유용한 이유
// useRef → 1개의 값만 저장 가능. 블록 3개면?
const ref = useRef(null); // 마지막 블록만 참조됨 ❌
// ref 콜백 → 각 블록마다 개별 실행됨
blocks.map(block => (
<div ref={(el) => {
// block 1일 때 호출 ✓
// block 2일 때 호출 ✓
// block 3일 때 호출 ✓
// 각각의 el이 다른 DOM 요소!
}} />
))
onInput도 같은 맥락
ref와 마찬가지로, onInput도 React 바깥 영역을 다루기 위해 존재합니다.
onChange |
onInput |
|
|---|---|---|
<input>, <textarea> |
값 바뀔 때 발생 | 값 바뀔 때 발생 |
contentEditable div |
동작 안 함 | 동작함 |
contentEditable은 일반 <input>이 아니라 브라우저가 직접 DOM을 수정하는 방식입니다.
React의 onChange는 폼 요소용이라 여기선 안 먹히고, 브라우저 네이티브 이벤트인 onInput으로만 잡을 수 있습니다.
// <input>에서는 이렇게 쓰지만
<input onChange={(e) => setValue(e.target.value)} />
// contentEditable에서는 이렇게 써야 함
<div contentEditable onInput={(e) => {
const text = e.currentTarget.innerText; // DOM에서 직접 읽기
}} />
정리
| React 기본 방식 | ref / onInput | |
|---|---|---|
| 언제 | state → 화면 업데이트 | DOM 직접 접근이 필요할 때 |
| 왜 | React가 DOM을 대신 관리해줌 | React가 못 하는 영역이 있어서 |
| 예시 | {name} 렌더링 |
focus(), 초기 텍스트 삽입, contentEditable |
ref는 React가 “나는 여기까지만 할게, 나머지는 너가 해”라고 열어준 문입니다.
가능하면 안 쓰는 게 좋지만, contentEditable 같은 경우엔 필수입니다.