Skip to content

✨ 차트의 반응형 구현과 useRef 타입 문제

baegyeong edited this page Nov 29, 2024 · 2 revisions
분야 작성자 작성일
FE 조배경 24년 11월 12일

useChartResize 구현

하지만 useRef 타입과 관련된 문제가 발생한다.


useChartResize 구현

차트 라이브러리 코드를 보던 중 차트의 반응형 디자인을 고려한 코드가 있었다.

왜 반응형 디자인을 고려해야할까?

이렇게 잘 배치된 그래프라 하더라도 화면의 크기를 줄인다면,


줄여진 화면 크기에 즉각 반응하지 않고 다른 범위를 침범한다.

그리고 새로고침을 해야만 그래프가 제 방향대로 배치된다.


이 현상을 해결하기 위해 useChartResize 라는 훅으로 따로 분리해 개선하고자 했다.


interface 설정 및 훅 선언

// frontend/src/pages/stock-detail/hooks/useChartResize.ts
interface UseChartResize {
  containerRef: RefObject<HTMLDivElement>;
  chart: RefObject<IChartApi>;
}

export const useChartResize = ({ containerRef, chart }: UseChartResize) => {
 // ...
}
  • 우선 차트 컨테이너 요소의 ref인 containerRef와 차트 인스턴스의 chart를 prop으로 받는다.

  • 차트의 크기를 동적으로 변화시키기 위해 직접 DOM을 조작해야하기 때문에 useRef를 사용하고, useRef로 선언될 containerRef는 useRef의 타입인 RefObject로 선언한다.

    💡 useRef란?

    렌더링에 필요하지 않은 값을 참조할 수 있고, 초기값으로 설정된 단일 current 프로퍼티가 있는 ref 객체를 반환한다. ref를 변경해도 리렌더링을 촉발하지 않으며, DOM을 조작할 수 있다ref는 자바스크립트 객체이기 때문에 react는 사용자가 언제 변경했는지 알지 못한다.


resizeObserver 설정

const resizeObserver = useRef<ResizeObserver>();
  • 컨테이너의 크기 변화를 감지하기 위해 useRef로 선언하며 ResizeObserver 타입을 부여한다.

  • ResizeObserver

    요소의 콘텐츠 또는 테두리 상자의 크기 또는 SVGElement의 경계 상자에 대한 변경 사항을 보고한다. (mdn)

    • 브라우저의 크기 변화를 감지하는 window의 resize 이벤트와 달리, 특정 요소 자체의 크기 변화를 감지할 수 있다.

변화된 DOM의 크기 정보를 얻어서 적용하기

useEffect(() => {
  if (!containerRef.current || !chart.current) return;
  
	resizeObserver.current = new ResizeObserver((entries) => {
		const { width, height } = entries[0].contentRect;
	
		chart.current?.applyOptions({ width, height });
	// ...
  • useRef는 .current 프로퍼티에 내부의 값을 업데이트 하기 때문에, resizeObserver.current 에는 ResizeObserver() 을 할당한다.
  • ResizeObserver()
    • ResizeObserver 콜백함수로는 관찰할 대상 요소(ResizeObserverEntry)를 넘겨준다.
    • contentRect를 통해 요소의 크기, 위치 정보 등을 제공받을 수 있다.
  • 제공받은 widthheight를 chart에 적용한다.
    • applyOptions: 차트에 새로운 옵션을 적용하는 lightweight-charts 라이브러리의 메서드

레이아웃 계산하여 재배치하기

// ...
  requestAnimationFrame(() => {
    chart.current?.timeScale().fitContent();
  });
 //...
  • 차트에 새로운 크기 옵션을 지정했다면, 화면에 반영할 차례다.

    • timeScale: Returns API to manipulate the time scale
    • fitContent: Automatically calculates the visible range to fit all data from all series.
    • 차트의 시간 스케일을 조정한다.
  • 만약 새로운 크기 옵션 지정 후 바로 화면에 반영하려 하면, 동시에 실행되어 레이아웃 계산이 부정확해질 수 있다. 따라서 차트 크기 변경이 완전히 적용된 후에 내용을 맞추는 게 좋다.

  • 따라서 requestAnimationFrame을 적용했다.

    💡 requestAnimationFrame

    함수가 실행되면 브라우저는 다음 프레임이 그려지기 전에 함수를 실행하도록 예약한다.

    따라서 각 프레임이 16.6ms 간격으로 렌더링되게 한다.

    • setInterval과 달리, 백그라운드에서는 동작하지 않기 때문에 CPU 리소스나 배터리 수명을 낭비하지 않게 된다.
    • 또한 setInterval은 시간 간격을 정해두어 호출 횟수를 설정하지만, 모니터의 주사율에 따라 최적화할 수 있다.
    • 그렇기 때문에 setInterval은 delay만 지나면 repaint를 요청하지만, rAF는 다음 repaint가 진행되기 전에 애니메이션 업데이트를 요청하기 때문에 순서가 보장될 수 있다.

ResizeObserver에게 특정 DOM요소를 관찰하도록 지시하기

resizeObserver.current.observe(containerRef.current);
  • observe 메서드를 통해 containerRef를 관찰하도록 지시한다.
  • 따라서 containerRef의 크기가 변경될 때마다, ResizeObserver에 설정한 콜백함수가 실행된다.

cleanup

return () => {
  resizeObserver.current?.disconnect();
};
  • 컴포넌트가 언마운트되거나 의존성이 변경될 때, disconnect()를 통해 관찰을 중지한다.
  • useEffect 내부에는 return문을 통한 cleanup을 통해 메모리 누수를 방지하고 정리할 수 있다.

전체 코드

// frontend/src/pages/stock-detail/hooks/useChartResize.ts
import { IChartApi } from 'lightweight-charts';
import { useEffect, useRef, type RefObject } from 'react';

interface UseChartResize {
  containerRef: RefObject<HTMLDivElement>;
  chart: RefObject<IChartApi>;
}

export const useChartResize = ({ containerRef, chart }: UseChartResize) => {
  const resizeObserver = useRef<ResizeObserver>();

  useEffect(() => {
    if (!containerRef.current || !chart.current) return;

    resizeObserver.current = new ResizeObserver((entries) => {
      const { width, height } = entries[0].contentRect;

      chart.current?.applyOptions({ width, height });

      requestAnimationFrame(() => {
        chart.current?.timeScale().fitContent();
      });
    });

    resizeObserver.current.observe(containerRef.current);

    return () => {
      resizeObserver.current?.disconnect();
    };
  }, [chart, containerRef]);
};

하지만 useRef 타입과 관련된 문제가 발생한다.

TradingChart 컴포넌트에서, useChartResize 훅을 사용하려하니 문제가 발생했다.

// frontend/src/pages/stock-detail/TradingChart.tsx
// ...
const containerRef = useRef<HTMLDivElement>(null);
const chart = useChart({ containerRef, theme });
useChartResize({ containerRef, chart });
  • useChartResize의 매개변수 중 chart에 ‘'MutableRefObject<IChartApi | undefined>' 형식은 'RefObject' 형식에 할당할 수 없습니다.’ 에러가 발생했다.
  • useRef에도 MutableRefObject, RefObject 등 다양한 타입이 존재한다는 것을 알게 되었다.

useRef의 타입

  • useRef<T>(initialValue: T): MutableRefObject<T>;
    • current 프로퍼티 그 자체를 직접 변경할 수 있다.
  • useRef<T>(initialValue: T|null): RefObject<T>;
    • 인자의 타입이 null을 허용하는 경우, RefObject를 반환한다.
    • current 프로퍼티를 직접 수정할 수 없다.
    • 즉 null을 할당하는 경우 current 프로퍼티가 readonly이다. 하지만 current의 하위 프로퍼티는 여전히 수정 가능하다.
  • useRef<T = undefined>(): MutableRefObject<T | undefined>;
    • 제네릭의 타입이 undefined인 경우(타입을 제공하지 않은 경우), MutableObject<T | undefined>를 반환한다.

이를 바탕으로 코드를 다시 살펴보자!

const chart = useChart({ containerRef, theme });

// frontend/src/pages/stock-detail/hooks/useChart.ts
export const useChart = ({ containerRef, theme }: UseChartProps) => {
  const chart = [useRef](https://ko.react.dev/reference/react/useRef)<IChartApi>();
  
  useEffect(() => {
	  chart.current = createChart( //...
  //...
  
  return chart;
}

인자로 넘긴 chart의 타입은 useRef<IChartApi>()로 선언했기 때문에, MutableRefObject<IChartApi | undefined를 반환한다.

따라서 current 프로퍼티를 직접 수정할 수 있다.

interface UseChartResize {
  containerRef: RefObject<HTMLDivElement>;
  chart: RefObject<IChartApi>;
}

export const useChartResize = ({ containerRef, chart }: UseChartResize) => {
	// ...
}

useChartResize의 매개변수인 chart 타입은 RefObject<IChartApi> 로 current의 속성이 IChartAPI 또는 null일 수 있다.

즉 넘기려는 chart의 타입은 MutableRefObject<IChartApi | undefined>이고, 받는 쪽에서는 RefObject<IChartApi | null> 이기 때문에 에러가 발생한 것이다.

chart의 경우는 변경이 가능해야 하기 때문에, readonly인 RefObject말고 MutableRefObject로 통일하는 게 좋겠다.

useChartResize의 interface prop의 chart를 MutableRefObject<IChartApi | undefined>로 변경하여 해결하였다.


여기 생긴 의문

명시적으로 값이 없음을 나타내려면 undefined보다 null을 쓰는 게 낫지 않을까? 하고 의문이 생겼다.

하지만 이 경우는 의도적으로 값이 없는 것이 아닌, 초기에 아직 할당되지 않은 것인 옵셔널한 상태인 것이다.


참고 레퍼런스

useRef

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

[Javascript] 자바스크립트 애니케이션, requestAnimationFrame

Typescript React에서 useRef의 3가지 정의와 각각의 적절한 사용법

🐜 팀 개미

🏛️ 팀 문화

개발 위키

FE

BE

Infra

🗣️ 발표

📚 회의록

🔴 인터미션
🟠 1주차
🟡 2주차
🟢 3주차
🔵 4주차
🟣 5주차
🟤 6주차

💭 회고

🧑‍🤝‍🧑 멘토링

Clone this wiki locally