React 선언형을 보완하는 예외 도구
useRef·Portal·useImperativeHandle 등 선언형을 보완하는 예외 도구 사용 기준을 정리
선언적 React에서 벗어나는 예외 도구들: Ref, Portal, Imperative API
React의 기본 원칙은 명확하다.
UI는 state로부터 계산된다. DOM은 직접 만지지 않는다.
그런데 실무를 하다 보면 이 원칙만으로는 해결되지 않는 영역이 분명히 존재한다. React는 이를 “무시”하지 않고, 의도적으로 통제된 예외 도구를 제공한다.
이 글은 그 예외들을 왜 존재하는지, 언제 써야 하는지, 언제 쓰면 설계가 무너지는지 기준으로 정리한다.
왜 React는 예외를 허용하는가
개념
React는 선언적 모델을 기본으로 하지만, 현실의 UI는 100% 선언적으로 표현되지 않는다.
대표적인 영역:
- 포커스 제어
- 스크롤 위치
- DOM 크기 측정
- 모달/툴팁의 레이어 분리
- 외부 라이브러리(DOM 기반) 연동
이 영역들은 “상태 → UI 계산”만으로는 다루기 어렵다. 그래서 React는 탈출구(Escape Hatch) 를 제공한다.
중요한 점은 이것이다.
React의 예외 도구는 “선언적 모델을 버리기 위한 수단”이 아니라 선언적 모델을 유지하기 위한 보조 수단이다.
useRef의 역할: “렌더링과 무관한 값 보관함”
개념
useRef는 두 가지 용도로 사용된다.
- DOM 요소에 접근
- 렌더링과 무관한 값을 저장
1
2
3
const inputRef = useRef(null);
<input ref={inputRef} />
왜 중요한가
state는 바뀌면 렌더링을 유발한다. 하지만 어떤 값은 렌더링과 상관없이 유지되어야 한다.
대표 사례:
- input 포커스
- 이전 값 비교
- 타이머 ID
- 외부 라이브러리 인스턴스
이런 값들을 state로 관리하면:
- 불필요한 렌더 발생
- 의존성 관리 복잡화
- 로직이 왜곡됨
useRef는 “렌더 트리와 분리된 저장소”다.
언제 문제가 되는가
1) ref로 UI 상태를 표현하려 할 때
1
if (inputRef.current.value === "") { ... }
이걸 기준으로 UI를 바꾸면:
- React는 이 변경을 모른다
- UI와 실제 DOM이 어긋난다
원칙
UI에 영향을 주는 값은 state UI에 영향을 주지 않는 값만 ref
2) ref를 state 대용으로 남용
- “렌더 안 일어나서 편하네” → 장기적으로 디버깅 지옥
- React의 데이터 흐름을 우회
DOM 접근이 필요한 이유와 허용 기준
개념
React는 DOM을 직접 만지지 않지만, DOM을 읽는 것은 허용한다.
허용되는 접근 유형:
- focus / blur
- scroll 위치 제어
- getBoundingClientRect()
- 외부 라이브러리 연결
왜 중요한가
이 작업들은 DOM이 실제로 렌더된 이후에만 가능하다. 그래서 다음 규칙이 중요하다.
- 접근 수단:
ref - 접근 시점: 이벤트 핸들러 또는
useEffect
1
2
3
useEffect(() => {
inputRef.current.focus();
}, []);
언제 문제가 되는가
- 렌더 중 DOM 접근
- 조건부 렌더링과 ref 접근 순서 착각
- DOM 값을 “진실(source of truth)”로 사용
기억할 기준
DOM은 결과물이지 상태의 근원이 아니다.
Portal의 사용 목적: “DOM 위치와 UI 구조의 분리”
개념
Portal은 렌더링 위치(DOM 트리) 와 컴포넌트 구조(React 트리) 를 분리한다.
1
createPortal(<Modal />, document.getElementById("overlay"));
왜 중요한가
모달, 툴팁, 드롭다운은 다음 문제를 가진다.
- 부모 컨테이너의
overflow,z-index영향을 받음 - 시각적으로는 최상단에 있어야 함
- 논리적으로는 특정 컴포넌트의 일부임
Portal은 이 모순을 해결한다.
- React 트리: 부모-자식 관계 유지
- DOM 트리: 독립된 최상단 레이어
언제 문제가 되는가
- Portal을 “레이아웃 도구”로 남용
- 모든 컴포넌트를 최상단으로 보내버림
- 포커스 트랩, 접근성 고려 없이 사용
Portal은 레이어 분리가 필요한 UI에만 써야 한다.
useImperativeHandle의 의미: “의도적으로 명령형 API를 노출”
개념
useImperativeHandle은 부모에게 명령형 인터페이스를 제공한다.
1
2
3
4
5
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
}
}));
부모는 이렇게 사용한다.
1
childRef.current.focus();
왜 중요한가
React는 기본적으로 props 기반 선언형 통신을 권장한다. 그럼에도 명령형 API가 필요한 경우가 있다.
대표 사례:
- 포커스 제어
- 애니메이션 트리거
- 외부 라이브러리 래핑 컴포넌트
이때 중요한 건 무엇을 노출하느냐다.
useImperativeHandle은:
- 내부 구현은 숨기고
- 최소한의 명령만 공개한다
즉, 캡슐화된 예외다.
언제 문제가 되는가
- 부모가 자식의 내부 상태를 직접 조작
- “props로 해결하기 귀찮아서” 사용
- 명령형 API가 여러 개로 늘어남
이 순간 컴포넌트는:
- 재사용성 하락
- 테스트 난이도 상승
- 의존 관계 꼬임
원칙
선언형으로 표현 가능하면 imperative API는 쓰지 않는다.
언제 써야 하고, 언제 쓰면 안 되는가 (요약 기준)
써야 하는 경우
- DOM 포커스/측정/스크롤
- 모달/툴팁 같은 레이어 분리
- 외부 DOM 기반 라이브러리 연동
- 선언형 표현이 오히려 복잡해지는 경우
쓰면 안 되는 경우
- UI 상태 표현
- 데이터 흐름 제어
- 부모-자식 통신 대체
- state 관리 회피 수단
정리: 예외 도구는 “규칙을 깨기 위한 도구”가 아니다
useRef: 렌더와 무관한 값 보관- DOM 접근: 읽기 위주, 결과물 취급
- Portal: DOM 구조 문제 해결용
useImperativeHandle: 최소한의 명령형 인터페이스
공통 원칙은 하나다.
선언적 모델을 유지하기 위해서만 예외를 허용한다.
이 기준이 무너지면 React는 곧 “제어 불가능한 DOM 조작 도구”로 퇴화한다.
