초기 프로젝트 기획 당시에는 스켈레톤 UI는 구현하지 않으려고 했었다. 아무래도 기획 당시에 너무 많은 것들을 구현해야 했어서 이 부분까지 신경쓰면서 UI를 진행할 수 없을 것 같다는 생각이 팀원들의 주 의견이었기 때문이다.
하지만, 프로젝트 마무리에 가까워지면서 프로젝트 성능을 신경쓰게 되었고 그렇게 알게된게 next.js에서 제공하는 dynamic이었다. dynamic을 쓰면 쉽게 로딩 시 화면을 구현할 수 있고, 그때의 skeleton UI를 react-content-loader로 편하게 구현할 수 있어서 스켈레톤 UI까지 도입하게 되었다.
위와 같은 화면이 로딩 전에는 아래와 같이 표시되게 되었다.

Next.js의 dynamic import는 import() 구문을 사용하여 코드 분할(Code Splitting)을 구현하는 방법이다. 이에 대해서는 이전 게시글에서 한 번 정리한 적이 있기 때문에 자세하게는 설명하지 않겠다.
프로젝트를 진행하다보면 코드 길이가 굉장히 길어지게 되는데 이를 유지보수 등을 위해서 코드 분할을 하게 된다. 하지만, 코드 분할을 한다고 해서 성능이 무조건 좋아지는 것은 아니다. 실제로 dynamic을 적용하기 전까지는 코드 분할 전 성능이 더 좋다고 나왔기 때문이다. 결국 분할된 컴포넌트 등을 import 하는 초기 로딩 시간이 추가되기 때문이다. 코드 분할을 해서 유의미하게 사용하기 위해서는 필요한 부분 외에는 lazy loading을 통해 필요할 때 로딩시키는 방법을 사용해야 한다. 이를 위해서 사용하는 게 dynamic이라고 한다.
공식 문서에서는 dynamic을 다음과 같이 표현하고 있다.
next/dynamic is a composite of React.lazy() and Suspense. It behaves the same way in the app and pages directories to allow for incremental migration.
dynamic은 SSR/SSG까지 고려한 동적 로드에 최적화되어 있어 Next.js에서 더 유용하다. 특히 ssr: false를 통해 클라이언트 전용 로드를 쉽게 설정할 수 있다.
하지만 모든 컴포넌트를 dynamic으로 import 하게 되면 오히려 좋지 않은 성능을 보여준다. dynamic import를 사용하면 추가적인 HTTP 요청이 발생하기 때문이다.
import dynamic from 'next/dynamic';
const EventSection = dynamic(
() => import('./_components/_landing/EventSection')
);
const HotCarousel = dynamic(() => import('./_components/_landing/HotCarousel'));
const RecommendCarousel = dynamic(
() => import('./_components/_landing/RecommendCarousel')
);
const NewCarousel = dynamic(() => import('./_components/_landing/NewCarousel'));
위와 같이 사용해주면 된다. 단순히 저 작업들만 해주어도 성능 점수가 많이 오르게 된다.


위 점수가 처음 성능 점수였고, dynamic만 추가해주었을 때의 점수이다. 메인 랜딩 페이지라서 carousel이 굉장히 많아서 성능 점수가 다른 페이지에 비해 현저히 낮은 편이었다.
dynamic에는 여러 모듈이 존재하는데 loading 모듈을 사용하면 로딩 전 화면을 보여줄 수 있다. 즉, 이를 이용해서 스켈레톤 UI를 표시해주면 된다. 직접 구현하는 방식도 있지만, react-content-loader가 간단하고 예쁘게 스켈레톤 UI를 생성할 수 있어서 사용하게 되었다.
import ContentLoader from 'react-content-loader';
const HotRanking = dynamic(() => import('./HotRanking'), {
loading: () => (
<ContentLoader
speed={2}
width={1020}
height={210}
viewBox="0 0 1020 210"
backgroundColor="#f3f3f3"
foregroundColor="#ecebeb"
>
<rect x="0" y="5" rx="0" ry="0" width="200" height="200" />
<rect x="216" y="25" rx="4" ry="4" width="220" height="20" />
<rect x="216" y="49" rx="4" ry="4" width="230" height="56" />
<rect x="216" y="110" rx="4" ry="4" width="130" height="28" />
<rect x="340" y="140" rx="4" ry="4" width="110" height="36" />
<rect x="529" y="5" rx="4" ry="4" width="488" height="30" />
<rect x="529" y="45" rx="4" ry="4" width="488" height="30" />
<rect x="529" y="85" rx="4" ry="4" width="488" height="30" />
<rect x="529" y="125" rx="4" ry="4" width="488" height="30" />
<rect x="529" y="165" rx="4" ry="4" width="488" height="30" />
</ContentLoader>
),
});
단순하게 ContentLoader 컴포넌트 안에 <rect>를 필요한 만큼 추가해주면 된다. x와 y는 시작 위치를 의미하고 rx/ry는 만들어지는 rect의 rounded 속성이 된다. 너무 각지면 예쁘지 않으므로 텍스트 영역들은 4정도 주면 깔끔하다.
최종적으로 개발 서버에서 측정한 점수는 아래와 같다. 현재는 Best Practices는 수정 중에 Slider도 dynamic으로 import 방식을 변경해주었는데 그 뒤부터 ref를 전달하면 오류가 나서 제거해주면서 다시 100점으로 만들어주었다. 성능 측정은 개발 서버보다 프로덕션 서버에서 하는 게 좋다고 하니 아마 실성능은 이 부분보다 좋을 것으로 예상한다.

성능 측정은 이번에 처음 해본 과정인데 아직은 어떻게 해결해줘야 할지 잘 모를 때가 많지만, 그래도 어디가 문제인지를 알려줘서 처리하다보면 조금씩 점수가 올라서 해결하는 재미가 있다.