Progressive JSON ๐จ
์ด๋ฏธ์ง๋ฅผ ์์์ ์๋๋ก ์์ฐจ์ ์ผ๋ก ๋ก๋ฉํ๋ ๊ฒ์ด ์๋๋ผ, ์ฒ์์๋ ํ๋ฆฟํ๊ฒ ๋ณด์ด๋ค๊ฐ ์ ์ ์ ๋ช ํด์ง๋ ๋ฐฉ์์ผ๋ก ๋ก๋ฉํ๋ ๋ฉ์ปค๋์ฆ์ ์ ์ง์ JPEG๋ผ๊ณ ํฉ๋๋ค. ์ด ์์ด๋์ด๋ฅผ JSON ์ ์ก ๋ฐฉ์์ ์ ์ฉํ ๊ฒ์ด Progressive JSON์ ๋๋ค.
const data = {
header: 'Welcome to my blog',
post: {
content: 'This is my article',
comments: [
'First comment',
'Second comment',
// ...
]
},
footer: 'Hope you like it'
}
๊ธฐ์กด JSON์ ๋ง์ง๋ง ๋ฐ์ดํธ๊น์ง ๋ชจ๋ ๋ก๋๋์ด์ผ ์ ํจํ ๊ฐ์ฒด ํธ๋ฆฌ๋ฅผ ์ป์ ์ ์์ต๋๋ค. ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ผ๋ง JSON.parse๋ฅผ ํธ์ถํ ์ ์๊ฒ ๋๋ ๊ฒ์ด์ฃ . ๋ง์ฝ JSON์ ์ผ๋ถ ์์ฑ์ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฐ๋ค๋ฉด, ํด๋ผ์ด์ธํธ๋ ์๋ฒ๊ฐ ๋ชจ๋ ์์
์ ์๋ฃํ ๋๊น์ง ์ด๋ค ์์
๋ ์์ํ ์ ์์ต๋๋ค.
์ด๋ ๋ง์น ๋ชจ๋ ๋ด์ฉ์ ์๋ฒฝํ๊ฒ ์ดํดํด์ผ๋ง ๋ค์ ๋จ์์ผ๋ก ๋์ด๊ฐ๋ ํ์ต ๋ฐฉ์๊ณผ ๊ฐ์ต๋๋ค. ์งํฉ๊ณผ ๋ช ์ ๋ฅผ ๋ฐ์ฌ ์์ค์ผ๋ก ์๋ฒฝํ๊ฒ ์ดํดํ์ง ๋ชปํ๋ค๋ฉด ํจ์์ ๋ฏธ์ ๋ถ ๋ฑ ๋ค์ ๋จ์์ผ๋ก ๋์ด๊ฐ์ง ๋ชปํ๋ ๊ฒ์ฒ๋ผ ๋ง์ด์ฃ . ์ด๊ฒ์ ์ข์ ํ์ต ๋ฐฉ์์ด๋ผ๊ณ ๋ณด๊ธฐ ์ด๋ ต์ต๋๋ค.
JSON ํ์๋ JSON ๋ฌธ์์ด์ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ก ๋ณํํ๋ ์ญํ ์ ํฉ๋๋ค. ์์ ์ ์ํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด Streaming JSON Parser๋ฅผ ๊ตฌํํด๋ณผ ์ ์์ต๋๋ค. ์ฆ, ๋ถ์์ ํ ์ ๋ ฅ์ผ๋ก๋ถํฐ ๊ฐ์ฒด ํธ๋ฆฌ๋ฅผ ์์ฑํ๋ Parser์ ๋๋ค.
{
header: 'Welcome to my blog',
post: {
content: 'This is my article',
comments: [
'First comment',
'Second comment'
// ์ฌ๊ธฐ์ ์คํธ๋ฆผ์ด ๋์ด์ง ์ํ
์ด ์์ ์์ ๊ฒฐ๊ณผ๋ฅผ ์์ฒญํ๋ฉด, Streaming Parser๋ ๋ค์๊ณผ ๊ฐ์ ๊ฐ์ฒด๋ฅผ ๋ฐํํ ๊ฒ์ ๋๋ค:
{
header: 'Welcome to my blog',
post: {
content: 'This is my article',
comments: [
'First comment',
'Second comment'
// (๋๋จธ์ง ๋๊ธ๋ค์ด ๋๋ฝ๋จ)
]
}
// (footer ์์ฑ์ด ๋๋ฝ๋จ)
}
Streaming Parser์ ๋ฌธ์ ์ :
header / post / footer ์ธ ๊ฐ์ ์์ฑ์ ๊ฐ์ ธ์ผ ํ์ง๋ง footer๊ฐ ๋๋ฝ์ด๋ ์ฌ์ ํ ํํฅ์(top-down) ๋ก๋ฉ ๋ฉ์ปค๋์ฆ์ ๋ฐ๋ฆ ๋๋ค. ์ด๋ฏธ์ง์ ์๋จ 10%๋ ์ ๋ช ํ์ง๋ง ์ ์ฒด ๊ทธ๋ฆผ์ ์ดํดํ๊ธฐ ์ด๋ ค์ด ๊ฒ์ฒ๋ผ, top-down ๋ฐฉ์์ ๋ถ์์ ํ ๊ฐ์ฒด๋ ๊ทธ ์์ฒด๋ก ๋ถ์์ ์ฑ์ ๋ดํฌํ๊ณ ์์ต๋๋ค.
์ง๊ธ๊น์ง ์ค๋ช ํ ๋ฐฉ์์ ๊น์ด ์ฐ์ (depth-first) ๋ฐฉ์์ ๋ฐ์ดํฐ ์ ์ก์ ๋๋ค. ์ต์์ ๊ฐ์ฒด์ ์์ฑ๋ถํฐ ์์ํ์ฌ post, comments ์์ผ๋ก ๋ค์ด๊ฐ๋ ๋ฐฉ์์ด์ฃ .
Progressive JSON์ ๋๋น ์ฐ์ (breadth-first) ๋ฐฉ์์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์กํฉ๋๋ค:
{
header: "$1",
post: "$2",
footer: "$3"
}
$ ํ์๋ ์์ง ์ ์ก๋์ง ์์ ์ ๋ณด๋ฅผ ๋ํ๋ด๋ placeholder์
๋๋ค. ์ดํ ์คํธ๋ฆผ์์ ์ ์ง์ ์ผ๋ก ์ฑ์์ง ๊ฒ์
๋๋ค:
{
header: "$1",
post: "$2",
footer: "$3"
}
/*$1*/
"Welcome to my blog"
/*$3*/
"Hope you like it"
$1๊ณผ $3์ ๋จผ์ ๋ณด๋์ง๋ง $2๋ ์์ง ๋๊ธฐ ์ค์ธ ์ํฉ์ ๋๋ค. ํด๋ผ์ด์ธํธ๊ฐ ํธ๋ฆฌ๋ฅผ ์ฌ๊ตฌ์ฑํ๋ฉด:
{
header: "Welcome to my blog",
post: new Promise(/* ... ์์ง ์ดํ๋์ง ์์ ... */),
footer: "Hope you like it"
}
์์ง ๋ก๋๋์ง ์์ ๋ถ๋ถ๋ค์ Promise๋ก ํํ๋ฉ๋๋ค. ์๋ฒ๊ฐ ๊ณ์ ์คํธ๋ฆฌ๋ฐํ๋ฉด:
{
header: "$1",
post: "$2",
footer: "$3"
}
/*$1*/
"Welcome to my blog"
/*$3*/
"Hope you like it"
/*$2*/
{
content: "$4",
comments: "$5"
}
/*$4*/
"This is my article"
/*$5*/
["$6", "$7", "$8"]
/*$6*/
"This is the first comment"
/*$7*/
"This is the second comment"
/*$8*/
"This is the third comment"
์ต์ข ์ ์ผ๋ก ํด๋ผ์ด์ธํธ์์๋ ์์ ํ ํธ๋ฆฌ๊ฐ ์กฐ๋ฆฝ๋ฉ๋๋ค:
{
header: "Welcome to my blog",
post: {
content: "This is my article",
comments: [
"This is the first comment",
"This is the second comment",
"This is the third comment"
]
},
footer: "Hope you like it"
}
๋ฐ์ดํฐ๋ฅผ ๋๋น ์ฐ์ ๋ฐฉ์์ผ๋ก ์ฒญํฌ ๋จ์๋ก ์ ์กํจ์ผ๋ก์จ, ํด๋ผ์ด์ธํธ์์๋ ์ด๋ฅผ ์ ์ง์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๊ฒ ๋์์ต๋๋ค.
์ธ๋ผ์ด๋์ ๋๋ฆฌ๊ฒ ์์ฑ๋๋ ๋ฐ์ดํฐ๋ง ์ง์ฐ ๋ก๋ฉํ๊ณ ๋๋จธ์ง๋ ์ฆ์ ํฌํจ์์ผ ์ ์กํ๋ ์ ํ์ ์ง์ฐ ์ ์ก ๊ธฐ๋ฒ์ ๋๋ค.
์์ ์์ ์์๋ ์คํธ๋ฆฌ๋ฐ์ ๊ณผ๋ํ๊ฒ ์ฌ์ฉํ์ต๋๋ค. ์ผ๋ถ ์ฝํ ์ธ ์์ฑ์ด ์ค์ ๋ก ๋๋ฆฌ์ง ์๋ค๋ฉด, ๊ฐ๊ธฐ ๋ค๋ฅธ ํ์ผ๋ก ๋๋ ๋ณด๋ด๋ ๊ฒ์๋ ์ด์ ์ด ์์ต๋๋ค.
post ๋ณธ๋ฌธ๊ณผ ๋๊ธ์ ๋ถ๋ฌ์ค๋ ์์ ๋ง ํนํ ๋๋ฆฐ ์ํฉ์ด๋ผ๊ณ ๊ฐ์ ํด๋ด ์๋ค:
1๋จ๊ณ: ๊ฐ์ฅ ๋ฐ๊นฅ ์์๋ฅผ ๋จผ์ ์ ์ก
{
header: "Welcome to my blog",
post: "$1",
footer: "Hope you like it"
}
ํด๋ผ์ด์ธํธ ํด์:
{
header: "Welcome to my blog",
post: new Promise(/* ... ์์ง ์ดํ๋์ง ์์ ... */),
footer: "Hope you like it"
}
2๋จ๊ณ: comments๋ฅผ ์ ์ธํ post ๋ฐ์ดํฐ ์ ์ก
{
header: "Welcome to my blog",
post: "$1",
footer: "Hope you like it"
}
/*$1*/
{
content: "This is my article",
comments: "$2"
}
ํด๋ผ์ด์ธํธ ํด์:
{
header: "Welcome to my blog",
post: {
content: "This is my article",
comments: new Promise(/* ... ์์ง ์ดํ๋์ง ์์ ... */),
},
footer: "Hope you like it"
}
3๋จ๊ณ: ๋๊ธ์ ํ ๋ฒ์ ๋ฌถ์ด์ ์ ์ก
/*$2*/
[
"This is the first comment",
"This is the second comment",
"This is the third comment"
]
์ธ๋ผ์ด๋์ ํต์ฌ์ ์๋์ ํ์์ ๋ฐ๋ฅธ ๊ท ํ ์๋ ๋ฐ์ดํฐ ๋ถํด์
๋๋ค. ๋๋ฆฐ ๋ถ๋ถ๋ง ๋ณ๋๋ก ์ง์ฐ์ํค๊ณ , ๋น ๋ฅด๊ฒ ์ค๋น๋๋ ๋ถ๋ถ์ ์ฆ์ ํฌํจ์์ผ ์ ์กํจ์ผ๋ก์จ ์ ์ฒด ์ฌ์ฉ์ ๊ฒฝํ์ ๋งค๋๋ฝ๊ฒ ๋ง๋๋ ์ ๋ต์
๋๋ค.
์์๋ผ์ด๋์ ์ค๋ณต๋๊ฑฐ๋ ์ฐธ์กฐ ๊ฐ๋ฅํ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํ ํฐ์ผ๋ก ๋์ฒดํ ํ, ๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ณ๋์ ์คํธ๋ฆผ์ผ๋ก ๋ถ๋ฆฌํ์ฌ ์์ฐจ์ ์ผ๋ก ์ ์กํจ์ผ๋ก์จ ์ค๋ณต ์ ๊ฑฐ์ ์ํ ์ฐธ์กฐ ์ง์์ ๊ฐ๋ฅํ๊ฒ ํ๋ '์ ๋ฉด' ์ง์ฐ ์ ์ก ๊ธฐ๋ฒ์ ๋๋ค.
๋ค์๊ณผ ๊ฐ์ ๊ฐ์ฒด ํธ๋ฆฌ๊ฐ ์๋ค๊ณ ๊ฐ์ ํด๋ด ์๋ค:
const userInfo = { name: 'Dan' };
[
{ type: 'header', user: userInfo },
{ type: 'sidebar', user: userInfo },
{ type: 'footer', user: userInfo }
]
์ผ๋ฐ JSON์ผ๋ก ์ง๋ ฌํํ๋ฉด { name: 'Dan' }์ด ๋ฐ๋ณต๋ฉ๋๋ค:
[
{ type: 'header', user: { name: 'Dan' } },
{ type: 'sidebar', user: { name: 'Dan' } },
{ type: 'footer', user: { name: 'Dan' } }
]
Progressive JSON์์๋ ์์๋ผ์ธ์ผ๋ก ๋ถ๋ฆฌํ ์ ์์ต๋๋ค:
[
{ type: 'header', user: "$1" },
{ type: 'sidebar', user: "$1" },
{ type: 'footer', user: "$1" }
]
/* $1 */
{ name: "Dan" }
์์๋ผ์ด๋์ ์ฅ์ :
์ธ๋ผ์ด๋๊ณผ ์์๋ผ์ด๋์ ํผํฉํ์ฌ ๋ ๊ท ํ ์กํ ์ ๋ต์ ์ทจํ ์๋ ์์ต๋๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก ๊ฐ์ฒด๋ฅผ ์ธ๋ผ์ธ์ผ๋ก ์ง๋ ฌํํ๋, ๊ฐ์ ๊ฐ์ฒด๊ฐ ๋ ๋ฒ ์ด์ ๋ฑ์ฅํ๋ฉด ๋ณ๋๋ก ๋ถ๋ฆฌํด์ ์ค๋ณต์ ์ ๊ฑฐํ๋ ๋ฐฉ์์ ๋๋ค.
์ง๊ธ๊น์ง์ ์ค๋ช ์ ์ฌ์ค ๋ฆฌ์กํธ ์๋ฒ ์ปดํฌ๋ํธ(React Server Components)๊ฐ ๋์ํ๋ ๋ฐฉ์๊ณผ ๋์ผํฉ๋๋ค.
๋ฆฌ์กํธ ์๋ฒ ์ปดํฌ๋ํธ๋ก ๋ค์๊ณผ ๊ฐ์ ํ์ด์ง๋ฅผ ๋ง๋ ๋ค๊ณ ํด๋ด ์๋ค:
function Page() {
return (
<html>
<body>
<header>Welcome to my blog</header>
<Post />
<footer>Hope you like it</footer>
</body>
</html>
);
}
async function Post() {
const post = await loadPost();
return (
<article>
<p>{post.text}</p>
<Comments />
</article>
);
}
async function Comments() {
const comments = await loadComments();
return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}
๋ฆฌ์กํธ๋ Page์ ๊ฒฐ๊ณผ๋ฅผ Progressive JSON ์คํธ๋ฆผ์ผ๋ก ํด๋ผ์ด์ธํธ์ ์ ๊ณตํฉ๋๋ค. ํด๋ผ์ด์ธํธ์์๋ ํด๋น ์คํธ๋ฆผ์ด ์ ์ง์ ์ผ๋ก ๋ฆฌ์กํธ ํธ๋ฆฌ๋ก ๋ณต์๋ฉ๋๋ค.
๋ก๋ฉ ๋จ๊ณ๋ณ UI ์ํ:
1๋จ๊ณ: ์ด๊ธฐ ์ํ
<html>
<body>
<header>Welcome to my blog</header>
{new Promise(/* ... ์์ง ์ดํ๋์ง ์์ */)}
<footer>Hope you like it</footer>
</body>
</html>
2๋จ๊ณ: loadPost() ์๋ฃ ํ
<html>
<body>
<header>Welcome to my blog</header>
<article>
<p>This is my post</p>
{new Promise(/* ... ์์ง ์ดํ๋์ง ์์ */)}
</article>
<footer>Hope you like it</footer>
</body>
</html>
3๋จ๊ณ: loadComments() ์๋ฃ ํ
<html>
<body>
<header>Welcome to my blog</header>
<article>
<p>This is my post</p>
<ul>
<li key="1">This is the first comment</li>
<li key="2">This is the second comment</li>
<li key="3">This is the third comment</li>
</ul>
</article>
<footer>Hope you like it</footer>
</body>
</html>
๋ฐ์ดํฐ๊ฐ ์ ์ง์ ์ผ๋ก ๋ค์ด์ค๋๋ผ๋, ํ์ด์ง๊ฐ ์์๋ก ์ ํํ๋ฏ ๋ ๋๋ง๋๊ธธ ์ํ์ง๋ ์์ ๊ฒ์ ๋๋ค. ํฌ์คํธ ๋ณธ๋ฌธ ์์ด ํค๋์ ํธํฐ๋ง ๋จผ์ ๋ณด์ด๋ ๊ฒ์ ์ข์ง ์๊ฒ ์ฃ .
๋ฆฌ์กํธ๋ <Suspense>๋ฅผ ํตํด ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํฉ๋๋ค:
import { Suspense } from 'react';
function Page() {
return (
<html>
<body>
<header>Welcome to my blog</header>
<Post />
<footer>Hope you like it</footer>
</body>
</html>
);
}
async function Post() {
const post = await loadPost();
return (
<article>
<p>{post.text}</p>
<Suspense fallback={<CommentsGlimmer />}>
<Comments />
</Suspense>
</article>
);
}
async function Comments() {
const comments = await loadComments();
return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}
์ฌ์ฉ์๋ ์ด์ ๋ ๋จ๊ณ์ ๊ฑธ์น ๋ก๋ฉ ์ํ์ค๋ฅผ ๊ฒฝํํ๊ฒ ๋ฉ๋๋ค:
๋ฆฌ์กํธ ํธ๋ฆฌ ์์ Promise๊ฐ throw๋ผ๋ฉด, Suspense๋ catch์ ๊ฐ์ ์ญํ ์ ํฉ๋๋ค. ๋ฐ์ดํฐ๋ ์๋ฒ๊ฐ ์ค๋น๋๋ ๋๋ก ๊ฐ๋ฅํ ํ ๋นจ๋ฆฌ ๋์ฐฉํ์ง๋ง, ๋ฆฌ์กํธ๋ ์ด๋ฌํ ์ํฉ์ ์ฐ์ํ๊ฒ ๋ณด์ฌ์ฃผ๋ ํ๋ฆ์ ๊ด๋ฆฌํด์ฃผ๊ณ , ๊ฐ๋ฐ์๊ฐ ์๊ฐ์ ์ธ ๊ณต๊ฐ๋ฅผ ์ ์ดํ ์ ์๋๋ก ํฉ๋๋ค.
Progressive JSON์ ๋จ์ํ ๊ธฐ์ ์ ์ต์ ํ๋ฅผ ๋์ด์, ์ฌ์ฉ์๊ฐ ๊ธฐ๋ค๋ฆฌ๋ ์๊ฐ์ ์๋ฏธ ์๊ฒ ๋ง๋๋ ์ฒ ํ์ ๋ด๊ณ ์์ต๋๋ค. ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ ๋์ , ์ค๋น๋ ๊ฒ๋ถํฐ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ณด์ฌ์ฃผ๋ฉฐ ์ ์ฒด์ ์ธ ๊ฒฝํ์ ๊ฐ์ ํ๋ ๊ฒ์ด ๋ฐ๋ก Progressive JSON์ ํต์ฌ ๊ฐ์น์
๋๋ค.
reference
: https://overreacted.io/progressive-json/#streaming-data-vs-streaming-ui
: https://hanameee.github.io/posts/progressive_json?utm_source=substack&utm_medium=email
: https://www.liquidweb.com/blog/what-is-a-progressive-jpeg/