cloneNode API 를 활용한 성능개선기타임라인에서 페이지를 렌더링할 때 N개의 PageRenderer가 각각 마운트되어 발생하는 성능 문제를 개선작성일 2025.06.10페이지가 생성된 시간 2026.02.03 00:47:07
1
cloneNode란 무엇인가?
기본 개념
cloneNode는 DOM API의 한 메서드로, 기존 DOM 노드를 복사하는 기능을 제공한다. React 컴포넌트처럼 매번 새로 마운트하지 않고, 이미 렌더링된 DOM 요소를 복사해서 재사용할 수 있다.
cloneNode 사용법
const original = document.getElementById("original");
const cloned = original.cloneNode(true); // true: 자식 노드까지 복사
document.body.appendChild(cloned);
cloneNode의 장점
- 마운트 비용 절감: 새로운 컴포넌트 마운트 과정을 건너뛴다
- DOM 조작 속도: 기존 노드 구조를 그대로 복사하므로 빠르다
- 메모리 효율: 컴포넌트 인스턴스가 아닌 DOM 노드만 복사
PageRenderer란 무엇인가?
기본 개념
PageRenderer는 페이지 모델을 받아서 DOM을 그리는 렌더러 컴포넌트다. 캔버스에서 페이지(Page 모델)가 가진 모든 노드(텍스트, 이미지, 도형 등)를 실제 DOM 요소로 변환해서 화면에 표시한다.
동작 원리

PageRenderer 특징
- 무거운 초기화: 페이지 모델의 모든 노드를 순회하며 DOM 생성
- 재귀적 렌더링: 하위 노드까지 재귀적으로 DOM 트리 구성
- 마운트 비용 큼: 복잡한 페이지일수록 마운트에 시간이 오래 걸림
문제: PageRenderer 다중 마운트로 인한 성능 저하

AS-IS: N개의 PageRenderer 마운트
기존 구현에서는 페이지가 반복되는 만큼 PageRenderer를 각각 마운트했다.
// 기존 코드
<PageNodeWrapper>
{Array.from({ length: pageCount }).map((_, idx) => (
<PageRenderer key={idx} node={page} />
))}
</PageNodeWrapper>
문제점
- 마운트 비용: 페이지 반복 횟수만큼 PageRenderer가 마운트됨
- INP 저하: 텍스트 요소 60개 기준 INP 1832ms로 성능 저하 (cpu 쓰로틀 상태)
- 불필요한 초기화: 동일한 페이지를 N번이나 초기화
해결책: PageRepeat 컴포넌트와 cloneNode 활용
아키텍처 변경
변경 전

변경 후

PageRepeat 컴포넌트 구현
export const PageRepeat = ({ page, pageCount, pageRatio }) => {
const pageId = useMemo(() => page.getId(), [page]);
const containerRef = useRef(null);
const { updateKey, forceUpdate } = useForceUpdate();
// 첫 번째 노드 복제로 나머지 생성
useEffect(() => {
const container = containerRef.current;
if (!container || updateKey === undefined) return;
const clonedNodes = [];
Array.from({ length: pageCount - 1 }).forEach(() => {
const cloned = container.firstChild?.cloneNode(true);
container.appendChild(cloned);
clonedNodes.push(cloned);
});
return () => {
clonedNodes.forEach((node) => container.removeChild(node));
};
}, [updateKey, pageCount]);
// 페이지 뷰 로드 감지
useEffect(() => {
const subscription = eventManager.subscribe("PAGE_LOADED", (event) => {
if (event.pageId === pageId) forceUpdate();
});
return () => subscription.unsubscribe();
}, [pageId, forceUpdate]);
return (
<PageNodeWrapper ref={containerRef} pageRatio={pageRatio}>
<PageRenderer node={page} />
</PageNodeWrapper>
);
};
사용처에서 적용
// 변경 후
<PageRepeat page={page} pageCount={pageCount} pageRatio={pageRatio} />
주요 개선 사항
성능 최적화
| 항목 | 변경 전 | 변경 후 |
|---|---|---|
| PageRenderer 마운트 | pageCount 만큼 | 1개만 |
| 초기화 비용 | N번 수행 | 1번만 수행 |
| INP (텍스트 60개) | 1832ms | 296ms |
| 마운트 방식 | map으로 개별 마운트 | cloneNode로 복사 |
코드 구조 개선
- 관심사 분리: 페이지 반복 로직을 PageRepeat 컴포넌트로 분리
- 업데이트 전략: RxJS Observable로 효율적인 업데이트 처리 (이 글에서는 생략)
cloneNode 활용 전략
- 첫 번째 노드만 정상적으로 마운트
- 나머지는 cloneNode로 DOM만 복사
- 업데이트가 필요할 때 전체 재생성
결과
사용자 경험 개선
- 부드러운 스크롤: 타임라인 스크롤 시 프리징 크게 감소
- 빠른 초기화: 페이지 로딩 속도 향상
- 낮은 INP: 텍스트 요소 60개 기준 1832ms → 296ms (83.8% 개선)
기술적 배운 점
cloneNode 사용 시나리오
- 동일한 컴포넌트 반복: 내용이 같은 UI가 여러 번 필요할 때
- 마운트 비용이 큰 컴포넌트: PageRenderer처럼 초기화에 시간이 걸리는 경우
- 정적 콘텐츠: 동적 업데이트보다 표시가 주 목적인 경우
cloneNode 주의사항
- 이벤트 핸들러: 복사된 노드는 이벤트 핸들러를 따로 바인딩해야 할 수 있음
- React 상태: 복사된 DOM은 React 상태와 연결되지 않음
업데이트 처리 전략
// 디자인 변경 감지
pageChange$
.pipe(filter((event) => event.pageId === pageId))
.subscribe(() => forceUpdate());
// 페이지 뷰 로드 감지
eventManager.subscribe("PAGE_LOADED", (event) => {
if (event.pageId === pageId) forceUpdate();
});
적절한 Observable과 이벤트를 구독해서 변경 이벤트를 감지하고, forceUpdate로 전체 재생성한다.
정리
cloneNode를 활용하여 PageRenderer 마운트를 최적화한 결과다.
- 마운트 비용 절감: N개에서 1개로 마운트 횟수 감소
- 성능 개선: INP 1832ms → 296ms (83.8% 개선)
- 코드 구조 개선: PageRepeat 컴포넌트로 반복 로직 분리
- 업데이트 전략: RxJS Observable로 효율적인 업데이트 처리
cloneNode는 DOM 복사라는 간단한 방식으로 렌더링 성능을 크게 개선할 수 있는 강력한 도구다. 특히 동일한 컴포넌트를 여러 번 렌더링해야 하는 상황에서 효과적이다.