더보기 버튼을 렌더링 시키는 커스텀 훅이 있다. 댓글(혹은 답글)의 내용이 컴포넌트의 높이를 넘칠때에만 더보기 버튼을 렌더링 시켜줘야하는데, 유저가 브라우저의 가로 길이를 늘리거나 줄이는 등 변하게 할 수 있기 때문에 이를 대응하기 위해 동적으로 컨텐츠의 높이를 계산해야 할 필요가 있었다.
코드를 살펴보도록 하자
import { useEffect, useRef, useState } from "react";
import useDevice from "@/hooks/useDevice";
export default function useNeedMoreButton(
type: "talk" | "reply",
showSpoiler?: boolean,
) {
const { device } = useDevice();
const contentRef = useRef<HTMLParagraphElement>(null);
const [contentHeight, setContentHeight] = useState(0);
const [showMoreButton, setShowMoreButton] = useState(false);
useEffect(() => {
const handleResizeContentHeight = () => {
if (type === "talk") {
if (showSpoiler && contentRef.current) {
setContentHeight(contentRef.current.scrollHeight);
}
} else {
if (contentRef.current) {
setContentHeight(contentRef.current?.scrollHeight);
}
}
};
handleResizeContentHeight();
// resize 이벤트가 발생할 때마다 handleResizeContentHeight를 호출하여 contentHeight를 동적으로 계산
addEventListener("resize", handleResizeContentHeight);
return () => removeEventListener("resize", handleResizeContentHeight);
}, [contentRef, showSpoiler, type]);
useEffect(() => {
const isShowMoreButtonNeeded = (contentHeight: number) => {
const maxHeight = device === "mobile" ? 63 : 72;
return contentHeight > maxHeight;
};
setShowMoreButton(isShowMoreButtonNeeded(contentHeight));
}, [contentHeight, device]);
return { contentRef, showMoreButton };
}
이 커스텀 훅을 사용하는 컴포넌트에서 "Maximum update depth exceeded" 오류가 발생했다. 이 오류는 컴포넌트가 내부 상태를 업데이트하는 동안 발생했다. 이 컴포넌트는 사용자가 창 크기를 변경할 때마다 resize 이벤트를 통해 실행되는 함수 내에서 setState를 호출하여, 컴포넌트의 높이 상태를 업데이트하려고 했다. 그러나 이 과정에서 예상치 못하게 컴포넌트가 너무 많은 상태 변화가 일으켰고, 이는 React가 정한 업데이트 깊이 한계를 초과하여 오류를 발생시켰다.
이 문제의 원인은 setContentHeight 함수 호출이 반복적으로 이루어졌기 때문이다. setContentHeight는 resize 이벤트 리스너 내에서 호출되며, 이 함수는 컴포넌트의 상태를 업데이트하는 역할을 한다. 이 상태 업데이트는 컴포넌트의 리렌더링을 트리거하는데, 만약 브라우저 창의 크기 변경이 빠르고 연속적으로 이루어진다면, 이 함수는 계속해서 호출될 수 있다. 특히, 이 컴포넌트에서는 이전 상태와 새로운 높이 값이 동일하더라도 상태를 업데이트하는 로직을 포함하고 있지 않았기 때문에, 높이 값이 변하지 않았음에도 불구하고 불필요하게 상태 업데이트를 계속 수행했다. 이러한 무한 루프는 업데이트 최대 깊이를 초과하게 만들었고, 결국 애플리케이션의 안정성과 성능을 저하시키는 에러로 이어졌다.
이 문제를 해결하기 위한 첫 번째 단계는 불필요한 상태 업데이트를 줄이는 것이었다. 이를 위해, 코드에서 ResizeObserver API를 도입하여 요소의 크기 변화를 감지하고, 이에 따라 상태 업데이트를 트리거하도록 변경했다. ResizeObserver는 타겟 요소의 크기가 실제로 변화할 때만 콜백 함수를 실행하므로, 이벤트 기반의 접근 방식보다 훨씬 효율적이다.
import { useEffect, useRef, useState } from "react";
import useDevice from "@/hooks/useDevice";
export default function useNeedMoreButton(
type: "talk" | "reply",
showSpoiler?: boolean,
) {
const { device } = useDevice();
const contentRef = useRef<HTMLParagraphElement>(null);
const [contentHeight, setContentHeight] = useState(0);
const [showMoreButton, setShowMoreButton] = useState(false);
useEffect(() => {
const contentElement = contentRef.current;
if (!contentElement) return;
// resizeObserver 사용
const resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
const newHeight = entry.target.scrollHeight;
if (type === "talk" && showSpoiler) {
if (contentHeight !== newHeight) {
setContentHeight(newHeight);
}
} else {
if (contentHeight !== newHeight) {
setContentHeight(newHeight);
}
}
});
resizeObserver.observe(contentElement);
return () => {
resizeObserver.unobserve(contentElement);
};
}, [contentHeight, type, showSpoiler]);
useEffect(() => {
const isShowMoreButtonNeeded = (contentHeight: number) => {
const maxHeight = device === "mobile" ? 63 : 72;
return contentHeight > maxHeight;
};
setShowMoreButton(isShowMoreButtonNeeded(contentHeight));
}, [contentHeight, device]);
return { contentRef, showMoreButton };
}
ResizeObserver를 사용하는 전략은 성능 최적화와 불필요한 렌더링 방지에 효과적이다. 이 방법을 통해 애플리케이션의 성능을 향상시키고, "Maximum update depth exceeded" 같은 에러를 방지할 수 있었다. 이러한 접근 방식은 다른 많은 상황에서도 유용하게 적용될 수 있으며, 성능 중심의 애플리케이션 개발에 있어 중요한 기법 중 하나라고 생각한다.