Post

React 선언형을 보완하는 예외 도구

useRef·Portal·useImperativeHandle 등 선언형을 보완하는 예외 도구 사용 기준을 정리

React 선언형을 보완하는 예외 도구

선언적 React에서 벗어나는 예외 도구들: Ref, Portal, Imperative API

React의 기본 원칙은 명확하다.

UI는 state로부터 계산된다. DOM은 직접 만지지 않는다.

그런데 실무를 하다 보면 이 원칙만으로는 해결되지 않는 영역이 분명히 존재한다. React는 이를 “무시”하지 않고, 의도적으로 통제된 예외 도구를 제공한다.

이 글은 그 예외들을 왜 존재하는지, 언제 써야 하는지, 언제 쓰면 설계가 무너지는지 기준으로 정리한다.


왜 React는 예외를 허용하는가

개념

React는 선언적 모델을 기본으로 하지만, 현실의 UI는 100% 선언적으로 표현되지 않는다.

대표적인 영역:

  • 포커스 제어
  • 스크롤 위치
  • DOM 크기 측정
  • 모달/툴팁의 레이어 분리
  • 외부 라이브러리(DOM 기반) 연동

이 영역들은 “상태 → UI 계산”만으로는 다루기 어렵다. 그래서 React는 탈출구(Escape Hatch) 를 제공한다.

중요한 점은 이것이다.

React의 예외 도구는 “선언적 모델을 버리기 위한 수단”이 아니라 선언적 모델을 유지하기 위한 보조 수단이다.


useRef의 역할: “렌더링과 무관한 값 보관함”

개념

useRef는 두 가지 용도로 사용된다.

  1. DOM 요소에 접근
  2. 렌더링과 무관한 값을 저장
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 조작 도구”로 퇴화한다.


This post is licensed under CC BY 4.0 by the author.