본문으로 건너뛰기

더 빠른 웹페이지를 만드는 방법::HTTP 압축 - webp, gzip, Brotli, gRPC

· 약 4분

좋은 사용자경험을 위해서는 가장 큰 요소(LCP)가 2.5초 이내 혹은, 의미있는 콘텐츠(FMP)가 1.8초 이내에 일어날 수 있도록 권장하고 있다. 최적화를 덮어놓고 개발을 하다보면 위에서 말한 임계치가 넘어서곤 하는데, 이때 웹페이지를 더 빠르게 할 수 있는 방법을 공유하려 한다.

어떻게 하면 더 빠르게 만들 수 있을까?

파일 포맷 압축

JPEG 파일 크기: 43.84KB

WebP 파일 크기: 29.61KB

webp

가끔 이미지를 다운받으면 볼 수 있는 webp 확장자는 2010년 구글에서 만든 이미지포맷으로, JPEG, PNG, GIF를 대체할 수 있도록 손실, 무손실 압축 방식을 모두 지원하며, 기존 포맷대비 약 30% 정도 압축률이 좋다. 때문에 자주 사용하는 이미지들은 webp 포맷을 사용하면 네트워크 속도를 높일 수 있다.


손실, 무손실 압축에 대해 짧게 설명하자면, JPEG와 같은 포맷은 사용자가 알아채기 힘들 정도의 데이터를 버리면서 압축하며, PNG와 같은 포맷은 데이터를 버리지 않고 압축한다. 오래된 고전 짤들이 여기저기 옮겨다니며 손실 압축이 반복되어 디지털 풍화가 생기기도 한다.

(디지털풍화 예시)


종단간 압축

웹서버에서 압축된 리소스를 보낸 후 브라우저에서 압축을 풀어 사용하는 방식이다. 압축된 더 작은 크기의 데이터를 보내면서 네트워크 요청과정을 빠르게 하는 것이다.

gzip

가장 흔히 사용하는 것은 gzip 방식으로, 최대 70%까지 데이터를 압축할 수 있다. 중복되고 필요없는 코드를 압축하여 동작하며, 압축단계를 조절할 수도 있다. 사용방식은 서버에서 nginx 혹은 apache 설정을 통해 비교적 간편하게 사용할 수 있다.

Brotli

구글에서 만든 압축 알고리즘인 Brotli은 gzip에 비해 최대 20% 더 압축할 수 있다고 한다. gzip과 마찬가지로 서버에서 nginx 혹은 apache 설정을 통해 사용할 수 있으며, ie에서는 동작하지 않는다.

gRPC

gRPC는 HTTP/2, Protocol Buffers를 사용하여 데이터를 압축하여 통신한다. HTTP/2 프로토콜을 통해 여러 요청을 동시에 처리할 수 있으며 ProtoBuf는 xml보다 3~10배 작다고 할 정도로 Protocol Buffers를 통해 데이터를 압축할 수 있다.

드라마틱하게 압축할 수 있는 이유는 위처럼 이진 데이터 구조를 사용하기 때문인데, 우리가 주로 사용하는 방식인 REST에서는 데이터를 주고받는 포맷으로 xml, json을 사용한다. 하지만 ProtoBuf는 '00001...'과 같이 이진 데이터 구조로 만들어 전달하고, 이진 데이터를 다시 변환하여 사용한다.

때문에 gRPC를 사용하기 위해서는 아래처럼 이진 데이터를 만들고 해제할 수 있는 포맷을 만들어야 하며, 데이터를 주고받는 동안 이진데이터 형식이기 때문에 사람이 읽을 수 없다는 단점이 있다.

message Person {
required string user_name = 1;
optional int64 favourite_number = 2;
repeated string interests = 3;
}

참고자료:

Compression in HTTP, google webp, Protocol Buffer 원리로 배우는 고성능 직렬화, 역직렬화 전략, [네이버클라우드 기술&경험] 시대의 흐름, gRPC 깊게 파고들기

렌더링이 얼마나 빨라요 하나요?::RAIL 성능모델

· 약 4분

프론트엔드 실무를 진행하다 보면 이런 말을 듣거나 해야할 때가 있다.
여기 좀 느린 것 같지 않아요?

개발환경에서는 문제없었던 부분들이 배포 후에 느리게 느껴져서 일 수도 있고, API 응답속도가 느려져 서버팀을 설득해야 할 때에도 있다. 그럴 때 마다 어느 기준에 맞춰야 하는지에 대해 어려울 수 있는데, RAIL 성능 모델을 기준으로 한다면 개발환경에서 속도를 측정하는데 꽤나 도움이 될 것이며, 다른 사람을 설득하는데에도 도움이 될 것이다.


RAIL은 Response, Animation, Idle, and Load 의 축약어로 2015년 사용자 경험을 위해 구글 크롬에서 만들어진 사용자 중심의 성능 모델이다. UX 연구에 기반하여 응답(Response)은 100ms, 애니메이션(Animation)은 16ms, 유휴(Idle)은 50ms, 로드(Load)는 5s 이내에 동작하는 것이 사용자 경험에 좋다는 것인데 각각의 내용을 좀 더 자세히 설명하자면,

Response

좋아요 버튼을 누른 후 약 0.5초 후에 좋아요 여부가 바뀌는 것이 답답한 느낌을 준다. 아무생각없이 개발하다보면 마주칠 수 있는 상황인데, 좋아요와 같이 즉각적으로 UI가 바뀌어야 하는 것은 사실 0.2초만 되어도 묘하게 답답하다는 느낌이 들 수 있다. 때문에 이러한 기능들을 개발할 때에는 변화가 필요한 곳만 렌더링 혹은 API 호출을 하거나, 낙관적 업데이트(Optimistic update)를 고려해보는 것이 좋다.

Animation

흔히 볼 수 있는 애니메이션을 가져왔는데, 프레임이 낮다보니 눈에 거슬릴 뿐 아니라 렉이 걸리고 있다는 느낌마저 든다. RAIL 모델에서는 60프레임을 1초로 나눈 0.016초 마다 렌더링 하기를 권장하고 있는데, 실제 브라우저에서 렌더링을 할 때에는 각 프레임에 대한 자체 오버헤드가 있기 때문에 10ms으로 구현하는 것이 좋다. [참고] 렌더링성능

Idle

웹서핑을 하다보면 쉽게 볼 수 있는 현상들인데, 다른 동작들로 인해 사용자의 응답이 지연되는 경우이다. (물론 위 이미지는 성능을 낮춰 일부러 구현한 현상으로, velog의 성능과는 무관하다.)

이러한 현상은 스크롤과 같이 이벤트가 자주 일어날 때 생길 수 있는데, 때문에 리액트를 사용한다면 스크롤 이벤트를 사용하기 보다는 intercepter observer 를 사용하는 것을 추천한다. 만약 이벤트 리스너를 사용해야 한다면 passive event listeners를 사용해서 Idle을 줄일 수 있다.

Load

상호작용할 수 있는 콘텐츠, 쉽게 말하면 클릭할 수 있는 콘텐츠가 5초 이내로 나타나야 한다는 것이다. 페이지의 핵심이 되는 이미지, 글과 같은 것이 나타나는 것이 중요하긴 하지만, 상호작용할 수 있는 콘텐츠가 나타나지 않는다면 사용자는 작업이 중단되었다고 인식한다는 것이다.

위 이미지에서는 대부분이 상호작용할 수 있는 콘텐츠로, 개인적으로는 Load 보다는 의미있는 콘텐츠(FMP)가 1.8s 안에 나타나야 한다는 것이 좀 더 실무에 가깝지 않을까 싶다.



끝으로,

RAIL 모델이라는 것이 엄청 유명하지는 않지만, 그래도 구글에서 나온 모델로, 좋은 사용자경험을 만들 때 뿐 아니라 권위자로 인한 설득력을 얻을 수 있다는 점에서 알아두면 좋은 모델인 듯하다. 사족으로, RAIL 모델을 알아보면서 MDN에 잘못된 정보가 있는 것을 발견하고 MDN contributor가 되는 좋은 경험을 하기도 했다.

전역상태를 사용하지 않고 props drilling을 없애보자::Observer Component

· 약 4분

섬세한 ISFP의 코드 가독성 개선 경험에서 가독성 향상을 위해 사용한 ObserverComponent을 통해 전역상태관리 라이브러리 의존 낮추기


요즘 개발을 할 때 편리함과 가독성이라는 이유로 recoil을 애용하고 있는데, 끝없이 늘어나는 atom과 리코일 import문을 보면서 전역상태관리 라이브러리에 너무 의존하고 있다는 생각이 들었다. 그러던 중 섬세한 ISFP의 코드 가독성 개선 경험에서 ObserverComponent를 보게 되었는데 영상에서 나온 패턴에 어떤 장점을 가지고 있는지 궁금했기도 했고, 전역상태관리를 어느정도 대체할 수 있을거라 생각되어 직접 구현해보았다.

Observer Component

Observer Component는 이벤트핸들러를 정의한 뒤, 하위 컴포넌트의 Observed Component에서 발생하는 이벤트 버블링을 캡쳐하여 정의한 이벤트핸들러를 실행하는 패턴이다.


구현내용은 다음과 같다.

  • HOC패턴을 사용하여 obsevedComponent를 만드는 Observer Component를 구현한다.
  • data-click-log와 data-click-param을 갖는 observered component를 생성한다.
  • App 컴포넌트에서 이벤트핸들러를 정의한다.
  • onClick 이벤트에서 id가 data-click인 경우, 해당 클릭이 일어난 components 정보와 이벤트 정보를 가져온다.
  • 해당 컴포넌트의 데이터를 바탕으로 정의된 이벤트를 실행한다

여기서 HOC(higher-order-components)는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수이며, 현대 React에서는 주로 사용되지는 않는다고 한다. higherOrderComponent


직접 만들며 느낀 장점은 이벤트 버블링을 이용하기 때문에 props를 전달하지 않아도 된다는 점, 단점은 최상위 컴포넌트에서 이벤트를 정의, 관리 번거로움이 있다는 것이 있었다.


구현 코드

타입스크립트를 사용하였고, 영상에서 모든 코드가 나오지 않기 때문에 임의로 작성/수정한 코드가 많다.

ObserberComponent

export default function ObserberComponent<P extends JSX.IntrinsicAttributes>(
Component: React.ComponentType<P>,
handleEvent: (event: { [key: string]: string } | string) => void
) {
function Observer(props: P) {
return (
<div
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
const target =
((e.target as HTMLElement)?.closest(
'[data-click-log]'
) as HTMLElement) || null;
if (!target) return;
handleEvent(
extractParams({
el: target,
paramTarget: 'data-click-log',
paramLabel: 'data-click-param',
})
);
}}
>
<Component {...props} />
</div>
);
}
return Observer;
}

interface ExtractParams {
el: HTMLElement;
paramTarget: string;
paramLabel: string;
depth?: number;
}

/** @description i번째의 부모 엘리먼트까지 paramTarget 있는지 체크한 뒤 paramLabel으로 구별하여 객체를 반환한다. */
const extractParams = ({
el,
paramTarget,
paramLabel,
depth,
}: ExtractParams): { [key: string]: string } => {
if (!el) return {};
let paramEl = el;
const params = {};
const param = `[${paramTarget}]`;

for (let i = 0; i < (depth || 3); i++) {
const paramsEl = paramEl.closest(param);
if (!paramEl || !paramEl?.parentElement) break;
const targetAttribute = paramsEl?.getAttribute(paramTarget);
const labelAttribute = paramsEl?.getAttribute(paramLabel);
if (!targetAttribute || !labelAttribute) break;
Object.assign(params, { [labelAttribute]: targetAttribute });
paramEl = paramsEl?.parentElement!;
}
return params;
};

ObserverComponent를 정의하는 App 컴포넌트

const ObservedFirstPage = ObserberComponent(FirstPage, arg => console.log(arg));

function App() {
return (
<div>
<ObservedFirstPage />
</div>
);
}

export default App;

data-click-param과 data-click-log 속성을 가진 FirstPage

export default function FirstPage() {
return (
<div data-click-param="First Page Param" data-click-log="First Page Log">
FirstPage
<button type="button">Fist Page Button</button>
<SecondPage />
</div>
);
}

버튼 클릭 시 { First Page Param ****: "First Page Log" } console 출력


data-click-param과 data-click-log 속성을 가진 FirstPage 내부의 SecondPage

export default function SecondPage() {
return (
<div data-click-param="Second Page Param" data-click-log="Second Page Log">
SecondPage
<button type="button">Second Page Button</button>
</div>
);
}

버튼 클릭 시 {Second Page Param: 'Second Page Log', First Page Param: 'First Page Log'} console 출력

Vite에 대한 간단한 소개::CRA 대신 Vite를 사용해보는 것은 어떤가요?

· 약 3분

Vite 개요

빠르다는 의미의 프랑스어 vite는 바이트가 아닌 비트라고 읽는다. 이름과 같이 vite는 매우 빠른 속도를 보여주는데, Vite의 사전 번들링 기능은 Go 언어로 작성된 Esbuild을 사용하여 기존 Webpack, Parcel과 같은 번들러 대비 10-100배 빠른 속도를 가진다.


Vite가 생겨나게 된 배경

vite를 비롯하여 이렇게 빠른 속도를 가진 도구들이 출시될 수 있는 배경에는 메이저 브라우저 엔진들이 네이티브 자바스크립트 모듈을 지원하기 시작하면서이다.

브라우저에서 네이티브 자바스크립트 모듈을 지원하기 전까지는, JavaScript 모듈화를 네이티브 레벨에서 진행할 수 밖에 없었다. 때문의 개발자들은 번들링(Bundling)이라는 우회적인 방법을 사용할 수 밖에 없었다.

때문에 개발 서버를 실행할 때 오랜 시간이 걸릴 수 있으며, 편집한 코드가 브라우저에 반영되기 까지 수 초 이상의 시간이 소요되기도 했다.


Vite는 어떻게 빠른 속도를 가질 수 있는가?

vite는 이를 해결하기 위해 내용이 바뀌지 않을 Plain JavaScript 소스 코드를 Go로 작성된 Esbuild를 사용하여 사전번들링하고, JSX, CSS와 같이 컴파일이 필요하고 수정이 잦은 Non-plain JavaScript 소스 코드에 대해서는 Native ESM을 이용하여 코드를 편집하자마자 브라우저가 변경될 수 있도록 만들었다. (하지만 사전번들링 시 Esbuild는 아직 불안정하기 때문에 Rollup을 사용하고 있다.)

Native ESM은 소스코드를 제공하는 방식을 사용하는데, 개발 서버를 구동할 때, 애플리케이션 내 모든 소스 코드에 대해 크롤링 및 빌드 작업을 마쳐야지만이 실제 페이지를 제공할 수 있었던 번들러 기반의 도구와 달리, 브라우저가 삽입 구문을 찾고 HTTP로 해당 모듈을 요청하고, 요청된 모듈과 모듈의 가져오기 트리에 있는 모든 잎 노드에 변환을 적용한 다음 이를 브라우저에 제공하는 방식이다.


그렇다면 Vite에는 장점만 있는 것일까?

일반적으로 자주 사용하는 번들러인 CRA의 경우, jest, svg-loader 등이 기본적으로 설치되어 있으며 eslint, prettier 등을 바로 설치해서 사용할 수 있다.

하지만 Vite의 경우 jest, svg-loader 등을 하나하나 설치해줘야 하며 eslint, prettier, favicon 등을 사용할 때 플러그인을 별도로 설치해줘야 한다.

하지만 웹팩에 추가 설정을 하기 위해 craco와 같은 라이브러리로 조작해야하는 CRA에 비해 자체적으로 config를 수정할 수 있도록 지원하여 손쉬운 절대경로 설정이 가능하며 CRA의 대부분의 기능을 대체할 수 있으므로, 다음 프로젝트를 기획하고 있다면 CRA 대신 Vite를 사용해보는 것을 적극 권장한다.



참고자료:

vitejs-kr, TOAST UI https://wonillism.tistory.com/271