HomeFeedAbout

next/Link 제대로 활용하기

김성현 어제
Web Performance

프론트엔드 개발자라면 한 번 쯤은 콘솔에서 다음과 같은 경고를 본 적이 있을 것이다.

The resource https://oilater.com/_next/static/css/0c12345678abc.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate 'as' /feed:1 value and it is preloaded intentionally

브라우저가 'CSS를 preload 해왔는데, 한 5초가 지나도 사용을 안하네?' 라며 경고를 던지는 것이다. 나는 preload 하라고 시킨 적이 없는데 브라우저는 왜 해당 리소스를 preload 해온 걸까?

next/Link의 기본 동작

범인은 next/Link다. Link의 기본 동작은 prefetch={true}로, 뷰포트에 해당 링크가 보이면 사용자가 해당 링크를 클릭할 것을 대비해 미리 prefetch 해놓는다.

블로그 살펴보기

내 블로그의 Feed 페이지를 살펴보면, 3개의 Link를 담고 있는 FloatingNav 컴포넌트가 있다.

처음에 봤던 경고와 함께 네트워크, 성능 탭을 살펴보니 지금 사용하지 않는 Home, About 페이지의 CSS같은 리소스들을 preload 해오고 있었다.

심플한 페이지라 다행이지만, 리소스가 많은 사이트의 경우엔 불필요한 preload로 인해 정작 불러와야 할 리소스와 대역폭 경쟁을 하게 된다. LCP로 측정되는 이미지의 로드가 늦어질 수 있고, 다운로드 속도도 느려질 수 있다.

그럼 prefetch={false}를 설정하면 어떻게 달라질까?
아예 prefetch를 안하겠구나 생각이 들지만, 그건 아니다. prefetch={false}는 사용자가 링크에 마우스를 hover하는 시점에 prefetch 한다.

따라서 최대한 빠르게 보여주지 않아도 될 페이지는 prefetch={false}만으로도 충분하다. 예를 들면, 무한 스크롤의 상세 포스트 링크에도 적용할 수 있다.

코드 개선하기

그래서 Nav의 링크를 map을 돌리기보단 하나씩 제어 가능한 구조로 변경해줬다.

Before

// FloatingNav.tsx
import { header, innerNav } from '#/layout.css';
import { ThemeSwitch } from '#components/ThemeSwitch';
import { BlogConfig } from '#constants/config';
import { NavItem } from './NavItem';

export function FloatingNav() {
  return (
    <header className={header}>
      <div className={innerNav}>
        {BlogConfig.menu.map((link) => (
          <NavItem
            key={link.label}
            href={link.path}
            label={link.label}
          />
        ))}
        <ThemeSwitch />
      </div>
    </header>
  );
}

After

Google Analytics에서 사용자의 Feed 클릭율이 50%가 넘었긴 했지만, 이미 어느 정도 로딩 성능을 최적화 했기 때문에 모든 prefetch를 비활성화했다.

import { header, innerNav } from '#/app/layout.css';
import { ThemeSwitch } from '#components/ThemeSwitch';
import { NavItem } from './NavItem';

export function FloatingNav() {
  return (
    <header className={header}>
      <div className={innerNav}>
        <NavItem 
          href="/" 
          label="Home" 
          prefetch={false} // 첫 화면이므로 불필요, 다시 올 땐 어차피 캐싱됨
        />
        <NavItem 
          href="/feed" 
          label="Feed"
          prefetch={false} // hover 시 prefetch
        />
        <NavItem 
          href="/about" 
          label="About" 
          prefetch={false} // 중요하지 않은 페이지
        />
        <ThemeSwitch />
      </div>
    </header>
  );
}

이후 검사해보니, 네트워크 탭에서 불필요한 리소스 요청이 사라졌다.

마치며

우리가 쓰는 next/Link는 기본적으로 뷰포트에 들어오면 해당 페이지의 리소스들을 가져오는데, 결국 HTML의 <head> 태그 안에 <link rel="preload" ... /> 형식으로 추가된다. 하지만 너무 많은 preload는 대역폭을 경쟁시키며, 느린 LCP 지표로 이어질 수 있다. next/Link의 prefetch는 간단한 설정이지만 잘 사용한다면 효과적으로 네트워크 요청을 제어할 수 있다.