React/React 문법

[React]Element 크기 조절을 감지하는 방법

DevStory 2023. 1. 9. 19:22

개요

일반적으로 웹 페이지를 제작할 때, Element의 크기를 사용하는 기기(PC, 태블릿, 모바일)에 따라 설정하고 크기를 조절할 수 없도록 개발하지만 반대로 사용자가 컴포넌트의 크기를 조절할 수 있도록 개발하는 경우도 존재한다.

 

컴포넌트의 크기를 변경할 수 있는 경우 크기가 변경되었는지 감지해야 하는 상황이 존재한다. 예를 들어, 특정 컴포넌트가 DOM에 마운트(mount) 되었는데 textarea의 크기를 변경하는 경우 특정 컴포넌트를 언마운트(unmount) 해야 한다고 가정하자. 이러한 경우 textarea의 크기가 변경되면 특정 state의 값을 변경해서 특정 컴포넌트를 언마운트하는 코드를 작성해야 한다.

 

하지만, textarea에는 resize라는 이벤트가 존재하지 않기에 textarea의 크기를 감지하려면 width, height를 state로 관리하고 마우스 이벤트들(mouseenter, mouseleave, mousemove, mouseup 등)을 활용하고 textarea를 접근하기 위해 클래스 컴포넌트라면 createRef, 함수형 컴포넌트라면 useRef를 사용해야 한다.

 

위에서 말한 방법을 사용하여 textarea의 크기 조절을 감지하면 소스 코드가 굉장히 길어지고 복잡해진다. 그리고 mouse 이벤트 순서에 대한 이해가 부족하면 소스 코드가 원하는 대로 동작하지 않을 수 있다.

 

본 포스팅은 ResizeObserver 인터페이스를 사용하여 복잡한 코드를 작성하지 않고 React에서 Element의 크기 조절을 감지할 수 있는 방법과 ResizeObserver 인터페이스를 효율적으로 사용할 수 있는 방법을 소개한다.

 

Element 크기 조절을 감지하는 방법

Element의 크기 조절을 감지하는 방법으로 JavaScript에서 제공하는 ResizeObserver 인터페이스를 사용할 수 있다. ResizeObserver 인터페이스는 Resize(크기 조절) + Observer(관찰자)의 합성어로 특정 Element의 크기 조절을 감지한다.

 

1. new ResizeObserver()

먼저, ResizeObserver를 사용하기 위해 new 연산자를 사용하여 ResizeObserver 객체를 생성한다.

const observer = new ResizeObserver((entries, observer) => {
  for (const entry of entries) {
    // ...
  }
});

ResizeObserver() 생성자에 Callback 함수를 전달한다. ResizeObserver() 생성자에 전달되는 Callback 함수는 두 개의 매개변수를 가진다.

 

entries

- resize가 발생한 요소의 새로운 크기를 접근할 수 있는 객체이며, ResizeObserverEntry 타입의 배열이다. 

 

observer(생략 가능)

- 콜백 함수를 호출한 ResizeObserver 객체를 참조하는 객체다.

 

2. ResizeObserverEntry

ResizeObserverEntry 객체는 ResizeObserver() 생성자의 Callback 함수에 전달된 객체이며, 읽기 전용의 5개 프로퍼티를 가지고 있다.

const observer = new ResizeObserver((entries, observer) => {
  for (const entry of entries) {
    const {width, height, top, left} = entry.contentRect;
    
    console.log(entry.target);
    console.log(`width: ${width}px;, height: ${height}px`);
    console.log(`top: ${top}px;, left: ${left}px`);
  }
});

borderBoxSize

- 관찰된 요소의 새로운 borderBox 크기를 가지는 객체다.

 

contentBoxSize

- 관찰된 요소의 새로운 contentBox 크기를 가지는 객체다.

 

devicePixelContentBoxSize

- 관찰된 요소의 디바이스 픽셀 단위로 새로운 borderBox 크기를 가지는 객체다.

 

contentRect

- 콜백 함수가 실행될 때 관찰된 요소의 새로운 크기를 포함하는 DOMRectReadOnly 객체다.

 

target

- ResizeObserver 객체가 관찰하고 있는 참조 대상이다.

 

3. ResizeObserver.disconnect()

disconnect() 메서드는 ResizeObserver 객체가 관찰하는 모든 관찰 요소를 해제한다. 즉, discoonect() 메서드를 호출하면 사이즈가 변경되었을 때, ResizeObserver() 생성자에 전달된 Callback 함수가 실행되지 않는다.

observer.disconnect();

 

4. ResizeObserver.observe()

observer() 메서드는 ResizeObserver 객체가 관찰을 시작하도록 한다. observer() 메서드를 호출하지 않으면 textarea의 크기가 변경되더라도 ResizeObserver 객체에 전달된 콜백 함수가 실행되지 않는다.

observer(target, options);

target

- 관찰하려는 Element 요소이며 React에서는 ref 객체를 전달한다.

 

options(생략 가능)

- 관찰에 대한 옵션을 설정할 수 있는 옵션 객체다.

 

두 번째 매개변수인 options은 웹에서 사용되는 경우가 거의 없기에 자세한 설명은 생략한다.

 

5. ResizeObserver.unobserve()

disconnect() 메서드가 모든 요소의 관찰을 해제한다면, unobserver() 메서드는 특정 요소의 관찰을 해제한다.

observer.unobserver(target)

target

- 관찰을 해제하려는 Element 요소를 전달한다.

반응형

React에서 ResizeObserver 객체 사용

아래는 React에서 ResizeObserver 객체를 사용하는 간단한 예시로 textarea의 크기를 조절하면 변경된 크기가 콘솔에 출력된다.

1. useRef()를 사용하여 textarea를 참조한다.

2. useEffect()의 두 번째 인자로 빈 배열을 전달하여 컴포넌트가 처음 마운트될 때, ResizeObserver 객체가 textarea를 관측하도록 한다.

 

위 예제는 정상적으로 실행되지만 ResizeObserver 객체가 첫 마운트 되는 useEffect()에 선언되었으며 관측을 종료하는 unobserve(), disconnect() 메서드가 존재하지 않는다. 따라서, textarea의 크기를 변경하면 ResizeObserver 생성자 함수에 전달된 콜백함수를 중단할 수 없는 문제가 발생한다.

 

아래 예제는 관측 시작&종료를 구분할 수 있는 isObserve라는 state를 선언하고 ResizeObserver 객체를 state로 선언한다. 그리고 useEffect()에서 관측 시작&종료를 구분하는 isObserve의 값이 변경되면 ResizeObserver 객체의 관측을 시작하거나 종료한다.

'Resize 활성화' 버튼을 클릭하면 textarea의 관측을 시작하고 변경된 크기를 콘솔에 출력한다. 반대로 'Resize 비활성화' 버튼을 클릭하면 textarea의 관측을 종료하므로 변경된 크기를 콘솔에 출력하지 않는다.

 

문제점

위 예제는 정상적으로 실행되지만 두 가지 문제점이 존재한다.

 

첫 번째 문제점은 App라는 컴포넌트에 너무 많은 기능이 포함되어 있다. 위 예제는 간단한 예제라서 문제가 안될 수 있지만 규모가 큰 컴포넌트라고 가정했을 때, 코드가 굉장히 길어질 수 있다. ResizeObserver 객체를 초기화하는 useState() 때문에 코드가 길어 보이며 Element의 크기를 감지해야 하는 프로세스가 많다면 재사용할 수 있도록 코드를 분리하는 것이 좋다.

 

두 번째 문제점은 사이즈를 1px이라도 변경하면 ResizeObserver() 생성자에 전달된 콜백 함수가 실행된다는 것이다. 콜백 함수가 복잡한 코드로 구성이 되어 있거나 API를 호출하는 프로세스가 존재한다면 너무 빈번한 호출로 인해 웹 페이지가 비정상적으로 동작하거나 멈출 수 있다. 따라서, 콜백 함수를 제어할 수 있는 기능이 필요하다.

 

첫 번째 문제점을 해결하기 위해 Custom Hook을 사용하며, 두 번째 문제점은 Custom Hook에 Debounce를 적용하는 방법을 보여준다.

 

해결방안 1. Custom Hook

먼저 Custom Hook을 구현해야 하는 합당한 이유를 살펴보자.

 

첫 번째 이유는 위에서 말했듯이 App 컴포넌트에 ResizeObserver 객체를 초기화하는 코드가 길어 보인다. 따라서, App 컴포넌트의 정확한 기능&목적을 파악할 수 없는 문제가 발생한다.

 

두 번째 이유는 ResizeObserver 객체를 사용해야 하는 또 다른 컴포넌트가 존재할 수 있기 때문이다. 재사용성을 고려하여 코드를 분리하는 것이 좋다.

 

따라서 다음 기능이 포함된 Custom Hook을 구현한다.

  • Custom Hook에서 ResizeObserver 객체를 초기화한다.
  • Custom Hook에서 ResizeObserver 객체의 관측 활성화&비활성화 로직을 구현한다.
  • useCallback()을 사용하여 관측하고 있는 Element의 크기가 변경될 때마다 콜백 함수를 호출하도록 한다. 따라서, 컴포넌트마다 Element의 크기가 변경되었을 경우 호출하고자 하는 함수를 다르게 설정할 수 있다.
  • 변경된 width, height, top, left를 반환하도록 한다.

아래는 위 기능이 포함된 Custom Hook 예제이며, App 컴포넌트의 코드가 짧아진 것을 확인할 수 있다.

참고로 App 컴포넌트에서 isObserve가 false로 초기화된 경우 Custom Hook인 useResizeObserver()에 전달된 콜백 함수가 실행되지 않으므로 width, height, top, left가 undefined라는 것을 주의해야 한다.

 

해결방안 2. Custom Hook + Debounce

Debounce는 이벤트 또는 함수가 실행되었을 때 일정 시간을 기다렸다가 이벤트를 수행하도록 하는 기법이다. 예를 들어, 일정 시간을 5초라고 설정하고 5초 이내에 특정 함수를 10번 호출했을 경우 마지막에 호출한 함수만 실행된다.

 

Custom Hook에서 콜백 함수를 호출하는 부분에 Debounce를 적용하면 되므로 Custom Hook에 작성된 코드를 이해했다면 Debounce가 적용된 코드도 쉽게 이해할 수 있다.

 

참고 자료

[1] stackoverflow - Resize event for textarea?

[2] ye-yo.github - Debounce와 Throttle

[3] mong-blog - [JS] 크기 변화를 감지하는 두 가지 방법(resize, ResizeObserver)

[4] medium - React의 렌더링 퍼포먼스 개선기 (부제: 수백개의 아이템을 가진 리스트를 개선하기)

[5] mozilla - ResizeObserver

[6] web.dev - ResizeObserver: it's like document.onresize for elements

[7] velog - useEffect 의존성에 ref를 담을 때마다 찜찜하신 분들을 위해

반응형