
🚨 Next.js + GSAP 사용 시 Failed to execute 'removeChild' on 'Node' 에러 원인과 해결
gsap의 섹션 고정 후 가로 스크롤 구현을 한 후, 다른 페이지(라우터 이동 시(router.push 혹은 next/link 태그) )로 이동할 때 에러가 발생했다.

처음엔 React 쪽 문제인가 했는데,
결론부터 말하면 GSAP 애니메이션과 Next.js 라우팅 타이밍 충돌 문제였다.
🔥 에러 원인
GSAP 애니메이션이 끝나기도 전에 페이지 전환(라우팅)이 일어나면서 발생하는 전형적인 충돌이다.
React는 페이지 이동 시 기존 DOM을 정리하려고 하고,GSAP은 동시에 DOM을 직접 조작하고 있다.
이 상태에서 DOM 구조가 꼬이면 React가
“이 노드… 내가 알던 자식이 아닌데?”
하면서 removeChild 에러를 던짐
특히 ScrollTrigger + pin: true 를 쓰면 이 문제가 더 잘 터진다.
무슨 소리나면...
나는 pin을 사용해 해당 섹션을 고정 중이었다.
const triggerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!isClient || !triggerRef.current || !horizontalRef.current || isMobile)
return
const totalWidth = horizontalRef.current.scrollWidth
const viewportWidth = window.innerWidth
const scrollDistance = totalWidth - viewportWidth
const ctx = gsap.context(() => {
// 메인 타임라인: 섹션 고정 + 가로 스크롤 + 라인 애니메이션 동기화
const tl = gsap.timeline({
scrollTrigger: {
trigger: triggerRef.current,
pin: true,
scrub: 1,
start: 'top top',
end: () => `+=${totalWidth}`,
invalidateOnRefresh: true,
},
})
tl.to(
horizontalRef.current,
{
x: -scrollDistance,
ease: 'none',
},
0,
)
tl.fromTo(
lineRef.current,
{ scaleX: 0, transformOrigin: 'left center' },
{ scaleX: 1, ease: 'none' },
0,
)
}, triggerRef)
return () => ctx.revert()
}, [isClient, isMobile])
<section
ref={triggerRef}
className="relative h-screen w-full overflow-hidden bg-slate-950"
>
🧨 진짜 문제의 핵심: ScrollTrigger.pin
1️⃣ GSAP의 Pin 동작 (DOM 강제 변경)
pin: true를 설정하면 GSAP은 DOM 구조를 직접 바꾼다.
원래 구조)
<section ref={sectionRef}>
...
</section>
GSAP 실행 후)
<div class="pin-spacer">
<section ref={sectionRef}>
...
</section>
</div>
아래 사진을 보면 GSAP이 pin-spacer라는 div를 임의로 만들고,
기존 section을 그 안으로 집어넣어 버린다.

2️⃣ React의 당혹감 🤯
React는 자신이 렌더링한 DOM 구조를 Virtual DOM으로 기억하고 있다.
React 입장에서는 구조가 이랬다 👇
부모
└ section
그런데 실제 브라우저 DOM은 GSAP 때문에 이렇게 바뀐다 👇
부모
└ pin-spacer
└ section
이 상태에서 라우팅이 발생하면 React는
“자 이제 section을 지워야지”
하고 기존 부모에게 removeChild(section)을 시도함.
하지만 실제 부모는 이미 pin-spacer 로 바뀌어 있음
👉 "이건 내가 알던 자식이 아닌데?"
👉 removeChild 에러 발생하는 것!!!
✅ 해결 방법 1: gsap.context() + revert() (기본 중의 기본)
이미 ctx.revert()를 쓰고 있었지만,
페이지 이동 시 ScrollTrigger를 확실히 정리해주는 게 중요하다.
useEffect(() => {
const ctx = gsap.context(() => {
// GSAP 애니메이션 로직
}, sectionRef);
return () => {
// 모든 ScrollTrigger 강제 제거
ScrollTrigger.getAll().forEach(t => t.kill());
ctx.revert();
};
}, []);
👉 이건 필수 세팅
하지만 이거만으로는 pin 관련 문제를 완전히 못 잡는 경우도 있다.
✅ 해결 방법 2: pin 대상 바깥에 div 하나 더 감싸기 (✔ 실제 해결)
구조를 이렇게 바꿨다 👇
<div> {/* 고정되지 않는 상위 div */}
<section ref={sectionRef}>
...
</section>
</div>
왜 이게 해결이 됐을까?
GSAP은 이제 section 안에서만 DOM을 바꾼다.
div (React 기준 최상위)
└ pin-spacer
└ section
React 입장에서는
1. 최상위 div 구조는 절대 안 바뀜
2. 페이지 이동 시 그냥 div 통째로 제거하면 끝
3. GSAP이 안에서 무슨 짓(pin-spacer 생성)을 하든 React는 부모-자식 관계가 깨지지 않아서 에러가 안 나는 것 👍
결론: pin 쓸 땐 section을 최상위로 두지 말고, wrapper div를 두자!!!!!!
'✨FRONTEND > 📍Next.js' 카테고리의 다른 글
| 내가 pnpm을 쓰는 이유 (0) | 2026.02.13 |
|---|---|
| Next.js App Router에서 React Query 쓰면서 metadata 쓰기 (0) | 2025.09.06 |
| Next.js App Router + Tailwind + Prettier + ESLint + husky 세팅하기 (2) | 2025.05.17 |
| 프로필 이미지 업로더 구현하기(feat. 이미지 미리보기) (1) | 2025.05.10 |
| 검색 기능 구현(feat. 디바운스) (0) | 2025.04.07 |