Next.js

Next.js에서 GSAP 최적화하기: 중앙집중화로 성능 개선

해보구 2025. 1. 22. 17:05

 

최근 Next.js 프로젝트에서 GSAP를 활용해 섹션마다 애니메이션을 적용했는데, 모바일에서 로딩이 너무 느려서 LCP가 30초 이상 찍히기도 하고, Hydration Error까지 발생했다. 다음 버전으로 마이그레이션을 했더니 어느 정도 성능은 좋아졌지만, 여전히 스크롤 트리거가 제대로 동작하지 않거나, 불필요하게 렌더링이 많이 일어나는 문제가 있었다. 특히 섹션별로 GSAP 초기화를 각각 따로 두면서 “use client”를 남발하다 보니 코드가 복잡해지고 성능이 떨어진 느낌이었다.

 

해결 방안

처음에는 “애니메이션 초기화만 따로 떼어 놓으면 되지 않을까?” 하고 단순하게 생각했다. 그런데 막상 적용해보니 각 컴포넌트에서 불필요한 “use client” 선언이 많고, ScrollTrigger를 초기화할 때마다 중복 호출이 생겨서 Total Blocking Time(TBT)이 올라갔다.
그래서 중앙집중화 방법을 쓰기로 했다. 즉, HomeAnimation.jsx라는 파일에 gsap과 ScrollTrigger 초기화 로직을 몰아 넣고, 섹션마다 필요한 ref만 넘기는 방식으로 구조를 바꿨다.

아래처럼 PhotosSection 안에서 이미지가 전부 로딩된 뒤 ScrollTrigger.refresh()를 다시 호출하도록 했는데, 이 부분이 특히 큰 효과가 있었다.

<Swiper
  onImagesReady={() => {
    import("gsap/ScrollTrigger").then(({ ScrollTrigger }) => {
      ScrollTrigger.refresh();
    });
  }}
>
  {/* 슬라이드들 */}
</Swiper>
 

이 덕분에 늦게 로딩되는 이미지 때문에 스크롤 위치나 pin이 망가지는 일이 줄어들었다.

 

결과

  • 중앙집중화 이전에는 LCP가 3.6초, Speed Index가 15.6초였는데, 중앙집중화와 onImagesReady 콜백을 적용했더니 LCP가 2.6초, Speed Index가 5.6초까지 줄었다.
  • “use client”를 최소한으로만 쓰게 되면서, 불필요한 리렌더링이 줄어든 것 같다.
  • 개별 섹션의 코드가 훨씬 간결해졌고, ScrollTrigger를 여기저기서 중복으로 초기화하지 않으니 TBT도 함께 개선됐다.

 


회고

처음에는 Next.js 버전을 내리거나 올리기만 하면 문제가 해결될 줄 알았는데, 단순히 구조를 바꾸는 것만으로는 부족했다. 오히려 마이그레이션 도중에 Total Blocking Time이 올라서 더 느려지는 경험도 했다.
결국 핵심은 GSAP와 Next.js를 잘 조합해 쓰는 “방법” 자체였던 것 같다. 특히, 이미지를 쓰는 섹션이 느리게 로딩되는 경우에는 스크롤 트리거를 다시 계산해 주어야 애니메이션이 안 깨진다는 걸 배웠다.
앞으로는 섹션별로 애니메이션을 붙일 때, 컴포넌트 하나하나에 다 “use client”와 ScrollTrigger 등록을 때려넣는 게 아니라, 한곳에서 깔끔하게 관리하면서 섹션에 props만 전달하는 식으로 유지보수를 해야겠다. 그게 훨씬 편리하고 성능에도 좋은 것 같다.