중복되어도, 복습의 개념 및 설명할 수 있는 정도로의 개념으로 재학습해서 정리한다
주기적으로 블로그에 내용이 노출되면 나중에 볼 때에도 반복학습을 할 수 있는 계기가 된다.
항상 재정리를 하는 근본적인 이유는 하나다. 기억을 못해서이다.
블로깅에 정리를 했다고 한들, 인간인지라 늘 까먹는 것은 변함이 없고 그럴때마다 계속 주기적으로 보면서 확인하는 습관을 가지게 만들어야 한다. 네트워크 최적화와 관련된 내용은 전에 한번 정리를 한 적이 있지만, 그때 당시에는 내용 이해에 벅차서 그런지 머릿속에 실질적으로 남아있는게 별로 없었다. 이는 결국 면접때 아무 말도 못하고 버버버벅 하는 결과를 가져온다.
그리고 늘 생각하는건데, 재정리를 하게 되면 이미 다 안다고 생각했던 내용에서도 새로운 깨달음이 찾아오기에 그런 재미도 있으므로 다시금 정리해보는 시간을 가진다
보통 일반적으로 클라이언트에서 최적화를 진행한다고 한다면 아무래도 FTP ( first time to paint)를 얼마만큼 빨리 할 수 있는가에 대해서를 생각해봐야 할 것이다.
일반적으로 사용되는 라이브러리 "React"의 경우 4가지의 단계를 거친다
1. http 요청을 통해 html 파일을 서버로부터 전송받는다
2. html 파일을 파싱한다
3. (http2.0일 경우) link 태그와 같은 요소를 만났을 시 비동기적으로 파일을 요청해 받는다
4. script 태그를 만날 시 진행을 멈추고 js 파일을 전송받은 후, hydration을 진행한다.
hydration이란, 만들어진 html 프레임에 상호작용이 가능한 js 코드들이 덧붙여지는 과정을 뜻한다.
이때, 전체 코드들의 모듈 병합중에서 특정 모듈의 내용에 또다른 서버요청이나 아주 큰 데이터 처리를 해야되는 내용들이 존재할 경우, 기존의 react의 CSR이라면 결국 js 파일 전체의 처리가 느려지는 결과를 가져오게 되어 사용자가 더 오랫동안 빈 화면을 보게 된다.
즉, 이렇게 복잡한 로직을 가진 컴포넌트는 따로 분리하여 독립적으로 처리되게 만드는 것이 더 좋을 것이다.
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
따라서 React 18에서는 Suspense라는 추가 태그를 이용하여 해당 컴포넌트는 따로 Data Fetching이 완료되기 전까지 대체적인 fallback 프로퍼티에 담겨있는 요소를 보여주다가 Data fetching이 완료되면 처리 후 해당 컴포넌트로 대체하는 방식을 제공하고 있다.
여기에 추가적으로
위의 내용은 어디까지나 웹팩으로 병합된 큰 모듈 내에서 하나의 모듈이 비용이 비싼 네트워크 요청을 하는 경우에 사용하는 방법을 뜻하고,
만약 어차피 해당 화면에서 보여주지도 않을 모듈인데 그 모듈에 코드가 너무 많아서 전체 로딩을 느리게 만드는 경우는 어떻게 해야할까?
그렇다면 어차피 지금 당장 사용하지 않을 코드는 나중에 느긋하게 패칭되도록 따로 빼서 만드는 것이 좋을 것이다. 이때 사용하는 것이 React.lazy이다
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent /> // lazy 대상은 항상 Suspense 태그의 자식이어야 한다
</Suspense>
</div>
);
}
위와 같이 적용을 한다면, 해당 내용은 배포 당시에 자동으로 리엑트에서 웹팩으로 분리하는 Code Spliting으로 분할되고, 만약 필요한 순간이 된다면 해당 내용을 서버로부터 받아서 뒤늦게 처리되어 전체 js 모듈 파일의 hydration을 방해하지 않는다.
Code spliting은 next.js에서 서버사이드 렌더링을 할 때에 build하면 만들어주는 분할 파일들을 연상하면 이해하기 쉽다.
초록색 부분은 첫 html 파일을 받아서 랜더링 당시 script 태그로 받아오는 자바스크립트 파일이라고 이해하면 편하다. 해당 js 파일에는 이미 chunk 단위로 code Splitting 된 js가 분리되어 있기 때문에 전체 js파일이 html과 hydration 하는 것을 방해하지 않고 독자적으로 진행이 된다.
여기까지가 React 18을 도입하였을 때에 기대할 수 있는 네트워크 최적화이다.
하지만, 해당 버전은 아직 알파 단계로 실전에서 사용하기에는 예기치못한 에러를 마주할 수 있으므로 조금 더 지켜보는 과정이 필요할 것으로 보인다.
대부분 http를 통한 요청의 내용은 모듈들의 패키지화를 통해 번들링이 된 상태로 전달되는데, 이 번들링 자체의 크기가 클 경우 당연히 네트워크적으로 응답이 느려지는 결과를 낳게 된다.
즉, 이 번들링을 한 패키지를 또다시 한번 더 압축하여 경량화할 필요성이 있다는 의미이다.
다행이도, 세계에는 천재들이 많은 터라 해당 압축을 자동으로 알고리즘을 통해 해주는 편리한 기능이 있는데 그것이 바로 http request header에 존재하는 "Accept-Encoding"과 http response header에 존재하는 "Content-Encoding" 이다.
두개는 서로 쌍으로 이루어져 있고 서버와 클라이언트 둘 다 적용되어 있어야 한다.
이때 가장 많이 사용하는 옵션이 Gzip인데, 해당 압축 알고리즘을 받아들인다고 요청 헤더에 Accept-Encoding: gzip로 설정하고 응답 헤더 역시 gzip으로 전달하겠다는 의미의 Content-Encoding: gzip을 설정해주면 알아서 최적화 압축을 통해 번들을 경량화해준다.
참고로 최근에 구글에서 Brotli라는 형식을 발표했다고 하는데 압축량이 아주 좋긴 하다만 IE를 지원하지 않으므로 IE까지 고려한다면 "gzip"을, Brotli를 쓰겠다면 "br"로 헤더설정을 해주면 된다.
CDN은 말 그대로 데이터 전송의 중계기 역할을 한다.
만약 내가 대한민국에서 미국 서버에 있는 데이터를 요청한다고 하자(ex, 유튜브)
저 지구 반대편에 있는 데이터가 여기까지 순식간에 올 수 있는 이유는 거기서 직접적으로 오는 것이 아니라 CDN 서버를 통해 캐싱 내용을 전달받고 있기 때문이다.
CDN 서버를 통해 자주 사용되는 정적 파일들을 캐싱하여 저장한 후, 이것을 요청마다 전달해주면 용량의 최대 80%까지 절약하여 효과적으로 전송할 수 있다고 하니 만약 글로벌한 서비스를 제공하길 원한다면 모든 앱을 CDN을 통해 전파해주는 배포서비스 (ex AWS) 를 사용하는 것이 좋아보인다.
사실 모든 요청을 느려지게 만드는 가장 주된 원인은 이미지때문이다. (전체 용량의 50% 이상을 이미지가 차지한다고 한다)
따라서 이미지를 사용할 때에 모든 이미지마다 요청을 날리게 된다면 자동적으로 네트워크 병목현상이 발생할 수 밖에 없는 원인이 된다.
만약 위에 있는 아이콘들마다 전부 다 네트워크 요청을 날린다고 생각해보자. 요청만 벌써 5개 이상이다.
차라리 이렇게 한 페이지에 사용할 이미지들을 하나의 이미지로 압축한 후, 이것을 background-position 옵션을 이용하여 위치조정을 해서 나타내게 하는 것이 더 효과적인 요청이라고 할 수 있을것이다.
참고로 background-poisition은 특정 이미지에 대해서 위치조정을 할 수 있는 옵션이다. 예를들어 img태그를 통해 이미지가 받아들어졌을 경우, img 태그 자체에 display:inline-block을 설정하면 해당 태그 요소의 크기 이외의 이미지는 보이지가 않게 된다. 이때 background-position으로 보여져야 할 이미지를 조정하면 나머지는 보이지 않고 해당 이미지에서 특정 부위만 보이게 하는 것이 가능하다.
이미지를 특정 포맷으로 압축하여 변경할 경우, 손실량을 최소화하면서 원하는 이미지를 보여줄 수 있는 기능을 보여준다. 최근 나온 webP 포멧은 png에 비교하면 26% 이상 용량이 감소되지만 실제 보여지는 유실률은 몹시 적다고 알려져 있다.
하지만 해당 포멧들은 최신기법이라 구형 브라우저에서 지원을 하지 않는다는 단점이 존재하므로, 소비층을 잘 판단해서 사용해야 한다.
만약 Next.js를 사용한다면 알아서 해당 프레임워크가 제공하는 Image 컴포넌트를 이용하면 webP로 지원여부를 판단하여 서버에 요청하는 방식을 사용할 수 있다.
import Image from 'next/image'
const myLoader = ({ src, width, quality }) => {
return `https://example.com/${src}?w=${width}&q=${quality || 75}`
}
const MyImage = (props) => {
return (
<Image
loader={myLoader}
src="me.png"
alt="Picture of the author"
width={500}
height={500}
/>
)
}
만약 리엑트와 같은 일반 자바스크립트 라이브러리를 사용한다면, 내부에 picture 태그로 감싼 형태로 만들어주면 된다.
(사실 지금 적어놓고 보니까 Next.js 의 Image 컴포넌트가 이것을 컴포넌트화해서 만든것으로 보인다)
<picture>
<source srcset="logo.webp" type="image/webp">
<img src="logo.png" alt="logo">
</picture>
위와 같이 만들어놓는다면, 만약 브라우저가 source 태그에 있는 scrset 안의 포멧을 지원하지 않는다면 해당 source가 무시되고 img 태그가 사용된다.