Skip to main content

최근에 제가 Production NextJS 웹사이트의 성능을 개선해야 했습니다.

웹사이트는 여기에서 방문하실 수 있습니다 (변경 사항이 현재 적용되었습니다).

업데이트: 검색 엔진 결과 (24–08–24):

이러한 변경 사항을 발표한 후, 인덱스된 페이지 수가 79k에서 123k로 하루 만에 증가했습니다!

None

인덱스된 페이지 수의 증가.

이것은 하루 만에 인덱스된 페이지 수에서 가장 큰 성과였습니다.

오늘은 우리가 어떻게 이를 달성했는지와 그 과정에 대해 설명드리겠습니다.

점수를 시작하겠습니다.

여기가 우리가 시작한 지점입니다.

None

시작

공정한 비교를 위해 Vercel을 사용하여 테스트 계정에 배포하였으며, 이 계정을 모든 벤치마크에 사용할 예정입니다.

저의 초점은 기술적 개선뿐만 아니라 사용자 경험 개선에도 있습니다.

그럼 시작해 보겠습니다…

최적화 1: 병렬 요청

우리의 필터 페이지는 해당 지역에 적합한 필터를 얻기 위해 4개의 API 호출을 수행합니다.

None

병렬 요청

하지만 여러분이 짐작하셨겠지만, 우리는 이러한 요청을 병렬로 처리하는 것을 막는 것은 없습니다.

단순히 모든 것을 Promise.all()로 감싸는 것이었습니다.

None

병렬 요청

이 최적화는 경험을 개선하였지만, 개선 효과에 대한 벤치마크는 하지 않았습니다.

최적화 2: 페이지의 정적 생성

저희 페이지는 getServerSideProps를 사용하여 서버 측 렌더링되었습니다. 그러나 초기 페이지를 이미 예측할 수 있기 때문에, 제가 한 두 번째 작업은 이러한 페이지를 정적으로 생성하는 것이었습니다.

None

주요 이점은 빌드 시간 동안 이미 페이지가 생성되었기 때문에 사용자가 페이지를 보기 위해 버튼을 클릭할 때, 첫 번째 로드에서 위에서 논의한 4개의 API 호출을 할 필요가 없다는 점입니다.

None

두 번째 최적화 후 점수

이것은 점수를 개선하고 사용자 경험을 향상시킵니다.

최적화 3: 정적 페이지 미리 가져오기

저는 사용자 경험을 더욱 향상시키기 위해 한 걸음 더 나아가고 싶었습니다.

Next Link 컴포넌트를 사용하여 URL을 미리 가져올 수 있습니다.

그러나 우리가 논의하고 있는 페이지는 웹사이트의 여러 곳에서 참조되고 있으며, 사용자가 이 페이지들을 자주 방문하기를 원합니다.

그래서 저는 웹사이트의 어떤 페이지에든 사용자가 방문할 때마다 이 5-6개의 URL을 미리 가져오기로 결정했습니다.

이를 수행하는 방법은 PrefetchStaticPaths라는 사용자 정의 컴포넌트를 만드는 것입니다.


const PrefetchStaticPaths: React.FC = () => {
  const router = useRouter();

  useEffect(() => {
    const prefetchPaths = async () => {
      const paths = [...] // 미리 가져오고 싶은 경로 목록

      for (const path of paths) {
        await router.prefetch(path);
      }
    };

    prefetchPaths();
  }, [router]);

  return null;
};

그런 다음 이 컴포넌트를 _app.tsx 파일에 포함시켰습니다.

이는 사용자가 웹사이트를 방문할 때 페이지가 즉시 볼 준비가 되어 있음을 의미합니다. 방문하려고 할 때 거의 즉시 로드됩니다.

또한 위의 코드를 개선하여 이러한 요청을 병렬로 처리하도록 하였습니다.

수정된 버전은 다음과 같습니다.


const prefetchPromises = paths.map((path) => {
  const startTime = performance.now();
  console.log('미리 가져오기 시작:', path);

  return router
    .prefetch(path)
    .then(() => {
      const endTime = performance.now();
      const duration = endTime - startTime;
      console.log(`미리 가져오기 완료: ${path}. 소요 시간: ${duration.toFixed(2)}ms`);
    })
    .catch((error) => {
      console.error(`미리 가져오기 오류: ${path}`, error);
    });
});

await Promise.all(prefetchPromises);

이것은 확실히 사용자 경험을 향상시켰지만 페이지 속도 점수에는 큰 영향을 미치지 않았습니다.

None

점수 29
최적화 4: 큰 이미지 최적화

이미지는 페이지에서 가장 무거운 부분 중 하나입니다. 필터 페이지에서 여러 배너가 페이지 크기를 증가시켰습니다.

여기에서 TinyPng라는 멋진 무료 도구가 있습니다. 이미지를 끌어다 놓으면 비슷한 품질의 더 작은 크기의 이미지를 얻을 수 있습니다.

변환 후의 모습은 다음과 같습니다.

None

65% 개선

이 도구만으로도 이미지 크기가 65% 줄어들었습니다. 이는 상당한 차이입니다. 각 이미지마다 300kb를 절약하는 셈입니다.

더 나아가고 싶으신가요?

더욱 활용할 수 있는 다른 도구들도 있습니다. 예를 들어, 이미지 압축기를 사용할 수 있습니다.

이 도구는 색상 수를 지정할 수 있게 해주며, 이를 통해 이미지 크기를 추가로 50% 줄일 수 있습니다.

None

보통 PNG 이미지는 256색을 사용합니다. 그러나 8색을 사용하면 또 다른 50% 최적화된 이미지를 얻을 수 있습니다.

하지만 과도하게 사용하지 않도록 주의해야 합니다. 그렇지 않으면 시각적으로 품질이 저하될 수 있습니다.

None

점수 35
최적화 5: 스크립트 로딩 최적화

어떤 진지한 프로덕션 애플리케이션에는 많은 추적 도구가 필요합니다. 저희 애플리케이션도 예외는 아닙니다. 저희는 Google Tag Manager와 몇 가지 다른 도구를 사용하고 있습니다.

NextJS는 이러한 스크립트를 지연 로딩하는 좋은 방법을 제공합니다.

스크립트 태그에 전략을 추가해야 합니다.

None

스크립트의 지연 로딩.

이 방법은 초기 로드 시간을 줄여줍니다.

None

점수 46

최적화 6: 코드 분할 및 지연 로딩.

모든 것이 첫 번째 빌드에서 로드될 필요는 없습니다. yarn build를 실행할 때 출력 결과를 검사하여 어떤 컴포넌트가 필요한 것보다 더 많은 항목을 로드하고 있는지 확인할 수 있습니다.

None

yarn build의 결과

모든 사람이 공유하는 첫 번째 로드 JS라는 섹션이 있음을 알 수 있습니다.

이 리소스는 어떤 경우에도 로드됩니다.

좀 더 깊이 살펴보겠습니다.

이름을 확인하면 하나는 framework이고, 다른 하나는 main임을 알 수 있습니다. 나중에 확인하겠지만, 지금은 _app으로 시작하는 파일에 집중해 주세요.

이 파일은 전체 프로젝트에서 공유되므로, 여기서 로드하는 모든 것은 어디에서나 로드됩니다. 따라서 여기서 모든 것이 올바른지 확인하는 것이 매우 중요합니다.

효율적으로 로드하는 한 가지 방법은 구성 요소를 동적으로 로드하는 것입니다. 스크립트를 포함해서요.

그래서 저는 다음과 같이 가져오기를 변환했습니다.

import BottomNavbar from '@/widgets/navigation/BottomNavBar';

를 다음과 같이 변환했습니다.

const BottomNavbar = dynamic(() => import('@/widgets/navigation/BottomNavBar'), { ssr: false });

그리고 모든 구성 요소에 대해 이렇게 했습니다.

이제 출력을 살펴보세요.

None

최적화 후 청크.

이 간단한 트릭을 사용하여 60kb를 줄였습니다. 나쁘지 않죠?

현재까지의 최종 결과

이 모든 작업을 한 후, 현재 점수는 다음과 같습니다.

None

현재 점수

그렇게 좋지는 않지만, 이제는 사용할 수 있죠, 맞죠?

더 나아질 수 있을까요?

네. 어떻게요?

잘 들어보세요.

가능성 1: 번들 분석하기

아주 유용한 도구가 있습니다. webpack bundle analyzer입니다. 이 도구를 NextJS 프로젝트와 연결하면 출력의 어떤 부분이 가장 많은 부하를 주는지 확인할 수 있습니다.

저는 이 도구를 실행해 보았고, 다음과 같은 결과를 보았습니다.

None

Webpack 번들 분석기

보시다시피, 저희는 lucide-react라는 아이콘 라이브러리를 사용했습니다. 그리고 이것이 대부분의 공간을 차지하고 있습니다.

이를 대체할 맞춤 아이콘이 필요하며, 이를 관리하는 데 시간이 다소 소요될 것입니다. 그러므로 이는 다른 날에 진행할 예정입니다.

잠재적인 두 번째: 패키지 분석

프로젝트에서 라이브러리를 사용할 때, 우리는 종종 그 크기를 고려하지 않습니다.

또 다른 도구는 BundlePhobia입니다. 이 도구를 사용하면 패키지의 크기를 확인하고, 너무 무겁다면 대안을 고려할 수 있습니다.

None

Bundlephobia

저는 앞으로 며칠 안에 이것을 진행하여 얼마나 더 발전시킬 수 있을지 살펴보겠습니다.

마무리 발언

웹사이트 최적화는 지속적인 과정이며 끊임없는 노력입니다. 이 글에서는 우리가 활용할 수 있는 몇 가지 방법을 소개하려고 하였습니다. 물론 더 많은 방법이 있습니다.

그에 대해서는 다음에 이야기해보도록 하겠습니다.

읽어주셔서 감사합니다. 좋은 하루 되세요! 😀