본문 바로가기
기타

웹 페이지 최적화하기 - 3

by 유세지 2022. 9. 18.

이전 글에서 이어집니다.

 

지난 포스트에서는 네트워크 페이로드의 가장 큰 부분을 차지하는 이미지 파일의 최적화에 대해 알아보고, 처리된 이미지를 안전하게 표시해주는 방법에 대해 알아보았습니다.

 

지금까지의 과정은 공통적으로 서버에서 받아오는 리소스의 크기들을 줄여서 최적화를 진행했습니다. 이번에는 프로젝트의 로직이나 코드의 개선을 통해 사용자 경험을 증진시키는 방향으로 최적화를 진행해보겠습니다.

 

 

네트워크 요청 횟수 줄이기

우리가 서버로부터 데이터를 받아온다고 했을때, 그 데이터가 실시간으로 최신 상태를 유지할 필요가 없거나 변경될 일이 거의 없는 데이터들인 경우가 종종 있습니다. 이런 데이터들을 지속적으로 갱신해 주는 것은 의미가 없습니다.

 

만약 위의 조건이 확실한 데이터들이라면, 다시 요청하는 대신 기존에 받았던 데이터를 재사용하여 불필요한 네트워크 요청을 방지할 수 있습니다. 이러한 동작은 어떻게 구현해 줄 수 있을까요?

 

이를 위해서는 두 가지를 생각해봐야 합니다. 바로 '내가 서버에 보내는 요청' 과, '서버로부터 받은 응답' 입니다. 내가 서버에 보내는 요청이 무엇인지 기억해야 이후에 요청이 보낼때 이미 보냈었던 요청인지를 구별할 수 있습니다. 만약 기존에 보냈던 요청이라면 그 때 서버로부터 받은 응답을 대신 반환해 줄 수 있겠네요.

 

일반적인 fetch 요청을 통해 데이터를 받아온다고 한다면, 이 기능은 아래처럼 구현해 줄 수 있습니다.

 

type cacheData = Record<string, Response>;

const cacheStore: cacheData = {};

export const cacheFetch = async (targetUrl: string): Promise<Response> => {
  if (cacheStore[targetUrl]) return Promise.resolve(cacheStore[targetUrl].clone());

  cacheStore[targetUrl] = await fetch(targetUrl);
  return Promise.resolve(cacheStore[targetUrl].clone());
};

 

요청과 응답을 기억할 수 있는 함수인 cacheFetch의 예시 코드입니다.

 

코드에 다소 생소할 수 있는 clone() 메서드가 보이는데, fetch로 받아온 데이터는 한 번 resolve 되고 나면 다시 사용할 수 없습니다. 궁여지책으로 clone() 을 사용해 복사본을 만들어 Promise.resolve에 담아 반환해주었습니다. 기존 fetch의 반환 형식을 맞춰주려 넣었을 뿐, 상세한 코드가 중요한건 아니니 여기서는 이 정도로 설명하고 넘어가겠습니다. 

 

여기서 핵심은 요청을 기억하고, 해당 요청이 다시 들어오면 저장된 값을 반환하는 구조를 만들었다는 점이니 이 부분에 초점을 맞춰주시면 좋을 것 같습니다.

 

직접 구현하면 이런 모양이고, 대부분의 경우 라이브러리를 사용합니다. react-query(tanstack query)SWR 같은 라이브러리를 사용하여 통신하면 이러한 캐싱 기능을 기본으로 지원합니다.

 

Fetching 라이브러리들은 다양한 기능을 지원합니다.

 

 

레이아웃 변경 줄이기

애니메이션은 정적인 사이트에 생기를 불어 넣어주는 감초같은 존재입니다. 그러나 몇몇 애니메이션 속성들은 잘못 사용하게 되면 성능 이슈를 일으킬 수 있습니다.


성능 이슈가 일어나는 이유

애니메이션이 어떻게 성능에 영향을 미치는지를 이해하려면 브라우저의 렌더링 과정을 먼저 알아야합니다. 브라우저는 화면을 표시할때 다섯 단계를 거칩니다.

브라우저에 따라서 부르는 이름이 다르다는 사소한 차이점은 있지만, 큰 틀은 같습니다.


1. HTML 파일을 해석하는 Parsing
2. DOM Tree와 CSSOM Tree를 매칭해 Render Tree를 구성하는 Style
3. Render Tree를 토대로 화면 상의 위치와 크기를 계산하는 Layout
4. 계산된 정보를 바탕으로 Render Tree를 실제 픽셀로 변환하는 Paint
5. 변환된 픽셀들을 합성하여 화면으로 옮기는 Composite

이 중 가장 많은 리소스를 소모하는 부분이 바로 Layout -> Paint로 이어지는 과정인데요. 요소의 속성이 변경되면 스타일이 다시 계산될 때가 있습니다. 이런 경우 Layout 과정 또는 Paint 과정을 다시 수행하게 되고, 각각 리플로우와 리페인트라고 부릅니다.

리플로우와 리페인트 과정은 많은 연산이 들어가기 때문에, 당연히 성능에 좋지 않은 영향을 끼치게 됩니다. 리플로우나 리페인트 과정을 유발하는 요인은 정말 다양합니다.


- border, margin, padding 등의 공간 속성 변경

- 폰트 크기 변경

- 윈도우 레이아웃 (창 크기) 변경

- 스타일 속성 참조 또는 사용

- 이러한 작업들이 포함된 클래스 속성 변경

- ......

이 외에도 정말 많으니 잘 정리된 csstrigger.com을 참조해보시면 좋을 것 같습니다.

 

css triggers에 웹 엔진별로 일으키는 렌더링 단계가 정리되어 있습니다.

 

개선해보기

위에서 살펴본 리플로우/리렌더링 과정을 일으키는 코드를 변경해주는 것으로 불필요한 렌더링 과정을 제거하여 성능을 개선할 수 있습니다. 예시로 이번에 진행중이던 프로젝트에 실제로 삽입된 애니메이션을 수정해보겠습니다.

 

from {
  bottom: 0;
  opacity: 0;
}

to {
  bottom: 2rem;
  opacity: 1;
}

 

제가 프로젝트에서 플로팅 버튼을 띄울때 사용했던 애니메이션 코드입니다. 현재 이 코드는 요소의 bottom 속성을 변경하기 때문에 리플로우를 발생시키고 있어 뚝뚝 끊기는 동작이 다소 보입니다. 포지션 속성 대신 리플로우를 일으키지 않는 translate을 이용하여 변경해주면 해결됩니다.

 

from {
  transform: translate3d(-50%, 2rem, 0);
  opacity: 0;
}

to {
  transform: translate3d(-50%, 0, 0);
  opacity: 1;
}

 

translate 대신 translate3d를 사용하면 하드웨어 가속을 활용하여 동작하기 때문에 대부분의 경우 더 빠른 결과를 얻을 수 있습니다. 마찬가지로 scale은 scale3d, rotate는 rotate3d 등을 사용할 수 있습니다.

 

왼쪽 : translate 반영 전 / translate 반영 후

 

gif 캡쳐 특성상 개선된 부분이 명확하게 보이지는 않는데, 최적화가 이루어지며 꽤 부드러워진 모습입니다.

 

 

마무리

성능 최적화의 결과는 직접 확인해보며 느낄수도 있지만, 수치상으로 확인할 수 있는 지표들이 있습니다. 대표적으로는 FCP(First Contentful Paint), LCP(Largest Contentful Paint), CLS(Cumulative Layout Shift) 등의 분류가 있는데, 라이트하우스라고 하는 측정 프로그램을 통해 내 페이지의 점수와 각 지표들을 쉽게 확인할 수 있으니 함께 보시면 더욱 좋습니다. 

 

하나의 웹 사이트를 만들때, 최적화 외에도 고려해야 할 사항들이 정말 많습니다. 이번 최적화 포스트들이 사용자가 불편하지 않은, 좋은 사이트를 만드는데 도움이 되었다면 좋겠습니다.

 

참고 문서

CSS Triggers

Avoid Reflow & Repaint - Pedro Oliveira

CSS 3D Transforms - W3 schools

 

 

반응형

댓글