이 글은 저도 프로덕션에서 사용해본 적이 없는 PoC 수준의 내용을 다룹니다.
브라우저의 동작 원리를 알고 계신가요?
주소창에 주소를 입력하고 엔터를 누르면 어떤일이 일어날까요?
그럼 우리의 첫번째 자바스크립트 코드는 엔터를 누르고 몇 초 후에 실행되는지도 알고 계신가요?
이를 알아보기 위해 CRA로 앱을 생성하고 아주 간단한 리액트 코드를 작성해 보았습니다.
(이 글에서 알아보고자 하는것은 정확히 말하면 첫번째 자바스크립트 코드가 몇 초 후에 실행될까요
가 아니라 첫 번째 네트워크 요청이 몇 초 후에 실행될까요
입니다.)
const Product = ({ id }) => {
const [data, setData] = React.useState();
React.useEffect(() => {
(async () => {
setData(
await fetch(
`https://63da01a2b28a3148f67cef79.mockapi.io/products/${id}/`
)
);
})();
}, [id]);
return (
<div>
<div>ProductName: {data?.name}</div>
<img src={data?.avatar} style={{ width: "100px", height: "100px" }} />
</div>
);
};
const App = () => {
return (
<Product id={3} />
);
};
이 코드를 실행하면 일반적인 CRA 앱에서 첫 번째 네트워크 요청이 언제 발생하는지를 알아볼 수 있습니다.
여기서는 0.4초 후네요.
API 응답은 약 0.2초가 걸렸습니다.
그럼 우리는 0.6초(0.4 + 0.2)후에 (그것도 아주 빠른 인터넷 환경일 때) 유저에게 컨텐츠를 보여줄 수 있게 됩니다.
물론 이부분에 대해 더 자세히 알고 계시는 분들은 0.6초도 끝이 아니라는것을 알것입니다. 왜냐면 이미지는 0.6초부터 로드를 시작하기 때문에 정말로 로드가 끝나는 시점은 더 더 길어질수도 있다는걸 말입니다.
0.6초는 느린 시간은 아니지만, 퍼포먼스는 빠르면 빠를수록 좋습니다.
심지어 이 글에서 다루는 내용은 눈으로 체감할 수 있습니다.
글의 마지막 부분에 보여드릴 예정입니다만, SSR은 얼마나 빠른지, CSR은 얼마나 느린지, 제가 이번에 소개할 SSR과 CSR의 중간 지점을 만들면 얼만큼 따라잡을 수 있는지를 영상으로 보여드립니다.
일단 분명히 해야 할 점은 모든 자바스크립트 프로젝트가 0.4초후를 소비하지 않습니다.
프로젝트의 규모, 혹은 네트워크 상황에 따라 천차만별이겠지만 제 테스트 환경은 localhost에 CRA 기본 구성이니 대부분의 경우에 이것보다 늘어날수는 있어도 줄어들기는 쉽지 않을수도 있겠네요.
이 부분을 이해하기 위해선 정말로 브라우저 주소창에 엔터를 누르면 무슨일이 일어나야 하는지 알아야 합니다.
물론 여기서는 DNS라던가 TCP라던가 이런부분은 다루지 않습니다.
xxx.html
페이지를 로드합니다. 이 작업은 대부분의 경우에 굉장히 빠른 속도로 수행됩니다.<script>
태그가 있다면 이를 로드합니다. CRA 환경이라면 아래 사진의 bundle.js 입니다. (이름은 다를 수 있습니다.)---- 여기까지 0.16초가 걸렸습니다. ------
---- 여기까지 0.4초가 걸렸습니다.
우리가 데이터를 보여주기 위해선 API콜을 수행해야 하는데, API 콜은 bundle.js 가 로드되어야 수행할 수 있습니다.
bundle.js는 index.html가 로드되어야 로드할 수 있습니다.
API 요청을 시작하기 전에도 여러단계의 waterfall이 이미 생겨버렸고 0.4초를 그냥 기다릴수 밖에 없겠네요.
물론 가능합니다!
HTML에는 script
태그를 작성하고 스크립트를 작성할 수 있습니다.
<body>hi</script>
<script>console.log('hello world');</script>
index.html
에 스크립트를 직접 작성하는 방식은 옛날에는 흔한 방법이었지만 다양한 프레임워크와 번들링 도구가 발전하면서 이제는 금기시되는 일이 되었습니다. 저도 거의 몇년간 index.html
에 script
태그를 안써본 것 같네요.
우리는 잠시 과거로 돌아가서 우리의 코드를 가장 빨리 실행시킬수있는 index.html
에 코드를 적어보겠습니다.
<script>
prefetchStorage = {};
const prefetch = (url) => {
const task = async () => {
const json = await (await fetch(url)).json();
return json;
};
prefetchStorage[url] = task();
};
const sp = new URLSearchParams(window.location.search);
const id = sp.get("product_id");
if (id) {
prefetch(`https://63da01a2b28a3148f67cef79.mockapi.io/products/${id}/`);
}
</script>
리액트가 초기화되지 않아도, 혹은 react-router-dom을 쓰지 않아도 이미 window.location
에는 주소를 식별할 수 있는 기능이 있습니다.
이를 이용해 해당 주소에서 어떤 API를 사용하게 될지 미리 예측하고 prefetch 하는 코드를 하드코딩할 수 있습니다.
const myfetch = async (url) => {
if (prefetchStorage[url]) {
console.log("cache hit: " + url);
return await prefetchStorage[url];
} else {
const json = await (await fetch(url)).json();
return json;
}
};
앱 내부의 fetch
함수는 이렇게 한번 래핑했습니다. 실제 네트워크 요청을 수행하기 이전에 prefetch pool 에 데이터가 있으면 해당 Promise를 대신 반환합니다.
(before)
(after)
이제 우리 앱은 bundle.js
가 로딩되기보다 전에도 API 요청을 수행합니다!
그리고 해당 요청을 react에서 그대로 이어와서 처리할 수 있게 되었습니다.
이것 또한 가능합니다.. 만 이 글의 주제에서 벗어납니다.
그래도 어떤 방식이 있는지 설명해보도록 하겠습니다.
CSR과 SSR의 비교
ping 100ms의 LTE 환경이라고 가정하겠습니다.
(50ms) 라고 적힌 부분은 모바일 기기에서 혹은 모바일 기기로의 단방향 전송
(10ms) 라고 적힌 부분은 서버와 서버간의 유선 환경에서의 단방향 전송을 의미합니다.
CSR: GET /index.html
(50ms) -> RESPONSE /index.html
(50ms) -> GET /bundle.js
(50ms) -> RESPONSE /bundle.js
(50ms) -> GET /products/3
(50ms) -> RESPONSE /products/3
(50ms) -> GET /imgs/product3.png
(50ms) -> RESPONSE /imgs/products3.png
(50ms) = 400ms
SSR: GET /idnex.html
(50ms) -> GET /products/3
(10ms) -> RESPONSE /products/3
(10ms) -> RESPONSE /index.html
(50ms) -> GET /imgs/product3.png
(50ms) -> RESPONSE /imgs/products3.png
(50ms) = 220ms
여기서 우리는 두가지 차이점을 발견할 수 있습니다.
첫번째는 bundle.js
를 아예 로드하지 않아도 페이지를 그릴 수 있다는 것이고 (여기서 100ms가 줄어듭니다)
두번째는 API 요청이 서버와 서버간에 이루어지기 때문에 안정적이고 빠른 유선랜을 통해 일어난다는 점 입니다. (50ms vs 10ms) 여기서 80ms가 줄어듭니다.
이 두 라이브러리는 속도를 위해 아주 근본적인 부분부터 다시 고려하여 작성된 프레임워크들입니다.
저도 사용해 본 적이 없어서 위 2개에 대해 자세히 적지는 못합니다만, 관심이 있으시면 읽어보셔도 좋을 것 같습니다.
마지막으로 제가 간단한 테스트 코드를 작성해 실행해본 결과를 올려드리겠습니다.
위에서부터 차례대로
index.html
에 하드코딩된 div와 img입니다. 다르게 말하면 이건 SSG가 해주는 일을 손으로 직접 한 것 이라고 볼 수도 있습니다.index.html
은 인프라적 차이, 그리고 파이프라인상 차이가 있을 수 있습니다만 이 글의 주제는 TTFB(Time to First Byte)가 아닌걸 감안하고 봐주세요.이 방법은 굉장히 실험적이며, 개선해야 할 부분이 많습니다. (예를들어 prefetch가 실패하면 어쩔건지)
저는 이 글을 통해 실험적인 prefetch
가 아니라 CSR과 SSR의 근본적 속도 차이는 어디서 나오는가를 알아가실 수 있으면 좋을 것 같습니다.
(뭐가 어떻게 돌아가는지를 아는것이 빠르게 만드는 첫걸음입니다)