๐Ÿ”ค ํ•œ๊ธ€ ์•ž์— ์ˆจ์€ NUL ๋ฐ”์ดํŠธ: React 18 ์ŠคํŠธ๋ฆฌ๋ฐ SSR์ด ํƒœ๊ทธ ๋งํฌ๋ฅผ ๊นจ๋œจ๋ฆฐ ๊ณผ์ •

@leekh8 ยท October 20, 2026 ยท 11 min read

์ง€๋‚œ์ฃผ HTTP ๋ณด์•ˆ ํ—ค๋” ๊ธ€์„ ๋ฐœํ–‰ํ•˜๊ณ  ์‹ค์ œ ์‚ฌ์ดํŠธ์—์„œ ๋ Œ๋”๋ง์„ ํ™•์ธํ–ˆ๋‹ค. ๋ณธ๋ฌธ, ํ‘œ, ๊ฐ์ฃผ, Mermaid๋Š” ๋ชจ๋‘ ์ •์ƒ์ด์—ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๊ธ€ ์•„๋ž˜ "๋ณด์•ˆ ํ—ค๋”" ํƒœ๊ทธ๋ฅผ ๋ˆ„๋ฅด๋ฉด ํƒœ๊ทธ ๋ชฉ๋ก์ด ๊ฑธ๋Ÿฌ์ง€์ง€ ์•Š์•˜๋‹ค.

๋งํฌ ์ฃผ์†Œ๋ฅผ ๋ฐ”์ดํŠธ๋กœ ์ฐ์–ด ๋ณด๋‹ˆ /tags/?q= ์™€ ๋ณด์•ˆ ํ—ค๋” ์‚ฌ์ด์— 0x00 ํ•œ ๋ฐ”์ดํŠธ๊ฐ€ ๋ผ์–ด ์žˆ์—ˆ๋‹ค. ํ™”๋ฉด์—๋Š” ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ๋ณด์ด๋Š” NUL ๋ฌธ์ž๋‹ค. ์ด ๊ธ€ ํ•˜๋‚˜์˜ ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ์—ˆ๋‹ค.

ํ•ญ๋ชฉ ๊ฐ’
๋ฐฐํฌ๋ณธ HTML ํŒŒ์ผ 99๊ฐœ
NUL์ด ๋“ค์–ด ์žˆ๋Š” ํŒŒ์ผ 29๊ฐœ
NUL ๊ฐœ์ˆ˜ 57๊ฐœ
๊ทธ์ค‘ ํƒœ๊ทธ ๋งํฌ ์ฃผ์†Œ ์•ˆ 11๊ฐœ
NUL์ด ๋ถ™์€ ์œ„์น˜ ์ „๋ถ€ ํ•œ๊ธ€ ๊ธ€์ž ๋ฐ”๋กœ ์•ž
์›๋ณธ md, ์†Œ์Šค ์ฝ”๋“œ์˜ NUL 0๊ฐœ

๋‹ค๋ฅธ ๊ธ€์˜ ํƒœ๊ทธ์—์„œ๋„ ๊ฐ™์€ ์ผ์ด ์žˆ์—ˆ๋‹ค. ์•„๋ž˜์—์„œ โ€๋Š” NUL ํ•œ ๋ฐ”์ดํŠธ๋ฅผ ๋œปํ•œ๋‹ค.

์›น ์Šคํฌโ€โ€๋ž˜ํ•‘
์›น โ€โ€๊ฐœ๋ฐœ
ํ”„๋กฌํ”„ํŠธ โ€์ธ์ ์…˜
/tags/?q=โ€๋ณด์•ˆ ํ—ค๋”

๊ฒฐ๋ก ๋ถ€ํ„ฐ ์ ์œผ๋ฉด ์›์ธ์€ react-dom 18.3.1์˜ ์„œ๋ฒ„ ๋ Œ๋”๋Ÿฌ ๋ฒ„๊ทธ๋‹ค. React ์ชฝ์—์„œ๋Š” 2023๋…„ 2์›”์— ๊ณ ์ณค์ง€๋งŒ ๊ทธ ์ˆ˜์ •์€ 19์—๋งŒ ๋“ค์–ด๊ฐ”๋‹ค.react-26228 18.x๋ฅผ ์“ฐ๋Š” ์ •์  ์‚ฌ์ดํŠธ ์ƒ์„ฑ๊ธฐ๋Š” ์ง€๊ธˆ๋„ ๊ฐ™์€ ์ผ์„ ๊ฒช๋Š”๋‹ค.

ํ™”๋ฉด์€ ๋ฉ€์ฉกํ•œ๋ฐ ๋งํฌ๋งŒ ๊นจ์ง€๋Š” ์ด์œ 

NUL์ด 57๊ฐœ๋‚˜ ์žˆ์—ˆ๋Š”๋ฐ ํ™”๋ฉด์—์„œ ์ด์ƒํ•œ ๊ธ€์ž๊ฐ€ ํ•˜๋‚˜๋„ ์•ˆ ๋ณด์˜€๋‹ค. HTML ํŒŒ์„œ๊ฐ€ NUL์„ ๋งŒ๋‚˜๋Š” ์œ„์น˜์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. HTML ํ‘œ์ค€์€ ์ด ๊ฒฝ์šฐ๋ฅผ unexpected-null-character ํŒŒ์‹ฑ ์˜ค๋ฅ˜๋กœ ์ •ํ•˜๊ณ , ์œ„์น˜๋ณ„ ์ฒ˜๋ฆฌ๋ฅผ ๋”ฐ๋กœ ์ ์–ด ๋‘์—ˆ๋‹ค.whatwg-parsing

NUL ์œ„์น˜ ํ‘œ์ค€์ด ์ •ํ•œ ์ฒ˜๋ฆฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๋Š” ๊ฒฐ๊ณผ
๋ณธ๋ฌธ ํ…์ŠคํŠธ (<p>, <a> ์•ˆ์˜ ๊ธ€์ž) ํ† ํฐ์„ ๋ฒ„๋ฆฐ๋‹ค ์•„๋ฌด ๋ณ€ํ™” ์—†์Œ
์†์„ฑ๊ฐ’ (href, content ๋“ฑ) U+FFFD(๏ฟฝ)๋กœ ๋ฐ”๊พผ๋‹ค ๋งํฌ ์ฃผ์†Œ, ๋ฉ”ํƒ€ ๊ฐ’์ด ๋ฐ”๋€œ
<title> ์•ˆ U+FFFD๋กœ ๋ฐ”๊พผ๋‹ค ํƒญ ์ œ๋ชฉ, ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ œ๋ชฉ์— ๏ฟฝ
<script> ์•ˆ U+FFFD๋กœ ๋ฐ”๊พผ๋‹ค JSON-LD ๊ฐ™์€ ๋ฌธ์ž์—ด์— ๏ฟฝ

๊ทธ๋ž˜์„œ ํƒœ๊ทธ ์ด๋ฆ„ ํ…์ŠคํŠธ๋Š” ๋ฉ€์ฉกํ•˜๊ฒŒ ๋ณด์ด๊ณ , ๊ฐ™์€ ํƒœ๊ทธ ์ด๋ฆ„์ด ๋“ค์–ด๊ฐ„ href๋งŒ /tags/?q=๏ฟฝ๋ณด์•ˆ ํ—ค๋”๊ฐ€ ๋œ๋‹ค. ํƒœ๊ทธ ํŽ˜์ด์ง€๋Š” q ๊ฐ’๊ณผ ํƒœ๊ทธ ์ด๋ฆ„์„ ๋น„๊ตํ•˜๋ฏ€๋กœ ์ผ์น˜ํ•˜๋Š” ํƒœ๊ทธ๊ฐ€ ์—†๋‹ค.

React๊ฐ€ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๋‹จ๊ณ„์—์„œ ๊ณ ์ณ ์ค„ ๊ฒƒ ๊ฐ™์ง€๋งŒ ๊ทธ๋ ‡์ง€ ์•Š๋‹ค. React ๊ณต์‹ ๋ฌธ์„œ๋Š” ์„œ๋ฒ„ HTML๊ณผ ํด๋ผ์ด์–ธํŠธ ๋ Œ๋”๋ง ๊ฒฐ๊ณผ๊ฐ€ ๋‹ค๋ฅผ ๋•Œ ์†์„ฑ ์ฐจ์ด๋ฅผ ๋ฐ”๋กœ์žก๋Š”๋‹ค๋Š” ๋ณด์žฅ์ด ์—†๋‹ค๊ณ  ์ ๊ณ  ์žˆ๋‹ค.react-hydrate ๊ฐœ๋ฐœ ๋ชจ๋“œ์—์„œ ๊ฒฝ๊ณ ๋งŒ ํ•˜๊ณ , ์šด์˜ ๋นŒ๋“œ์—์„œ๋Š” ๊นจ์ง„ href๊ฐ€ ๊ทธ๋Œ€๋กœ ๋‚จ๋Š”๋‹ค.

โš ๏ธ ์ฃผ์˜: ๋ณธ๋ฌธ ํ…์ŠคํŠธ์— ์„ž์ธ NUL์€ ๋ธŒ๋ผ์šฐ์ € ํ™”๋ฉด๋งŒ ๋ด์„œ๋Š” ์ฐพ์„ ์ˆ˜ ์—†๋‹ค. curl๋กœ ๋ฐ›์€ HTML์„ ๋ฐ”์ดํŠธ ๋‹จ์œ„๋กœ ์„ธ์•ผ ๋ณด์ธ๋‹ค.

๋นŒ๋“œ ๋ฌธ์ œ์ธ์ง€ CDN ๋ฌธ์ œ์ธ์ง€๋ถ€ํ„ฐ ๊ฐ€๋ฅธ๋‹ค

๋ฐฐํฌ๋œ HTML์— ์ด์ƒํ•œ ๋ฐ”์ดํŠธ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฒ”์œ„๋ฅผ ๋จผ์ € ์ขํžŒ๋‹ค. ์›๋ณธ, ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ, ๋ฐฐํฌ๋œ ์‘๋‹ต ์ค‘ ์–ด๋””์„œ ์ฒ˜์Œ ์ƒ๊ธฐ๋Š”์ง€ ๋ณด๋ฉด ๋œ๋‹ค.

0๊ฐœ
์žˆ์Œ
์žˆ์Œ
์—†์Œ
์ „๋ถ€ ํ•œ๊ธ€
๋ผ์ด๋ธŒ HTML์— NUL 57๊ฐœ
์›๋ณธ md, ์†Œ์Šค์— NUL?
๋ฐฐํฌ artifact HTML์— NUL?
์›๋ณธ ์ˆ˜์ •
๋นŒ๋“œ ๋‹จ๊ณ„์—์„œ ์ƒ๊น€
CDN, ์„œ๋ฒ„ ์ชฝ ํ™•์ธ
NUL ๋’ค ๊ธ€์ž์— ๊ณตํ†ต์ ?
UTF-8 ์ธ์ฝ”๋”ฉ ๊ฒฝ๊ณ„ ๋ฌธ์ œ๋กœ ์ถ”์ •
react-dom ์„œ๋ฒ„ ๋ Œ๋”๋Ÿฌ ์ฝ”๋“œ ํ™•์ธ

GitHub Actions์˜ ๋ฐฐํฌ artifact๋ฅผ ๋‚ด๋ ค๋ฐ›์•„ ์„ธ์–ด ๋ณด๋‹ˆ HTML ์•ˆ์— ์ด๋ฏธ NUL์ด ์žˆ์—ˆ๋‹ค. CDN์ด ์•„๋‹ˆ๋ผ ๋นŒ๋“œ๊ฐ€ ๋งŒ๋“  ๊ฒƒ์ด๋‹ค. NUL ๋ฐ”๋กœ ๋’ค ๊ธ€์ž๋Š” ์ „๋ถ€ ํ•œ๊ธ€์ด์—ˆ๋‹ค. ํ•œ๊ธ€์€ UTF-8๋กœ 3๋ฐ”์ดํŠธ์ด๋ฏ€๋กœ, ๋ฐ”์ดํŠธ๋ฅผ ๋‚˜๋ˆ  ์“ฐ๋Š” ๊ฒฝ๊ณ„์—์„œ ๋ญ”๊ฐ€ ์–ด๊ธ‹๋‚˜๋Š” ๊ฒƒ์œผ๋กœ ๋ฒ”์œ„๋ฅผ ์ขํ˜”๋‹ค.

์„ธ๋Š” ๋ฐฉ๋ฒ•์€ ๋‹จ์ˆœํ•˜๋‹ค.

from pathlib import Path

# ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ์˜ ๋ชจ๋“  html์—์„œ 0x00 ๊ฐœ์ˆ˜๋ฅผ ์„ผ๋‹ค
total = 0
for f in Path("public").rglob("*.html"):
    n = f.read_bytes().count(b"\x00")
    if n:
        print(f, n)
        total += n
print("NUL ํ•ฉ๊ณ„:", total)

์›์ธ: 2048๋ฐ”์ดํŠธ ๋ฒ„ํผ์™€ 3๋ฐ”์ดํŠธ ํ•œ๊ธ€

Gatsby 5๋Š” ์ •์  HTML์„ ๋งŒ๋“ค ๋•Œ renderToPipeableStream์„ ์“ด๋‹ค.gatsby-static-entry ์ด ํ•จ์ˆ˜๋Š” ์ถœ๋ ฅ์„ ๋ฌธ์ž์—ด ํ•˜๋‚˜๋กœ ๋ชจ์œผ์ง€ ์•Š๋Š”๋‹ค. ํ…์ŠคํŠธ ์กฐ๊ฐ์„ ๋ฐ›์„ ๋•Œ๋งˆ๋‹ค 2048๋ฐ”์ดํŠธ์งœ๋ฆฌ Uint8Array์— UTF-8๋กœ ์ฑ„์šฐ๊ณ , ๋‹ค ์ฐจ๋ฉด ๋‚ด๋ณด๋‚ธ๋‹ค.

๋ฌธ์ œ๋Š” ๋‚จ์€ ์นธ์ด 1~2๋ฐ”์ดํŠธ์ธ๋ฐ ๋‹ค์Œ ๊ธ€์ž๊ฐ€ 3๋ฐ”์ดํŠธ ํ•œ๊ธ€์ผ ๋•Œ๋‹ค. TextEncoder.encodeInto()๋Š” ๊ธ€์ž๋ฅผ ๋ฐ˜์œผ๋กœ ์ชผ๊ฐœ ์“ฐ์ง€ ์•Š๋Š”๋‹ค. ๋“ค์–ด๊ฐ€๋Š” ๋ฐ๊นŒ์ง€๋งŒ ์“ฐ๊ณ  ๋ฉˆ์ถ˜๋‹ค.mdn-encodeinto ๊ทธ๋‹ค์Œ react-dom 18.3.1์€ ๋ฒ„ํผ๋ฅผ ๋น„์šฐ๋Š”๋ฐ, ์ฑ„์šด ๋งŒํผ์ด ์•„๋‹ˆ๋ผ ๋ฒ„ํผ ์ „์ฒด๋ฅผ ๋‚ด๋ณด๋‚ธ๋‹ค.

// react-dom 18.3.1 cjs/react-dom-server.node.development.js, writeStringChunk
var _textEncoder$encodeIn = textEncoder.encodeInto(stringChunk, target),
    read = _textEncoder$encodeIn.read,
    written = _textEncoder$encodeIn.written;

writtenBytes += written;

if (read < stringChunk.length) {
  // โŒ 2048๋ฐ”์ดํŠธ ์ „์ฒด๋ฅผ ๋‚ด๋ณด๋‚ธ๋‹ค. ๋ชป ์ฑ„์šด 1~2์นธ์€ 0์œผ๋กœ ๋‚จ์•„ ์žˆ๋‹ค
  writeToDestination(destination, currentView);
  currentView = new Uint8Array(VIEW_SIZE);
  writtenBytes = textEncoder.encodeInto(stringChunk.slice(read), currentView).written;
}

React 19์˜ ๊ฐ™์€ ์ž๋ฆฌ๋Š” ์ด๋ ‡๊ฒŒ ๋ฐ”๋€Œ์–ด ์žˆ๋‹ค.

// react-dom 19.0.0
read < chunk.length &&
  // โœ… ์‹ค์ œ๋กœ ์“ด writtenBytes๊นŒ์ง€๋งŒ ๋‚ด๋ณด๋‚ธ๋‹ค
  (writeToDestination(destination, currentView.subarray(0, writtenBytes)),
  (currentView = new Uint8Array(2048)),
  (writtenBytes = textEncoder.encodeInto(chunk.slice(read), currentView).written));

๋ฒ„ํผ ๋์—์„œ ์ผ์–ด๋‚˜๋Š” ์ผ์„ ๋ฐ”์ดํŠธ๋กœ ๊ทธ๋ฆฌ๋ฉด ์ด๋ ‡๋‹ค.

18.3.1์€ ์ „์ฒด๋ฅผ ๋‚ด๋ณด๋ƒ„
๋ฒ„ํผ 2
์•ˆ = EC 95 88
๋ณด = EB B3 B4
๋ฒ„ํผ 1: 2048๋ฐ”์ดํŠธ
๋นˆ์นธ 2๋ฐ”์ดํŠธ = 0x00 0x00
ASCII 2046๋ฐ”์ดํŠธ

๋ณด๋Š” 3๋ฐ”์ดํŠธ๋ผ ๋‚จ์€ 2์นธ์— ์•ˆ ๋“ค์–ด๊ฐ„๋‹ค. 18.3.1์€ 2์นธ์ด ๋นˆ ์ฑ„๋กœ ๋ฒ„ํผ 1์„ ํ†ต์งธ๋กœ ๋‚ด๋ณด๋‚ด๊ณ , ๋ณด๋Š” ๋ฒ„ํผ 2 ๋งจ ์•ž์— ์˜จ์ „ํžˆ ์“ด๋‹ค. ๊ทธ๋ž˜์„œ ๊ธ€์ž๋Š” ๊นจ์ง€์ง€ ์•Š๊ณ  ๊ธ€์ž ์•ž์— NUL๋งŒ ๋ง๋ถ™๋Š”๋‹ค. ์œ„์—์„œ ๋ณธ ์›น ์Šคํฌโ€โ€๋ž˜ํ•‘์ด ์ •ํ™•ํžˆ ์ด ๋ชจ์–‘์ด๋‹ค.

์žฌํ˜„

react-dom์„ ์„ค์น˜ํ•˜์ง€ ์•Š๊ณ  ์ด ๋กœ์ง๋งŒ ๋–ผ์–ด Node๋กœ ๋Œ๋ ค ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ASCII๋ฅผ 100๋ฐ”์ดํŠธ์”ฉ ๋‚˜๋ˆ  ์“ฐ๊ณ  ๋งˆ์ง€๋ง‰์— ๋ณด์•ˆ์„ ์“ด๋‹ค. ๋ Œ๋”๋Ÿฌ๋Š” ์‹ค์ œ๋กœ ํ…์ŠคํŠธ ๋…ธ๋“œ, ์†์„ฑ๊ฐ’ ๊ฐ™์€ ์ž‘์€ ์กฐ๊ฐ์„ ์—ฐ๋‹ฌ์•„ ์“ฐ๋ฏ€๋กœ ์กฐ๊ฐ์„ ๋‚˜๋ˆ ์•ผ ์žฌํ˜„๋œ๋‹ค. 682์ž๊ฐ€ ๋„˜๋Š” ์กฐ๊ฐ์€ ๋ฒ„ํผ๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ ๋‚ด๋ณด๋‚ด๋Š” ๋‹ค๋ฅธ ๊ธธ๋กœ ๋น ์ง„๋‹ค.

const VIEW_SIZE = 2048
function makeWriter(fixed) {
  const out = []
  let view = new Uint8Array(VIEW_SIZE), written = 0
  const enc = new TextEncoder()
  function write(chunk) {
    const target = written > 0 ? view.subarray(written) : view
    const { read, written: w } = enc.encodeInto(chunk, target)
    written += w
    if (read < chunk.length) {
      out.push(fixed ? view.subarray(0, written) : view) // 18.3.1์€ view ์ „์ฒด
      view = new Uint8Array(VIEW_SIZE)
      written = enc.encodeInto(chunk.slice(read), view).written
    }
    if (written === VIEW_SIZE) { out.push(view); view = new Uint8Array(VIEW_SIZE); written = 0 }
  }
  const end = () => Buffer.concat([...out, view.subarray(0, written)])
  return { write, end }
}

for (const pad of [2045, 2046, 2047]) {
  for (const fixed of [false, true]) {
    const w = makeWriter(fixed)
    for (let i = 0; i < pad; i += 100) w.write("a".repeat(Math.min(100, pad - i)))
    w.write("๋ณด์•ˆ")
    const nul = w.end().filter(b => b === 0).length
    console.log(`ASCII ${pad}๋ฐ”์ดํŠธ ๋’ค ํ•œ๊ธ€, ${fixed ? "19  " : "18.3"}: NUL ${nul}๊ฐœ`)
  }
}
ํ•œ๊ธ€ ์•ž๊นŒ์ง€ ์ฑ„์šด ๋ฐ”์ดํŠธ ๋‚จ์€ ์นธ 18.3.1 19
2045 3 0 0
2046 2 2 0
2047 1 1 0

๋‚จ์€ ์นธ์ด 3๋ฐ”์ดํŠธ ์ด์ƒ์ด๋ฉด ํ•œ๊ธ€์ด ๋“ค์–ด๊ฐ€๋ฏ€๋กœ ์•„๋ฌด ์ผ๋„ ์—†๋‹ค. 12๋ฐ”์ดํŠธ์ผ ๋•Œ๋งŒ ๊ทธ ์ˆ˜๋งŒํผ NUL์ด ์ƒ๊ธด๋‹ค. 4๋ฐ”์ดํŠธ์ธ ์ด๋ชจ์ง€๋Š” ๋‚จ์€ ์นธ์ด 13๋ฐ”์ดํŠธ์ผ ๋•Œ ์ƒ๊ธด๋‹ค. ASCII๋Š” 1๋ฐ”์ดํŠธ๋ผ ์ ˆ๋Œ€ ์•ˆ ์ƒ๊ธด๋‹ค. ์˜์–ด ์œ„์ฃผ ์‚ฌ์ดํŠธ์—์„œ๋Š” ๊ฑฐ์˜ ์•ˆ ๋ณด์ด๊ณ , ํ•œ๊ธ€, ์ผ๋ณธ์–ด, ์ค‘๊ตญ์–ด ์‚ฌ์ดํŠธ๋‚˜ โ€”, โ†’ ๊ฐ™์€ ๊ธฐํ˜ธ๋ฅผ ๋งŽ์ด ์“ฐ๋Š” ๋ฌธ์„œ์—์„œ ๋ณด์ด๋Š” ์ด์œ ๋‹ค.

์™œ 18์—์„œ ๊ณ ์ณ์ง€์ง€ ์•Š์•˜๋‚˜

์‹œ์  ์ผ
18.1.0 ์ŠคํŠธ๋ฆฌ๋ฐ ์„ฑ๋Šฅ ๊ฐœ์„ (#24291)์œผ๋กœ ๋ฒ„ํผ ๋ฐฉ์‹์ด ๋“ค์–ด์˜ค๋ฉด์„œ ๋ฒ„๊ทธ ์‹œ์ž‘react-31134
2023-02-24 ์ˆ˜์ • PR #26228 "Fix null bytes written at text chunk boundaries" ๋ณ‘ํ•ฉreact-26228
2024-04-26 18.3.1 ๋ฐฐํฌ. 18.3์€ 19 ์ด์ „ ๊ฒฝ๊ณ ๋ฅผ ์ถ”๊ฐ€ํ•œ ํŒ์ด๋ผ ์ด ์ˆ˜์ •์€ ์—†์Œreact-31134
2024-10 Docusaurus ๋ฉ”์ธํ…Œ์ด๋„ˆ๊ฐ€ ๊ฐ™์€ ์ฆ์ƒ์„ ์ด์Šˆ #31134๋กœ ๋ณด๊ณ , "19์—์„œ ๊ณ ์ณ์กŒ๋‹ค"๋ฉฐ ๋‹ซ์Œreact-31134
2024-12-05 19.0.0 ๋ฐฐํฌ

18.3.1์ด 18์˜ ๋งˆ์ง€๋ง‰ ํŒ์ด๋‹ค. 18์— ๋จธ๋ฌด๋Š” ํ•œ react-dom ๋ฒ„์ „์„ ์˜ฌ๋ ค์„œ ๊ณ ์น  ๋ฐฉ๋ฒ•์€ ์—†๋‹ค. ๊ฐ™์€ ์ด์Šˆ์—์„œ React ํŒ€์˜ sebmarkbage๋Š” ์„œ๋ฒ„ ์ถœ๋ ฅ๊ณผ ํด๋ผ์ด์–ธํŠธ ๋ Œ๋”๋ง์ด ๋‹ฌ๋ผ์ง€๋Š” ์‹ค์ œ ๋ฒ„๊ทธ๋ผ๊ณ  ์ธ์ •ํ–ˆ๋‹ค.react-31134

renderToString์€ ๋ฒ„ํผ๋ฅผ ์“ฐ์ง€ ์•Š๊ณ  ๋ฌธ์ž์—ด์„ ์ด์–ด ๋ถ™์ด๋ฏ€๋กœ ์ด ๋ฌธ์ œ๊ฐ€ ์—†๋‹ค. ๊ฐ™์€ ์ด์Šˆ์—์„œ renderToReadableStream๋„ renderToString๊ณผ ๊ฐ™์€ ๊ฒฐ๊ณผ๋ฅผ ๋ƒˆ๋‹ค๊ณ  ๋ณด๊ณ ๋๋‹ค.

๊ณ ์น  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ• ๋„ค ๊ฐ€์ง€

๋ฐฉ๋ฒ• ์žฅ์  ์ด ๋ธ”๋กœ๊ทธ์—์„œ ๊ฑธ๋ฆฌ๋Š” ์ 
React 19๋กœ ์˜ฌ๋ฆฐ๋‹ค ์›์ธ์ด ์‚ฌ๋ผ์ง„๋‹ค Gatsby๋Š” 5.16.0๋ถ€ํ„ฐ React 19๋ฅผ ํ—ˆ์šฉํ•˜์ง€๋งŒ, ์“ฐ๊ณ  ์žˆ๋Š” react-redux 8, react-spring 9, utterances-react-component 1.0์ด React 18๊นŒ์ง€๋งŒ ๋ฐ›๋Š”๋‹ค
patch-package๋กœ 18.3.1์— #26228์„ ์˜ฎ๊ธด๋‹ค ์›์ธ ์ฝ”๋“œ๋งŒ ๊ณ ์นœ๋‹ค ๊ฐœ๋ฐœ, ์šด์˜ ๋นŒ๋“œ ํŒŒ์ผ์„ ๋‘˜ ๋‹ค ๊ณ ์ณ์•ผ ํ•˜๊ณ , ์šด์˜ ํŒŒ์ผ์€ ์••์ถ•๋ผ ์žˆ๋‹ค. ์˜์กด์„ฑ์„ ๋‹ค์‹œ ๋ฐ›์„ ๋•Œ๋งˆ๋‹ค ํŒจ์น˜๊ฐ€ ๋งž๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค
๋ Œ๋” ํ•จ์ˆ˜๋ฅผ renderToString ๋“ฑ์œผ๋กœ ๋ฐ”๊พผ๋‹ค ๋ฒ„ํผ๋ฅผ ์•ˆ ๊ฑฐ์นœ๋‹ค Gatsby ๋‚ด๋ถ€ static-entry.js๋ฅผ ๊ณ ์ณ์•ผ ํ•œ๋‹ค. Docusaurus๋Š” ์ด ๋ฐฉ๋ฒ•์„ PR #10562๋กœ ์‹œ๋„ํ–ˆ๋‹ค๊ฐ€ 19 ์—…๊ทธ๋ ˆ์ด๋“œ๋กœ ํ•ด๊ฒฐ๋œ๋‹ค๋ฉฐ ๋‹ซ์•˜๋‹คdocusaurus-10562
๋นŒ๋“œ ํ›„ HTML์—์„œ NUL์„ ์ง€์šด๋‹ค ์„ค์ • ๋ช‡ ์ค„, ์˜์กด์„ฑ ๋ณ€๊ฒฝ ์—†์Œ ์›์ธ์€ ๋‚จ๋Š”๋‹ค. ๋Œ€์‹  CI ๊ฒ€์‚ฌ๋กœ ๋‹ค์‹œ ๋“ค์–ด์˜ค๋ฉด ๋ฐ”๋กœ ์žก๋Š”๋‹ค

๋„ค ๋ฒˆ์งธ๋ฅผ ๊ณจ๋ž๋‹ค. ์ง€์›Œ๋„ ๋˜๋Š” ๊ทผ๊ฑฐ๋Š” ๋‘ ๊ฐ€์ง€๋‹ค.

  1. ๋ง๋ถ™์€ ๊ฒƒ์€ ๋นˆ์นธ์˜ 0๋ฟ์ด๋‹ค. ๊ธ€์ž๋Š” ๋‹ค์Œ ๋ฒ„ํผ์— ์˜จ์ „ํžˆ ๋“ค์–ด๊ฐ€๋ฏ€๋กœ ๋น ์ง„ ๋ฐ”์ดํŠธ๊ฐ€ ์—†๋‹ค. NUL์„ ์ง€์šฐ๋ฉด ์›๋ž˜ ๋‚˜์™€์•ผ ํ–ˆ๋˜ ์ถœ๋ ฅ๊ณผ ๋ฐ”์ดํŠธ ๋‹จ์œ„๋กœ ๊ฐ™๋‹ค.
  2. UTF-8์—์„œ 0x00์€ U+0000์ผ ๋•Œ๋งŒ ๋‚˜์˜จ๋‹ค. ๋‹ค๋ฅธ ๊ธ€์ž์˜ ์—ฌ๋Ÿฌ ๋ฐ”์ดํŠธ ์ค‘ ํ•˜๋‚˜๋กœ 0x00์ด ๋“ค์–ด๊ฐ€๋Š” ์ผ์€ ์—†๋‹ค. UTF-8์—์„œ 2๋ฐ”์ดํŠธ ์ด์ƒ์ธ ๊ธ€์ž๋Š” ๋ชจ๋“  ๋ฐ”์ดํŠธ๊ฐ€ 0x80 ์ด์ƒ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.rfc3629 HTML์—์„œ U+0000์„ ์ผ๋ถ€๋Ÿฌ ์“ธ ์ผ๋„ ์—†๋‹ค.

React 19๋กœ ์˜ฌ๋ฆฌ๋Š” ๊ฒƒ์ด ๊ทผ๋ณธ ํ•ด๊ฒฐ์ด๋‹ค. ๋ง‰๊ณ  ์žˆ๋Š” ์˜์กด์„ฑ ์…‹์€ ๊ฐ๊ฐ ์ƒ์œ„ ๋ฒ„์ „(react-redux 9, @react-spring/web 10)์ด ์žˆ๊ฑฐ๋‚˜ ๊ต์ฒดํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์—…๊ทธ๋ ˆ์ด๋“œ๋Š” ๋”ฐ๋กœ ์ง„ํ–‰ํ•  ์ผ๋กœ ๋‚จ๊ฒผ๋‹ค.

onPostBuild๋กœ ์ง€์šฐ๊ณ  CI๋กœ ๋‹ค์‹œ ๋ณธ๋‹ค

Gatsby์˜ onPostBuild๋Š” public/ ํด๋”๊ฐ€ ๋‹ค ๋งŒ๋“ค์–ด์ง„ ๋’ค์— ์‹คํ–‰๋œ๋‹ค.gatsby-onpostbuild ์—ฌ๊ธฐ์„œ ๋ชจ๋“  .html์„ ๋Œ๋ฉฐ 0x00์„ ์ง€์šด๋‹ค.

// gatsby-node.js
const fs = require("fs")
const path = require("path")

function stripNulBytes(dir) {
  let files = 0
  let bytes = 0
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    const full = path.join(dir, entry.name)
    if (entry.isDirectory()) {
      const sub = stripNulBytes(full)
      files += sub.files
      bytes += sub.bytes
    } else if (entry.name.endsWith(".html")) {
      const buf = fs.readFileSync(full)
      if (!buf.includes(0)) continue
      const clean = buf.filter(b => b !== 0)
      fs.writeFileSync(full, clean)
      files += 1
      bytes += buf.length - clean.length
    }
  }
  return { files, bytes }
}

exports.onPostBuild = ({ reporter }) => {
  const { files, bytes } = stripNulBytes(path.join(__dirname, "public"))
  reporter.info(`NUL ๋ฐ”์ดํŠธ ์ œ๊ฑฐ: html ${files}๊ฐœ์—์„œ ${bytes}๋ฐ”์ดํŠธ`)
}

buf.filter()๋Š” ๋ฌธ์ž์—ด๋กœ ๋””์ฝ”๋”ฉํ•˜์ง€ ์•Š๊ณ  ๋ฐ”์ดํŠธ ๋ฐฐ์—ด์—์„œ ๋ฐ”๋กœ ์ง€์šด๋‹ค. ๋ฌธ์ž์—ด๋กœ ๋ฐ”๊ฟจ๋‹ค๊ฐ€ ๋‹ค์‹œ ์ธ์ฝ”๋”ฉํ•˜๋ฉด ์›๋ž˜ ๋ฐ”์ดํŠธ์™€ ๋‹ฌ๋ผ์งˆ ์—ฌ์ง€๊ฐ€ ์ƒ๊ธด๋‹ค.

์ง€์šฐ๋Š” ๋‹จ๊ณ„๊ฐ€ ๋‚˜์ค‘์— ๋น ์ง€๊ฑฐ๋‚˜ ๋ฐ”๋€Œ๋ฉด NUL์ด ๋‹ค์‹œ ๋“ค์–ด์˜จ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋ณธ๋ฌธ ํ…์ŠคํŠธ์— ์„ž์ธ NUL์€ ํ™”๋ฉด์— ์•ˆ ๋ณด์ด๋ฏ€๋กœ ์•„๋ฌด๋„ ๋ชจ๋ฅธ๋‹ค. ๊ทธ๋ž˜์„œ ๋นŒ๋“œ ๊ฒ€์ฆ ์Šคํฌ๋ฆฝํŠธ์— ๊ฒ€์‚ฌ๋ฅผ ํ•˜๋‚˜ ๋” ๋„ฃ์—ˆ๋‹ค. index.html๋งŒ์ด ์•„๋‹ˆ๋ผ ๋ชจ๋“  HTML์„ ๋ณธ๋‹ค.

// scripts/verify-build-seo.js
function checkNulBytes() {
  const problems = []
  let checked = 0
  for (const file of walkAllHtml(PUBLIC_DIR)) {
    checked += 1
    const buf = fs.readFileSync(file)
    let count = 0
    for (let i = buf.indexOf(0); i !== -1; i = buf.indexOf(0, i + 1)) count += 1
    if (count > 0) problems.push(`${file}: NUL ๋ฐ”์ดํŠธ ${count}๊ฐœ`)
  }
  return { checked, problems }
}

๊ฒ€์‚ฌ๋ฅผ ๋ฏฟ์œผ๋ ค๋ฉด ๊ฒ€์‚ฌ๊ฐ€ ์‹คํŒจํ•˜๋Š” ๊ฒƒ๋ถ€ํ„ฐ ๋ด์•ผ ํ•œ๋‹ค. ๊ณ ์น˜๊ธฐ ์ „ ๋ฐฐํฌ๋ณธ์— ๋จผ์ € ๋Œ๋ ธ๋‹ค.

ํ™•์ธ ๊ฒฐ๊ณผ
๊ณ ์น˜๊ธฐ ์ „ ๋ฐฐํฌ๋ณธ์— ์ƒˆ ๊ฒ€์‚ฌ 29๊ฐœ ํŒŒ์ผ์—์„œ ์‹คํŒจ (๊ฒ€์‚ฌ๊ฐ€ ์žก๋Š”๋‹ค)
๊ฐ™์€ ๋ฐฐํฌ๋ณธ์—์„œ NUL์„ ์ง€์šด ๋’ค 0๊ฑด
์ˆ˜์ • ํ›„ CD ๋นŒ๋“œ ๋กœ๊ทธ NUL ๋ฐ”์ดํŠธ ์ œ๊ฑฐ: html 30๊ฐœ์—์„œ 59๋ฐ”์ดํŠธ
์ˆ˜์ • ํ›„ CI ๊ฒ€์‚ฌ HTML 104๊ฐœ, NUL 0๊ฑด
์‹ค์ œ ์‚ฌ์ดํŠธ 7๊ฐœ ํŽ˜์ด์ง€ NUL 0๊ฐœ, ํƒœ๊ทธ ๋งํฌ /tags/?q=๋ณด์•ˆ ํ—ค๋” ์ •์ƒ

์ˆ˜์ • ํ›„ ๋นŒ๋“œ์—์„œ ์ง€์šด ์–‘(30๊ฐœ, 59๋ฐ”์ดํŠธ)์ด ์ฒ˜์Œ ์„ผ ๊ฐ’(29๊ฐœ, 57๊ฐœ)๊ณผ ์กฐ๊ธˆ ๋‹ค๋ฅด๋‹ค. ๊ทธ์‚ฌ์ด ์ƒˆ ๊ธ€์ด ํ•˜๋‚˜ ๋Š˜์—ˆ๊ณ , ๋ฒ„ํผ ๊ฒฝ๊ณ„๋Š” HTML ์ „์ฒด ๊ธธ์ด์— ๋”ฐ๋ผ ์›€์ง์ด๋ฏ€๋กœ ์–ด๋А ๊ธ€์ž๊ฐ€ ๊ฒฝ๊ณ„์— ๊ฑธ๋ฆด์ง€๋Š” ๋นŒ๋“œ๋งˆ๋‹ค ๋ฐ”๋€๋‹ค. ์ด ๋ฒ„๊ทธ๊ฐ€ ์ƒ๊ฒผ๋‹ค ์•ˆ ์ƒ๊ฒผ๋‹ค ํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ™์€ ์ด์œ ๋‹ค. ๊ธ€ ์•ž๋ถ€๋ถ„์„ ์กฐ๊ธˆ ๊ณ ์ณค๋”๋‹ˆ ๋ฉ€๋ฆฌ ๋–จ์–ด์ง„ ๊ณณ์˜ NUL์ด ์‚ฌ๋ผ์กŒ๋‹ค๋Š” ๋ณด๊ณ ๋„ ๊ฐ™์€ ์ด์Šˆ์— ์žˆ๋‹ค.react-31134

์ •๋ฆฌ

  1. React 18.1~18.3.1์˜ renderToPipeableStream์€ ํ•œ๊ธ€ ์•ž์— NUL์„ ๋ผ์›Œ ๋„ฃ์„ ์ˆ˜ ์žˆ๋‹ค. 2048๋ฐ”์ดํŠธ ๋ฒ„ํผ์— 3๋ฐ”์ดํŠธ ๊ธ€์ž๊ฐ€ ์•ˆ ๋“ค์–ด๊ฐˆ ๋•Œ ๋ฒ„ํผ ์ „์ฒด๋ฅผ ๋‚ด๋ณด๋‚ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค
  2. HTML ํŒŒ์„œ๋Š” ์œ„์น˜๋งˆ๋‹ค NUL์„ ๋‹ค๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•œ๋‹ค. ๋ณธ๋ฌธ์—์„œ๋Š” ๋ฒ„๋ ค์„œ ์•ˆ ๋ณด์ด๊ณ , ์†์„ฑ๊ฐ’, <title>, <script>์—์„œ๋Š” ๏ฟฝ๋กœ ๋ฐ”๊พผ๋‹ค. ๊ทธ๋ž˜์„œ ํ™”๋ฉด์€ ๋ฉ€์ฉกํ•˜๊ณ  ๋งํฌ๋งŒ ๊นจ์ง„๋‹ค
  3. ํ•˜์ด๋“œ๋ ˆ์ด์…˜์€ ๊นจ์ง„ ์†์„ฑ์„ ๊ณ ์ณ ์ฃผ์ง€ ์•Š๋Š”๋‹ค. ์„œ๋ฒ„ HTML์— ๋“ค์–ด๊ฐ„ href๊ฐ€ ๊ทธ๋Œ€๋กœ ๋‚จ๋Š”๋‹ค
  4. ์ˆ˜์ •์€ React 19์—๋งŒ ์žˆ๋‹ค. 18.3.1์ด 18์˜ ๋งˆ์ง€๋ง‰ ํŒ์ด๋‹ค
  5. NUL๋งŒ ์ง€์šฐ๋ฉด ์›๋ž˜ ์ถœ๋ ฅ๊ณผ ๊ฐ™๋‹ค. ๋ง๋ถ™์€ ๊ฒƒ์€ ๋นˆ์นธ์˜ 0๋ฟ์ด๊ณ  UTF-8์—์„œ 0x00์€ ๋‹ค๋ฅธ ๊ธ€์ž ์•ˆ์— ๋‚˜์˜ค์ง€ ์•Š๋Š”๋‹ค
  6. ๊ฒ€์‚ฌ๋Š” ์‹คํŒจํ•˜๋Š” ๊ฒƒ๋ถ€ํ„ฐ ํ™•์ธํ•œ๋‹ค. ๊ณ ์น˜๊ธฐ ์ „ ์‚ฐ์ถœ๋ฌผ์— ๋Œ๋ ค ์žกํžˆ๋Š”์ง€ ๋ณธ ๋’ค CI์— ๋„ฃ๋Š”๋‹ค

์‹ค๋ฌด ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • React 18 ์ŠคํŠธ๋ฆฌ๋ฐ SSR(renderToPipeableStream)์„ ์“ฐ๋Š” ์‚ฌ์ดํŠธ๋ผ๋ฉด ๋นŒ๋“œ ์‚ฐ์ถœ๋ฌผ HTML์˜ 0x00 ๊ฐœ์ˆ˜๋ฅผ ์„ผ๋‹ค
  • ๋ธŒ๋ผ์šฐ์ € ํ™”๋ฉด์ด ์•„๋‹ˆ๋ผ curl๋กœ ๋ฐ›์€ ๋ฐ”์ดํŠธ๋กœ ํ™•์ธํ•œ๋‹ค. ๋ณธ๋ฌธ ํ…์ŠคํŠธ์˜ NUL์€ ํ™”๋ฉด์— ์•ˆ ๋ณด์ธ๋‹ค
  • ์ด์ƒํ•œ ๋ฐ”์ดํŠธ๊ฐ€ ์žˆ์œผ๋ฉด ์›๋ณธ, ๋นŒ๋“œ ์‚ฐ์ถœ๋ฌผ, ๋ฐฐํฌ ์‘๋‹ต ์ˆœ์œผ๋กœ ์–ด๋””์„œ ์ฒ˜์Œ ์ƒ๊ธฐ๋Š”์ง€ ๊ฐ€๋ฅธ๋‹ค
  • href, <title>, <meta content>, JSON-LD์— U+FFFD(๏ฟฝ)๊ฐ€ ์žˆ๋Š”์ง€ ๋ณธ๋‹ค. NUL์ด ๋ฐ”๋€ ํ”์ ์ด๋‹ค
  • ๋ฐ”์ดํŠธ๋ฅผ ์ง€์šธ ๋•Œ๋Š” ๋ฌธ์ž์—ด๋กœ ๋””์ฝ”๋”ฉํ•˜์ง€ ๋ง๊ณ  ๋ฐ”์ดํŠธ ๋ฐฐ์—ด์—์„œ ์ง€์šด๋‹ค
  • ๋นŒ๋“œ ํ›„์ฒ˜๋ฆฌ๊ฐ€ ๋น ์ ธ๋„ ์•Œ ์ˆ˜ ์žˆ๊ฒŒ CI์— "NUL 0๊ฐœ" ๊ฒ€์‚ฌ๋ฅผ ๋„ฃ๋Š”๋‹ค. ๊ณ ์น˜๊ธฐ ์ „ ์‚ฐ์ถœ๋ฌผ์—์„œ ์‹คํŒจํ•˜๋Š”์ง€ ๋จผ์ € ๋ณธ๋‹ค
  • React 19๋กœ ๋ชป ์˜ฌ๋ฆฌ๋Š” ์ด์œ ๋ฅผ ์˜์กด์„ฑ ๋‹จ์œ„๋กœ ์ ์–ด ๋‘”๋‹ค. ๋ง‰๋Š” ํŒจํ‚ค์ง€๊ฐ€ ํ’€๋ฆฌ๋ฉด ํ›„์ฒ˜๋ฆฌ๋ฅผ ๊ฑท์–ด๋‚ธ๋‹ค

๋‹ค์Œ ๊ธ€์€ ์ˆœํ™˜ ์ˆœ์„œ๋Œ€๋กœ ๋„คํŠธ์›Œํฌ ์ชฝ์„ ๋‹ค๋ฃฌ๋‹ค. HTTP ๋ณด์•ˆ ํ—ค๋” ๊ธ€์—์„œ ์˜ˆ๊ณ ํ•œ CSP Report-Only ํŽธ์€ ๋‹ค์Œ ๋ณด์•ˆ ์ฐจ๋ก€์— ์›น ๋ณด์•ˆ ๊ธฐ์ดˆ ์‹œ๋ฆฌ์ฆˆ๋กœ ์ด์–ด์ง„๋‹ค.

์ฐธ๊ณ ๋ฌธํ—Œ


์ด์–ด์„œ ์ฝ๊ธฐ


  1. facebook/react. "[Fizz Node] Fix null bytes written at text chunk boundaries" (PR #26228, 2023-02-24 ๋ณ‘ํ•ฉ). https://github.com/facebook/react/pull/26228โ†ฉ
  2. WHATWG. "HTML Standard - 13.2 Parsing HTML documents" (unexpected-null-character, Data state, RCDATA state, Script data state, Attribute value state, "in body" ์‚ฝ์ž… ๋ชจ๋“œ์˜ U+0000 ์ฒ˜๋ฆฌ). https://html.spec.whatwg.org/multipage/parsing.htmlโ†ฉ
  3. React. "hydrateRoot - Caveats" ("There are no guarantees that attribute differences will be patched up in case of mismatches"). https://react.dev/reference/react-dom/client/hydrateRootโ†ฉ
  4. gatsbyjs/gatsby. packages/gatsby/cache-dir/static-entry.js (renderToPipeableStream์œผ๋กœ ํŽ˜์ด์ง€ HTML ์ƒ์„ฑ). https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/static-entry.jsโ†ฉ
  5. MDN Web Docs. "TextEncoder: encodeInto() method" (๋Œ€์ƒ ๋ฐฐ์—ด์— ๋“ค์–ด๊ฐ€๋Š” ๋งŒํผ๋งŒ ์“ฐ๊ณ  read, written์„ ๋Œ๋ ค์ค€๋‹ค). https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeIntoโ†ฉ
  6. facebook/react. "Bug: React 18 emit null chars in renderToPipeableStream" (Issue #31134, 18.1.0๋ถ€ํ„ฐ ๋ฐœ์ƒ, 19์—์„œ ์ˆ˜์ •, 18.x ๋ฐฑํฌํŠธ ์—†์Œ). https://github.com/facebook/react/issues/31134โ†ฉ
  7. facebook/docusaurus. "fix(core): fix React rendering null chars in HTML output - Use renderToReadableStream" (PR #10562, ๋ณ‘ํ•ฉํ•˜์ง€ ์•Š๊ณ  ๋‹ซ์Œ). https://github.com/facebook/docusaurus/pull/10562โ†ฉ
  8. IETF. "RFC 3629 - UTF-8, a transformation format of ISO 10646" (์—ฌ๋Ÿฌ ๋ฐ”์ดํŠธ ๊ธ€์ž์˜ ๊ฐ ๋ฐ”์ดํŠธ๋Š” ์ตœ์ƒ์œ„ ๋น„ํŠธ๊ฐ€ 1). https://www.rfc-editor.org/rfc/rfc3629โ†ฉ
  9. Gatsby. "Gatsby Node APIs - onPostBuild" (๋นŒ๋“œ๊ฐ€ ๋๋‚œ ๋’ค ์‹คํ–‰). https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/#onPostBuildโ†ฉ

@leekh8
๋ณด์•ˆ, ์›น ๊ฐœ๋ฐœ, Python์„ ๋‹ค๋ฃจ๋Š” ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ