์ง๋์ฃผ 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์ ์ด์ํ ๋ฐ์ดํธ๊ฐ ์์ผ๋ฉด ๋ฒ์๋ฅผ ๋จผ์ ์ขํ๋ค. ์๋ณธ, ๋น๋ ๊ฒฐ๊ณผ๋ฌผ, ๋ฐฐํฌ๋ ์๋ต ์ค ์ด๋์ ์ฒ์ ์๊ธฐ๋์ง ๋ณด๋ฉด ๋๋ค.
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));๋ฒํผ ๋์์ ์ผ์ด๋๋ ์ผ์ ๋ฐ์ดํธ๋ก ๊ทธ๋ฆฌ๋ฉด ์ด๋ ๋ค.
๋ณด๋ 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 ๊ฒ์ฌ๋ก ๋ค์ ๋ค์ด์ค๋ฉด ๋ฐ๋ก ์ก๋๋ค |
๋ค ๋ฒ์งธ๋ฅผ ๊ณจ๋๋ค. ์ง์๋ ๋๋ ๊ทผ๊ฑฐ๋ ๋ ๊ฐ์ง๋ค.
- ๋ง๋ถ์ ๊ฒ์ ๋น์นธ์ 0๋ฟ์ด๋ค. ๊ธ์๋ ๋ค์ ๋ฒํผ์ ์จ์ ํ ๋ค์ด๊ฐ๋ฏ๋ก ๋น ์ง ๋ฐ์ดํธ๊ฐ ์๋ค. NUL์ ์ง์ฐ๋ฉด ์๋ ๋์์ผ ํ๋ ์ถ๋ ฅ๊ณผ ๋ฐ์ดํธ ๋จ์๋ก ๊ฐ๋ค.
- 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
์ ๋ฆฌ
- React 18.1~18.3.1์
renderToPipeableStream์ ํ๊ธ ์์ NUL์ ๋ผ์ ๋ฃ์ ์ ์๋ค. 2048๋ฐ์ดํธ ๋ฒํผ์ 3๋ฐ์ดํธ ๊ธ์๊ฐ ์ ๋ค์ด๊ฐ ๋ ๋ฒํผ ์ ์ฒด๋ฅผ ๋ด๋ณด๋ด๊ธฐ ๋๋ฌธ์ด๋ค - HTML ํ์๋ ์์น๋ง๋ค NUL์ ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌํ๋ค. ๋ณธ๋ฌธ์์๋ ๋ฒ๋ ค์ ์ ๋ณด์ด๊ณ , ์์ฑ๊ฐ,
<title>,<script>์์๋ ๏ฟฝ๋ก ๋ฐ๊พผ๋ค. ๊ทธ๋์ ํ๋ฉด์ ๋ฉ์ฉกํ๊ณ ๋งํฌ๋ง ๊นจ์ง๋ค - ํ์ด๋๋ ์ด์
์ ๊นจ์ง ์์ฑ์ ๊ณ ์ณ ์ฃผ์ง ์๋๋ค. ์๋ฒ HTML์ ๋ค์ด๊ฐ
href๊ฐ ๊ทธ๋๋ก ๋จ๋๋ค - ์์ ์ React 19์๋ง ์๋ค. 18.3.1์ด 18์ ๋ง์ง๋ง ํ์ด๋ค
- NUL๋ง ์ง์ฐ๋ฉด ์๋ ์ถ๋ ฅ๊ณผ ๊ฐ๋ค. ๋ง๋ถ์ ๊ฒ์ ๋น์นธ์ 0๋ฟ์ด๊ณ UTF-8์์
0x00์ ๋ค๋ฅธ ๊ธ์ ์์ ๋์ค์ง ์๋๋ค - ๊ฒ์ฌ๋ ์คํจํ๋ ๊ฒ๋ถํฐ ํ์ธํ๋ค. ๊ณ ์น๊ธฐ ์ ์ฐ์ถ๋ฌผ์ ๋๋ ค ์กํ๋์ง ๋ณธ ๋ค 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 ํธ์ ๋ค์ ๋ณด์ ์ฐจ๋ก์ ์น ๋ณด์ ๊ธฐ์ด ์๋ฆฌ์ฆ๋ก ์ด์ด์ง๋ค.
์ฐธ๊ณ ๋ฌธํ
์ด์ด์ ์ฝ๊ธฐ
- HTTP ๋ณด์ ํค๋ ์ ๊ฒ: ๋ด ๋ธ๋ก๊ทธ ์๋ต์๋ HSTS ํ๋๋ง ๋ถ์ด ์์๋ค
- Gatsby ๋ธ๋ก๊ทธ SEO ์ค์ ๊ฐ์ด๋: Google Search Console, sitemap, robots.txt
- ๋คํฌ ๋ชจ๋์์ ๊ธ์๊ฐ ๋ฐฐ๊ฒฝ์ ๋ฌปํ๋ค: ์์ธ ์ ํ์ 19๊ฐ์ ์ธ๋ผ์ธ ์คํ์ผ 38๊ฐ๊ฐ ์ฌ๋ผ์ง ํ ํฐ ์ ๋ฆฌ๊ธฐ
- facebook/react. "[Fizz Node] Fix null bytes written at text chunk boundaries" (PR #26228, 2023-02-24 ๋ณํฉ). https://github.com/facebook/react/pull/26228โฉ
- 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โฉ - 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โฉ
- 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โฉ - MDN Web Docs. "TextEncoder: encodeInto() method" (๋์ ๋ฐฐ์ด์ ๋ค์ด๊ฐ๋ ๋งํผ๋ง ์ฐ๊ณ
read,written์ ๋๋ ค์ค๋ค). https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeIntoโฉ - 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โฉ - facebook/docusaurus. "fix(core): fix React rendering null chars in HTML output - Use
renderToReadableStream" (PR #10562, ๋ณํฉํ์ง ์๊ณ ๋ซ์). https://github.com/facebook/docusaurus/pull/10562โฉ - IETF. "RFC 3629 - UTF-8, a transformation format of ISO 10646" (์ฌ๋ฌ ๋ฐ์ดํธ ๊ธ์์ ๊ฐ ๋ฐ์ดํธ๋ ์ต์์ ๋นํธ๊ฐ 1). https://www.rfc-editor.org/rfc/rfc3629โฉ
- Gatsby. "Gatsby Node APIs - onPostBuild" (๋น๋๊ฐ ๋๋ ๋ค ์คํ). https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/#onPostBuildโฉ




