개발자
류준열

랜딩페이지 성능개선 여정

유감스럽지만, 에듀테크 제품을 개발할거라고 기대한 회사에서 마크업만 했다.

이사님께는 비밀이지만, 입사 1달도 안되어 면접을 보러 다니기도 했다.

그렇게 입사 초부터 짧은 방황을 하다가 삶에서 쓸데없는 경험은 없다고 믿기에 주어진 환경에서 최선을 다하는 경험을 하기로 결정했다.

그 상황에서 내가 기술적으로 할 수 있는 일은 SEO 최적화, 성능개선이었고

개발자 상의없이 정해지는 일정속에서 고.작. 마크업을 위해 야근하며 틈날때마다 랜딩페이지 성능 개선을 했다.

  1. 레이지 로딩을 통한 네트워크 경쟁 완화
  2. 인증과 렌더링이 순차적으로 진행되는 현상
  3. 스트리밍 렌더링

측정방식은 fast 4g, cpu slowdown 4x 환경에서 라이트하우스를 10회 측정 후 평균을 내었다가, 너무 오래걸려서 lighthouse api를 이용해서 수동측정을 자동화 했다.

1차 개선

1차 개선 가장 간단히 할 수 있는 폰트최적화와 이미지, 동영상 레이지 로딩을 했다.

폰트최적화

ttf하나가 길게 늘어지고 있어서 subset woff2로 변경하니 756kb에서 167kb로 경량화되었다. (subset은 한글, 영어, 숫자들만 다운받았다.)

폰트 경량화를 통해 FCP, TBT 등의 개선을 의도했다.

lazy loading을 통한 네트워크 경쟁 완화

LCP 이미지를 반으로 쪼갰지만 다운받아야 하는 에셋,영상들이 너무 많아 네트워크 경쟁이 심한 상태였다.

네트워크 리소스 경쟁을 줄이는 것이 LCP를 개선하는지 확인하기 위해 아래 2가지를 테스트 했다.

  1. lazy loading 했을때 LCP가 개선되는지
  2. 영상을 모두 제거했을때 LCP가 개선되는지

두 시나리오 모두 LCP가 개선되었다.

그래서 이미지와 동영상들에 레이지 로딩을 걸었다.

Next Image quality

next image에 quality 속성이 있는데 거의 모든 이미지의 quality가 100이었다. (기본값 75)

왜 이렇게 되어있냐고 동료한테 물어보니, 이전에 모바일에서 해상도 피드백이 들어와서 그 이후로 다 100을 박아놨다고 했다.

quality가 기본값일때, 100일때를 비교해보니 시각적인 차이는 없음에도, 용량차이는 2배 이상이었다.

render delay가 1084ms에서 66ms로 크게 10배 이상 단축되었다.

before

after

2차 개선

Hydration Error를 막기 위해 렌더링 시점을 늦추는 코드가 있었다. 어떤 Hydration Error인지 말하자면,

SSR에서 JS로 반응형을 했을때

이 글에서 보다시피, window 객체로 반응형 처리를 하면 SSR에서 window가 없기 때문에 Hydration 단계에서 Layout shift가 발생한다.

기존 코드베이스는 useMediaQuery를 이용하고 있었고 useMediaQuery가 발생하는 Layout Shift를 막기 위해 AuthProvider에서 유저정보를 확인하기 전까지 렌더링을 지연시키고 있었다.

// AuthProvider.tsx
export const AuthContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {

 const [isAuthLoading, setIsAuthLoading] = useState(true)


if (isAuthLoading) {
    return null 
 }
	
 return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>

}

위와 같이 AuthProvider에서 isAuthLoading이 완료된 후에 page가 로딩되고 그 전까지 흰화면임을 볼 수 있다.

Next.js를 쓰고 있지만 사실상 CSR을 하는 이 방식을 개선하기 위해 useMediaQuery를 css media query로 바꾸었다.

2차 개선 후 발생한 사이드이펙트

svg 중복 id

// ASIS

 <div className="block pc:hidden size-[45px]"><Icons ../></div>
 <div className="mobile:hidden block size-[60px]"><Icons ../></div>

PC 화면에서 svg가 보이지 않는 문제가 있었는데, 이유는 Icons 내부 svg의 mask id가 중복 id가 되었기 때문이다.

display:none은 css로 가리는것뿐 dom에 mount시킨다. 그러다보니 동일한 id를 가진 svg 2개가 렌더링되고, 브라우저는 어떤걸 숨길지 모르는 상태가 되었던 것이다.

이를 해결하기 위해 하나의 Icon만 렌더링시켰다.

// TOBE

  <Icons .. className="mobile:size-[45px] pc:size-[60px]" />

렌더링 시점으로 인한 문제

유저가 쿠폰을 받았는지 여부를 확인후 모달을 띄우는 로직임에도 그 조건은 유저 정보를 간접적으로 간접적으로 참조하고 있었다. (오늘 하루 안보기 여부, session에 정보 저장 여부 같은 것들)

그러다보니 동일유저임에도 디바이스가 변경되거나 시크릿탭으로 접속하는 등 session 정보가 일치하지 않는 상황에서 문제가 발생했다.

기존에는 유저정보를 확인 후에 렌더링을 하니 문제가 없었다.

// AuthProvider.tsx
export const AuthContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {

 const [isAuthLoading, setIsAuthLoading] = useState(true)


if (isAuthLoading) {
    return null 
 }
	
 return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>

}

하지만 개선 이후 인증과 렌더링이 병렬로 이뤄지면서 렌더링 후 유저의 쿠폰정보가 확인되었다.

이때 디바이스 변경이나 시크릿탭 등 session에 유저의 쿠폰정보가 남아 있지 않은 경우, 모달이 갑자기 꺼지면서 모달의 body.overflow=hidden만 남아 스크롤이 안되는 문제가 있었다.

유저의 쿠폰 유무에 따라 모달이 켜져야 한다면, 모달을 켜는 시점이 유저정보를 확인한 이후여야 했는데, 그렇지 않았다.

현실을 반영하지 않은 비즈니스 로직의 문제였다.

3차개선

스트리밍렌더링과 API 병렬 요청을 진행했다.

스트리밍렌더링

Pages router의 getStaticProps처럼 모든 CMS데이터를 한번에 받은 후에 렌더링을 하고 있었다.

 const [data1, data2, data3, data4, date5, date6] = await Promise.all(
    [
      ...
    ],
  )
	
	return (<>
    <A data={data1} />
      <B />
    <C data={data2} />
    <D data={data3} />
      <E />
      <F />
    <G data={data4} />
	</>
	)

위 상황에서 B,E,F는 data를 기다릴 필요가 없는데도 promise.all이 끝나기를 기다려야 한다.

이는 App router의 이점이 적용되지 않은 아키텍처다.

App router에서는 server componentuse client 로 서버, 클라이언트 작업 경계를 세분화 할 수 있고 이를 통해 server 작업과 무관한 컴포넌트들은 즉시 렌더링하고 suspense를 이용하여 promise가 걸린 컴포넌트는 fallback ui를 노출할 수 있다.

스트리밍 렌더링에 대한 회고

작업 시작하기 전에 문제 해결에 영향이 있을지 확인을 해야 했는데, 단순히 App router를 쓰는데 Page router getStaticProps처럼 모든 데이터를 다 받은 후에 페이지 렌더링을 한다고? 이럼 안되지 하는 생각에서 시작한 작업이다.

LCP 데이터가 그 외 다른 데이터 로드와 분리되면서 LCP가 빨라지는걸 기대했지만 API 로드시간이 애초에 길지 않아서 그랬는지 별로 개선이 없었다.

LCP와 무관한 개선

위와 다르게 JS가 점진적으로 로드되며 다른 mp4등의 로드 마무리 시점이 빨라지는 것을 확인 할 수 있다.

성능개선과 별개로 네트워크가 느려질때 점진적 렌더링이라던가, Error boundary를 통해 에러 전파 방지를 통한 방어적 아키텍처로써의 의미가 있다.

결과

  • 효과가 있었던 작업
    • lazy loading
    • quality 100에서 기본값으로 변경
    • 인증,렌더링의 관계를 waterfall에서 병렬로 수정

이전 작업