최근 사내 세미나를 통해 접하게 된 103 Early Hints를 사용해보려고 했지만, 아쉽게도 실패했습니다.
다시 확인 해 본 결과, 실패 한 줄 알았는데 잘 작동하고있었습니다.
다만, 브라우저의 워터폴에서는 200 응답을 받은 이후에 리소스를 받아오는 것으로 나타나서, 103 Early Hints가 제대로 작동하는지 확인하기 어려웠습니다.
그러면, 제가 어떤 방식으로 103 Early Hints를 사용하려고 했는지, 그리고 어떻게 성공했다고 말 할 수 있는지 한번 알아보겠습니다.
새로 추가된 HTTP의 응답 코드로, 서버가 최종 응답을 준비하는 동안 서버가 최종 응답에서 사용할 리소스에 대한 힌트를 클라이언트에게 미리 전달할 수 있도록 하는 응답 코드입니다.
사실 최초 제안은 2017년에 이루어졌고, 실제 추가는 2022년에 이루어졌습니다.(구글 크롬v103)
생소하지만, 생각보다 등장한지는 꽤 오래되었습니다.
이를 사용하면 사용자가 최종 응답을 받기 전에 필요한 리소스를 미리 다운로드 받을 수 있어 페이지 로딩 속도를 향상시킬 수 있습니다. 네, 그런데 어떻게 이게 가능할까요?
빨간 선으로 표시된 FCP가 조금씩 올라가고 있다는 것에 주목해주세요!
초기: HTML 문서만 사용
서버에서 보내주는 HTML 문서를 브라우저가 직접 파싱하고, 이후 필요한 리소스를 서버에 요청하고 받아오는 방식
HTTP/1.1 200 OK
Content-Type: text/html
<!DOCTYPE html>
...
<link rel="stylesheet" href="/style.css">
<script src="/script.js"></script>
...
</html>
첫번째 발전: 헤더의 사용
응답에서 가장 먼저 받을 수 있는 헤더 부분에 Link
헤더를 사용하여 필요한 리소스를 미리 알려주는 방식
HTML을 모두 받기 전에, 필요한 리소스를 미리 요청하고 다운로드 받을 수 있어 페이지 로딩 속도를 향상시킬 수 있음
HTTP/1.1 200 OK
Link: </style.css>; rel=preload; as=style
Link: </script.js>; rel=preload; as=script
...
<!DOCTYPE html>
...
</html>
두번째 발전: 103 Early Hints
서버가 최종 응답을 준비하는 동안 서버가 최종 응답에서 사용할 리소스에 대한 힌트를 클라이언트에게 미리 전달할 수 있도록 하는 응답 코드
사용자가 최종 응답을 받기 전에 필요한 리소스를 미리 다운로드 받을 수 있어 페이지 로딩 속도를 향상시킬 수 있음
HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style
Link: </script.js>; rel=preload; as=script
HTTP/1.1 200 OK
Content-Type: text/html
<!DOCTYPE html>
...
</html>
서버에서 103 Early Hints를 보내기 위해서는 위에서 다룬 것과 같은 방식으로 Link 헤더를 사용하여 필요한 리소스를 미리 알려주어야 합니다.
HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style
Link: </script.js>; rel=preload; as=script
Link: </image.png>; rel=preload; as=image
Link: <LOCATION>; rel=preload | preconnect; as=TYPE
LOCATION
: 리소스의 위치를 알려주는 헤더rel=preload | preconnect
: 리소스를 어떻게 사용할 것인지 알려주는 속성preload
: 리소스를 미리 다운로드 받아놓을 것을 알려주는 속성preconnect
: 리소스를 미리 연결해놓을 것을 알려주는 속성prefetch
: 아직 사용 할 수 없음as=TYPE
: 리소스의 타입을 알려주는 속성여기까지 설명한 것만 보면, Early Hints를 사용하면 모든 경우에 페이지 로딩 속도가 빨라질 것 같지만, 그렇지 않습니다. 여기에는 몇 가지 이유가 있고, 최적의 사용을 위한 몇 가지 중요한 사항이 있습니다.
Early Hints의 사용이 효과를 볼 수 있는 경우
Early Hints의 사용이 효과를 보기 힘든 경우
import http2 from 'http2';
import fs from 'fs';
import url from 'url';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const options = {
key: fs.readFileSync(join(__dirname, 'ssl', 'localhost.key')),
cert: fs.readFileSync(join(__dirname, 'ssl', 'localhost.crt')),
allowHTTP1: false,
};
const server = http2.createSecureServer(options);
server.on('stream', (stream, headers) => {
const parsedUrl = url.parse(headers[':path'], true);
const pathname = parsedUrl.pathname;
const filePath = join(__dirname, 'public', pathname);
// 일반적인 단일 200 응답
if (pathname === '/') {
fs.readFile(join(__dirname, 'public', 'index.html'), (err, data) => {
if (err) {
stream.respond({ ':status': 500 });
stream.end('Internal Server Error');
return;
}
setTimeout(() => {
stream.respond({
'content-type': 'text/html', ':status': 200
});
stream.end(data);
}, 300);
});
}
// 103 Early Hints 응답 사용
else if (pathname === '/early-hints') {
stream.additionalHeaders({
':status': 103,
'Link': '<https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHjx4wXiWtFCc.woff2>; rel=preload; as=font, </image.png>; rel=preload; as=image, </script.js>; rel=preload; as=script, </style.css>; rel=preload; as=style'
});
fs.readFile(join(__dirname, 'public', 'index.html'), (err, data) => {
if (err) {
stream.respond({ ':status': 500 });
stream.end('Internal Server Error');
return;
}
setTimeout(() => {
stream.respond({
'content-type': 'text/html', ':status': 200
});
stream.end(data);
}, 300);
});
}
// 이하는 기타 정적 파일 요청 처리
else if (pathname === '/style.css') {
fs.readFile(filePath, (err, data) => {
if (err) {
stream.respond({ ':status': 500 });
stream.end('Internal Server Error');
return;
}
stream.respond({ 'content-type': 'text/css', ':status': 200 });
stream.end(data);
});
} else if (pathname === '/script.js') {
fs.readFile(filePath, (err, data) => {
if (err) {
stream.respond({ ':status': 500 });
stream.end('Internal Server Error');
return;
}
stream.respond({ 'content-type': 'application/javascript', ':status': 200 });
stream.end(data);
});
} else if (pathname === '/image.png') {
fs.readFile(filePath, (err, data) => {
if (err) {
stream.respond({ ':status': 500 });
stream.end('Internal Server Error');
return;
}
stream.respond({ 'content-type': 'image/png', ':status': 200 });
stream.end(data);
});
} else {
stream.respond({ ':status': 404 });
stream.end('Not Found');
}
});
server.listen(3000, (error) => {
if (error) {
console.error(error);
return process.exit(1);
} else {
console.log('Server started on https://localhost:3000');
}
});
위 구현에서는 아래와 같이 총 5개의 엔드포인트를 사용하였습니다.
/
: 일반적인 200 응답을 사용한 엔드포인트/early-hints
: 103 Early Hints를 사용한 엔드포인트/style.css
: CSS 파일 요청/script.js
: JS 파일 요청/image.png
: 이미지 파일 요청별다를 건 없는, 우리가 아는 일반적인 요청과 응답 헤더입니다.
이전 응답과는 다르게 Early Hints Headers
가 추가된 것을 확인할 수 있습니다.
원래대로라면 해당 리소스를 200 응답에서 주어지는 HTML보다 먼저 받아올 수 있어야 합니다. 있습니다.
하지만 아래와 같이, 실제로는 해당 리소스를 절대로 먼저 받아오질 않네요..
확인결과, 개발자 도구의 워터폴에서는 200 응답을 받은 이후에 리소스를 받아오는 것으로 나타나서, 103 Early Hints가 제대로 작동하는지 확인하기 어려웠습니다.
결과적으로는 잘 작동하고 있었습니다!
처음에는 이 결과만 보고, 대체 왜 안되는걸까.. 하면서 고민을 많이 했습니다. 그리고 테스트가 실패 한 줄 알았죠.
하다못해 프레임워크나 언어 수준에서 지원하지 않는걸까, 하고 http2
, express
, fastify
에 나중에는 flask
, go
, nginx
등 다른 언어 및 툴까지도 시도를 해보고 있었습니다.
그런데 알고보니 단순히 크롬에서 제대로 노출을 안 해 주더라구요? 상상하지도 못했습니다.
좀 더 자세히 알아보기 위해, 크롬에서 제공하는 export 기능을 사용하여 네트워크 로그를 확인해보았습니다.
chrome://net-export/
위 이미지를 보시면, 브라우저에서 https://localhost:3000/
엔드포인트에 요청을 하고, 응답으로 200 OK와 함께 HTML 문서만을 받아온 것을 확인할 수 있습니다.
이미지의 가운데 쯤 표시한 t=3176
은 제가 레코딩을 누른 뒤 3.176초가 지난 시점에서 요청을 보냈다는 것을 나타냅니다.
즉 해당 요청은 t=3176
에 시작되어 t=3482
에 끝난 것을 확인할 수 있습니다.
그러면, 리소스는 언제 받아왔는지 볼까요?
위 이미지에서는 https://localhost:3000/script.js
엔드포인트에 t=3534
에 요청을 보내고, t=3540
에 응답을 받아온 것을 확인할 수 있습니다.
즉, HTML 문서를 받은 이후에 필요한 리소스를 받아온 것을 확인할 수 있습니다.
반면 위 이미지에서는 https://localhost:3000/early-hints
엔드포인트에 요청을 하고, 응답으로 103 Early Hints를 먼저 받고, 이어서 200 OK와 함께 HTML 문서를 받아온 것을 확인할 수 있습니다.
103 응답의 Link 헤더가 일부 잘려 있지만, 내용은 위의 예시 코드에서 작성한것과 동일하고 /script.js
에 대한 preload
를 포함하고있습니다.
이번에는 제가 레코딩 후 조금 빠르게 요청을 보냈나 보네요. 요청은 t=812
에 보냈고 응답은 t=1118
에 받았습니다.
그리고 그 사이인 t=814
에 미리 103 Early Hints를 받아온 것을 확인할 수 있습니다.
위 이미지에서는 https://localhost:3000/script.js
엔드포인트에 t=820
에 요청을 보내고, t=826
에 응답을 받아온 것을 확인할 수 있습니다.
결과적으로 HTML 본문을 받아오기 이전에, 필요한 script.js 리소스를 먼저 받아온 것을 확인할 수 있습니다.
아쉽게도 103 Early Hints를 사용한 엔드포인트에서도, 리소스를 먼저 받아오지 못했습니다.
문제점을 좀 더 찾아보고, 해결책을 찾아봐야겠습니다.
결과적으로, 103 Early Hints는 잘 작동했고, 이를 적절히 쓰면 사이트의 노출 속도를 빠르게 해 줄 수 있다! 였습니다.
읽어주셔서 감사합니다!
MDN - Early Hints
RFC-8297
Chrome for Developers - 103 Early Hints