-
Notifications
You must be signed in to change notification settings - Fork 1
✨ 차트의 반응형 구현과 useRef 타입 문제
분야 | 작성자 | 작성일 |
---|---|---|
FE | 조배경 | 24년 11월 12일 |
차트 라이브러리 코드를 보던 중 차트의 반응형 디자인을 고려한 코드가 있었다.
왜 반응형 디자인을 고려해야할까?
이렇게 잘 배치된 그래프라 하더라도 화면의 크기를 줄인다면,
줄여진 화면 크기에 즉각 반응하지 않고 다른 범위를 침범한다.
그리고 새로고침을 해야만 그래프가 제 방향대로 배치된다.
이 현상을 해결하기 위해 useChartResize
라는 훅으로 따로 분리해 개선하고자 했다.
// 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는 사용자가 언제 변경했는지 알지 못한다.
const resizeObserver = useRef<ResizeObserver>();
-
컨테이너의 크기 변화를 감지하기 위해 useRef로 선언하며 ResizeObserver 타입을 부여한다.
-
ResizeObserver
요소의 콘텐츠 또는 테두리 상자의 크기 또는 SVGElement의 경계 상자에 대한 변경 사항을 보고한다. (mdn)
- 브라우저의 크기 변화를 감지하는 window의 resize 이벤트와 달리, 특정 요소 자체의 크기 변화를 감지할 수 있다.
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
를 통해 요소의 크기, 위치 정보 등을 제공받을 수 있다.
-
- 제공받은
width
와height
를 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.current.observe(containerRef.current);
-
observe
메서드를 통해 containerRef를 관찰하도록 지시한다. - 따라서 containerRef의 크기가 변경될 때마다, ResizeObserver에 설정한 콜백함수가 실행된다.
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]);
};
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<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을 쓰는 게 낫지 않을까? 하고 의문이 생겼다.
하지만 이 경우는 의도적으로 값이 없는 것이 아닌, 초기에 아직 할당되지 않은 것인 옵셔널한 상태인 것이다.
참고 레퍼런스
[JS] 크기 변화를 감지하는 두 가지 방법(resize, ResizeObserver)
- 🚩 FE 기술 선택이유
- ✨ 차트의 반응형 구현과 useRef 타입 문제
- 🐣 부모 요소의 상태에 따라 자식 요소도 스타일 변화 부여하기
- 📁 zod 도입하기
- 🔖 useInfiniteQuery를 사용한 그래프 무한스크롤 구현
- 🎫 사용자의 시점 변화 없는 그래프 스크롤 구현하기
- 🧪 수많은 그래프 데이터 요청을 어떻게 줄일까
- 🌚 다크모드에서 새로고침 시 라이트모드가 잠깐 보이는 문제
- 👊 웹소켓의 채팅 데이터와 REST API의 채팅 데이터를 함께 관리하기
- 📡 BE 기술 선택 이유
- ⛏️ Node WebSocket 파고들기
- ✏️ TypeORM Datasource mock 만들기
- ☁️ oauth ID range 문제
- 📖 custom pipe에서 Nan이 받아지는 문제
- 🪒 nest Websocket에 세션이 안된다고?
- 🏴 nginx websocket 연결 시 문제 발생
- 🆘 WebPush 구현
- 🧊 우선순위 큐로 요청 제어하기
- 🔌 websocket이 늦게 할당되어 발생되는 문제
- 🥳 typeorm을 이용한 FCM 알림 서비스
- 🚦 다중 유저 동시성 제어 ‐ 싱글톤, 뮤텍스
- 🍙 그래프 데이터를 실시간으로 제공하기위한 전략