๋ฐ˜๋ณต๋˜๋Š” ๊ธฐ์ค€ ๋ฌธ์„œ๋Š” ์บ์‹œ์— ๋‘๊ณ , ์‚ฌ๋ฃŒ๋ณ„ ์ž…๋ ฅ๋งŒ ์ƒˆ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” Prompt Caching ๊ตฌ์กฐ๋ฅผ ํ‘œํ˜„ํ•œ AI ์ƒ์„ฑ ์ด๋ฏธ์ง€!

 

์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์˜ ์ฒซ ๊ธ€์ด๋‹ˆ ๋ฐฐ๊ฒฝ์„ ๊ฐ„๋‹จํžˆ ์ ์ž๋ฉด, ํ•ด๋‹น ํ”„๋กœ์ ํŠธ๋Š” ์‚ฌ๋ฃŒ ๋ผ๋ฒจ๊ณผ ์„ฑ๋ถ„ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋ณดํ˜ธ์ž์—๊ฒŒ ๋ณด์—ฌ์ค„ ์‚ฌ๋ฃŒ ์„ฑ์ ํ‘œ๋ฅผ ๋งŒ๋“ ๋‹ค. ์„ฑ์ ํ‘œ์˜ ํ•ต์‹ฌ ํŒ๋‹จ์€ ๊ทœ์น™ ๊ธฐ๋ฐ˜ ํŒŒ์ดํ”„๋ผ์ธ์ด ๋‹ด๋‹นํ•œ๋‹ค. LLM์€ ๋“ฑ๊ธ‰์ด๋‚˜ ์‚ฌ์‹ค์„ ์ƒˆ๋กœ ํŒ๋‹จํ•˜์ง€ ์•Š๊ณ  ์ด๋ฏธ ๊ณ„์‚ฐ๋œ ๊ฒฐ๊ณผ๋ฅผ ๋ณดํ˜ธ์ž๊ฐ€ ์ฝ๊ธฐ ์‰ฌ์šด ๋ฌธ์žฅ์œผ๋กœ ๋‹ค๋“ฌ๋Š” ์—ญํ• ๋งŒ ๋งก๋Š”๋‹ค.

 

๊ทธ๋Ÿฐ๋ฐ ์ด ๋ฌธ์žฅ ๋‹ค๋“ฌ๊ธฐ ํ•œ ๋ฒˆ์— ์•ฝ `$0.06`์ด ๋‚˜์™”๋‹ค..

 

์ฒ˜์Œ์—๋Š” ๋ชจ๋ธ ๋‹จ๊ฐ€๋ถ€ํ„ฐ ์˜์‹ฌํ–ˆ๋‹ค.. ๋ฌธ์žฅ ๋ช‡ ๊ฐœ๋ฅผ ๋‹ค๋“ฌ๋Š” ์ผ์ธ๋ฐ ํ•œ ๋ฒˆ์— `$0.06`์ด๋ฉด ์šด์˜ ๋น„์šฉ์œผ๋กœ ๋ถ€๋‹ด์ด ํฌ๋‹ค๊ณ  ๋А๊ผˆ๋‹ค.

๋‹ค๋งŒ ๋ฐ”๋กœ ๋ชจ๋ธ์„ ๋‚ฎ์ถ”๊ฑฐ๋‚˜ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ค„์ด๊ธฐ ์ „์—, ๋จผ์ € ์‹ค์ œ usage๋ถ€ํ„ฐ ํ™•์ธํ–ˆ๋‹ค. ๋น„์šฉ์ด ์ •๋ง ์ด์ƒํ•˜๊ฒŒ ๋‚˜์˜จ ๊ฒƒ์ธ์ง€, ์•„๋‹ˆ๋ฉด ๋‚ด๊ฐ€ ์š”์ฒญ์˜ ํฌ๊ธฐ๋ฅผ ์ž‘๊ฒŒ ์ฐฉ๊ฐํ•˜๊ณ  ์žˆ์—ˆ๋Š”์ง€๋ถ€ํ„ฐ ๋ถ„๋ฆฌํ•ด์„œ ๋ด์•ผ ํ–ˆ๋‹ค.

 

ํ™•์ธํ•ด๋ณด๋‹ˆ ๊ณผ๊ธˆ ์ž์ฒด๋Š” ์ด์ƒํ•˜์ง€ ์•Š์•˜๋‹ค.

input_tokens: 14,448
output_tokens: 1,034
model: claude-sonnet-4-20250514
cache_creation_input_tokens: 0
cache_read_input_tokens: 0

 

Anthropic ๊ณต์‹ ๊ฐ€๊ฒฉํ‘œ ๊ธฐ์ค€์œผ๋กœ Claude Sonnet 4 ๊ณ„์—ด์€ ์ž…๋ ฅ `$3 / 1M tokens`, ์ถœ๋ ฅ `$15 / 1M tokens`๋‹ค.

๊ณ„์‚ฐ์€ ๋‹จ์ˆœํ–ˆ๋‹ค.

์ž…๋ ฅ: 14,448 * $3 / 1,000,000 = ์•ฝ $0.043
์ถœ๋ ฅ: 1,034 * $15 / 1,000,000 = ์•ฝ $0.015
ํ•ฉ๊ณ„: ์•ฝ $0.058

 

์ด๊ฑธ ๋ณด๋‹ˆ.. `$0.06`์€ ๋‹น์—ฐํ•œ ๊ณผ๊ธˆ์ด์—ˆ๋‹ค ๐Ÿฅฒ

 

(ํ•ด๋‹น ๋ฌธ๋‹จ์€ ์ฝ์ง€ ์•Š์•„๋„ ๊ดœ์ฐฎ์•„์š”!)

์ด ๊ณผ์ •์—์„œ ํ•˜๋‚˜ ๋” ๋ฐœ๊ฒฌํ•œ ๊ฒƒ๋„ ์žˆ์—ˆ๋‹ค. ๋‹น์‹œ ํ”„๋กœ์ ํŠธ์—์„œ ์“ฐ๋˜ `claude-sonnet-4-20250514`๋Š” 2026๋…„ 5์›” ๊ธฐ์ค€ Anthropic ๋ฌธ์„œ์—์„œ deprecated ์ƒํƒœ๋กœ ํ‘œ์‹œ๋˜์–ด ์žˆ์—ˆ๋‹ค. deprecated๋Š” ์•„์ง ๋™์ž‘ํ•˜์ง€๋งŒ ๋” ์ด์ƒ ๊ถŒ์žฅ๋˜์ง€ ์•Š๋Š” ์ƒํƒœ์ธ๋ฐ, retirement ์ดํ›„์—๋Š” ์‹คํŒจํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฌธ์„œ์— ์ ํžŒ retirement ์˜ˆ์ •์ผ์€ 2026๋…„ 6์›” 15์ผ ์ด์—ˆ๊ณ  ๋ฐ”๋กœ `claude-sonnet-4-6`์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ๋‹ค ๐Ÿ˜Ž

 

๊ทธ๋ž˜์„œ ์ด ๋ฌธ์ œ๋Š” ๋‘ ๊ฐˆ๋ž˜์˜€๋‹ค.

 

ํ•˜๋‚˜๋Š” `$0.06`์ด ์™œ ๋‚˜์™”๋Š”์ง€ ์ดํ•ดํ•˜๋Š” ๊ฒƒ!

๋‹ค๋ฅธ ํ•˜๋‚˜๋Š” deprecated ๋ชจ๋ธ์„ ๊ณ„์† ์“ฐ์ง€ ์•Š๋„๋ก ๊ธฐ๋ณธ ๋ชจ๋ธ์„ ๋ฐ”๊พธ๋Š” ๊ฒƒ!

๋‘˜์€ ๊ด€๋ จ์€ ์žˆ์ง€๋งŒ ๊ฐ™์€ ๋ฌธ์ œ๋Š” ์•„๋‹ˆ์—ˆ๋‹ค. ๋ชจ๋ธ์„ ๋ฐ”๊พผ๋‹ค๊ณ  ์ด ์š”์ฒญ์ด ๊ฐ‘์ž๊ธฐ ์‹ธ์ง€๋Š” ๊ฒƒ์ด ์•„๋‹ˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.. Sonnet 4์™€ Sonnet 4.6์˜ ๊ธฐ๋ณธ ์ž…๋ ฅ/์ถœ๋ ฅ ๋‹จ๊ฐ€๋Š” ๊ฐ™์€ ๊ตฌ๊ฐ„์ด์—ˆ๊ณ  ๊ทธ๋ ‡๊ธฐ์— ๋น„์šฉ ๋ฌธ์ œ๋Š” ์š”์ฒญ ๊ตฌ์กฐ๋ฅผ ๋ด์•ผ ํ–ˆ๋‹ค.

 

๋‹ค๋งŒ ๋ฐ”๋กœ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ค„์ด๊ธฐ์—” ๋ถˆ์•ˆํ–ˆ๋‹ค. ์ด 1.4๋งŒ ํ† ํฐ ์•ˆ์—๋Š” ๋ฌด์˜๋ฏธํ•œ ๋ฌธ์žฅ์ด ์•„๋‹ˆ๋ผ, ์‚ฌ๋ฃŒ ์„ฑ์ ํ‘œ์˜ ํ’ˆ์งˆ์„ ๋งž์ถ”๊ธฐ ์œ„ํ•œ ๊ธฐ์ค€ ๋ฌธ์„œ์™€ ๋ชจ๋ฒ” ์˜ˆ์‹œ๊ฐ€ ๋“ค์–ด ์žˆ์—ˆ๋‹ค. ๊ณผ๊ธˆ์ด ์ •์ƒ์ด๋ผ๋Š” ๊ฒƒ๊ณผ, ๊ทธ ํ”„๋กฌํ”„ํŠธ ์–‘์ด ์ตœ์ ์ด๋ผ๋Š” ๊ฒƒ์€ ๋‹ค๋ฅธ ๋ฌธ์ œ์˜€๊ธฐ์—.. ๋น„์šฉ์ด ์™œ ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ๋‚˜์„œ ๋ฌด์—‡์„ ์œ ์ง€ํ•˜๊ณ  ๋ฌด์—‡์„ ์ค„์ผ ์ˆ˜ ์žˆ๋Š”์ง€ ๋‚˜๋ˆ ์„œ ๋ด์•ผ ํ–ˆ๋‹ค.

 

์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ ์ฝ๊ธฐ ํŽธํ•˜๊ฒŒ ์ฃผ์ œ๋ฅผ ๋‚˜๋ˆ ๋ณด๊ฒ ๋‹ค

 

๋ฌธ์ œ๋Š” ๋ชจ๋ธ์ด ์•„๋‹ˆ๋ผ ์š”์ฒญ ๊ตฌ์กฐ์˜€๋‹ค

์‚ฌ๋ฃŒ ์„ฑ์ ํ‘œ๋ฅผ ๋งŒ๋“ค ๋•Œ LLM ํ˜ธ์ถœ์—๋Š” ์‚ฌ๋ฃŒ๋งˆ๋‹ค ๋‹ฌ๋ผ์ง€๋Š” ์ •๋ณด๋งŒ ๋“ค์–ด๊ฐ€์ง€ ์•Š์•˜๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ํŠน์ • ์ œํ’ˆ์˜ ์›์žฌ๋ฃŒ, ๋ณด์ฆ์„ฑ๋ถ„, ์นผ์Š˜/์ธ ํ‘œ๊ธฐ ์—ฌ๋ถ€, ์ฒ˜๋ฐฉ์‹ ์—ฌ๋ถ€์ฒ˜๋Ÿผ ์ œํ’ˆ ๋ผ๋ฒจ์—์„œ ์ถ”์ถœํ•œ ์‚ฌ์‹ค ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด๊ฐ„๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์—ฌ๊ธฐ์— ๋”ํ•ด, ๋ฌธ์žฅ ํ’ˆ์งˆ์„ ๋งž์ถ”๊ธฐ ์œ„ํ•œ ๊ณ ์ • ๋ฌธ์„œ๋„ ํ•จ๊ป˜ ๋“ค์–ด๊ฐ”๋‹ค.

 

์˜ˆ๋ฅผ ๋“ค๋ฉด ์ด๋Ÿฐ ๊ฒƒ๋“ค์ด๋‹ค.

- 2026๋…„ ์‚ฌ๋ฃŒ ์„ฑ์ ํ‘œ ์ž‘์„ฑ ๊ฐ€์ด๋“œ
- ๋ณดํ˜ธ์ž์šฉ ๋ฌธ์žฅ ํ†ค์•ค๋งค๋„ˆ
- ๋‚ด๋ถ€ ์ฑ„์  ์šฉ์–ด๋ฅผ ๋…ธ์ถœํ•˜์ง€ ๋ง๋ผ๋Š” ๊ทœ์น™
- ์นผ์Š˜/์ธ ๋ˆ„๋ฝ, ์ฒ˜๋ฐฉ์‹ ๋“ฑ ํ•„์ˆ˜ ๊ณ ์ง€ ๋ฌธ๊ตฌ
- ์ฐธ๊ณ ๋ฅผ ์œ„ํ•œ ๋ชจ๋ฒ” ์„ฑ์ ํ‘œ ์˜ˆ์‹œ

 

์ด ๋ฌธ์„œ๋“ค์€ ์‚ฌ๋ฃŒ๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ ๊ฑฐ์˜ ๋™์ผํ•˜๋‹ค. ๋กœ์–„์บ๋‹Œ์„ ๋ถ„์„ํ•  ๋•Œ๋„ ๋“ค์–ด๊ฐ€๊ณ , ๋‹ค๋ฅธ ๋ธŒ๋žœ๋“œ๋ฅผ ๋ถ„์„ํ•  ๋•Œ๋„ ๋“ค์–ด๊ฐ„๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์บ์‹ฑ์ด ์—†์œผ๋ฉด ๋งค ์š”์ฒญ๋งˆ๋‹ค ๊ฐ™์€ ๋ฌธ์„œ๋ฅผ ์ƒˆ ์ž…๋ ฅ ํ† ํฐ์œผ๋กœ ๊ฒฐ์ œํ•œ๋‹ค.

 

๊ทธ๋ ‡๊ฒŒ ์ •๋ฆฌํ•œ ์š”์ฒญ ๊ตฌ์กฐ ๋ฌธ์ œ๋Š” ์ด๋ ‡๋‹ค.

์‚ฌ๋ฃŒ๋ณ„๋กœ ๋‹ฌ๋ผ์ง€๋Š” ์ •๋ณด๋Š” ์ผ๋ถ€์ธ๋ฐ, ์‚ฌ๋ฃŒ๋ณ„๋กœ ๋‹ฌ๋ผ์ง€์ง€ ์•Š๋Š” ๊ธด ๊ธฐ์ค€ ๋ฌธ์„œ๊ฐ€ ๋งค๋ฒˆ ์ƒˆ๋กœ ์ฝํžˆ๊ณ  ์žˆ์—ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ์ด ๊ธฐ์ค€ ๋ฌธ์„œ๋ฅผ ์–ผ๋งˆ๋‚˜ ์ค„์ผ ์ˆ˜ ์žˆ๋Š”์ง€๋Š” ๋ณ„๋„๋ผ ์ƒ๊ฐํ•œ๋‹ค. ๋ฌด์ž‘์ • ์ค„์ด๋ฉด ๋น„์šฉ์€ ๋‚ด๋ ค๊ฐ€๊ฒ ์ง€๋งŒ, ์„ฑ์ ํ‘œ ๋ฌธ์žฅ์ด ๋ชจ๋ฒ” ์„ฑ์ ํ‘œ ์˜ˆ์‹œ์—์„œ ๋ฉ€์–ด์งˆ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ฒซ ๋ฒˆ์งธ ์„ ํƒ์ง€๋Š” ํ”„๋กฌํ”„ํŠธ ์ถ•์†Œ๊ฐ€ ์•„๋‹ˆ๋ผ ์บ์‹ฑ์ด์—ˆ๋‹ค. ๊ฐ™์€ ๊ธฐ์ค€ ๋ฌธ์„œ๋ฅผ ์œ ์ง€ํ•˜๋˜, ๋ฐ˜๋ณต ์ž…๋ ฅ ๋น„์šฉ๋ถ€ํ„ฐ ์ค„์ด๋Š” ์ชฝ์ด ํ’ˆ์งˆ ๋ฆฌ์Šคํฌ๊ฐ€ ๊ฐ€์žฅ ์ž‘์•˜๋‹ค.

 

Prompt Caching์€ ๋‹ต๋ณ€ ์บ์‹œ๊ฐ€ ์•„๋‹ˆ๋‹ค

Prompt Caching์€ ์™„์„ฑ๋œ ์„ฑ์ ํ‘œ๋ฅผ ์ €์žฅํ•ด๋‘๋Š” ๊ธฐ๋Šฅ์ด ์•„๋‹ˆ๋‹ค. LLM ๋ชจ๋ธ์ด ๋ฐ˜๋ณต๋˜๋Š” ํ”„๋กฌํ”„ํŠธ ์•ž๋ถ€๋ถ„์„ ๋‹ค์‹œ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๋„๋ก ์žฌ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

 

์ด๋•Œ ๋ฐ˜๋ณต๋˜๋Š” ์•ž๋ถ€๋ถ„์„ ์บ์‹œ ๋Œ€์ƒ์ด ๋˜๋Š” ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ๋ผ๊ณ  ๋ถ€๋ฅด๊ฒ ๋‹ค.

๋‚ด๊ฐ€ ์ƒ๊ฐํ•˜๋Š” ์ข‹์€ ๊ตฌ์กฐ๋Š” ์ด๋ ‡๋‹ค.

[๊ณ ์ • ์‹œ์Šคํ…œ ์ง€์นจ]
[์‚ฌ๋ฃŒ ์„ฑ์ ํ‘œ ์ž‘์„ฑ ๊ฐ€์ด๋“œ]
[๋ชจ๋ฒ” ์„ฑ์ ํ‘œ ์˜ˆ์‹œ]
[ํ•„์ˆ˜ ๊ณ ์ง€ ๊ทœ์น™]
-------------------------  ์—ฌ๊ธฐ๊นŒ์ง€๋ฅผ cache ๋Œ€์ƒ์œผ๋กœ ๋ดค๋‹ค
[์‚ฌ๋ฃŒ๋ณ„๋กœ ๋‹ฌ๋ผ์ง€๋Š” ์›์žฌ๋ฃŒ·์„ฑ๋ถ„ํ‘œ·๊ธ‰์—ฌ ์ •๋ณด]
[๋ฃฐ ์—”์ง„์ด ์ด๋ฏธ ๊ณ„์‚ฐํ•œ ๋“ฑ๊ธ‰ ๊ฒฐ๊ณผ]
[๋ฃฐ ์—”์ง„์ด ๋งŒ๋“  ๊ธฐ๋ณธ ์‚ฌ๋ฃŒ ๋“ฑ๊ธ‰ํ‘œ]
[์ด๋ฒˆ ์š”์ฒญ์˜ ์‚ฌ์šฉ์ž ์กฐ๊ฑด]

 

์œ„์ชฝ์˜ ๊ณ ์ • ์ง€์นจ๊ณผ ์˜ˆ์‹œ๋Š” ์‚ฌ๋ฃŒ๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ ๊ฑฐ์˜ ๊ฐ™๋‹ค. ๊ทธ๋ž˜์„œ cache ๋Œ€์ƒ์ด ๋œ๋‹ค.

 

ํ•˜์ง€๋งŒ ๊ตฌ๋ถ„์„  ์•„๋ž˜์˜ ๋‚ด์šฉ์€ ์š”์ฒญ๋งˆ๋‹ค ๋‹ฌ๋ผ์ง„๋‹ค. ์ด ๊ฐ’๋“ค์€ cache ๋Œ€์ƒ์ด ์•„๋‹ˆ๋‹ค. ํŠนํžˆ `๋ฃฐ ์—”์ง„์ด ์ด๋ฏธ ๊ณ„์‚ฐํ•œ ๋“ฑ๊ธ‰ ๊ฒฐ๊ณผ`๋Š” LLM์ด ์ƒˆ๋กœ ๋งŒ๋“œ๋Š” ๊ฒฐ๊ณผ๊ฐ€ ์•„๋‹ˆ๋ผ, ๋ฃฐ ์—”์ง„์ด ๊ณ„์‚ฐํ•œ ๊ฒฐ๊ณผ๋ฅผ LLM์—๊ฒŒ ๋„˜๊ฒจ์ฃผ๋Š” ๊ฐ’์ด๋‹ค. LLM์€ ์ด ๊ฐ’์„ ๋ณด๊ณ  ๋ฌธ์žฅ์„ ๋งž์ถœ ๋ฟ, ๋“ฑ๊ธ‰ ์ž์ฒด๋ฅผ ๋ฐ”๊พธ๋ฉด ์•ˆ ๋œ๋‹ค.

 

๊ทธ๋ž˜์„œ ๋‚˜์œ ๊ตฌ์กฐ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค๊ณ  ๋ณธ๋‹ค.

[์‚ฌ๋ฃŒ ์ด๋ฆ„]
[์‚ฌ์šฉ์ž ์กฐ๊ฑด]
[ํ˜„์žฌ ์‹œ๊ฐ„]
[๊ณ ์ • ๊ฐ€์ด๋“œ]
[๋ชจ๋ฒ” ์„ฑ์ ํ‘œ]

 

๋งค๋ฒˆ ๋ฐ”๋€Œ๋Š” ๊ฐ’์ด ์•ž์— ์„ž์ด๋ฉด cache hit๊ฐ€ ๊นจ์ง„๋‹ค. Prompt Caching์€ ๋™์ผํ•œ prefix๊ฐ€ ๋‹ค์‹œ ๋“ค์–ด์™”์„ ๋•Œ ํšจ๊ณผ๊ฐ€ ๋‚˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

Claude์—์„œ๋Š” ์–ด๋–ป๊ฒŒ ํ™•์ธํ•˜๋‚˜

Anthropic Claude API์—์„œ๋Š” Prompt Caching์„ ์“ฐ๋ ค๋ฉด ์š”์ฒญ์— `cache_control`์„ ๋„ฃ์–ด์•ผ ํ•œ๋‹ค.

(Anthropic์€ block๋ณ„ ์ง€์ • ์—†์ด top-level cache_control๋กœ breakpoint๋ฅผ ์ž๋™ ๋ฐฐ์น˜ํ•˜๋Š” ๋ฐฉ์‹๋„ ์ง€์›ํ•˜๋‹ˆ ์ด ๋ถ€๋ถ„ ๋งํฌ๋„ ์ฒจ๋ถ€ํ•ด ๋‘๊ฒ ๋‹ค!)

 

๊ทธ๋ฆฌ๊ณ  ์บ์‹ฑ์ด ์‹ค์ œ๋กœ ๋๋Š”์ง€๋Š” ์‘๋‹ต์˜ `usage`ํ•„๋“œ๋ฅผ ๋ณด๋ฉด ๋œ๋‹ค.

top-level ๋˜๋Š” content block ๋‹จ์œ„์˜ cache_control์„ ๋„ฃ๋Š”๋‹ค.

ํ•ต์‹ฌ ํ•„๋“œ๋Š” ์„ธ ๊ฐœ๋‹ค.

{
  "usage": {
    "input_tokens": 500,
    "cache_creation_input_tokens": 12095,
    "cache_read_input_tokens": 0,
    "output_tokens": 800
  }
}

 

์˜๋ฏธ๋Š” ์ด๋ ‡๋‹ค.

cache_creation_input_tokens
์ด๋ฒˆ ์š”์ฒญ์—์„œ ์บ์‹œ์— ์ƒˆ๋กœ ์“ด ํ† ํฐ ์ˆ˜

cache_read_input_tokens
์ด๋ฒˆ ์š”์ฒญ์—์„œ ์ด๋ฏธ ์บ์‹œ๋œ prefix๋ฅผ ์ฝ์€ ํ† ํฐ ์ˆ˜

input_tokens
์บ์‹œ๋˜์ง€ ์•Š์€ ์ผ๋ฐ˜ ์ž…๋ ฅ ํ† ํฐ ์ˆ˜

 

์ด ๋ถ€๋ถ„์€ ์‹ค์ œ๋กœ ๋‘ ๋ฒˆ ์—ฐ์† ํ˜ธ์ถœํ•ด์„œ ํ™•์ธํ–ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ์š”์ฒญ์—์„œ๋Š” ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ๊ฐ€ cache write๋กœ ์žกํžˆ๋Š”์ง€ ๋ณด๊ณ , ๊ฐ™์€ ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ๋กœ ๋‘ ๋ฒˆ์งธ ์š”์ฒญ์„ ๋ณด๋‚ด cache read๊ฐ€ ์žกํžˆ๋Š”์ง€ ๋ดค๋‹ค. ๋น„์šฉ ๊ทธ๋ž˜ํ”„๋ฅผ ๋ˆˆ์œผ๋กœ ๋ณด๋Š” ๊ฒƒ๋ณด๋‹ค ์‘๋‹ต usage์— ์ฐํžˆ๋Š” ์ˆซ์ž๋ฅผ ํ™•์ธํ•˜๋Š” ํŽธ์ด ํ›จ์”ฌ ๋ช…ํ™•ํ–ˆ๋‹ค. (usage๋Š” API ์‘๋‹ต JSON body ์•ˆ์˜ ํ•„๋“œ๋‹ค)

 

์ •์ƒ์ ์œผ๋กœ ์บ์‹œ๊ฐ€ ์žกํžˆ๋ฉด ์ฒซ ์š”์ฒญ๊ณผ ๋‘ ๋ฒˆ์งธ ์š”์ฒญ์˜ usage๊ฐ€ ๋‹ค๋ฅด๊ฒŒ ๋‚˜์˜จ๋‹ค.

<์ฒซ ๋ฒˆ์งธ ์š”์ฒญ>
cache_creation_input_tokens = 12095
cache_read_input_tokens = 0

<๋‘ ๋ฒˆ์งธ ์š”์ฒญ>
cache_creation_input_tokens = 0
cache_read_input_tokens = 12095

 

์ด ๊ฒฐ๊ณผ๋Š” ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ 12,095ํ† ํฐ์ด ์ฒซ ์š”์ฒญ์—์„œ ์บ์‹œ์— ์“ฐ์˜€๊ณ , ๋‘ ๋ฒˆ์งธ ์š”์ฒญ์—์„œ ์žฌ์‚ฌ์šฉ๋๋‹ค๋Š” ๋œป์ด๋‹ค.

๋ฐ˜๋Œ€๋กœ ์•„๋ž˜์ฒ˜๋Ÿผ ๋‚˜์˜ค๋ฉด ์บ์‹ฑ์ด ์•ˆ ๋œ ๊ฒƒ์ด๋‹ค.

cache_creation_input_tokens = 0
cache_read_input_tokens = 0

 

์ด๋•Œ๋Š” `cache_control`์ด ๋น ์กŒ๋Š”์ง€, ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์š”์ฒญ๋งˆ๋‹ค ๋‹ฌ๋ผ์ง€๋Š”์ง€, ์ œํ’ˆ๋ณ„ ๋ฐ์ดํ„ฐ๊ฐ€ ์บ์‹œ ๊ตฌ๊ฐ„ ์•ˆ์— ์„ž์˜€๋Š”์ง€, ์บ์‹œ ์œ ํšจ์‹œ๊ฐ„์ด ์ง€๋‚ฌ๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค.

 

Claude์—์„œ ์ „์ฒด ์ž…๋ ฅ ํ† ํฐ์„ ๋ณผ ๋•Œ๋„ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•˜๋‹ค. `input_tokens`๋งŒ ๋ณด๋ฉด ์ „์ฒด ์ž…๋ ฅ์ด ์•„๋‹ˆ๋‹ค.

const total_input_tokens =
    input_tokens
    + cache_creation_input_tokens
    + cache_read_input_tokens

 

ํ•œ ๋ฒˆ์˜ ํ…Œ์ŠคํŠธ ํ˜ธ์ถœ์—์„œ๋Š” usage๋ฅผ ์ง์ ‘ ๋ณด๋ฉด ๋œ๋‹ค. ํ•˜์ง€๋งŒ ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์š”์ฒญ์ด ๊ณ„์† ๋“ค์–ด์˜ด์œผ๋กœ ๋งค๋ฒˆ ์‘๋‹ต JSON์„ ์‚ฌ๋žŒ์ด ํ™•์ธํ•˜๊ธด ํž˜๋“ค๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ๋ฌธ์žฅ ๋‹ค๋“ฌ๊ธฐ ํ˜ธ์ถœ๋งˆ๋‹ค ์ตœ์†Œํ•œ ์•„๋ž˜ ๊ฐ’์€ ๋กœ๊ทธ๋‚˜ ๋ฉ”ํŠธ๋ฆญ์œผ๋กœ ๋‚จ๊ฒจ๋‘๋ ค ํ•œ๋‹ค.

provider
model
input_tokens
output_tokens
cache_creation_input_tokens
cache_read_input_tokens
copy_source report_reused
estimated_cost

 

์บ์‹ฑ์„ ์ ์šฉํ•œ ๋’ค์—๋Š” ๋‹จ์ˆœํžˆ ์ฒญ๊ตฌ ๊ธˆ์•ก์ด ์ค„์—ˆ๋Š”์ง€๋งŒ ๋ณด๋ฉด ๋ถ€์กฑํ•˜๋‹ค. Claude ์‘๋‹ต์˜ `cache_read_input_tokens`๊ฐ€ 0๋ณด๋‹ค ํฌ๊ฒŒ ๋‚˜์˜ค๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค. ์ด ๊ฐ’์ด ์žˆ์–ด์•ผ ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋งค๋ฒˆ ์ƒˆ๋กœ ์ฝ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์‹ค์ œ๋กœ ์บ์‹œ์—์„œ ์žฌ์‚ฌ์šฉํ–ˆ๋‹ค๋Š” ๋œป์ด ๋œ๋‹ค!

 

์บ์‹œ๋Š” ์–ผ๋งˆ๋‚˜ ์œ ์ง€๋˜๋‚˜

๋‹ค๋งŒ ๋‹น์—ฐํ•˜๊ฒŒ๋„ ์บ์‹œ๋Š” ์˜๊ตฌ ์ €์žฅ์†Œ๊ฐ€ ์•„๋‹ˆ๋‹ค!

์œ ํšจ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ๋‹ค์Œ ์š”์ฒญ์€ ๋‹ค์‹œ cache miss๊ฐ€ ๋‚˜๊ณ , ๊ทธ ์ˆœ๊ฐ„ ์ž…๋ ฅ ๋น„์šฉ์ด ๋‹ค์‹œ ํŠ„๋‹ค ๐Ÿฅฒ 

 

Anthropic ๊ณต์‹ ๋ฌธ์„œ ๊ธฐ์ค€์œผ๋กœ Claude์˜ `ephemeral` cache๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ 5๋ถ„ lifetime์„ ๊ฐ€์ง„๋‹ค. 1์‹œ๊ฐ„ TTL๋„ ์„ ํƒํ•  ์ˆ˜ ์žˆ์ง€๋งŒ.. write ๋น„์šฉ์ด ๋” ๋น„์‹ธ๋‹ค..! (๊ทธ๋Ÿฌ๋‹ˆ ๋ฌด์ž‘์ • ์„ ํƒํ•˜์ง€ ๋ง์ž!)

 

๊ฐ€๊ฒฉ ๊ตฌ์กฐ๋Š” ์ด๋ ‡๋‹ค.

5๋ถ„ cache write: ์ผ๋ฐ˜ input ๊ฐ€๊ฒฉ์˜ 1.25๋ฐฐ
1์‹œ๊ฐ„ cache write: ์ผ๋ฐ˜ input ๊ฐ€๊ฒฉ์˜ 2๋ฐฐ
cache read: ์ผ๋ฐ˜ input ๊ฐ€๊ฒฉ์˜ 0.1๋ฐฐ

 

๋”ฐ๋ผ์„œ ํŠธ๋ž˜ํ”ฝ์ด ๊พธ์ค€ํžˆ ๋“ค์–ด์˜ค๋Š” ์„œ๋น„์Šค๋ผ๋ฉด 5๋ถ„ ์บ์‹œ๊ฐ€ ํšจ์œจ์ ์ด๋ผ ์ƒ๊ฐํ•œ๋‹ค. 5๋ถ„ ์•ˆ์— ๊ฐ™์€ ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋ฐ˜๋ณตํ•ด์„œ ์‚ฌ์šฉ๋˜๋ฉด cache read๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ , ๊ณต์‹ ๋ฌธ์„œ ๊ธฐ์ค€์œผ๋กœ 5๋ถ„ ์บ์‹œ๋Š” ๊ณ„์† refresh๋  ์ˆ˜ ์žˆ๋‹ค.

 

๋ฐ˜๋Œ€๋กœ ์š”์ฒญ ๊ฐ„๊ฒฉ์ด 5๋ถ„์„ ์ž์ฃผ ๋„˜๋Š”๋‹ค๋ฉด ์ฒซ ์š”์ฒญ๋งˆ๋‹ค ๋‹ค์‹œ cache write๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ์ด ๊ฒฝ์šฐ ๋น„์šฉ์ด ๋‹ค์‹œ ํŠ„๋‹ค..

์ด๋Ÿฐ ์›Œํฌ๋กœ๋“œ์—์„œ๋Š” 1์‹œ๊ฐ„ TTL์„ ๊ฒ€ํ† ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‹ค๋งŒ 1์‹œ๊ฐ„ cache write๋Š” 2๋ฐฐ ๊ฐ€๊ฒฉ์ด๋ฏ€๋กœ, ์š”์ฒญ ๋นˆ๋„์™€ cache hit ๊ฐ€๋Šฅ์„ฑ์„ ๋ณด๊ณ  ๊ฒฐ์ •ํ•ด์•ผ ํ•œ๋‹ค.

 

์š”์•ฝ ์ •๋ฆฌํ•˜์ž๋ฉด ์ด๋ ‡๋‹ค.

์งง์€ ์‹œ๊ฐ„ ์•ˆ์— ๋ฐ˜๋ณต ํ˜ธ์ถœ์ด ๋งŽ๋‹ค → 5๋ถ„ cache๊ฐ€ ์œ ๋ฆฌํ•  ๊ฐ€๋Šฅ์„ฑ์ด ํผ
์š”์ฒญ ๊ฐ„๊ฒฉ์ด 5๋ถ„์„ ์ž์ฃผ ๋„˜์ง€๋งŒ 1์‹œ๊ฐ„ ์•ˆ์—๋Š” ๋ฐ˜๋ณต → 1์‹œ๊ฐ„ TTL ๊ฒ€ํ† 
์š”์ฒญ์ด ํ•˜๋ฃจ์— ๋ช‡ ๋ฒˆ ์ˆ˜์ค€ → Prompt Caching๋ณด๋‹ค DB ์žฌ์‚ฌ์šฉ, ๋ฐฐ์น˜, ํ”„๋กฌํ”„ํŠธ ์ถ•์†Œ๊ฐ€ ๋” ์ค‘์š”ํ•  ์ˆ˜ ์žˆ์Œ

 

๊ทธ๋ž˜์„œ ์บ์‹ฑ์„ ์ ์šฉํ–ˆ๋‹ค๊ณ  ๋์ด ์•„๋‹ˆ๋ผ cache hit ๋น„์œจ์„ ๋ด์•ผ ํ•œ๋‹ค!

const cache_hit_ratio =
	cache_read_input_tokens / total_input_tokens

 

์œ„์˜ ์ฝ”๋“œ์— ์•„๋ž˜ ๋‚ด์šฉ์„ ๋Œ€์ž…ํ•ด๋ณด์ž.

input_tokens = 500
cache_creation_input_tokens = 0
cache_read_input_tokens = 12095

 

๊ทธ๋Ÿผ ๊ฒฐ๊ณผ๋Š” ์•„๋ž˜์ฒ˜๋Ÿผ ๋‚˜์˜จ๋‹ค.

total_input_tokens = 12595
cache_hit_ratio = ์•ฝ 96%

 

์ด ์ •๋„๋ฉด ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ ์บ์‹ฑ์ด ์ž˜ ๋จน๊ณ  ์žˆ๋‹ค๊ณ  ๋ณธ๋‹ค.

๋ฐ˜๋Œ€๋กœ ํŠธ๋ž˜ํ”ฝ์ด ๋“œ๋ฌธ ์‹œ๊ฐ„๋Œ€๋งˆ๋‹ค ์•„๋ž˜์ฒ˜๋Ÿผ ๋‚˜์˜จ๋‹ค๋ฉด ์บ์‹œ๊ฐ€ ๋งŒ๋ฃŒ๋˜๊ณ  ์žˆ๋Š” ๊ฒƒ์ด๋‹ค.

cache_creation_input_tokens = 12095
cache_read_input_tokens = 0

 

์ด๊ฑด ์‹คํŒจ๊ฐ€ ์•„๋‹ˆ๋ผ cache miss๋‹ค. ๋‹ค๋งŒ ๋น„์šฉ์€ ๋‹ค์‹œ ์˜ค๋ฅธ๋‹ค ๐Ÿ˜ฑ

 

๊ณ ์ • ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฐ”๊ฟ”์•ผ ํ•  ๋•Œ

์บ์‹œ ๋Œ€์ƒ ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ๋Š” ๋ฐ”๊ฟ”๋„ ๋ ๊นŒ? ๋‹น์—ฐํžˆ ๋œ๋‹ค!

์ž‘์„ฑ ๊ฐ€์ด๋“œ๊ฐ€ ๊ฐœ์„ ๋˜๊ฑฐ๋‚˜, ๋ชจ๋ฒ” ์„ฑ์ ํ‘œ๊ฐ€ ๋ฐ”๋€Œ๊ฑฐ๋‚˜, ํ•„์ˆ˜ ๊ณ ์ง€ ๋ฌธ๊ตฌ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ๋‹น์—ฐํžˆ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•œ๋‹ค!

 

๋‹ค๋งŒ ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ๊ธฐ์กด ์บ์‹œ์™€ prefix๊ฐ€ ๋‹ฌ๋ผ์ง„๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ฒซ ์š”์ฒญ์€ cache miss๊ฐ€ ๋‚˜๊ณ , ์ƒˆ ๋ฒ„์ „์˜ ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋‹ค์‹œ cache write๋œ๋‹ค. ์ดํ›„ ๊ฐ™์€ ์ƒˆ ๋ฒ„์ „์ด ๋ฐ˜๋ณต๋˜๋ฉด ๋‹ค์‹œ cache hit๊ฐ€ ๋‚œ๋‹ค.

 

์ฆ‰ ๋ฐฐํฌ ์งํ›„์—๋Š” ๋น„์šฉ์ด ์ž ๊น ํŠˆ ์ˆ˜ ์žˆ๋‹ค.

 

์ด๋•Œ ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฑธ ๋ชจ๋ฅด๋Š” ๋‹ค๋ฅธ ํŒ€์› ์ž…์žฅ์—์„  ๋‹นํ™ฉ์Šค๋Ÿฌ์šธ ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋Ÿฌ๋‹ˆ ์šด์˜์—์„œ๋Š” ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋ฒ„์ „์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ํŽธ์ด ์ข‹๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

feed_report_copy_prompt_version = 2605.1
feed_report_copy_prompt_version = 2605.2

 

์ด๋ ‡๊ฒŒ ํ•ด๋‘๋ฉด ์–ด๋–ค ์„ฑ์ ํ‘œ๊ฐ€ ์–ด๋–ค ๊ธฐ์ค€ ๋ฌธ์„œ๋กœ ๋ฌธ์žฅ ๋‹ค๋“ฌ๊ธฐ๋ฅผ ๊ฑฐ์ณค๋Š”์ง€ ์ถ”์ ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ณ€๊ฒฝ์˜ ์„ฑ๊ฒฉ๋„ ๋‚˜๋ˆ ์•ผ ํ•œ๋‹ค๊ณ  ๋ณธ๋‹ค.

๋ฌธ์ œ๊ฐ€ ์กฐ๊ธˆ ๋ฐ”๋€ ๊ฒฝ์šฐ
-> ๊ธฐ์กด ๋ฐœํ–‰ ์„ฑ์ ํ‘œ๋ฅผ ๋ฐ˜๋“œ์‹œ ํ๊ธฐํ•  ํ•„์š”๋Š” ์—†์Œ

ํ•„์ˆ˜ ๊ณ ์ง€, ์•ˆ์ „ ๋ฌธ๊ตฌ, ์ •์ฑ… ํŒ๋‹จ์ด ๋ฐ”๋€ ๊ฒฝ์šฐ
-> ๊ธฐ์กด ์„ฑ์ ํ‘œ ์žฌ์‚ฌ์šฉ ๊ธฐ์ค€์„ ๋‹ค์‹œ ๋ด์•ผ ํ•จ

์ฑ„์  ๊ทœ์น™์ด๋‚˜ ์‚ฌ๋ฃŒ๋ณ„๋กœ ๋‹ฌ๋ผ์ง€๋Š” ์ •๋ณด๋“ค์˜ ํ•ด์„์ด ๋ฐ”๋€ ๊ฒฝ์šฐ (์‚ฌ๋ฃŒ๋ณ„๋กœ ๋‹ฌ๋ผ์ง€๋Š” ์ •๋ณด๋“ค์„ ์•ž์œผ๋กœ facts ๋ผ๊ณ  ํ•˜๊ฒ ๋‹ค!)
-> ruleset version ๋˜๋Š” report publication ๊ธฐ์ค€๊นŒ์ง€ ๊ฐ™์ด ๋ด์•ผ ํ•จ

 

์šฐ๋ฆฌ ์„œ๋น„์Šค์—์„œ๋Š” ์ด๋ฏธ ๋ฐœํ–‰๋œ ์„ฑ์ ํ‘œ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋Š” ๊ตฌ์กฐ๊ฐ€ ์žˆ๋‹ค. ์ด๋•Œ ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋ฐ”๋€Œ์—ˆ๋‹ค๊ณ  ๋ฌด์กฐ๊ฑด ๊ธฐ์กด ์„ฑ์ ํ‘œ๋ฅผ ๋ฒ„๋ฆด ํ•„์š”๋Š” ์—†๋‹ค. ํ•˜์ง€๋งŒ ๋ฒ•์  ๊ณ ์ง€, ์•ˆ์ „ ๋ฌธ๊ตฌ, ๋ฆฌํฌํŠธ ์ •์ฑ…์ด ๋ฐ”๋€ ๊ฒฝ์šฐ๋ผ๋ฉด ๊ฐ™์€ facts์—ฌ๋„ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ค„ ๋ฌธ์žฅ์ด ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿฐ ๋ณ€๊ฒฝ์€ prompt version์ด๋‚˜ ruleset version์œผ๋กœ ๋ถ„๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ๋ฐฐํฌ ์งํ›„ ์ฒซ ์‚ฌ์šฉ์ž์—๊ฒŒ cache miss ๋น„์šฉ๊ณผ latency๋ฅผ ๋„˜๊ธฐ๊ณ  ์‹ถ์ง€ ์•Š๋‹ค๋ฉด pre-warming๋„ ๊ฐ€๋Šฅํ•˜๋‹ค. Anthropic ๊ณต์‹ ๋ฌธ์„œ์—์„œ๋Š” `max_tokens: 0`์„ ์‚ฌ์šฉํ•ด system prompt๋‚˜ tool definition์„ ๋ฏธ๋ฆฌ ์บ์‹œ์— ์˜ฌ๋ฆฌ๋Š” ๋ฐฉ์‹์ด ์•ˆ๋‚ด๋˜์–ด ์žˆ๋‹ค. ๋‹ค๋งŒ ์ด ๊ฒฝ์šฐ์—๋„ cache write ๋น„์šฉ์€ ๋ฐœ์ƒํ•œ๋‹ค.

 

A ์‚ฌ์šฉ์ž์˜ ์บ์‹œ๋ฅผ B ์‚ฌ์šฉ์ž๊ฐ€ ์“ธ ์ˆ˜ ์žˆ๋‚˜

๊ฐ€๋Šฅํ•˜๋‹ค! ๋‹จ, ๊ฐ™์€ Anthropic workspace ์•ˆ์—์„œ ๋™์ผํ•œ ํ”„๋กฌํ”„ํŠธ prefix๊ฐ€ ๋‹ค์‹œ ๋“ค์–ด์™€์•ผ ํ•œ๋‹ค.

Anthropic ๋ฌธ์„œ ๊ธฐ์ค€์œผ๋กœ 2026๋…„ 2์›” 5์ผ๋ถ€ํ„ฐ Claude API์˜ prompt cache๋Š” workspace ๋‹จ์œ„๋กœ ๊ฒฉ๋ฆฌ๋œ๋‹ค. ๋‹ค๋ฅธ ์กฐ์ง๊ณผ๋Š” ๊ณต์œ ๋˜์ง€ ์•Š๊ณ , ๊ฐ™์€ ์กฐ์ง ์•ˆ์—์„œ๋„ workspace๊ฐ€ ๋‹ค๋ฅด๋ฉด ๋ถ„๋ฆฌ๋œ๋‹ค.

 

์ฆ‰ ์šฐ๋ฆฌ ์„œ๋ฒ„๊ฐ€ ๊ฐ™์€ Anthropic workspace๋กœ ํ˜ธ์ถœํ•˜๊ณ , ๊ณ ์ • ์‚ฌ๋ฃŒ ์„ฑ์ ํ‘œ ๊ฐ€์ด๋“œ๊ฐ€ 100% ๋™์ผํ•˜๋‹ค๋ฉด A ์‚ฌ์šฉ์ž์˜ ์š”์ฒญ์—์„œ ๋งŒ๋“ค์–ด์ง„ ์บ์‹œ๋ฅผ B ์‚ฌ์šฉ์ž์˜ ์š”์ฒญ์ด ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ์‚ฌ์šฉ์ž ์กฐ๊ฑด์ด๋‚˜ ์‚ฌ๋ฃŒ facts๊ฐ€ cache ๋Œ€์ƒ prefix ์•ˆ์— ๋“ค์–ด๊ฐ€๋ฉด ์•ˆ ๋œ๋‹ค. ๊ทธ ๊ฐ’๋“ค์€ ๋’ค๋กœ ๋นผ์•ผ ํ•œ๋‹ค.

 

OpenAI๋กœ ๋ฐ”๊พธ๋ฉด ์–ด๋–ป๊ฒŒ ๋˜๋‚˜

ํšŒ์˜์—์„œ OpenAI๋กœ ๋ณ€๊ฒฝ์€ ์–ด๋–ค๊ฐ€? ๋ผ๋Š” ์ด์•ผ๊ธฐ๊ฐ€ ๊ณ„์† ๋‚˜์˜ค๊ณ  ์žˆ๋Š” ์ค‘์ด๋ผ ์•Œ์•„๋ณธ ๊น€์— ์ ์–ด๋ณธ๋‹ค ๐Ÿ˜Ž

๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜์ž๋ฉด OpenAI๋Š” ๋ฐฉ์‹์ด ๋‹ค๋ฅด๋‹ค.

 

OpenAI ๊ณต์‹ ๋ฌธ์„œ ๊ธฐ์ค€์œผ๋กœ Prompt Caching์€ ์ง€์› ๋ชจ๋ธ์—์„œ ์ž๋™ ์ ์šฉ๋œ๋‹ค. Claude์ฒ˜๋Ÿผ `cache_control`์„ ์ง์ ‘ ๋ถ™์ด๋Š” ๋ฐฉ์‹์ด ์•„๋‹ˆ๋‹ค. ํ™•์ธ์€ `usage.prompt_tokens_details.cached_tokens`๋กœ ํ•œ๋‹ค.

 

์˜ˆ์‹œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ํ˜•ํƒœ๋‹ค.

{
  "usage": {
    "prompt_tokens": 2006,
    "completion_tokens": 300,
    "total_tokens": 2306,
    "prompt_tokens_details": {
      "cached_tokens": 1920
    }
  }
}

 

OpenAI๋„ ์บ์‹œ ์œ ์ง€์‹œ๊ฐ„์ด ์žˆ๋‹ค. 2026๋…„ 5์›” ๊ธฐ์ค€ ๊ณต์‹ ๋ฌธ์„œ์—๋Š” in-memory cache๊ฐ€ ๋ณดํ†ต 5~10๋ถ„์˜ ๋น„ํ™œ์„ฑ ์‹œ๊ฐ„ ๋™์•ˆ ์œ ์ง€๋˜๊ณ , ์ตœ๋Œ€ 1์‹œ๊ฐ„๊นŒ์ง€ ๊ฐˆ ์ˆ˜ ์žˆ๋‹ค๊ณ  ๋˜์–ด ์žˆ๋‹ค. ์ผ๋ถ€ ๋ชจ๋ธ์—์„œ๋Š” extended retention์„ ํ†ตํ•ด ์ตœ๋Œ€ 24์‹œ๊ฐ„ ์ •์ฑ…์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ํŠนํžˆ ์ตœ์‹  ๋ชจ๋ธ๋“ค์—์„œ๋Š” `prompt_cache_retention` ์„ค์ •์„ ์ง€์›ํ•œ๋‹ค.

 

๋”ฐ๋ผ์„œ OpenAI๋กœ ๋ฐ”๊พผ๋‹ค๊ณ  ์บ์‹œ๊ฐ€ ์˜๊ตฌ์ ์œผ๋กœ ์•Œ์•„์„œ ์œ ์ง€๋œ๋‹ค๊ณ  ๋ณด๋ฉด ์•ˆ ๋œ๋‹ค. OpenAI๋„ ๋ฐ˜๋ณต prefix๊ฐ€ ์•ˆ์ •์ ์œผ๋กœ ์œ ์ง€๋˜์–ด์•ผ ํ•˜๊ณ , cache hit ์—ฌ๋ถ€๋Š” `cached_tokens` ๋กœ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค.

 

ํšŒ์˜๋ฅผ ์ง„ํ–‰ํ• ์ˆ˜๋ก provider๊ฐ€ ์ž์ฃผ ๋ณ€๊ฒฝ๋  ๊ฒƒ์ด ์˜ˆ์ƒ๋๋‹ค ๐Ÿค”

๊ทธ๋ž˜์„œ ๋‚˜๋Š” provider๋ณ„ client๋ฅผ ์•„๋ž˜์ฒ˜๋Ÿผ ๋ถ„๋ฆฌํ•ด๋‘์—ˆ๋‹ค.

LLM_PROVIDER=anthropic
-> Anthropic client
-> cache_control ํฌํ•จ
-> cache_creation_input_tokens / cache_read_input_tokens ํ™•์ธ

LLM_PROVIDER=openai
-> OpenAI client
-> cache_control ์—†์Œ
-> prompt_tokens_details.cached_tokens ํ™•์ธ

 

์ด๋ ‡๊ฒŒ ๋˜์–ด ์žˆ์œผ๋ฉด ๋‚˜์ค‘์— OpenAI๋กœ ๋ฐ”๊ฟ”๋„ ์บ์‹ฑ ๊ตฌ์กฐ๋ฅผ ๋œฏ์–ด๊ณ ์น  ํ•„์š”๋Š” ์—†๋‹ค. provider๋ณ„ usage ํ•„๋“œ๋งŒ ๊ณตํ†ต ๋กœ๊ทธ ํ˜•์‹์œผ๋กœ ์ •๋ฆฌํ•˜๋ฉด ๋œ๋‹ค.

 

OpenAI๋„ ์บ์‹œ๊ฐ€ ์กฐ์ง ๊ฐ„ ๊ณต์œ ๋˜์ง€๋Š” ์•Š๋Š”๋‹ค. ๊ทธ๋ฆฌ๊ณ  Claude์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ output token ์ƒ์„ฑ์—๋Š” ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค.

Prompt Caching์€ ๋ฐ˜๋ณต ์ž…๋ ฅ ๋น„์šฉ๊ณผ ์ง€์—ฐ ์‹œ๊ฐ„์„ ์ค„์ด๋Š” ๊ธฐ๋Šฅ์ด์ง€, ๋‹ต๋ณ€ ์ƒ์„ฑ์„ ์ƒ๋žตํ•˜๋Š” ๊ธฐ๋Šฅ์ด ์•„๋‹ˆ๋‹ค.

 

๋” ์‹ผ ํ˜ธ์ถœ์€ ํ˜ธ์ถœํ•˜์ง€ ์•Š๋Š” ๊ฒƒ

์‚ฌ์‹ค ์บ์‹ฑ๋ณด๋‹ค ๋” ๊ทผ๋ณธ์ ์ธ ์ตœ์ ํ™”๊ฐ€ ์žˆ๋‹ค..!

์ด๋ฏธ ๊ฐ™์€ ์‚ฌ๋ฃŒ์— ๋Œ€ํ•ด ๋ฐœํ–‰๋œ ์„ฑ์ ํ‘œ๊ฐ€ DB์— ์žˆ๋‹ค๋ฉด LLM์„ ๋‹ค์‹œ ๋ถ€๋ฅด๋ฉด ์•ˆ ๋œ๋‹ค.

ํ•˜์ง€๋งŒ ๋‹จ์ˆœํžˆ ์‚ฌ๋ฃŒ ์ด๋ฆ„์ด ๊ฐ™๋‹ค๊ณ  ์žฌ์‚ฌ์šฉํ•˜๋ฉด ์œ„ํ—˜ํ•˜๋‹ค. ์žฌ์‚ฌ์šฉ ๊ธฐ์ค€์€ ๋ช…ํ™•ํ•ด์•ผ ํ•œ๋‹ค.

(์•„๋ž˜๋Š” ์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์šฉ์–ด๊ฐ€ ์„ž์—ฌ์žˆ์œผ๋‹ˆ ์šฉ์–ด ์ดํ•ด๋ณด๋‹จ ๋А๋‚Œ๋งŒ ์ฑ™๊ธฐ์ž!)

- ๊ฐ™์€ product revision์ธ๊ฐ€
- ๊ฐ™์€ resolved facts์ธ๊ฐ€
- ๊ฐ™์€ ruleset version์ธ๊ฐ€
- ๊ฐ™์€ life stage / size / health issues context์ธ๊ฐ€
- ์ด๋ฏธ done ์ƒํƒœ๋กœ ๋ฐœํ–‰๋œ ์„ฑ์ ํ‘œ์ธ๊ฐ€

 

์ด ์กฐ๊ฑด์ด ๊ฐ™๋‹ค๋ฉด ๊ธฐ์กด ์„ฑ์ ํ‘œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒŒ ๋งž๋‹ค. ๋น„์šฉ๋„ ์ค„๊ณ , ๊ฒฐ๊ณผ๋„ ๋” ์ผ๊ด€์ ์ด๋‹ค.

์ด๊ฑด ๋‹จ์ˆœ ์บ์‹œ๊ฐ€ ์•„๋‹ˆ๋ผ ์ œํ’ˆ ์ •์ฑ…์„ ํ†ตํ•œ ํŒ๋‹จ์ด๋‹ค. ์ด๋ฏธ ๊ฒ€์ฆ๋œ ๊ณต์‹ ์„ฑ์ ํ‘œ๊ฐ€ ์žˆ๋‹ค๋ฉด ๋‹ค์‹œ LLM์—๊ฒŒ ๋ฌธ์žฅ์„ ๋งก๊ธธ ์ด์œ ๊ฐ€ ์—†๋‹ค.

 

๊ทธ๋ ‡๊ธฐ์— ๊ฐ€์žฅ ์‹ผ LLM ํ˜ธ์ถœ์€ ํ˜ธ์ถœํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด๋‹ค! 

 

์ถœ๋ ฅ ๊ธธ์ด๋ฅผ ์ค„์ด๋‹ค๋Š” ๋ง

์ฒ˜์Œ์—๋Š” ์ถœ๋ ฅ ๊ธธ์ด๋„ ๋น„์šฉ ์ตœ์ ํ™” ํ›„๋ณด๋กœ ๋ดค๋‹ค. ํ•˜์ง€๋งŒ ์ž˜ ์ƒ๊ฐํ•ด๋ณด๋‹ˆ ์•„๋‹ˆ์—ˆ๋‹ค.. ๐Ÿ˜ฑ

 

์‚ฌ๋ฃŒ ์„ฑ์ ํ‘œ๊ฐ€ ์›๋ž˜ summary์™€ 10๊ฐœ์˜ ์นด๋“œ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋‹ค๋ฉด, ์นด๋“œ ์ˆ˜๋ฅผ ์ค„์ด๋Š” ๊ฒƒ์€ ๋น„์šฉ ์ตœ์ ํ™”๊ฐ€ ์•„๋‹ˆ๋‹ค. ์ œํ’ˆ ์š”๊ตฌ์‚ฌํ•ญ์„ ๋ฐ”๊พธ๋Š” ๊ฒƒ์ด๋‹ค.

 

๋งŒ์•ฝ ๊ทธ๋Ÿผ์—๋„ ๋ฌด์–ธ๊ฐ€๋ฅผ ์ค„์ธ๋‹ค๋ฉด, ์ค„์ผ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์€ ๊ตฌ์กฐ๊ฐ€ ์•„๋‹ˆ๋ผ ๊ตฐ๋”๋”๊ธฐ๋‹ค.

- JSON ๋ฐ–์˜ ์„ค๋ช…
- ๋ฐ˜๋ณต๋˜๋Š” ๋ฌธ์žฅ
- ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๊ธด line1 / line2
- ๋ชจ๋ธ์ด ๋ง๋ถ™์ด๋Š” ์‚ฌ์กฑ

 

์„ฑ์ ํ‘œ์˜ ํ˜•์‹์€ ์œ ์ง€ํ•ด์•ผ ํ–ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ์— ๋‚˜๋Š” ๋ณดํ˜ธ์ž๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ๊ฒฐ๊ณผ๋ฌผ์€ ๊ทธ๋Œ€๋กœ ๋‘๊ณ , ๋ชจ๋ธ์ด ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๊ธธ๊ฒŒ ์“ฐ์ง€ ์•Š๋„๋ก ์ œํ•œํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ์˜๊ฒฌ์„ ๋ƒˆ๋‹ค. (์•„์ง ๊ฒฐ๋ก ์ด ๋‚˜์ง„ ์•Š์•˜๋‹ค!

 

Haiku๋กœ ๋ฐ”๊พธ๋ฉด ๋˜๋‚˜

Claude Haiku 4.5๋Š” Sonnet๋ณด๋‹ค ์ €๋ ดํ•˜๋‹ค. Anthropic ๊ณต์‹ ๊ฐ€๊ฒฉํ‘œ ๊ธฐ์ค€์œผ๋กœ Haiku 4.5๋Š” ์ž…๋ ฅ `$1 / 1M tokens`, ์ถœ๋ ฅ `$5 / 1M tokens`๋‹ค. Sonnet 4 ๊ณ„์—ด์˜ ์ž…๋ ฅ `$3`, ์ถœ๋ ฅ `$15`์™€ ๋น„๊ตํ•˜๋ฉด ๊ฐ™์€ ํ† ํฐ ์ˆ˜์—์„œ ์•ฝ 1/3 ์ˆ˜์ค€์ด๋‹ค.

 

ํ•˜์ง€๋งŒ ์ƒ๋Œ€์ ์œผ๋กœ ์ €๋ ดํ•˜๋‹ˆ Haiku๋กœ ๋ฐ”๊พธ๊ธฐ์—๋Š” ๋ถˆ์•ˆํ–ˆ๋‹ค ๐Ÿ˜

 

์‚ฌ๋ฃŒ ์„ฑ์ ํ‘œ์˜ LLM์€ ์‚ฌ์‹ค์„ ํŒ๋‹จํ•˜์ง€ ์•Š์ง€๋งŒ, ๊ทธ๋ž˜๋„ ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•œ๋‹ค. ๋“ฑ๊ธ‰์„ ๋ฐ”๊พธ๋ฉด ์•ˆ ๋˜๊ณ , ๋‚ด๋ถ€ ์ฑ„์  ์šฉ์–ด๋ฅผ ๋…ธ์ถœํ•˜๋ฉด ์•ˆ ๋˜๊ณ , ์นผ์Š˜ ๋˜๋Š” ์ธ ๋ˆ„๋ฝ์ด๋‚˜ ์ฒ˜๋ฐฉ์‹ ์•ˆ๋‚ด ๊ฐ™์€ ํ•„์ˆ˜ ๋ฌธ๊ตฌ๋ฅผ ๋น ๋œจ๋ฆฌ๋ฉด ์•ˆ ๋œ๋‹ค.

 

๊ทธ๋ž˜์„œ ์ €๋ ดํ•œ ๋ชจ๋ธ์„ ์“ฐ๋ ค๋ฉด ์ž๋™ ๊ฒ€์ฆ์ด ๋จผ์ € ์žˆ์–ด์•ผ ํ•œ๋‹ค.

1. ์ผ๋ฐ˜ ์ผ€์ด์Šค๋Š” Haiku ๊ฐ™์€ ์ €๋ ดํ•œ ๋ชจ๋ธ๋กœ ์‹œ๋„ํ•œ๋‹ค.
2. ์„œ๋ฒ„๊ฐ€ ์‘๋‹ต์„ ๊ฒ€์ฆํ•œ๋‹ค.
3. ํ†ต๊ณผํ•˜๋ฉด ์‚ฌ์šฉํ•œ๋‹ค.
4. ์‹คํŒจํ•˜๋ฉด Sonnet์œผ๋กœ ์Šน๊ฒฉํ•œ๋‹ค.
5. Sonnet๋„ ์‹คํŒจํ•˜๋ฉด rule-based copy๋กœ fallbackํ•œ๋‹ค.

 

์—ฌ๊ธฐ์„œ ์‹คํŒจ๋Š” ์‚ฌ๋žŒ์ด ๊ฐ์œผ๋กœ ๋ฌธ์žฅ์ด ๋ณ„๋กœ๋„ค๋ผ๊ณ  ํŒ๋‹จํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค.

์„œ๋ฒ„๊ฐ€ ์ตœ์†Œํ•œ ์ด๋Ÿฐ ๊ธฐ์ค€์„ ๋ด์•ผ ํ•œ๋‹ค.

- JSON ํŒŒ์‹ฑ ๊ฐ€๋Šฅ ์—ฌ๋ถ€
- summary์™€ ์นด๋“œ ๋ฌธ์žฅ ์กด์žฌ ์—ฌ๋ถ€
- line1 / line2 ๊ธธ์ด ์ œํ•œ
- ๋‚ด๋ถ€ ์ฑ„์  ์šฉ์–ด ๋…ธ์ถœ ์—ฌ๋ถ€
- ํ•„์ˆ˜ ๊ณ ์ง€ ๋ฌธ๊ตฌ ์œ ์ง€ ์—ฌ๋ถ€
- ์ž…๋ ฅ์— ์—†๋˜ ์‚ฌ์‹ค์ด๋‚˜ ์ˆซ์ž ์ถ”๊ฐ€ ์—ฌ๋ถ€
- grade, rule_key, title ๊ฐ™์€ ์›๋ž˜ ๊ฒฐ๊ณผ ๋ณ€๊ฒฝ ์—ฌ๋ถ€

 

๋ชจ๋ธ์ด ์Šค์Šค๋กœ ์ž˜ ์ผ๋‹ค๊ณ  ๋งํ•˜๋Š” ๊ฒƒ์€ ๊ฒ€์ฆ์ด ์•„๋‹ˆ๋ผ๊ณ  ๋ณธ๋‹ค. ๊ฒ€์ฆ์€ ์šฐ๋ฆฌ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ํ•ด์•ผ ํ•œ๋‹ค.

 

๋‚ด๊ฐ€ ์ •๋ฆฌํ•œ ์ตœ์ ํ™” ์ˆœ์„œ

์ด๋ฒˆ ๋น„์šฉ ๋ฌธ์ œ๋ฅผ ๋ณด๋ฉฐ ์ •๋ฆฌํ•œ ์ˆœ์„œ๋Š” ์ด๋ ‡๋‹ค.

1. deprecated ๋ชจ๋ธ์€ retirement ์ „์— ๊ต์ฒดํ•œ๋‹ค.
2. ์ด๋ฏธ ๋ฐœํ–‰๋œ ๋™์ผ ์‚ฌ๋ฃŒ ์„ฑ์ ํ‘œ๋Š” DB์—์„œ ์žฌ์‚ฌ์šฉํ•œ๋‹ค.
3. ์ƒˆ ์„ฑ์ ํ‘œ๊ฐ€ ํ•„์š”ํ•˜๋ฉด ๊ณ ์ • ๊ฐ€์ด๋“œ์™€ ๋ชจ๋ฒ” ์˜ˆ์‹œ๋Š” Prompt Caching์„ ์ ์šฉํ•œ๋‹ค.
4. ์บ์‹œ TTL๊ณผ cache hit ๋น„์œจ์„ ๋กœ๊ทธ๋กœ ํ™•์ธํ•œ๋‹ค.
5. ๊ณ ์ • ํ”„๋กฌํ”„ํŠธ ๋ณ€๊ฒฝ์€ version์œผ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค.
6. Claude/OpenAI๋ณ„ usage ํ•„๋“œ๋ฅผ ๊ณตํ†ต ๋ฉ”ํŠธ๋ฆญ์œผ๋กœ ์ •๋ฆฌํ•œ๋‹ค.
7. ๊ฒ€์ฆ ๊ทœ์น™์„ ํ†ต๊ณผํ•˜๋Š” ๋ฒ”์œ„์—์„œ ์ €๋ ดํ•œ ๋ชจ๋ธ์„ ํ…Œ์ŠคํŠธํ•œ๋‹ค.
8. ์‹คํŒจํ•˜๊ฑฐ๋‚˜ ๊ณ ์œ„ํ—˜ ์ผ€์ด์Šค๋Š” ์ƒ์œ„ ๋ชจ๋ธ๋กœ ์Šน๊ฒฉํ•œ๋‹ค.
9. ๊ทธ๋ž˜๋„ ์‹คํŒจํ•˜๋ฉด rule-based copy๋กœ fallbackํ•œ๋‹ค.
10. ๋งˆ์ง€๋ง‰์œผ๋กœ ํ’ˆ์งˆ์„ ํ•ด์น˜์ง€ ์•Š๋Š” ๋ฒ”์œ„์—์„œ ํ”„๋กฌํ”„ํŠธ์™€ ์ถœ๋ ฅ ๊ตฐ๋”๋”๊ธฐ๋ฅผ ์ค„์ธ๋‹ค.

 

์ด๋•Œ ์ค‘์š”ํ•œ ๊ฒƒ์€ ์ˆœ์„œ๋‹ค.

 

deprecated ๋ชจ๋ธ ๊ต์ฒด๋Š” ์žฅ์•  ์˜ˆ๋ฐฉ์— ๊ฐ€๊น๊ณ , Prompt Caching์€ ๋ฐ˜๋ณต ์ž…๋ ฅ ๋น„์šฉ์„ ์ค„์ด๋Š” ์ผ์ด๋‹ค. ๋‘˜ ๋‹ค ํ•„์š”ํ•˜์ง€๋งŒ ์„œ๋กœ ํ•ด๊ฒฐํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋‹ค๋ฅด๋‹ค.

 

์ฒ˜์Œ๋ถ€ํ„ฐ ๊ฐ€์ด๋“œ๋ฅผ ์ค„์ด๋ฉด ์„ฑ์ ํ‘œ ํ’ˆ์งˆ์ด ํ”๋“ค๋ฆด ์ˆ˜ ์žˆ๋‹ค. ์ฒ˜์Œ๋ถ€ํ„ฐ ๋ชจ๋ธ์„ ๋‚ฎ์ถ”๋ฉด ํ•„์ˆ˜ ๊ณ ์ง€๋‚˜ ๋ฌธ์žฅ ์•ˆ์ •์„ฑ์ด ๊นจ์งˆ ์ˆ˜ ์žˆ๋‹ค. ๋ฐ˜๋ฉด Prompt Caching์€ ํ”„๋กฌํ”„ํŠธ ๋‚ด์šฉ์„ ์œ ์ง€ํ•œ ์ฑ„ ๋ฐ˜๋ณต ์ž…๋ ฅ ๋น„์šฉ์„ ์ค„์ธ๋‹ค.

 

๊ทธ๋ž˜์„œ ์ฒซ ๋ฒˆ์งธ ์•ˆ์ „ํ•œ ๋น„์šฉ ์ตœ์ ํ™”๋Š” ์บ์‹ฑ์ด์—ˆ๋‹ค.

 

๋‹ค๋งŒ ์บ์‹ฑ์€ ์˜๊ตฌ ๋ณด๊ด€์ด ์•„๋‹ˆ๋‹ค. 5๋ถ„ TTL์ด๋ฉด ์š”์ฒญ ๊ฐ„๊ฒฉ์ด 5๋ถ„์„ ๋„˜๋Š” ์ˆœ๊ฐ„ cache miss๊ฐ€ ๋‚  ์ˆ˜ ์žˆ๋‹ค. ์ด๋•Œ ๋น„์šฉ์ด ๋‹ค์‹œ ํŠ€๋Š” ๊ฒƒ์€ ๋ฒ„๊ทธ๊ฐ€ ์•„๋‹ˆ๋ผ ์บ์‹œ์˜ ์ˆ˜๋ช… ๋•Œ๋ฌธ์ด๋‹ค. ๊ทธ๋ž˜์„œ ์šด์˜์—์„œ๋Š” ์บ์‹ฑ์„ ์ผฐ๋‹ค๊ฐ€ ์•„๋‹ˆ๋ผ cache_read_input_tokens๊ฐ€ ๊พธ์ค€ํžˆ ์žกํžŒ๋‹ค๊นŒ์ง€ ํ™•์ธํ•ด์•ผ ํ–ˆ๋‹ค.

 

๊ฒฐ๋ก 

`$0.06`์€ ๋น„์ •์ƒ ๊ณผ๊ธˆ์ด ์•„๋‹ˆ์—ˆ๋‹ค.

 

์‚ฌ๋ฃŒ ์„ฑ์ ํ‘œ์˜ ํ’ˆ์งˆ์„ ๋งž์ถ”๊ธฐ ์œ„ํ•ด ๊ธด ๊ณ ์ • ๋ฌธ์„œ๋ฅผ ๋งค๋ฒˆ ๋„ฃ๊ณ  ์žˆ์—ˆ๊ณ , ๊ทธ ๋ฌธ์„œ๊ฐ€ ์บ์‹œ๋˜์ง€ ์•Š์•˜๊ณ , ์ถœ๋ ฅ๋„ 1์ฒœ ํ† ํฐ ์ •๋„ ์ƒ์„ฑ๋๊ธฐ ๋•Œ๋ฌธ์— ๋‚˜์˜จ ์ •์ƒ ๋น„์šฉ์ด์—ˆ๋‹ค.

 

๋™์‹œ์— deprecated ๋ชจ๋ธ์„ ๊ณ„์† ์“ฐ๊ณ  ์žˆ๋‹ค๋Š” ์šด์˜ ๋ฆฌ์Šคํฌ๋„ ๋ฐœ๊ฒฌํ–ˆ๋‹ค. ์ด๊ฑด ๋น„์šฉ ์ตœ์ ํ™”์™€๋Š” ๋ณ„๊ฐœ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•  ๋ฌธ์ œ์˜€๋‹ค. ๊ทธ๋ž˜์„œ ๊ธฐ๋ณธ ๋ชจ๋ธ์„ retirement ์ „์— `claude-sonnet-4-6`์œผ๋กœ ๋ฐ”๊พธ๋Š” ๊ฒƒ์ด ๋งž์•˜๋‹ค.

 

์ด๋ฒˆ์— ๋ฐ”๋กœ Haiku๋กœ ๋‚ฎ์ถ”๊ฑฐ๋‚˜ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ค„์ด์ง€ ์•Š์€ ์ด์œ ๋„ ์—ฌ๊ธฐ์— ์žˆ๋‹ค. ๋น„์šฉ์€ ์ค„์ผ ์ˆ˜ ์žˆ์–ด๋„, ์„ฑ์ ํ‘œ์˜ ์‹ ๋ขฐ๊ฐ€ ํ”๋“ค๋ฆฌ๋ฉด ์ œํ’ˆ ์ž…์žฅ์—์„œ๋Š” ๋” ํฐ ์†ํ•ด๋‹ค. ๋จผ์ € ๊ฐ™์€ ๊ฒฐ๊ณผ๋Š” ์žฌ์‚ฌ์šฉํ•˜๊ณ , ๋ฐ˜๋ณต๋˜๋Š” ๊ธฐ์ค€ ๋ฌธ์„œ๋Š” ์บ์‹œํ•˜๊ณ , cache read๊ฐ€ ์‹ค์ œ๋กœ ์žกํžˆ๋Š”์ง€ ๊ด€์ฐฐํ•˜๋Š” ์ชฝ์ด ๋” ์•ˆ์ „ํ–ˆ๋‹ค.

 

๊ทธ๋Ÿฌ๋‹ˆ LLM ๋น„์šฉ ์ตœ์ ํ™”๋Š” ๋” ์‹ผ ๋ชจ๋ธ์„ ๊ณ ๋ฅด๋Š” ์ผ๋กœ ์‹œ์ž‘ํ•˜๋ฉด ์•ˆ๋œ๋‹ค! ๋จผ์ € ๋ฌด์—‡์ด ๋งค๋ฒˆ ๋‹ค์‹œ ์ฝํžˆ๊ณ  ์žˆ๋Š”์ง€ ๋ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ชจ๋ธ lifecycle์ฒ˜๋Ÿผ ๊ณง ์žฅ์• ๊ฐ€ ๋  ์ˆ˜ ์žˆ๋Š” ์šด์˜ ๋ฆฌ์Šคํฌ๋„ ํ•จ๊ป˜ ๋ด์•ผ ํ•œ๋‹ค. ๊ทธ๋‹ค์Œ์— ๋ชจ๋ธ ๊ต์ฒด, ํ”„๋กฌํ”„ํŠธ ์ถ•์†Œ, fallback ์ „๋žต์„ ๊ฒ€์ฆ ๊ทœ์น™ ์œ„์—์„œ ๋‹ค๋ค„์•ผ ํ•œ๋‹ค.

 

๊ทธ ์ˆœ์„œ๋ฅผ ์ง€ํ‚ค๋ฉด ๋น„์šฉ์„ ์ค„์ด๋ฉด์„œ๋„ ์ œํ’ˆ ํ’ˆ์งˆ์„ ์ง€ํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.

 

์ฐธ๊ณ ๋ฌธ์„œ

 

 

3๋…„๊ฐ„ ํšŒ์‚ฌ์—์„œ ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ๋‚˜๋ฆ„ ๊ฒฝํ—˜์„ ์Œ“์•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ๋Œ์ด์ผœ๋ณด๋ฉด ๊ทธ ๊ฒฝํ—˜์€ ์ด๋ฏธ ์ž˜ ์งœ์ธ ๊ตฌ์กฐ ์•ˆ์—์„œ ์ฃผ์–ด์ง„ ์—ญํ• ๋งŒ ์ˆ˜ํ–‰ํ–ˆ์—ˆ๋˜ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.
๋””์ž์ด๋„ˆ์™€์˜ ํ˜‘์—… ๋ฐฉ์‹์€ ์ด๋ฏธ ์ •ํ˜•ํ™”๋˜์–ด ์žˆ์—ˆ๊ณ , ๋””์ž์ธ ์‹œ์Šคํ…œ๋„ ๊ตฌ์ถ•๋ผ ์žˆ์–ด์„œ ์‹œ์•ˆ์„ ๋ฐ›์•„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ๋งŒ ์ง‘์ค‘ํ•˜๋ฉด ๋๊ฑฐ๋“ ์š”.

์•Œ๋ฆฌ๋‹ˆ ์‹์‚ฌ ํ™”๋ฉด

 

 

์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ชจ๋“  ๊ฑธ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋งŒ๋“ค์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ๊ณผ์ •์—์„œ ๋งŽ์€ ๊ฒƒ๋“ค์„ ์ƒˆ๋กญ๊ฒŒ ๊นจ๋‹ฌ์•˜๊ณ , ํŠนํžˆ ๋””์ž์ด๋„ˆ์™€์˜ ํ˜‘์—…์—์„œ ์—ฌ๋Ÿฌ ๋ฐฐ์›€๋“ค์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค!

 

๊ฐ‘์ž๊ธฐ ๋ณด์ด๊ธฐ ์‹œ์ž‘ํ•œ ๊ฒƒ๋“ค

์ƒํƒœ์— ๋Œ€ํ•œ ์ •์˜

๋””์ž์ด๋„ˆ๋‹˜๊ณผ ๊ฐ™์€ "์ƒํƒœ"๋ผ๋Š” ๋‹จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ, ์‹ค์ œ๋กœ๋Š” ๋‹ค๋ฅธ ๋ฐฉ์‹์„ ์ƒ๊ฐํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  • ๋””์ž์ด๋„ˆ๋‹˜์ด ๋งํ•˜๋Š” ์ƒํƒœ๋Š” ์ฃผ๋กœ ์‹œ๊ฐ์ ์ธ ๋ณ€ํ™” — ์˜ˆ๋ฅผ ๋“ค์–ด ์ƒ‰๊น”, ํ…Œ๋‘๋ฆฌ, ์•„์ด์ฝ˜ ๊ฐ™์€ UI ์š”์†Œ๋“ค์ด์—ˆ๊ณ ,
  • ์ œ๊ฐ€ ๋งํ•˜๋Š” ์ƒํƒœ๋Š” ๋ฐ์ดํ„ฐ์˜ ์ƒํƒœ ํ˜น์€ ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ — ์˜ˆ๋ฅผ ๋“ค์–ด ์—๋Ÿฌ ์—ฌ๋ถ€, ์ž…๋ ฅ ์™„๋ฃŒ ์—ฌ๋ถ€, ๋„คํŠธ์›Œํฌ ์ƒํƒœ ๊ฐ™์€ ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜์—์„œ ์ž์„ธํžˆ ์„ค๋ช… ํ•˜๊ฒ ์ง€๋งŒ ๊ฐœ๋ฐœํ–ˆ๋˜ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ๋กœ ์„ค๋ช…ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์ฒ˜์Œ ์‹œ์•ˆ์—๋Š” "๊ธฐ๋ณธ, ํฌ์ปค์Šค, ์—๋Ÿฌ, ๋น„ํ™œ์„ฑํ™”" ์ •๋„๋งŒ ์ •์˜๋ผ ์žˆ์—ˆ์–ด์š”.

๊ทธ๋Ÿฐ๋ฐ ์‹ค์ œ๋กœ ๊ตฌํ˜„์— ๋“ค์–ด๊ฐ€๋ฉด์„œ ๊ณ ๋ฏผ์ด ์ปค์กŒ์Šต๋‹ˆ๋‹ค.

 

  • ์ด๋ฉ”์ผ ํ•„๋“œ์—์„œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋Š” ๋™์‹œ์— ํฌ์ปค์Šค๊ฐ€ ๊ฐ€๋ฉด ์–ด๋–ป๊ฒŒ ๋ณด์—ฌ์ค˜์•ผ ํ•˜์ง€?
  • ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด์„œ ๋™์‹œ์— ๋กœ๋”ฉ ์ƒํƒœ๋„ ์œ ์ง€ํ•ด์•ผ ํ•œ๋‹ค๋ฉด?
  • ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•„๋“œ์—์„œ๋Š” ํ˜ธ๋ฒ„ + ์—๋Ÿฌ ์ƒํƒœ๊ฐ€ ํ•จ๊ป˜ ์˜ค๋ฉด ์–ด๋–ค UI์—ฌ์•ผ ํ• ๊นŒ?

์ด๋ ‡๊ฒŒ ์ƒํƒœ ์กฐํ•ฉ ๋งŽ์•„์ง€๊ธฐ ์‹œ์ž‘ํ–ˆ์–ด์š”.
ํšŒ์‚ฌ๋Š” ์ด๋ฏธ ๋””์ž์ธ ์‹œ์Šคํ…œ๊ณผ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ž˜ ์ •๋ฆฌ๋ผ ์žˆ์–ด์„œ, ์ด๋Ÿฐ ์กฐํ•ฉ๋“ค์€ ์‹ ๊ฒฝ ์“ธ ํ•„์š”๊ฐ€ ๊ฑฐ์˜ ์—†์—ˆ๊ฑฐ๋“ ์š”.
ํ•˜์ง€๋งŒ ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ชจ๋“  ๊ฒƒ์„ ์ง์ ‘ ๊ธฐํšํ•˜๊ณ , ๋””์ž์ด๋„ˆ๋‹˜๊ณผ ๋…ผ์˜ํ•˜๊ณ , ๊ตฌํ˜„๊นŒ์ง€ ํ•ด๋ณด๋ฉด์„œ "์ƒํƒœ"๋ฅผ ์ •์˜ํ•œ๋‹ค๋Š”๊ฒŒ ์–ผ๋งˆ๋‚˜ ๋ณต์žกํ•˜๊ณ  ์„ฌ์„ธํ•œ ์ผ์ธ์ง€ ์ฒด๊ฐํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

 

 

๊ฐ€์žฅ ์ฒ˜์Œ ์‹œ์ž‘๋œ ์†Œํ†ต์ด ์‹œ์ž‘๋œ ํ™”๋ฉด

๋กœ๊ทธ์ธ ํ™”๋ฉด

 

ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐ ๋””์ž์ด๋„ˆ๋‹˜์ด ๋กœ๊ทธ์ธ ํ™”๋ฉด ์‹œ์•ˆ์„ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค. ๊น”๋”ํ•˜๊ณ  ์ง๊ด€์ ์ธ ๋””์ž์ธ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ตฌํ˜„์„ ์‹œ์ž‘ํ•˜๋ฉด์„œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ช‡ ๊ฐ€์ง€ ๊ถ๊ธˆํ•œ ์ ๋“ค์ด ์ƒ๊ฒผ์–ด์š”.

 

"๋กœ๊ทธ์ธ ์‹คํŒจํ–ˆ์„ ๋•Œ๋Š” ์–ด๋–ป๊ฒŒ ํ‘œ์‹œํ• ๊นŒ์š”?"

 

๋””์ž์ด๋„ˆ๋‹˜๋„ "์•„, ๊ทธ๋Ÿฌ๊ฒŒ์š”. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ํ•„์š”ํ•˜๊ฒ ๋„ค์š”"๋ผ๊ณ  ํ•ด์ฃผ์…จ๊ณ  ํ•จ๊ป˜ ๊ณ ๋ฏผํ•ด๋ดค์Šต๋‹ˆ๋‹ค.

 

 

๋กœ๊ทธ์ธ ์—๋Ÿฌ ํ™”๋ฉด ์˜ˆ์‹œ

 

์˜ˆ์‹œ ์ฝ”๋“œ๋Š” ์‹ค์ œ ๊ตฌํ˜„ ์ฝ”๋“œ์™€ ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

type LoginErrorType = 'INVALID_CREDENTIALS' | 'NETWORK_ERROR' | 'UNKNOWN_ERROR';

interface LoginError {
  type: LoginErrorType;
  message: string;
  field?: 'email' | 'password'; // ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ ์‚ฌ์šฉ
}

interface LoginState {
  status: 'idle' | 'loading' | 'success' | 'error';
  error: LoginError | null;
}

// ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ƒ์ˆ˜ ๋ถ„๋ฆฌ
const LOGIN_ERROR_MESSAGES = {
  INVALID_CREDENTIALS: '์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.',
  NETWORK_ERROR: '๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
  UNKNOWN_ERROR: '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.',
} as const;

// ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์šฉ ์ƒ์„ธ ๋ฉ”์‹œ์ง€
const DEV_ERROR_MESSAGES = {
  INVALID_EMAIL: '์•—! ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ์ด๊ฑฐ๋‚˜ ์ž˜๋ชป ์ž…๋ ฅํ–ˆ์–ด์š”.',
  INVALID_PASSWORD: '์•—! ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ‹€๋ ธ์–ด์š”. ๋‹ค์‹œ ํ•œ ๋ฒˆ ํ™•์ธํ•ด์ฃผ์„ธ์š”.',
} as const;

const useLogin = () => {
  const [state, setState] = useState<LoginState>({
    status: 'idle',
    error: null,
  });

  // ํ™˜๊ฒฝ๋ณ„ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
  const getErrorMessage = (type: LoginErrorType, field?: 'email' | 'password'): LoginError => {
    const isDevelopment = process.env.NODE_ENV === 'development';
    
    if (isDevelopment && field) {
      return {
        type,
        message: field === 'email' ? DEV_ERROR_MESSAGES.INVALID_EMAIL : DEV_ERROR_MESSAGES.INVALID_PASSWORD,
        field,
      };
    }

    return {
      type,
      message: LOGIN_ERROR_MESSAGES[type],
    };
  };

  const login = async (email: string, password: string) => {
    setState({ status: 'loading', error: null });
    
    try {
      const user = await authService.findUserByEmail(email);
      if (!user) {
        setState({
          status: 'error',
          error: getErrorMessage('INVALID_CREDENTIALS', 'email'),
        });
        return;
      }

      const isValidPassword = await authService.validatePassword(email, password);
      if (!isValidPassword) {
        setState({
          status: 'error',
          error: getErrorMessage('INVALID_CREDENTIALS', 'password'),
        });
        return;
      }

      await authService.setSession(user);
      setState({ status: 'success', error: null });
    } catch (error) {
      setState({
        status: 'error',
        error: getErrorMessage('NETWORK_ERROR'),
      });
    }
  };

  return {
    ...state,
    login,
    isLoading: state.status === 'loading',
    isSuccess: state.status === 'success',
    isError: state.status === 'error',
  };
};

 

๋””์ž์ด๋„ˆ๋‹˜์€ ๋กœ๊ทธ์ธ ํ™”๋ฉด์—์„œ ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋„์šธ์ง€ ๊ณ ๋ฏผํ•˜๊ณ  ๊ณ„์…จ์–ด์š”.

 

ํ”ํžˆ ๋ณผ ์ˆ˜ ์žˆ๋Š” "์•„์ด๋””๋‚˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"์™€ ๊ฐ™์€ ๋ชจํ˜ธํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋Œ€์‹ , "์ด๋ฉ”์ผ์ด ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค" ๋˜๋Š” "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค"์ฒ˜๋Ÿผ ๊ตฌ์ฒด์ ์œผ๋กœ ์•Œ๋ ค์ฃผ๋Š” ๊ฒŒ ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ ๋” ์นœ์ ˆํ•œ UX๊ฐ€ ์•„๋‹๊นŒ์— ๋Œ€ํ•ด ๋…ผ์˜ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ์™œ ๋งŽ์€ ์›น์‚ฌ์ดํŠธ๋“ค์ด ๋กœ๊ทธ์ธ ์‹คํŒจ ์‹œ "Incorrect username or password" ์ฒ˜๋Ÿผ ๋ชจํ˜ธํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฑธ๊นŒ์š”? ๋ผ๋Š” ์˜๋ฌธ์— ๋„๋‹ฌํ–ˆ์–ด์š”.

 

๊ด€๋ จํ•ด์„œ ์ฐพ์•„๋ณธ ๊ฒฐ๊ณผ, tarunbatra.com ๋ธ”๋กœ๊ทธ์—์„œ๋Š” ๋ณด์•ˆ์ ์ธ ์ด์œ ๋ฅผ ๊ฐ•์กฐํ–ˆ์Šต๋‹ˆ๋‹ค.

๋กœ๊ทธ์ธ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ์ œ๊ณตํ•  ๊ฒฝ์šฐ, ์•…์˜์ ์ธ ์‚ฌ์šฉ์ž๊ฐ€ ํ•ด๋‹น ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์œ ํšจํ•œ ๊ณ„์ •์„ ์ถ”์ธกํ•˜๊ฑฐ๋‚˜ ๋ฐ˜๋ณต์ ์œผ๋กœ ์กฐํ•ฉ์„ ์‹œ๋„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

์˜ˆ๋ฅผ ๋“ค์–ด, "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค"๋ผ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋ฉด ํ•ด๋‹น ์‚ฌ์šฉ์ž๋ช…์€ ์œ ํšจํ•˜๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ๊ณต๊ฒฉ์ž์—๊ฒŒ ๋‹จ์„œ๋ฅผ ์ฃผ๋Š” ์…ˆ์ด์ฃ . ๋ฐ˜๋ฉด "์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"์ฒ˜๋Ÿผ ๋ชจํ˜ธํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋ฉด ๊ณต๊ฒฉ์ž๋Š” ์–ด๋–ค ๋ถ€๋ถ„์ด ์ž˜๋ชป๋˜์—ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์•ˆ์ƒ ๋” ์•ˆ์ „ํ•ด์ง‘๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ๊ทธ ๋‹น์‹œ์—” ๋‚ด๋ถ€ ๋ฒ ํƒ€ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ์˜ˆ์ •์ด์—ˆ๋˜ ๋™๋ฌผ๋ณ‘์› ๊ด€๊ณ„์ž๋ถ„๋“ค์˜ ์š”์ฒญ์ด ์žˆ์—ˆ๊ณ , ํ…Œ์ŠคํŠธ ํŽธ์˜๋ฅผ ์œ„ํ•ด ๋กœ๊ทธ์ธ ์˜ค๋ฅ˜ ์‹œ ์–ด๋–ค ํ•„๋“œ๊ฐ€ ์ž˜๋ชป๋˜์—ˆ๋Š”์ง€๋ฅผ ๋ถ„๋ฆฌํ•ด์„œ ๋ณด์—ฌ์ฃผ๋Š” UX๋ฅผ ์ฑ„ํƒํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋–ค ํ•„๋“œ์—์„œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ๋Š”์ง€ ์ง๊ด€์ ์œผ๋กœ ์•Œ ์ˆ˜ ์žˆ๋„๋ก, ํ•ด๋‹น ํ•„๋“œ ๋ฐ”๋กœ ์•„๋ž˜์— ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋„์šฐ๋Š” ๋ฐฉ์‹์œผ๋กœ ํ˜‘์˜ํ–ˆ๊ณ , ์‹ค์ œ ์‚ฌ์šฉ์ž ํ…Œ์ŠคํŠธ์—์„œ๋„ ๊ธ์ •์ ์ธ ๋ฐ˜์‘์„ ์–ป์„ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค!

 

 

ํŽธํ•˜๊ฒŒ ์“ฐ๋˜ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ

์•Œ๋ฆฌ๋‹ˆ์—์„œ๋Š” ๋‹ค์–‘ํ•œ ์ž…๋ ฅ ํ•„๋“œ๊ฐ€ ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ „ํ™”๋ฒˆํ˜ธ, ์‚ฌ๋ฃŒ๋ช…, ๋ธŒ๋žœ๋“œ๋ช… ๋“ฑ๋“ฑ...

 

์ฒ˜์Œ์— ๋””์ž์ด๋„ˆ๋‹˜์ด ๋งŒ๋“  ํ…์ŠคํŠธํ•„๋“œ๋Š” ๊ธฐ๋ณธ์ ์ด ๋ชจ์Šต์ด์—ˆ๋Š”๋ฐ, ๊ตฌํ˜„ํ•˜๋ฉด์„œ ํ•จ๊ป˜ ๋‹ค์–‘ํ•œ ์ƒํƒœ๋ฅผ ๊ณ ๋ฏผํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

"๋น„๋ฐ€๋ฒˆํ˜ธ ํ•„๋“œ์—์„œ ์ž…๋ ฅ ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๋ˆˆ ๋ชจ์–‘ ์•„์ด์ฝ˜์ด ์žˆ์œผ๋ฉด ์ข‹๊ฒ ์–ด์š”."

 

"๋งž์•„์š”! ๊ทธ๋ฆฌ๊ณ  ์‚ฌ๋ฃŒ ๋“ฑ๋กํ•  ๋•Œ ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ์€ ๋ผ๋ฒจ์— ๋ณ„ํ‘œ(*)๋ฅผ ํ‘œ์‹œํ•˜๋ฉด ์–ด๋–จ๊นŒ์š”?"

 

์ด์ฒ˜๋Ÿผ ํ•˜๋‚˜ํ•˜๋‚˜ ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋˜๋ฉฐ ํ•„์š”ํ•œ ์ƒํƒœ์˜ ์ˆ˜๋Š” ๊ธฐํ•˜๊ธ‰์ˆ˜์ ์œผ๋กœ ๋Š˜์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค..

์˜ˆ๋ฅผ๋“ค์–ด ์•„๋ž˜์ฒ˜๋Ÿผ์š”. (์‹ค์ œ ์•Œ๋ฆฌ๋‹ˆ ๊ตฌํ˜„๊ณผ ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค)

  • ๊ธฐ๋ณธ ์ƒํƒœ 4๊ฐ€์ง€
  • ํ˜ธ๋ฒ„ ์ƒํƒœ ์ถ”๊ฐ€ → 4 × 2 = 8
  • ์ฝ๊ธฐ ์ „์šฉ(read-only) ๊ณ ๋ ค → 8 × 2 = 16
  • ํ•„์ˆ˜/์„ ํƒ ํ‘œ์‹œ → 16 × 2 = ์ด 32๊ฐ€์ง€ ์ƒํƒœ ์กฐํ•ฉ

๊ทธ๋Ÿฐ๋ฐ ๊ฐ€์žฅ ์–ด๋ ค์› ๋˜ ๊ฑด ์ด๋Ÿฐ ์กฐํ•ฉ๋“ค์ด ๋‹จ์ˆœํžˆ ์ถ”๊ฐ€๋งŒ ๋˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ์„œ๋กœ ์ถฉ๋Œํ•˜๊ฑฐ๋‚˜ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ์žˆ๋‹ค๋Š” ์ ์ด์—ˆ์–ด์š”.
์˜ˆ๋ฅผ ๋“ค์–ด disabled์™€ readOnly๋Š” ๋™์‹œ์— true์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” disabled๊ฐ€ readOnly๋ฅผ ๋ฌด์‹œํ•ด์š”.

์ฆ‰, disabled > readOnly์ธ ๊ฑฐ์ฃ .
์ด๋Ÿฐ ์ƒํ˜ธ ์˜์กด์„ฑ๊ณผ ์šฐ์„ ์ˆœ์œ„๋Š” ์ฒ˜์Œ์—” ์ „ํ˜€ ์ธ์ง€ํ•˜์ง€ ๋ชปํ–ˆ๊ณ , ๋””์ž์ด๋„ˆ๋‹˜๊ณผ ํ•จ๊ป˜ ํ•œ์ฐธ์„ ๋…ผ์˜ํ•˜๋ฉด์„œ ๊ทœ์น™์„ ์ •๋ฆฌํ•ด๋‚˜๊ฐ”์Šต๋‹ˆ๋‹ค:

  • disabled > readOnly (์šฐ์„ ์ˆœ์œ„ ์กด์žฌ)
  • error์™€ success๋Š” ์ƒํ˜ธ ๋ฐฐํƒ€์ 
  • focus๋Š” ์‚ฌ์šฉ์ž์˜ ์ง์ ‘์ ์ธ ์ƒํ˜ธ์ž‘์šฉ์œผ๋กœ๋งŒ ๋ฐœ์ƒ

์ธํ’‹ ์˜ˆ์‹œ ํ™”๋ฉด

 

์˜ˆ์‹œ ์ฝ”๋“œ๋Š” ์‹ค์ œ ๊ตฌํ˜„ ์ฝ”๋“œ์™€ ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

import { forwardRef, useState, useCallback, useMemo, useId } from 'react';

// ๋””์ž์ธ ํ† ํฐ
const INPUT_COLORS = {
  default: {
    border: '#e1e5e9',
    background: '#ffffff',
  },
  focused: {
    border: '#00b896', // ๋ธŒ๋žœ๋“œ ์ปฌ๋Ÿฌ
    background: '#ffffff',
  },
  error: {
    border: '#ff6b6b', // ๋นจ๊ฐ„์ƒ‰
    background: '#ffffff',
  },
  disabled: {
    border: '#f4f4f4',
    background: '#f9f9f9',
  },
} as const;

// ์ƒํƒœ ์šฐ์„ ์ˆœ์œ„ ๋ช…์‹œ์  ์ •์˜
const INPUT_STATE_PRIORITY = {
  disabled: 4,
  error: 3,
  focused: 2,
  default: 1,
} as const;

type InputState = keyof typeof INPUT_STATE_PRIORITY;

// Props ํƒ€์ž… ๋ถ„๋ฆฌ
interface BaseInputProps {
  label?: string;
  required?: boolean;
  error?: string;
  disabled?: boolean;
  placeholder?: string;
  value?: string;
  onChange?: (value: string) => void;
  onFocus?: () => void;
  onBlur?: () => void;
}

interface TextInputProps extends BaseInputProps {
  type?: 'text' | 'email' | 'tel';
  leftIcon?: React.ReactNode;
  rightButton?: React.ReactNode;
}

interface PasswordInputProps extends BaseInputProps {
  type: 'password';
  showPasswordToggle?: boolean;
}

type InputProps = TextInputProps | PasswordInputProps;

// ์ƒํƒœ ๊ณ„์‚ฐ ๋กœ์ง ๋ถ„๋ฆฌ
const getInputState = (isFocused: boolean, error?: string, disabled?: boolean): InputState => {
  if (disabled) return 'disabled';
  if (error) return 'error';
  if (isFocused) return 'focused';
  return 'default';
};

const getInputStyles = (state: InputState) => {
  const colors = INPUT_COLORS[state];
  return {
    borderColor: colors.border,
    backgroundColor: colors.background,
  };
};

const TextInput = forwardRef<HTMLInputElement, InputProps>(({
  label,
  required = false,
  type = 'text',
  error,
  disabled = false,
  placeholder,
  value,
  onChange,
  onFocus,
  onBlur,
  ...props
}, ref) => {
  const [showPassword, setShowPassword] = useState(false);
  const [isFocused, setIsFocused] = useState(false);
  
  const inputId = useId();
  const errorId = useId();
  
  const inputType = useMemo(() => {
    if (type === 'password' && !showPassword) return 'password';
    return type === 'password' ? 'text' : type;
  }, [type, showPassword]);

  const currentState = useMemo(() => 
    getInputState(isFocused, error, disabled), 
    [isFocused, error, disabled]
  );

  const inputStyles = useMemo(() => 
    getInputStyles(currentState), 
    [currentState]
  );

  // ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋“ค
  const handleFocus = useCallback(() => {
    if (disabled) return;
    setIsFocused(true);
    onFocus?.();
  }, [disabled, onFocus]);

  const handleBlur = useCallback(() => {
    setIsFocused(false);
    onBlur?.();
  }, [onBlur]);

  const togglePasswordVisibility = useCallback(() => {
    setShowPassword(prev => !prev);
  }, []);

  const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    onChange?.(e.target.value);
  }, [onChange]);

  // ํŒจ์Šค์›Œ๋“œ ํ† ๊ธ€ ๋ฒ„ํŠผ
  const passwordToggleButton = type === 'password' && (
    <button
      type="button"
      onClick={togglePasswordVisibility}
      className="password-toggle"
      aria-label={showPassword ? '๋น„๋ฐ€๋ฒˆํ˜ธ ์ˆจ๊ธฐ๊ธฐ' : '๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณด๊ธฐ'}
      disabled={disabled}
      style={{ 
        opacity: disabled ? 0.5 : 1,
        cursor: disabled ? 'not-allowed' : 'pointer'
      }}
    >
      {showPassword ? '๐Ÿ™ˆ' : '๐Ÿ‘๏ธ'}
    </button>
  );

  return (
    <div className="text-input">
      {label && (
        <label htmlFor={inputId} className="input-label">
          {label}
          {required && (
            <span className="required-indicator" aria-label="ํ•„์ˆ˜" style={{ color: '#ff6b6b' }}>
              *
            </span>
          )}
        </label>
      )}
      
      <div 
        className={`input-container input-container--${currentState}`}
        style={{
          ...inputStyles,
          border: `1px solid ${inputStyles.borderColor}`,
          borderRadius: '8px',
          padding: '12px',
          display: 'flex',
          alignItems: 'center',
          gap: '8px',
        }}
      >
        {'leftIcon' in props && props.leftIcon && (
          <span className="input-icon" aria-hidden="true">
            {props.leftIcon}
          </span>
        )}
        
        <input
          ref={ref}
          id={inputId}
          type={inputType}
          disabled={disabled}
          placeholder={placeholder}
          value={value}
          onChange={handleChange}
          onFocus={handleFocus}
          onBlur={handleBlur}
          aria-describedby={error ? errorId : undefined}
          aria-invalid={!!error}
          style={{
            flex: 1,
            border: 'none',
            outline: 'none',
            backgroundColor: 'transparent',
            fontSize: '16px',
          }}
        />
        
        {passwordToggleButton}
        
        {'rightButton' in props && props.rightButton}
      </div>
      
      {error && (
        <span 
          id={errorId} 
          className="error-message" 
          role="alert"
          style={{
            color: '#ff6b6b',
            fontSize: '14px',
            marginTop: '4px',
            display: 'block',
          }}
        >
          {error}
        </span>
      )}
    </div>
  );
});

TextInput.displayName = 'TextInput';

// ์‚ฌ์šฉ ์˜ˆ์‹œ
const LoginForm = () => {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
  });
  
  const { login, isLoading, error, status } = useLogin();
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await login(formData.email, formData.password);
  };
  
  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '400px', margin: '0 auto' }}>
      <TextInput
        label="์ด๋ฉ”์ผ"
        type="email"
        required
        value={formData.email}
        onChange={(email) => setFormData(prev => ({ ...prev, email }))}
        error={error?.field === 'email' ? error.message : undefined}
        placeholder="์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
        disabled={isLoading}
      />
      
      <div style={{ marginTop: '16px' }}>
        <TextInput
          label="๋น„๋ฐ€๋ฒˆํ˜ธ"
          type="password"
          required
          value={formData.password}
          onChange={(password) => setFormData(prev => ({ ...prev, password }))}
          error={error?.field === 'password' ? error.message : undefined}
          placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
          disabled={isLoading}
          showPasswordToggle
        />
      </div>
      
      <button 
        type="submit" 
        disabled={isLoading}
        style={{
          width: '100%',
          padding: '12px',
          marginTop: '24px',
          backgroundColor: isLoading ? '#cccccc' : '#00b896',
          color: 'white',
          border: 'none',
          borderRadius: '8px',
          fontSize: '16px',
          cursor: isLoading ? 'not-allowed' : 'pointer',
        }}
      >
        {isLoading ? '๋กœ๊ทธ์ธ ์ค‘...' : '๋กœ๊ทธ์ธ'}
      </button>
      
      {error && !error.field && (
        <div style={{ 
          marginTop: '16px', 
          padding: '12px', 
          backgroundColor: '#ffebee',
          borderRadius: '8px',
          color: '#ff6b6b'
        }}>
          {error.message}
        </div>
      )}
    </form>
  );
};

 

"๋น„ํ™œ์„ฑํ™” ์ƒํƒœ์ผ ๋•Œ๋Š” ํšŒ์ƒ‰์œผ๋กœ, ์—๋Ÿฌ ์ƒํƒœ์ผ ๋•Œ๋Š” ๋นจ๊ฐ„์ƒ‰ ํ…Œ๋‘๋ฆฌ๋กœ ํ‰๋ฒ”ํ•˜๊ฒŒ ๊ฐ€๊ณ  ํฌ์ปค์Šค ์ƒํƒœ์—์„œ๋Š” ๋ธŒ๋žœ๋“œ ์ปฌ๋Ÿฌ์ธ ์ดˆ๋ก์ƒ‰์œผ๋กœ ๊ฐ•์กฐํ•˜๋ฉด ์ข‹๊ฒ ์–ด์š”"๋ผ๋Š” ์˜๊ฒฌ์œผ๋กœ ์ผ์น˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

 

์•Œ๋ ˆ๋ฅด๊ธฐ ๊ธฐ๋ก์˜ ์‹œ๊ฐํ™”

์•Œ๋ฆฌ๋‹ˆ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ˜๋ ค๊ฒฌ์˜ ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ˜์‘์„ ๊ธฐ๋กํ•˜๊ณ  ๋ถ„์„ํ•˜๋Š” ๊ฑด๋ฐ, ์ด ๋ถ€๋ถ„์—์„œ ์ •๋ง ๋งŽ์€ ๋Œ€ํ™”๊ฐ€ ์˜ค๊ฐ”์Šต๋‹ˆ๋‹ค.

์‹œ๊ฐํ™” ์˜ˆ์‹œ ํ™”๋ฉด

 

 

ํ˜„์žฌ ๊ตฌํ˜„๋œ ์ƒํƒœ์™€๋Š” ๋‹ค๋ฅด์ง€๋งŒ ์•„๋ž˜์™€๊ฐ™์€ ๊ณ ๋ฏผ์„ ํ–ˆ์—ˆ์–ด์š”.

 

"์•Œ๋ ˆ๋ฅด๊ธฐ ์ •๋„๋ฅผ ์–ด๋–ป๊ฒŒ ํ‘œํ˜„ํ•˜๋ฉด ์ข‹์„๊นŒ์š”?"

 

"์ƒ‰๊น”๋กœ ๊ตฌ๋ถ„ํ•˜๋ฉด ์–ด๋–จ๊นŒ์š”? ๋นจ๊ฐ„์ƒ‰์€ ์‹ฌํ•จ, ์ฃผํ™ฉ์ƒ‰์€ ๋ณดํ†ต, ์ดˆ๋ก์ƒ‰์€ ๋‚ฎ์Œ?"

 

"์ƒ‰์•ฝ์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž๋„ ๊ณ ๋ คํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์•„์š”. ์ƒ‰๊น”๊ณผ ํ•จ๊ป˜ ํ…์ŠคํŠธ๋‚˜ ์•„์ด์ฝ˜๋„ ๋„ฃ์œผ๋ฉด ์–ด๋–จ๊นŒ์š”?"

 

์˜ˆ์‹œ ์ฝ”๋“œ๋Š” ์‹ค์ œ ๊ตฌํ˜„ ์ฝ”๋“œ์™€ ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

// ๋””์ž์ธ ํ† ํฐ ์ •์˜
const COLORS = {
  success: {
    main: '#00b896',
    background: '#e6f8f5',
    light: '#b3f0e6',
  },
  warning: {
    main: '#ffa726',
    background: '#fff3e0',
    light: '#ffd699',
  },
  error: {
    main: '#ff6b6b',
    background: '#ffebee',
    light: '#ffb3b3',
  },
  gray: {
    600: '#666666',
    500: '#999999',
    100: '#f5f5f5',
  },
} as const;

// ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ ˆ๋ฒจ ํƒ€์ž…
type AllergyLevel = 'low' | 'medium' | 'high';

// ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ์ดํ„ฐ ์ธํ„ฐํŽ˜์ด์Šค
interface AllergyData {
  level: AllergyLevel;
  percentage: number;
  ingredients: string[];
  timestamp?: Date;
}

// ์•„์ด์ฝ˜ ์ปดํฌ๋„ŒํŠธ๋“ค (์‹ค์ œ๋กœ๋Š” ์•„์ด์ฝ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ)
const CheckCircleIcon = ({ size = 16 }: { size?: number }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
    <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
  </svg>
);

const AlertCircleIcon = ({ size = 16 }: { size?: number }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
    <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
  </svg>
);

const XCircleIcon = ({ size = 16 }: { size?: number }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
    <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11H7v-2h10v2z"/>
  </svg>
);

// ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ ˆ๋ฒจ ์„ค์ •
interface AllergyLevelConfig {
  color: string;
  backgroundColor: string;
  text: string;
  icon: React.ReactNode;
  severity: number;
  description: string; // ์ ‘๊ทผ์„ฑ์„ ์œ„ํ•œ ์„ค๋ช… ์ถ”๊ฐ€
}

const ALLERGY_LEVEL_CONFIG: Record<AllergyLevel, AllergyLevelConfig> = {
  low: {
    color: COLORS.success.main,
    backgroundColor: COLORS.success.background,
    text: '์•Œ๋ ˆ๋ฅด๊ธฐ ๋‚ฎ์Œ',
    icon: <CheckCircleIcon size={16} />,
    severity: 1,
    description: '์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ˜์‘์ด ๋‚ฎ์€ ์ˆ˜์ค€์ž…๋‹ˆ๋‹ค',
  },
  medium: {
    color: COLORS.warning.main,
    backgroundColor: COLORS.warning.background,
    text: '์•Œ๋ ˆ๋ฅด๊ธฐ ๋ณดํ†ต',
    icon: <AlertCircleIcon size={16} />,
    severity: 2,
    description: '์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ˜์‘์ด ๋ณดํ†ต ์ˆ˜์ค€์ž…๋‹ˆ๋‹ค',
  },
  high: {
    color: COLORS.error.main,
    backgroundColor: COLORS.error.background,
    text: '์•Œ๋ ˆ๋ฅด๊ธฐ ๋†’์Œ',
    icon: <XCircleIcon size={16} />,
    severity: 3,
    description: '์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ˜์‘์ด ๋†’์€ ์ˆ˜์ค€์ž…๋‹ˆ๋‹ค',
  },
} as const;

// Props ์ธํ„ฐํŽ˜์ด์Šค
interface AllergyItemProps {
  level: AllergyLevel;
  percentage: number;
  ingredients?: string[];
  showDetails?: boolean;
  className?: string;
  onToggleDetails?: () => void;
}

// ํผ์„ผํ‹ฐ์ง€ ํฌ๋งทํŒ… ํ•จ์ˆ˜
const formatPercentage = (value: number): string => {
  return `${Math.round(value)}%`;
};

// ๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ
export const AllergyItem: React.FC<AllergyItemProps> = ({
  level,
  percentage,
  ingredients = [],
  showDetails = false,
  className = '',
  onToggleDetails,
}) => {
  const config = ALLERGY_LEVEL_CONFIG[level];
  
  return (
    <div
      className={`allergy-item ${className}`}
      style={{
        backgroundColor: config.backgroundColor,
        border: `1px solid ${config.color}20`, // 20% ํˆฌ๋ช…๋„
        borderRadius: '12px',
        padding: '16px',
        marginBottom: '8px',
        transition: 'all 0.2s ease', // ๋ถ€๋“œ๋Ÿฌ์šด ์ „ํ™˜
      }}
      role="article"
      aria-label={`${config.description}, ${formatPercentage(percentage)}`}
    >
      <div 
        className="allergy-header"
        style={{
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
          marginBottom: showDetails && ingredients.length > 0 ? '12px' : '0',
        }}
      >
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span 
            className="allergy-icon" 
            style={{ color: config.color, display: 'flex', alignItems: 'center' }}
            aria-hidden="true"
          >
            {config.icon}
          </span>
          <span 
            className="allergy-text" 
            style={{ 
              color: config.color, 
              fontWeight: '600',
              fontSize: '14px',
            }}
          >
            {config.text}
          </span>
        </div>
        
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span 
            className="allergy-percentage"
            style={{
              fontWeight: '700',
              fontSize: '16px',
              color: config.color,
            }}
          >
            {formatPercentage(percentage)}
          </span>
          
          {ingredients.length > 0 && (
            <button
              onClick={onToggleDetails}
              style={{
                background: 'none',
                border: 'none',
                cursor: 'pointer',
                padding: '4px',
                borderRadius: '4px',
                color: config.color,
                fontSize: '12px',
                transition: 'background-color 0.2s ease',
              }}
              aria-expanded={showDetails}
              aria-label={showDetails ? '์„ธ๋ถ€์‚ฌํ•ญ ์ˆจ๊ธฐ๊ธฐ' : '์„ธ๋ถ€์‚ฌํ•ญ ๋ณด๊ธฐ'}
            >
              {showDetails ? 'โ–ฒ' : 'โ–ผ'}
            </button>
          )}
        </div>
      </div>

      {/* ์ƒ‰์•ฝ ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ์ถ”๊ฐ€ ์‹œ๊ฐ์  ์š”์†Œ */}
      <div 
        style={{
          width: '100%',
          height: '4px',
          backgroundColor: `${config.color}30`,
          borderRadius: '2px',
          marginBottom: showDetails && ingredients.length > 0 ? '12px' : '0',
          overflow: 'hidden',
        }}
      >
        <div
          style={{
            width: `${percentage}%`,
            height: '100%',
            backgroundColor: config.color,
            borderRadius: '2px',
            transition: 'width 0.3s ease',
          }}
          role="progressbar"
          aria-valuenow={percentage}
          aria-valuemin={0}
          aria-valuemax={100}
          aria-label={`์•Œ๋ ˆ๋ฅด๊ธฐ ์ˆ˜์ค€ ${formatPercentage(percentage)}`}
        />
      </div>

      {showDetails && ingredients.length > 0 && (
        <div className="allergy-details">
          <h4 
            style={{ 
              margin: '0 0 8px 0',
              fontSize: '12px',
              color: COLORS.gray[600],
              fontWeight: '600',
            }}
          >
            ๊ด€๋ จ ์„ฑ๋ถ„ ({ingredients.length}๊ฐœ)
          </h4>
          <ul 
            className="allergy-ingredients"
            style={{
              margin: 0,
              padding: 0,
              listStyle: 'none',
              display: 'flex',
              flexWrap: 'wrap',
              gap: '6px',
            }}
          >
            {ingredients.map((ingredient, index) => (
              <li 
                key={`${ingredient}-${index}`}
                className="allergy-ingredient"
                style={{
                  backgroundColor: `${config.color}15`,
                  color: config.color,
                  padding: '4px 8px',
                  borderRadius: '12px',
                  fontSize: '12px',
                  fontWeight: '500',
                  border: `1px solid ${config.color}30`,
                }}
              >
                {ingredient}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
};

// ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ชฉ๋ก ์ปดํฌ๋„ŒํŠธ
interface AllergyListProps {
  allergies: AllergyData[];
  title?: string;
}

export const AllergyList: React.FC<AllergyListProps> = ({ 
  allergies, 
  title = "์•Œ๋ ˆ๋ฅด๊ธฐ ๋ถ„์„ ๊ฒฐ๊ณผ" 
}) => {
  const [expandedItems, setExpandedItems] = React.useState<Set<number>>(new Set());

  const toggleDetails = (index: number) => {
    setExpandedItems(prev => {
      const newSet = new Set(prev);
      if (newSet.has(index)) {
        newSet.delete(index);
      } else {
        newSet.add(index);
      }
      return newSet;
    });
  };

  // ๋ฐ์ดํ„ฐ ์ •๋ ฌ (์‹ฌ๊ฐ๋„ ์ˆœ)
  const sortedAllergies = React.useMemo(() => {
    return [...allergies].sort((a, b) => {
      const configA = ALLERGY_LEVEL_CONFIG[a.level];
      const configB = ALLERGY_LEVEL_CONFIG[b.level];
      return configB.severity - configA.severity; // ์‹ฌ๊ฐํ•œ ๊ฒƒ๋ถ€ํ„ฐ
    });
  }, [allergies]);

  if (allergies.length === 0) {
    return (
      <div style={{ 
        textAlign: 'center', 
        padding: '40px 20px',
        color: COLORS.gray[500],
      }}>
        ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
      </div>
    );
  }

  return (
    <div className="allergy-list" style={{ maxWidth: '600px', margin: '0 auto' }}>
      <h3 style={{ 
        fontSize: '18px', 
        fontWeight: '700', 
        marginBottom: '16px',
        color: COLORS.gray[600],
      }}>
        {title}
      </h3>
      
      {sortedAllergies.map((allergy, index) => (
        <AllergyItem
          key={index}
          level={allergy.level}
          percentage={allergy.percentage}
          ingredients={allergy.ingredients}
          showDetails={expandedItems.has(index)}
          onToggleDetails={() => toggleDetails(index)}
        />
      ))}
    </div>
  );
};

// ์‚ฌ์šฉ ์˜ˆ์‹œ
export const AllergyExample = () => {
  const sampleData: AllergyData[] = [
    {
      level: 'high',
      percentage: 85,
      ingredients: ['๋‹ญ๊ณ ๊ธฐ', '์Œ€', '์˜ฅ์ˆ˜์ˆ˜'],
    },
    {
      level: 'medium',
      percentage: 45,
      ingredients: ['์—ฐ์–ด', '๊ณ ๊ตฌ๋งˆ'],
    },
    {
      level: 'low',
      percentage: 15,
      ingredients: ['๋‹น๊ทผ', '๋ธŒ๋กœ์ฝœ๋ฆฌ', '์‚ฌ๊ณผ'],
    },
  ];

  return <AllergyList allergies={sampleData} title="์šฐ๋ฆฌ ๊ฐ•์•„์ง€ ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ถ„์„" />;
};

 

"์›”๋ณ„ ํ†ต๊ณ„๋ฅผ ๋ณด์—ฌ์ค„ ๋•Œ ๊ทธ๋ž˜ํ”„๋กœ ํ‘œํ˜„ํ•˜๋ฉด ๋” ์ง๊ด€์ ์ผ ๊ฒƒ ๊ฐ™์•„์š”"๋ผ๋Š” ๋””์ž์ด๋„ˆ๋‹˜์˜ ์ œ์•ˆ์— ์–ด๋А์ •๋„ ํ™”๋ฉด์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

 

๋งˆ์น˜๋ฉฐ

์ง„ํ–‰ํ•˜๋ฉฐ ๊ณ ๋ฏผํ•œ ๋””ํ…Œ์ผ๋“ค์€ ํšŒ์‚ฌ์—์„  ์ด๋ฏธ ๋””์ž์ธ ์‹œ์Šคํ…œ์ด๋‚˜ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ์— ๋ฐ˜์˜๋˜์–ด ์žˆ์–ด์„œ, ๋ณ„๋‹ค๋ฅธ ๊ณ ๋ฏผ ์—†์ด ๊ฐ€์ ธ๋‹ค ์“ฐ๊ธฐ๋งŒ ํ–ˆ๋˜ ๊ฒƒ๋“ค์ด์—ˆ์–ด์š”.

์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๊ทธ๋Ÿฐ ์š”์†Œ๋“ค์„ ์ง์ ‘ ๊ธฐํšํ•˜๊ณ , ๋””์ž์ธ์— ๋…น์—ฌ๋‚ด๊ณ , ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๊ณผ์ •์„ ํ•˜๋‚˜ํ•˜๋‚˜ ๊ฒฝํ—˜ํ•ด๋ณด๋‹ˆ, ๊ฒ‰์œผ๋กœ๋งŒ ์•Œ๊ณ  ์žˆ์—ˆ๋˜ UI ๊ตฌ์„ฑ ์š”์†Œ๋“ค์ด ์–ผ๋งˆ๋‚˜ ๋งŽ์€ ์˜๋„์™€ ๊ณ ๋ฏผ ์†์—์„œ ๋งŒ๋“ค์–ด์กŒ๋Š”์ง€๋ฅผ ์ƒˆ์‚ผ ๋А๋‚„ ์ˆ˜ ์žˆ์—ˆ์–ด์š”.

 

๋ฌด์—‡๋ณด๋‹ค ๊ทธ๋Ÿฐ ๊ณผ์ •์ด ์ƒ๊ฐ๋ณด๋‹ค ๊ฝค ์žฌ๋ฐŒ์—ˆ์Šต๋‹ˆ๋‹ค!

 

๊ธ€ ์ดˆ๋ฐ˜์— ์ด์•ผ๊ธฐํ–ˆ๋˜ ‘์ƒํƒœ’์— ๋Œ€ํ•œ ์ •์˜๋ฅผ ์Šคํ„ฐ๋””์›๋“ค๊ณผ ํ•จ๊ป˜ ๊ณ ๋ฏผํ•˜๊ณ  ์ฐพ์•„๋ณด๋˜ ์ค‘, ์•„๋ž˜์˜ ๋ธ”๋กœ๊ทธ ๊ธ€์„ ์ ‘ํ•˜๊ฒŒ ๋˜์—ˆ๊ณ  ํฐ ๋„์›€์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค. ๐Ÿ‘‰ The Sorry State of States - velog.io/@tap_kim

 

์ €ํฌ๊ฐ€ ๊ฒช๊ณ  ์žˆ๋˜ ๊ณ ๋ฏผ๊ณผ ๊ฐ™์€ ๋‚ด์šฉ์„ ๋ณด๋ฉฐ ์ •๋ง ๋ฐ˜๊ฐ€์› ๊ณ ,
๋•๋ถ„์— ๋ง‰์—ฐํ–ˆ๋˜ ๋ฌธ์ œ๋“ค์„ ์กฐ๊ธˆ ๋” ๋น ๋ฅด๊ฒŒ ์ •๋ฆฌํ•˜๊ณ , ๋ช…ํ™•ํ•œ ๊ธฐ์ค€์„ ์„ธ์šธ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

 

๐Ÿ“ƒ ์ฐธ๊ณ  ๋ฌธํ—Œ  
(๋ฒˆ์—ญ)์ƒํƒœ๋“ค์˜ ์ฐธ๋‹ดํ•œ ์ƒํƒœ
๋กœ๊ทธ์ธ ๊ณผ์ •์˜ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€: ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ ๋ฐ ๋ณด์•ˆ

 

์˜ค๋Š˜์€ ์ œ๊ฐ€ Context API์—์„œ Zustand๋กœ ์ƒํƒœ๊ด€๋ฆฌ ๋„๊ตฌ๋ฅผ ์ „ํ™˜ํ•˜๊ฒŒ ๋œ ๊ณผ์ •๊ณผ 2024๋…„ ํ˜„์žฌ ๋ฆฌ์•กํŠธ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์˜ ํ˜„ํ™ฉ์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค.

 

1. Context API์˜ ํ•œ๊ณ„์™€ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ํ•„์š”์„ฑ

Context API๋กœ ์‹œ์ž‘ํ•œ ์ด์œ 

์ฒ˜์Œ ์•Œ๋ฆฌ๋‹ˆ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•  ๋•Œ๋Š” Context API๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ธ์ฆ, ํ…Œ๋งˆ, ์–ธ์–ด ์„ค์ • ๋“ฑ ์ „์—ญ์ ์œผ๋กœ ๊ณต์œ ๋˜์–ด์•ผ ํ•˜๋Š” ์ƒํƒœ๋“ค์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ Context API ๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

// ์ดˆ๊ธฐ Context API ์‚ฌ์šฉ ์˜ˆ์‹œ
const PetContext = React.createContext<PetContextType | undefined>(undefined);

export const PetProvider: React.FC<PropsWithChildren> = ({ children }) => {
  const [petData, setPetData] = useState<PetData>({
    allergies: [],
    symptoms: [],
    foodLog: []
  });

  return (
    <PetContext.Provider value={{ petData, setPetData }}>
      {children}
    </PetContext.Provider>
  );
};

 

ํ•œ๊ณ„์  ๋ฐœ๊ฒฌ

ํ•˜์ง€๋งŒ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๋ฉด์„œ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ํ•œ๊ณ„์ ์ด ๋“œ๋Ÿฌ๋‚˜๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค:

 

  • ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง: Context์˜ ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ํ•ด๋‹น Context๋ฅผ ๊ตฌ๋…ํ•˜๋Š” ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง๋˜์—ˆ์Šต๋‹ˆ๋‹ค. 
  • ์ฝ”๋“œ ๋ณต์žก๋„ ์ฆ๊ฐ€: ์—ฌ๋Ÿฌ ๊ฐœ์˜ Context๋ฅผ ์ค‘์ฒฉํ•ด์„œ ์‚ฌ์šฉํ•˜๋‹ค ๋ณด๋‹ˆ Provider Hell์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋กœ์ง ๊ด€๋ฆฌ์˜ ์–ด๋ ค์›€: ๋น„๋™๊ธฐ ์ž‘์—…๊ณผ ๋ณต์žกํ•œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐ ํ•œ๊ณ„๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

2. ์ฃผ์š” ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋น„๊ต ๋ถ„์„

Redux

Redux๋Š” ์˜ค๋žซ๋™์•ˆ ๋ฆฌ์•กํŠธ ์ƒํƒœ๊ณ„์˜ ํ‘œ์ค€๊ณผ ๊ฐ™์€ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ž…๋‹ˆ๋‹ค.

Redux์˜ ํ•ต์‹ฌ ๊ฐœ๋…

 

  • Action: ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์œ„ํ•œ ์ด๋ฒคํŠธ๋ฅผ ์„ค๋ช…ํ•˜๋Š” ๊ฐ์ฒด
  • Reducer: Action์„ ๋ฐ›์•„ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜
  • Store: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ „์ฒด ์ƒํƒœ๋ฅผ ๋ณด๊ด€ํ•˜๋Š” ๊ฐ์ฒด

Redux ๋ฏธ๋“ค์›จ์–ด ์ƒํƒœ๊ณ„

 

  • Redux Thunk: ๋น„๋™๊ธฐ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ๋ฏธ๋“ค์›จ์–ด
  • Redux Saga: ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ํ๋ฆ„์„ ์ œ์–ดํ•˜๊ธฐ ์œ„ํ•œ ๋ฏธ๋“ค์›จ์–ด
    • Generator ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ ์„ ์–ธ์  ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
    • ๋ณต์žกํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค(API ์š”์ฒญ ์ทจ์†Œ, ์žฌ์‹œ๋„ ๋“ฑ) ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
  • Redux Toolkit: ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•œ ๊ณต์‹ ๋„๊ตฌ

๊ทธ๋Ÿฌ๋‚˜ Redux๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค:

 

  • ๋งŽ์€ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ
  • ํ•™์Šต ๊ณก์„ ์ด ๊ฐ€ํŒŒ๋ฆ„
  • ์ž‘์€ ์ƒํƒœ ๋ณ€ํ™”์—๋„ ๋งŽ์€ ์ฝ”๋“œ ํ•„์š”

Recoil

Facebook์—์„œ ๋งŒ๋“  ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.

 

์žฅ์ 

  • React์™€์˜ ๋†’์€ ํ˜ธํ™˜์„ฑ
  • ๊ฐ„๋‹จํ•œ API (atom, selector)
  • ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ฐ€ ์‰ฌ์›€

๋‹จ์ 

  • ์•„ํ†ฐ์œผ๋กœ ๊ด€๋ฆฌ๋˜๋Š” ์ „์—ญ ์ƒํƒœ๋ฅผ ์–ด๋–ค ์ปดํฌ๋„ŒํŠธ๋ผ๋„ ๋ฐ”๋กœ ์•„ํ†ฐ์„ ๊ตฌ๋…ํ•ด์„œ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐ›๊ธฐ์—, ์˜์กด์„ฑ์ด ์—ฌ๋Ÿฌ ๋ฐฉํ–ฅ์œผ๋กœ ์—ฎ์ด๋ฉด์„œ ์˜ˆ์ธกํ•˜๊ธฐ ํž˜๋“ค์–ด์ง‘๋‹ˆ๋‹ค.

Zustand

๊ฐ€๋ณ๊ณ  ์ง๊ด€์ ์ธ API๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ˜„๋Œ€์ ์ธ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.

 

์ฃผ์š” ํŠน์ง•

  • ์ตœ์†Œํ•œ์˜ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ
  • TypeScript ์ง€์›์ด ํ›Œ๋ฅญํ•จ
  • Redux DevTools ์ง€์›
  • ๋ฏธ๋“ค์›จ์–ด ์‹œ์Šคํ…œ (persist, devtools ๋“ฑ)

์˜ˆ์‹œ ์ฝ”๋“œ:

import create from 'zustand'

interface PetStore {
  symptoms: Symptom[]
  addSymptom: (symptom: Symptom) => void
  foodLogs: FoodLog[]
  addFoodLog: (log: FoodLog) => void
}

const usePetStore = create<PetStore>((set) => ({
  symptoms: [],
  addSymptom: (symptom) => set((state) => ({
    symptoms: [...state.symptoms, symptom]
  })),
  foodLogs: [],
  addFoodLog: (log) => set((state) => ({
    foodLogs: [...state.foodLogs, log]
  }))
}))

 

3. ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ์˜ ์„ ํƒ: Zustand

์•Œ๋ฆฌ๋‹ˆ ํ”„๋กœ์ ํŠธ์—์„œ์˜ ์š”๊ตฌ์‚ฌํ•ญ

  1. ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
  2. ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ (์•Œ๋ ˆ๋ฅด๊ธฐ ์ฆ์ƒ, ์Œ์‹ ๊ธฐ๋ก, ๋ถ„์„ ๋ฐ์ดํ„ฐ)
  3. ์„ฑ๋Šฅ ์ตœ์ ํ™”
  4. TypeScript ์ง€์›

Zustand ์„ ํƒ ์ด์œ 

  1. ๊ฐ„๋‹จํ•œ ์„ค์ •๊ณผ ์‚ฌ์šฉ๋ฒ•: ๋ณ„๋„์˜ Provider ์„ค์ •์ด ํ•„์š” ์—†๊ณ , ์ง๊ด€์ ์ธ API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  2. ์„ฑ๋Šฅ ์ตœ์ ํ™”: ํ•„์š”ํ•œ ์ƒํƒœ๋งŒ ๊ตฌ๋…ํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  3. TypeScript ์ง€์›: ํƒ€์ž… ์ถ”๋ก ์ด ์ž˜ ๋™์ž‘ํ•˜์—ฌ ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์ด ํ–ฅ์ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  4. ๋ฏธ๋“ค์›จ์–ด ์ง€์›: persist ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ†ตํ•ด ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์—ฐ๋™์ด ์‰ฌ์› ์Šต๋‹ˆ๋‹ค.

Context API์™€์˜ ์กฐํ™”

Context API์™€ Zustand๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์—ญํ• ์„ ๊ตฌ๋ถ„ํ–ˆ์Šต๋‹ˆ๋‹ค:

  • Context API: ํ…Œ๋งˆ, ์ธ์ฆ ๋“ฑ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ „๋ฐ˜์ ์ธ ์„ค์ •๊ณผ ๊ด€๋ จ๋œ ์ƒํƒœ ๊ด€๋ฆฌ (๋‹จ์–ด ๊ทธ๋Œ€๋กœ ํŠน์ • ๋งฅ๋ฝ์„ ์”Œ์šธ๋•Œ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.)
  • Zustand: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๊ด€๋ จ๋œ ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ

 

4. ๊ฒฐ๋ก 

2024๋…„ ํ˜„์žฌ, ์ €๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ์ค€์œผ๋กœ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค:

  1. ์ž‘์€ ํ”„๋กœ์ ํŠธ: Context API๋งŒ์œผ๋กœ๋„ ์ถฉ๋ถ„
  2. ์ค‘๊ฐ„ ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ: Zustand
  3. ๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ: Redux Toolkit ๋˜๋Š” Zustand

์•Œ๋ฆฌ๋‹ˆ์™€ ๊ฐ™์ด ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์™€ ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ํ”„๋กœ์ ํŠธ์—๋Š” Zustand๊ฐ€ ํŠนํžˆ ์ ํ•ฉํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ๊ฐ€ ๊ฐ„๊ฒฐํ•˜๋ฉด์„œ๋„ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋ฉฐ, ํŠนํžˆ TypeScript์™€์˜ ํ˜ธํ™˜์„ฑ์ด ๋›ฐ์–ด๋‚˜ ๊ฐœ๋ฐœ ๊ฒฝํ—˜์ด ๋งค์šฐ ์ข‹์•˜์Šต๋‹ˆ๋‹ค.

 

 

 

 

ํ”„๋ฆฌํ”ฝ์Šค!!

 

ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์„ ํ•˜๋‹ค ๋ณด๋ฉด ํฌ๋กœ์Šค๋ธŒ๋ผ์šฐ์ง• ์ด์Šˆ๋Š” ํ”ผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

::-webkit-input-placeholder์™€ ๊ฐ™์€ ๋ธŒ๋ผ์šฐ์ €๋ณ„ ํ”„๋ฆฌํ”ฝ์Šค(-webkit-, -moz-, -ms- ๋“ฑ)๋ฅผ ์ผ์ผ์ด ์ถ”๊ฐ€ํ•˜๋Š” ๊ฑด ๊ต‰์žฅํžˆ ๋ฒˆ๊ฑฐ๋กœ์šด ์ž‘์—…์ด์˜ˆ์š”.

 

์‚ฌ์‹ค ํ˜„์—…์—์„œ๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์ด๋Ÿฌํ•œ ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ๋งค์šฐ ๊ณค๋ž€ํ•œ ์ผ์ž…๋‹ˆ๋‹ค.

(์ €๋„ ๊ด€๋ จ๋œ ๋Œ€ํ™”๋ฅผ ๋‚˜๋ˆ„๋‹ค ์•Œ๊ฒŒ๋์–ด์š”!)

 

์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ฒˆ๋“ค๋Ÿฌ๋ฅผ ํ†ตํ•ด ์ž๋™์œผ๋กœ ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ๋ฐฉ๋ฒ•์ธ Autoprefixer๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

PostCSS๋ฅผ ๊ธฐ๋ณธ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ผ๋ฉด, ํ˜น์€ CSS๋กœ ๋ณ€ํ™˜ํ•œ ํ›„ postcss-loader๊ฐ€ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ํ•œ๋‹ค๋ฉด ๊ฐœ๋ฐœ์ž๊ฐ€ ๊ฐ ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ์Šคํƒ€์ผ์„ ๋”ฐ๋กœ ์ž‘์„ฑํ•˜์ง€ ์•Š์•„๋„ Autoprefixer๊ฐ€ ์›นํ‚ท ํ”„๋ฆฌํ”ฝ์Šค์™€ ๊ฐ™์€ ๊ฒƒ์„ ์ž๋™์œผ๋กœ ๋ถ™์—ฌ์ค๋‹ˆ๋‹ค.

 

์ด๋ฒˆ ํฌ์ŠคํŠธ์—์„œ๋Š” PostCSS์™€ Autoprefixer๋ฅผ ํ™œ์šฉํ•ด ํ•ด๋‹น ISSUE๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‹ค์ œ ํ”„๋กœ์ ํŠธ ๊ฒฝํ—˜์„ ๋ฐ”ํƒ•์œผ๋กœ ์ƒ์„ธํžˆ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!

 

PostCSS์™€ Autoprefixer๋ž€?

PostCSS

  • JavaScript ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•ด CSS๋ฅผ ๋ณ€ํ™˜ํ•˜๋Š” ๋„๊ตฌ
  • "Post-processor"๋ผ๊ณ ๋„ ๋ถˆ๋ฆฌ๋Š” ์ด์œ ๋Š” CSS๋ฅผ ์ž‘์„ฑํ•œ ํ›„์— ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ
  • CSS ๊ตฌ๋ฌธ ๋ถ„์„์— ์ตœ์ ํ™”๋œ ํŒŒ์„œ๋ฅผ ์‚ฌ์šฉํ•ด ๋†’์€ ์„ฑ๋Šฅ์„ ๋ณด์ž„
  • CSS ์ „์ฒ˜๋ฆฌ๊ธฐ(Sass)๋‚˜ ํ›„์ฒ˜๋ฆฌ๊ธฐ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ (์ด ๋ถ€๋ถ„์€ ์ œ๊ฐ€ ํ—ท๊ฐˆ๋ ธ๋˜ ๋ถ€๋ถ„์ด๋ผ ์•„๋ž˜ ์ถ”๊ฐ€์„ค๋ช…์„ ๋„ฃ์—ˆ์–ด์š”!)
    1. CSS ์ „์ฒ˜๋ฆฌ๊ธฐ(SASS):
      CSS ๊ธฐ๋Šฅ์„ ํ™•์žฅํ•œ ์–ธ์–ด ์ž…๋‹ˆ๋‹ค. ๋ณ€์ˆ˜, ์ค‘์ฒฉ ๊ทœ์น™, ๋ฏน์Šค์ธ ๋“ฑ์„ ์ง€์›ํ•˜๊ณ , PostCSS๋Š” Sass๋กœ ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, PostCSS ์ž์ฒด๋Š” Sass ๋ฌธ๋ฒ•์„ ์‚ฌ์šฉํ•˜์ง„ ์•Š์•„์š”.
    2. CSS ํ›„์ฒ˜๋ฆฌ๊ธฐ:
      PostCSS๋Š” CSS์ฝ”๋“œ๊ฐ€ ์ž‘์„ฑ๋œ ํ›„์— ์ถ”๊ฐ€์ ์ธ ๋ณ€ํ™˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋„๊ตฌ๋กœ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
      ์˜ˆ๋ฅผ ๋“ค์–ด, ์ œ๊ฐ€ ์ง€๊ธˆ ์ž‘์„ฑํ•˜๋Š” Autoprefixer ๊ฐ™์€ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•ด์„œ ์ž๋™์œผ๋กœ ๋ธŒ๋ผ์šฐ์ € ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ์š”!
  • 200๊ฐœ ์ด์ƒ์˜ ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ ์ œ๊ณต (์ƒํƒœ๊ณ„๊ฐ€ ํฝ๋‹ˆ๋‹ค!)
  • ๋ชจ๋“ˆํ™”๋œ ๊ตฌ์กฐ๋กœ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋งŒ ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ ์„ ํƒ์  ์ถ”๊ฐ€ ๊ฐ€๋Šฅ

Autoprefixer

  • PostCSS์˜ ๋Œ€ํ‘œ์ ์ธ ํ”Œ๋Ÿฌ๊ทธ์ธ
  • ๋ธŒ๋ผ์šฐ์ € ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ๋„๊ตฌ
  • Browserslist ์„ค์ •์„ ํ†ตํ•ด ์ง€์› ๋ธŒ๋ผ์šฐ์ € ๋ฒ”์œ„ ์ง€์ • ๊ฐ€๋Šฅ

 

Create React App๊ณผ Vite์˜ ๊ธฐ๋ณธ ์ง€์›

ํ•ด๋‹น ๋ถ€๋ถ„์€ Webpack์„ ์ง์ ‘ ์„ค์ •ํ•˜์ง€ ์•Š์„ ๋• ์–ด๋–ป๊ฒŒ ๋˜๋Š” ๊ฑด์ง€, ๊ธฐ๋ณธ ์„ค์ •์ด ๋˜์–ด ์žˆ๋Š”์ง€ ๋“ฑ์ด ๊ถ๊ธˆํ•ด ์ฐพ์•„๋ณด๊ฒŒ ๋์Šต๋‹ˆ๋‹ค.

Create React App (CRA)

  • PostCSS์™€ Autoprefixer๊ฐ€ ๊ธฐ๋ณธ์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์Œ
  • ๋ณ„๋„ ์„ค์ • ์—†์ด ๋ฐ”๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  • browserslist ์„ค์ •๋„ ๊ธฐ๋ณธ ์ œ๊ณต

CRA์˜ package.json์„ ๋ณด์‹œ๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

"browserslist": {
  "production": [
    ">0.2%",
    "not dead",
    "not op_mini all"
  ],
  "development": [
    "last 1 chrome version",
    "last 1 firefox version",
    "last 1 safari version"
  ]
}


Vite

  • PostCSS๋Š” ๊ธฐ๋ณธ ์ง€์›
  • Autoprefixer๋Š” ๋ณ„๋„ ์„ค์น˜ ํ•„์š”
yarn add -D autoprefixer
// vite.config.js
import autoprefixer from 'autoprefixer'

export default {
  css: {
    postcss: {
      plugins: [
        autoprefixer()
      ]
    }
  }
}

 

ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์— Custom Webpack ์„ค์ •ํ•˜๊ธฐ

์ปค์Šคํ…€ Webpack ์„ค์ •์—์„œ๋Š” ์ˆ˜๋™์œผ๋กœ PostCSS์™€ Autoprefixer๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์„ค์น˜

yarn add -D postcss postcss-loader autoprefixer


์›นํŒฉ ์„ค์ • ์˜ˆ์‹œ

module.exports = {
  module: {
    rules: [
      {
        test: /\.module\.scss$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              modules: {
                localIdentName: "[name]__[local]--[hash:base64:5]",
              },
              importLoaders: 2,
            },
          },
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  ["autoprefixer"]
                ],
              },
            },
          },
          "sass-loader",
        ],
      },
      {
        test: /\.(scss|css)$/,
        exclude: /\.module\.scss$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              importLoaders: 2,
            },
          },
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  ["autoprefixer"]
                ],
              },
            },
          },
          "sass-loader",
        ],
      },
    ],
  },
  // ... ๊ธฐํƒ€ ์›นํŒฉ ์„ค์ •
};

 

์ถ”๊ฐ€์ ์œผ๋กœ ์ €๋Š” ์„ค์ •ํ•˜์ง€ ์•Š์•˜์ง€๋งŒ ๋‹ค๋ฅธ ๋ธ”๋กœ๊ทธ ๊ธ€์„ ๋ณด์‹œ๋ฉด ๊ฐ„ํ˜น postcss-scss์— ๋Œ€ํ•œ ์ถ”๊ฐ€๊ฐ€ ๋ณด์ด์…จ์„ ํ…๋ฐ, ์ด๊ฑด ๋ฌด์Šจ ์—ญํ• ์ผ๊นŒ์š”?!

postcss-scss๋ž€?

  • ์ผ๋ฐ˜์ ์œผ๋กœ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค
  • sass-loader๊ฐ€ SCSS๋ฅผ CSS๋กœ ๋ณ€ํ™˜ํ•œ ํ›„ postcss-loader๊ฐ€ ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ
  • postcss-scss๋Š” SCSS ๊ตฌ๋ฌธ์„ ์ง์ ‘ PostCSS ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋งŒ ํ•„์š”

 

์‹ค์ˆ˜ํ–ˆ๋˜ ๋ถ€๋ถ„๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

package.json์— ์ž‘์„ฑ๋ผ์•ผ ํ•˜๋Š” json ๋ฌธ๋ฒ•์„. browserslistrc ํŒŒ์ผ์— ์ ์šฉํ•ด ๋ฒ„๋ ธ์–ด์š”. ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฝ๋‹ค ์ˆœ๊ฐ„์ ์œผ๋กœ ์ฐฉ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ๋„ˆ๋ฌด ๊ฐ„๋‹จํ•˜์ง€๋งŒ ์ œ ๊ธฐ์ค€์—์„œ ์น˜๋ช…์ ์ธ ์‹ค์ˆ˜๋ž€ ์ƒ๊ฐ์ด ๋“ค์–ด ๊ธฐ๋กํ•ด๋‘๋ ค ํ•ฉ๋‹ˆ๋‹ค!

Browserslist ์„ค์ • ์˜ค๋ฅ˜

Error [BrowserslistError]: Unknown browser query `{`. Maybe you are using old Browserslist or made typo in query.

 

 

.browserslistrc ํŒŒ์ผ ์‚ฌ์šฉ (๊ถŒ์žฅ)

์ €๋Š” ์ตœ์ข…์ ์œผ๋กœ ์•„๋ž˜์˜ ์ฝ”๋“œ๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค!

# ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ
[production]
>0.2% # ์ „ ์„ธ๊ณ„ ์‚ฌ์šฉ๋ฅ  0.2% ์ด์ƒ์ธ ๋ธŒ๋ผ์šฐ์ €
not dead # ๊ณต์‹ ์ง€์›์ด ์ค‘๋‹จ๋˜์ง€ ์•Š์€ ๋ธŒ๋ผ์šฐ์ €
not op_mini all # Opera Mini ๋ธŒ๋ผ์šฐ์ € ์ œ์™ธ

# ๊ฐœ๋ฐœ ํ™˜๊ฒฝ
[development]
last 1 chrome version # ์ตœ์‹  ํฌ๋กฌ ๋ฒ„์ „
last 1 firefox version # ์ตœ์‹  ํŒŒ์ด์–ดํญ์Šค ๋ฒ„์ „
last 1 safari version # ์ตœ์‹  ์‚ฌํŒŒ๋ฆฌ ๋ฒ„์ „


ํ•ด๊ฒฐ๋ฐฉ๋ฒ• 2: package.json์— ์„ค์ •

{
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}


Browserslist ์„ค์ • ๊ฐ€์ด๋“œ

์ฃผ์š” ์ฟผ๋ฆฌ ์„ค๋ช…

  • >0.2%: ์ „ ์„ธ๊ณ„ ์‚ฌ์šฉ๋ฅ  0.2% ์ด์ƒ์ธ ๋ธŒ๋ผ์šฐ์ €
  • not dead: ๊ณต์‹ ์ง€์›์ด ์ค‘๋‹จ๋˜์ง€ ์•Š์€ ๋ธŒ๋ผ์šฐ์ €
  • not op_mini all: Opera Mini ๋ธŒ๋ผ์šฐ์ € ์ œ์™ธ
  • last 1 chrome version: ์ตœ์‹  ํฌ๋กฌ ๋ฒ„์ „
  • last 2 versions: ๊ฐ ๋ธŒ๋ผ์šฐ์ €์˜ ์ตœ์‹  2๊ฐœ ๋ฒ„์ „
  • > 1%: ์ „ ์„ธ๊ณ„ ์ ์œ ์œจ 1% ์ด์ƒ
  • IE 11: IE 11 ์ง€์›

์„ค์ • ํ…Œ์ŠคํŠธ

  • browserslist.dev์—์„œ ์„ค์ •์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Œ
  • ์–ด๋–ค ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํฌํ•จ๋˜๋Š”์ง€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™•์ธ ๊ฐ€๋Šฅ

 

์‹ค์ œ ๋™์ž‘ ์˜ˆ์‹œ

๋ณ€ํ™˜ ์ „ CSS

.example {
  display: flex;
  user-select: none;
}

๋ณ€ํ™˜ ํ›„ CSS

.example {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-user-select: none;
     -moz-user-select: none;
      -ms-user-select: none;
          user-select: none;
}

 

์ฃผ์˜์‚ฌํ•ญ๊ณผ ํŒ!

  • ๋กœ๋” ์ˆœ์„œ: PostCSS ๋กœ๋”๋Š” Sass ๋กœ๋” ์ดํ›„, CSS ๋กœ๋” ์ด์ „์— ์œ„์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.  
    1. sass-loader (SCSS -> CSS ๋ณ€ํ™˜)
    2. postcss-loader (์ž๋™ ํ”„๋ฆฌํ”ฝ์Šค ์ถ”๊ฐ€)
    3. css-loader (CSS -> JS ๋ณ€ํ™˜)
    4. style-loader (JS -> style ํƒœ๊ทธ ์‚ฝ์ž…)
  • ์„ฑ๋Šฅ ์ตœ์ ํ™”: development ํ™˜๊ฒฝ์—์„œ๋Š” source map์„ ํ™œ์„ฑํ™”ํ•˜๊ณ , production์—์„œ๋Š” ๋น„ํ™œ์„ฑํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
  • ๋ธŒ๋ผ์šฐ์ € ์ง€์› ๋ฒ”์œ„: browserslist ์„ค์ •์„ ํ†ตํ•ด ์ง€์›ํ•  ๋ธŒ๋ผ์šฐ์ € ๋ฒ”์œ„๋ฅผ ์ ์ ˆํžˆ ์กฐ์ ˆํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

์ฃผ์˜์‚ฌํ•ญ๊ณผ ํŒ์—์„œ์˜ Development ํ™˜๊ฒฝ์—์„œ source map ํ™œ์„ฑํ™”ํ•˜๋Š” ์ด์œ  & Production ํ™˜๊ฒฝ์—์„œ ๋น„ํ™œ์„ฑํ™” ํ•˜๋Š” ์ด์œ ์— ๋Œ€ํ•ด์„œ๋งŒ ์ข€ ๋” ์ž์„ธํžˆ ์•Œ์•„๋ณผ๊ฒŒ์š”!

 

Development ํ™˜๊ฒฝ์—์„œ source map ํ™œ์„ฑํ™”ํ•˜๋Š” ์ด์œ :

  1. ๋””๋ฒ„๊น… ์šฉ์ด์„ฑ
    • ๊ฐœ๋ฐœ ์ค‘์—๋Š” ์›๋ณธ ์†Œ์Šค ์ฝ”๋“œ์™€ ๋ณ€ํ™˜๋œ ์ฝ”๋“œ๋ฅผ ๋งคํ•‘ํ•˜์—ฌ ์‰ฝ๊ฒŒ ๋””๋ฒ„๊น…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
    • ๋ธŒ๋ผ์šฐ์ €์˜ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ์‹ค์ œ ์ž‘์„ฑํ•œ CSS ํŒŒ์ผ์˜ ์œ„์น˜์™€ ๋ผ์ธ์„ ์ •ํ™•ํžˆ ํ™•์ธ ๊ฐ€๋Šฅ
    • PostCSS๋กœ ๋ณ€ํ™˜๋œ ์ฝ”๋“œ๊ฐ€ ์•„๋‹Œ ์›๋ณธ ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ํ™•์ธํ•˜๋ฉฐ ์ˆ˜์ • ๊ฐ€๋Šฅ
  2. ๊ฐœ๋ฐœ ํšจ์œจ์„ฑ
    • ์ฝ”๋“œ ์ˆ˜์ • ์‹œ ์ฆ‰๊ฐ์ ์ธ ํ”ผ๋“œ๋ฐฑ ํ™•์ธ ๊ฐ€๋Šฅ
    • CSS ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ์ •ํ™•ํ•œ ์œ„์น˜ ์ถ”์ ์ด ๊ฐ€๋Šฅํ•˜์—ฌ ์ˆ˜์ • ์‹œ๊ฐ„ ๋‹จ์ถ•
    • ๋ณต์žกํ•œ ์Šคํƒ€์ผ ๊ตฌ์กฐ์—์„œ๋„ ์›๋ณธ ์ฝ”๋“œ ์œ„์น˜๋ฅผ ์‰ฝ๊ฒŒ ํŒŒ์•…!

Production ํ™˜๊ฒฝ์—์„œ source map ๋น„ํ™œ์„ฑํ™”ํ•˜๋Š” ์ด์œ :

  1. ํŒŒ์ผ ํฌ๊ธฐ ์ตœ์ ํ™”
    • source map ํŒŒ์ผ์€ ์›๋ณธ ์ฝ”๋“œ์™€ ๋ณ€ํ™˜๋œ ์ฝ”๋“œ์˜ ๋งคํ•‘ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋ฏ€๋กœ ์ƒ๋‹นํ•œ ํฌ๊ธฐ๋ฅผ ์ฐจ์ง€
    • ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ์ „์†ก์„ ์ค„์—ฌ ๋กœ๋”ฉ ์†๋„ ํ–ฅ์ƒ
    • ์ผ๋ฐ˜์ ์œผ๋กœ source map ํŒŒ์ผ์€ ์›๋ณธ ํŒŒ์ผ ํฌ๊ธฐ์˜ ์ ˆ๋ฐ˜ ์ด์ƒ์„ ์ฐจ์ง€ํ•  ์ˆ˜ ์žˆ์Œ
  2. ๋ณด์•ˆ ๊ฐ•ํ™”
    • source map์ด ๋…ธ์ถœ๋˜๋ฉด ์›๋ณธ ์ฝ”๋“œ ๊ตฌ์กฐ๊ฐ€ ๋“œ๋Ÿฌ๋‚  ์ˆ˜ ์žˆ์Œ
    • ์•…์˜์ ๋ฅผ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž๊ฐ€ ์ฝ”๋“œ ๊ตฌ์กฐ๋ฅผ ๋ถ„์„ํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€
    • ๊ธฐ์—…์˜ proprietary ์ฝ”๋“œ ๋ณดํ˜ธ!!
  3. ์„ฑ๋Šฅ ์ตœ์ ํ™”
    • ๋ธŒ๋ผ์šฐ์ €๊ฐ€ source map์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ถ”๊ฐ€์ ์ธ ์ž‘์—… ์ œ๊ฑฐ
    • ์ดˆ๊ธฐ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์‹œ๊ฐ„ ๋‹จ์ถ•
    • ์„œ๋ฒ„ ๋Œ€์—ญํญ ์‚ฌ์šฉ๋Ÿ‰ ๊ฐ์†Œ

์‹ค์ œ ์„ค์ • ์˜ˆ์‹œ! (ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ๋ณด๋‹ค ๊ฐ„๋žตํ™”)

// webpack.config.js
module.exports = {
  mode: process.env.NODE_ENV,
  module: {
    rules: [{
      test: /\.css$/,
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            sourceMap: process.env.NODE_ENV === 'development'
          }
        },
        {
          loader: 'postcss-loader',
          options: {
            sourceMap: process.env.NODE_ENV === 'development'
          }
        }
      ]
    }]
  }
};

 

 

๊ฒฐ๋ก 

PostCSS์™€ Autoprefixer๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํฌ๋กœ์Šค๋ธŒ๋ผ์šฐ์ง•์„ ์œ„ํ•œ ์ˆ˜๋™ ์ž‘์—…์„ ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

CRA๋‚˜ Create Vite๊ฐ™์€ ๊ฒฝ์šฐ ์ด๋ฏธ ์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์„ ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์ปค์Šคํ…€ ์›นํŒฉ ์„ค์ •์—์„œ๋„ ๊ฐ„๋‹จํ•œ ์„ค์ •๋งŒ์œผ๋กœ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

ํŠนํžˆ Browserslist ์„ค์ •์„ ํ†ตํ•ด ํ”„๋กœ์ ํŠธ์˜ ๋ธŒ๋ผ์šฐ์ € ์ง€์› ๋ฒ”์œ„๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๊ณ , ํ•„์š”ํ•œ ํ”„๋ฆฌํ”ฝ์Šค๋งŒ ์ถ”๊ฐ€ํ•จ์œผ๋กœ์จ ์ตœ์ ํ™”๋œ CSS๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒ ์ฃ ?!

 

.browserslistrc ํŒŒ์ผ์„ ์‚ฌ์šฉํ•˜๋ฉด ์„ค์ • ๊ด€๋ฆฌ๊ฐ€ ๋”์šฑ ์šฉ์ดํ•ด์ง€๋ฉฐ, ๋‹ค๋ฅธ ๋„๊ตฌ๋“ค๊ณผ๋„ ์„ค์ •์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์—ฌ๊ธฐ์„œ "๋‹ค๋ฅธ ๋„๊ตฌ๋“ค"์€ ์ฃผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์€ CSS ๋˜๋Š” JavaScript ๋„๊ตฌ๋“ค์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค!

  1. Autoprefixer: CSS์— ํ•„์š”ํ•œ ๋ธŒ๋ผ์šฐ์ € ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ๋„๊ตฌ๋กœ, Browserslist ์„ค์ •์„ ํ†ตํ•ด ์–ด๋–ค ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ถ”๊ฐ€ํ• ์ง€ ๊ฒฐ์ • (์ง€๊ธˆ๊นŒ์ง€ ์„ค๋ช…๋“œ๋ฆฐ ๋‚ด์šฉ์ด์ฃ ?!)
  2. Babel: ์ตœ์‹  JavaScript ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €์—์„œ๋„ ์ž‘๋™ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ณ€ํ™˜ํ•ด์ฃผ๋Š” ๋„๊ตฌ๋กœ, Browserslist ์„ค์ •์„ ํ†ตํ•ด ์ง€์›ํ•  ๋ธŒ๋ผ์šฐ์ € ์ •์˜ ๊ฐ€๋Šฅ
  3. ESLint: JavaScript ์ฝ”๋“œ์˜ ํ’ˆ์งˆ์„ ๊ด€๋ฆฌํ•˜๋Š” ๋„๊ตฌ๋กœ, Browserslist์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด ํŠน์ • ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์— ๋งž๋Š” ์ฝ”๋“œ ์Šคํƒ€์ผ ์ ์šฉ ๊ฐ€๋Šฅ
  4. Stylelint: CSS ์Šคํƒ€์ผ์„ ๊ฒ€์‚ฌํ•˜๋Š” ๋„๊ตฌ๋กœ, Browserslist ์„ค์ •์„ ์ฐธ๊ณ ํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ ๊ด€๋ จ ๊ทœ์น™ ์ ์šฉ ๊ฐ€๋Šฅ

์ฆ‰, .browserslistrc ํŒŒ์ผ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋Ÿฌํ•œ ๋‹ค์–‘ํ•œ ๋„๊ตฌ๋“ค์ด ๋™์ผํ•œ ๋ธŒ๋ผ์šฐ์ € ์ง€์› ๋ฒ”์œ„๋ฅผ ๊ณต์œ ํ•˜๊ฒŒ ๋˜์–ด, ๊ฐœ๋ฐœ ๋ฐ ์œ ์ง€๋ณด์ˆ˜ ๊ณผ์ •์—์„œ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

 

 

๐Ÿ“ƒ ์ฐธ๊ณ  ๋ฌธํ—Œ  
PostCSS ๊ณต์‹ ๋ฌธ์„œ
Autoprefixer GitHub
Browserslist
Create React App PostCSS ์„ค์ •
Vite CSS ์„ค์ •
Webpack 5 postcss-loader 
Browserslist ๊ด€๋ จ ๊ฐœ์ธ ๋ธ”๋กœ๊ทธ

 

express-openapi-validator

 

์•ˆ๋…•ํ•˜์„ธ์š”! express-openapi-validator์˜ HTTP ์ธ์ฆ ๊ด€๋ จ ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด PR์„ ์ œ์ถœํ–ˆ๋˜ ๊ณผ์ •์„ ๊ณต์œ ๋“œ๋ฆฌ๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์ด์ „ ํฌ์ŠคํŠธ์—์„œ express-openapi-validator๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๋ฐœ๊ฒฌํ•œ ์ธ์ฆ ๊ด€๋ จ ์ด์Šˆ๋ฅผ ๋‹ค๋ค˜์—ˆ๋Š”๋ฐ์š”, ๊ฐ„๋‹จํžˆ ์š”์•ฝ๋“œ๋ฆฌ๋ฉด:

Express.js๋กœ REST API๋ฅผ ๊ตฌ์ถ•ํ•˜๋ฉฐ JWT๋ฅผ http-only ์ฟ ํ‚ค๋กœ ์ „๋‹ฌํ•˜๋ ค ํ–ˆ๋Š”๋ฐ,
Authorization ํ—ค๋”์™€ CSRF ํ† ํฐ ๊ฒ€์ฆ์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

ํŠนํžˆ validateHttp() ๋ฉ”์„œ๋“œ๊ฐ€ Authorization ํ—ค๋”๋ฅผ ํ•„์ˆ˜๋กœ ์š”๊ตฌํ•˜๋Š” ๋ฐ”๋žŒ์—,
์ฟ ํ‚ค๋กœ JWT๋ฅผ ์ „๋‹ฌํ•˜๋Š” ์ƒํ™ฉ์—์„œ๋„ ํ—ค๋”๊ฐ€ ํ•„์š”ํ•œ ์ƒํ™ฉ์ด ๋์ฃ .


์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•ด๋ณด๋‹ˆ SecuritySchemes์™€ AuthValidator ํด๋ž˜์Šค์˜ validateHttp ๋ฉ”์„œ๋“œ์—์„œ ์ด ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์—ˆ๊ณ ,
type: http๋กœ ์„ค์ •๋œ ์Šคํ‚ด์—์„œ Authorization ํ—ค๋”๋ฅผ ๊ฐ•์ œํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Pull Request๋ฅผ ์ œ์ถœํ–ˆ๊ณ , ์—ฌ๋Ÿฌ ๋ฒˆ์˜ ์ฝ”๋“œ ๋ฆฌ๋ทฐ์™€ ์ˆ˜์ • ๊ณผ์ •์„ ๊ฑฐ์ณ ๋งˆ์นจ๋‚ด Merge๊ฐ€ ๋˜์–ด express-openapi-validator์˜ Contributor๊ฐ€ ๋œ ์—ฌ์ •์„ ์˜ค๋Š˜ ์ƒ์„ธํžˆ ๊ณต์œ ๋“œ๋ฆฌ๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค! ๐ŸŽ‰

๐ŸŽฏ ์ฒซ ๋ฒˆ์งธ ์‹œ๋„: ์ฟ ํ‚ค ์ง€์› ์ถ”๊ฐ€

์ฒ˜์Œ์—๋Š” ๋‹จ์ˆœํžˆ Authorization ํ—ค๋” ์™ธ์—๋„ ์ฟ ํ‚ค๋ฅผ ํ†ตํ•œ ์ธ์ฆ์„ ์ง€์›ํ•˜๋„๋ก ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค:

if (!authHeader && !authCookie) {
  throw Error(`Authorization header or cookie required`);
}

if (type === 'bearer') {
  if (authHeader && !authHeader.includes('bearer')) {
    throw Error(`Authorization header with scheme 'Bearer' required`);
  }

  if (!authHeader && authCookie === undefined) {
    throw Error(`Bearer token required in authorization header or cookie`);
  }
}

๐Ÿ’ก ์ฝ”๋“œ ๋ฆฌ๋ทฐ ํ”ผ๋“œ๋ฐฑ

์ œ๊ฐ€ ์ œ์ถœํ•œ PR์— ๋Œ€ํ•ด ๋ฉ”์ธํ…Œ์ด๋„ˆ๊ป˜์„œ ์ค‘์š”ํ•œ ํ”ผ๋“œ๋ฐฑ์„ ์ฃผ์…จ์Šต๋‹ˆ๋‹ค:

  1. OpenAPI ๋ช…์„ธ์— ๋”ฐ๋ฅด๋ฉด, in: cookie๋กœ ์ง€์ •๋œ ๊ฒฝ์šฐ์™€ ๊ทธ๋ ‡์ง€ ์•Š์€ ๊ฒฝ์šฐ๋ฅผ ๊ตฌ๋ถ„ํ•ด์•ผ ํ•จ
  2. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋„ ๊ฐ๊ฐ์˜ ์ƒํ™ฉ์— ๋งž๊ฒŒ ๋‹ค๋ฅด๊ฒŒ ์ฒ˜๋ฆฌ๋˜์–ด์•ผ ํ•จ
    • ์ผ๋ฐ˜์ ์ธ ๊ฒฝ์šฐ: "Authorization header required"
    • ์ฟ ํ‚ค ์ธ์ฆ์˜ ๊ฒฝ์šฐ: "Cookie authentication required"

๐Ÿ”จ ๋‘ ๋ฒˆ์งธ ์‹œ๋„: ๋ช…์„ธ ๊ธฐ๋ฐ˜ ์ˆ˜์ •

ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ˜์˜ํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ–ˆ์ง€๋งŒ, ์—ฌ๊ธฐ์„œ ๋˜ ํ•˜๋‚˜์˜ ์‹ค์ˆ˜๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ”๋กœ ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ์กด ์ฝ”๋“œ ์Šคํƒ€์ผ์„ ํ•ด์น˜๋Š” ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ํฌํ•จ๋œ ๊ฒƒ์ด์—ˆ์ฃ ..

 

์˜ˆ๋ฅผ ๋“ค์–ด ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋‚˜ ๋“ค์—ฌ์“ฐ๊ธฐ, ๊ด„ํ˜ธ ์œ„์น˜ ๋“ฑ์ด ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ์กด ์Šคํƒ€์ผ๊ณผ ๋‹ฌ๋ž์ฃ .

// ์ œ๊ฐ€ ์ˆ˜์ •ํ•œ ์ฝ”๋“œ
function extractErrorsFromResults(
  results: (SecurityHandlerResult | SecurityHandlerResult[])[],
) {
  return results
    .map((result) => {
      if (Array.isArray(result)) {
        return result.map((it) => it).filter((it) => !it.success);
      }
      return [result].filter((it) => !it.success);
    })
    .flatMap((it) => [...it]);
}

// ์›๋ž˜ ํ”„๋กœ์ ํŠธ ์Šคํƒ€์ผ
function extractErrorsFromResults(results: (SecurityHandlerResult | SecurityHandlerResult[])[]) {
  return results.map(result => {
    if (Array.isArray(result)) {
      return result.map(it => it).filter(it => !it.success);
    }
    return [result].filter(it => !it.success);
  }).flatMap(it => [...it]);
}

 

์ด๋ฅผ ๋น ๋ฅด๊ฒŒ ์›๋ž˜ ํ”„๋กœ์ ํŠธ์˜ ์Šคํƒ€์ผ๋Œ€๋กœ ๋˜๋Œ๋ฆฌ๊ณ  ์ปค๋ฐ‹์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์„œ ๋์ด ์•„๋‹ˆ์—ˆ์ฃ . CI ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ค๋ณด๋‹ˆ ๋‘ ๊ฐ€์ง€ ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

๐Ÿ› ํ…Œ์ŠคํŠธ ์‹คํŒจ ๋ฐœ๊ฒฌ

์ดํ›„ CI์—์„œ ๋‘ ๊ฐ€์ง€ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค:

  1. Basic ์ธ์ฆ์—์„œ ํ—ค๋”๊ฐ€ ์—†์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ
  2. undefined.includes() ํ˜ธ์ถœ๋กœ ์ธํ•œ ์—๋Ÿฌ
AssertionError: expected 'Cannot read properties of undefined (…' 
to equal 'Authorization header required'

 

๊ฒฐ๋ก ์€ authHeader๊ฐ€ undefined์ผ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฌธ์ œ๋กœ ์ธํ•œ ํ…Œ์ŠคํŠธ ์‹คํŒจ์˜€์Šต๋‹ˆ๋‹ค.

โœจ ์ตœ์ข… ํ•ด๊ฒฐ์ฑ…

๋ชจ๋“  ํ”ผ๋“œ๋ฐฑ๊ณผ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ตœ์ข… ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค:

private validateHttp(): void {
  if (['http'].includes(scheme.type.toLowerCase())) {
    const authHeader = req.headers['authorization']?.toLowerCase();
    const authCookie = req.cookies[scheme.name] || req.signedCookies?.[scheme.name];

    const type = scheme.scheme?.toLowerCase();
    if (type === 'bearer') {
      if (authHeader && !authHeader.includes('bearer')) {
        throw Error(`Authorization header with scheme 'Bearer' required`);
      }
      
      if (!authHeader && !authCookie) {
        throw Error(scheme.in === 'cookie' 
          ? `Cookie authentication required`
          : `Authorization header required`);
      }
    }

    if (type === 'basic') {
      if (!authHeader) {
        throw Error(`Authorization header required`);
      }
      if (!authHeader.includes('basic')) {
        throw Error(`Authorization header with scheme 'Basic' required`);
      }
    }
  }
}

์ฃผ์š” ๊ฐœ์„ ์‚ฌํ•ญ:

  1. Basic ์ธ์ฆ๊ณผ Bearer ์ธ์ฆ์˜ ๋ช…ํ™•ํ•œ ๊ตฌ๋ถ„
  2. ์ฟ ํ‚ค ๊ธฐ๋ฐ˜ ์ธ์ฆ ์ง€์› (OpenAPI ๋ช…์„ธ ์ค€์ˆ˜)
  3. ์ ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
  4. undefined ์ฒดํฌ ๊ฐ•ํ™”

๐ŸŽ“ ๋ฐฐ์šด ์ 

  1. OpenAPI ๋ช…์„ธ๋ฅผ ๋” ๊ผผ๊ผผํžˆ ์‚ดํŽด๋ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค
  2. ํ”„๋กœ์ ํŠธ์˜ ์ „์ฒด ํ…Œ์ŠคํŠธ ์‹คํ–‰์˜ ์ค‘์š”์„ฑ์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ๊นจ๋‹ฌ์•˜์Šต๋‹ˆ๋‹ค
  3. ์ฝ”๋“œ ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ๋ฅผ ์ค€์ˆ˜ํ•˜๋Š” ๊ฒƒ์˜ ์ค‘์š”์„ฑ์„ ๋ฐฐ์› ์Šต๋‹ˆ๋‹ค

์ด๋ ‡๊ฒŒ ํ•ด์„œ express-openapi-validator์— ์ž‘์€ ๊ธฐ์—ฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์—ˆ๋„ค์š”. ์˜คํ”ˆ์†Œ์Šค ๊ธฐ์—ฌ๋Š” ์–ธ์ œ๋‚˜ ์ƒˆ๋กœ์šด ๋ฐฐ์›€์˜ ๊ธฐํšŒ์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์—๋Š” ๋˜ ์–ด๋–ค ์žฌ๋ฏธ์žˆ๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ฒŒ ๋ ์ง€ ๊ธฐ๋Œ€๊ฐ€ ๋ฉ๋‹ˆ๋‹ค! ๐Ÿ˜Š

React ๊ณต์‹๋ฌธ์„œ!!

๋“ค์–ด๊ฐ€๋ฉฐ

์ตœ๊ทผ Allini ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋ช‡ ๊ฐ€์ง€ ๊ณ ๋ฏผ์ด ์ƒ๊ฒผ์Šต๋‹ˆ๋‹ค.

over-fetching, waterfall ํ˜„์ƒ์œผ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ์ €ํ•˜, ๊ทธ๋ฆฌ๊ณ  ๋ฌด์—‡๋ณด๋‹ค ๋น ๋ฅธ ์‹œ์žฅ ๊ฒ€์ฆ์„ ์œ„ํ•ด ๋‹ค์–‘ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ฆ๊ฐ€ ๋ฌธ์ œ์˜€์ฃ .

 

์ข‹์€ ๊ฐœ๋ฐœ์ž๋Š” ๋‹จ์ˆœํžˆ ๋ชจ๋“  ๊ฒƒ์„ ์†์ˆ˜ ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์ ์ ˆํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ ํƒํ•˜๊ณ  ์กฐ์žกํ•ด ๋น ๋ฅด๊ฒŒ ์ œํ’ˆ์„ ๋งŒ๋“ค์–ด๋‚ด๋Š” ๋Šฅ๋ ฅ์ด ์ค‘์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ์ด๋Ÿฌํ•œ ์ ‘๊ทผ์€ ํ•„์—ฐ์ ์œผ๋กœ ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ฆ๊ฐ€๋ผ๋Š” ๋ฌธ์ œ๋ฅผ ๋™๋ฐ˜ํ•˜๊ฒŒ ๋˜์—ˆ๊ณ , ์ด๋Š” ์ œ๊ฐ€ React ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์— ๊ด€์‹ฌ์„ ๊ฐ–๊ฒŒ ๋œ ์ฃผ๋œ ๊ณ„๊ธฐ๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

 

1. React ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ํ˜„์žฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€

ํ˜„์žฌ ์ƒํƒœ

  • React 18 ๋ฒ„์ „: ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๋ฏธ์ง€์›
  • React 19 RC(Release Candidate): ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์ง€์›
 

ํ•˜์ง€๋งŒ ํ˜„์žฌ React 18.2.0์„ ์‚ฌ์šฉ์ค‘์ธ ํ”„๋กœ์ ํŠธ์—์„œ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๊ธฐ๋Š” ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

React 19๋กœ์˜ ์—…๊ทธ๋ ˆ์ด๋“œ๋ฅผ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์ง€๋งŒ, RC ๋ฒ„์ „์ด๊ธฐ ๋•Œ๋ฌธ์— ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ์˜ ์‚ฌ์šฉ์€ ์‹ ์ค‘ํžˆ ๊ฒฐ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

2. SSR๊ณผ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ๊ด€๊ณ„

ํ•™์Šต ์ดˆ๊ธฐ์— ๊ฐ€์žฅ ํ—ท๊ฐˆ๋ ธ๋˜ ๊ฐœ๋…์ž…๋‹ˆ๋‹ค!

SSR (Server-Side Rendering)

  • ๋ชฉ์ : ์ดˆ๊ธฐ ํŽ˜์ด์ง€ ๋กœ๋“œ ์„ฑ๋Šฅ ๊ฐœ์„  ๋ฐ SEO ์ตœ์ ํ™”
  • ๋™์ž‘ ๋ฐฉ์‹:
    • ์„œ๋ฒ„์—์„œ ์ „์ฒด ํŽ˜์ด์ง€์˜ ์ดˆ๊ธฐ HTML์„ ์ƒ์„ฑ
    • ํด๋ผ์ด์–ธํŠธ๋กœ HTML๊ณผ JS ๋ฒˆ๋“ค์„ ์ „์†ก
    • ํด๋ผ์ด์–ธํŠธ์—์„œ hydration ๊ณผ์ •์„ ํ†ตํ•ด ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•œ ์•ฑ์œผ๋กœ ์ „ํ™˜

์ด์ „ React ๊ณต์‹ ๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด, React์˜ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ๋ฉ”์„œ๋“œ๋“ค์€ ํฌ๊ฒŒ ์„ธ ๊ฐ€์ง€ ํ™˜๊ฒฝ์œผ๋กœ ๊ตฌ๋ถ„๋ฉ๋‹ˆ๋‹ค:

 

Node.js Streams ํ™˜๊ฒฝ์šฉ:

  • renderToPipeableStream()
  • renderToNodeStream() (Deprecated)
  • renderToStaticNodeStream()

Web Streams ํ™˜๊ฒฝ์šฉ (๋ธŒ๋ผ์šฐ์ €, Deno, modern edge runtimes):

  • renderToReadableStream()

์ŠคํŠธ๋ฆผ์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ™˜๊ฒฝ์šฉ:

  • renderToString()
  • renderToStaticMarkup()

๊ธฐ์กด SSR์˜ ๋™์ž‘ ๋ฐฉ์‹์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

// ๊ธฐ์กด SSR์˜ ๋™์ž‘ ๋ฐฉ์‹
// 1. ์„œ๋ฒ„์—์„œ ์ดˆ๊ธฐ ๋ Œ๋”๋ง
const app = ReactDOMServer.renderToPipeableStream(<App />);
// ํ•œ๋ฒˆ์— ์ „์ฒด HTML์„ ์ƒ์„ฑํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „์†ก

// 2. HTML ๋ฌธ์„œ์— ์‚ฝ์ž…
const html = `
  <!doctype html>
  <html>
    <body>
      <div id="root">${app}</div>
      <script src="/bundle.js"></script>
    </body>
  </html>
`;

 

 

๊ณต์‹ ๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด, renderToPipeableStream์„ ํ†ตํ•ด ์ดˆ๊ธฐ ์…ธ(shell)์„ ๋ Œ๋”๋งํ•˜๊ณ  ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

renderToPipeableStream์˜ ๊ธฐ๋ณธ ๊ฐœ๋…์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • Node.js ํ™˜๊ฒฝ์—์„œ React ํŠธ๋ฆฌ๋ฅผ HTML ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” API์ž…๋‹ˆ๋‹ค.
  • ์ ์ง„์ ์ธ ๋กœ๋”ฉ์ด ๊ฐ€๋Šฅํ•œ ์ŠคํŠธ๋ฆฌ๋ฐ SSR์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์‹œ ์ฝ”๋“œ

// ๋ฐœ์ „๋œ SSR (์ŠคํŠธ๋ฆฌ๋ฐ ๋ฐฉ์‹)
const { pipe } = renderToPipeableStream(<App />, {
  bootstrapScripts: ['/main.js'],
  onShellReady() {
    // 1. ๋จผ์ € ๊ธฐ๋ณธ ๋ผˆ๋Œ€(shell)๋ฅผ ๋ณด๋ƒ„
    // ์ดˆ๊ธฐ shell(Suspense ๊ฒฝ๊ณ„ ์œ„์˜ ์ฝ˜ํ…์ธ )์ด ์ค€๋น„๋˜๋ฉด ์ŠคํŠธ๋ฆฌ๋ฐ ์‹œ์ž‘

    pipe(response);
  },
  onAllReady() {
    // ๋ชจ๋“  ์ฝ˜ํ…์ธ ๊ฐ€ ์ค€๋น„๋˜๋ฉด ํ˜ธ์ถœ (ํฌ๋กค๋Ÿฌ๋‚˜ ์ •์  ์ƒ์„ฑ์šฉ)
  }
});

 

๊ธฐ์กด์˜ renderToString์€ ์ œํ•œ์ ์ธ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ๋ฐ˜๋ฉด, renderToPipeableStream์€ Suspense๋ฅผ ์™„์ „ํžˆ ์ง€์›ํ•˜๊ณ  HTML ์ŠคํŠธ๋ฆฌ๋ฐ์ด ๊ฐ€๋Šฅํ•œ ๋” ๋ฐœ์ „๋œ ํ˜•ํƒœ์˜ SSR ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

 

์ฃผ์š” ์ฐจ์ด์ :

  • ๊ธฐ์กด SSR: ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค ๋ชจ์•„์„œ ํ•œ๋ฒˆ์— ๋ณด๋ƒ„ (๋А๋ฆผ)
  • ์ƒˆ๋กœ์šด SSR:
    1. ๋จผ์ € ํŽ˜์ด์ง€์˜ ๊ธฐ๋ณธ ๋ผˆ๋Œ€๋ฅผ ๋ณด๋‚ด๊ณ 
    2. ๋‚˜๋จธ์ง€ ๋‚ด์šฉ์€ ์ค€๋น„๋˜๋Š” ๋Œ€๋กœ ์กฐ๊ธˆ์”ฉ ๋ณด๋ƒ„ (๋” ๋น ๋ฆ„)

 

์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, ๊ธฐ์กด SSR์€ "์‹๋‹น์—์„œ ๋ชจ๋“  ์š”๋ฆฌ๊ฐ€ ์™„์„ฑ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ ํ•œ๋ฒˆ์— ์„œ๋น™"ํ•˜๋Š” ๊ฒƒ์ด๊ณ , ์ƒˆ๋กœ์šด ๋ฐฉ์‹์€ "์ค€๋น„๋œ ๋ฉ”๋‰ด๋ถ€ํ„ฐ ๋จผ์ € ์„œ๋น™ํ•˜๊ณ , ๋‚˜๋จธ์ง€๋Š” ์™„์„ฑ๋˜๋Š” ๋Œ€๋กœ ๊ฐ€์ ธ๋‹ค์ฃผ๋Š”" ๊ฒƒ๊ณผ ๋น„์Šทํ•ฉ๋‹ˆ๋‹ค.

 

React ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ (RSC)

  • ๋ชฉ์ : ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ตœ์ ํ™” ๋ฐ ์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค ์ง์ ‘ ์ ‘๊ทผ
  • ๋™์ž‘ ๋ฐฉ์‹:
    • ํŠน์ • ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰๋˜๋„๋ก ์ง€์ •
    • ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง๋œ ๊ฒฐ๊ณผ๋งŒ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „์†ก
    • JS ๋ฒˆ๋“ค์— ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์ฝ”๋“œ๋Š” ๋ฏธํฌํ•จ

๋‘ ๊ธฐ์ˆ ์˜ ์ƒํ˜ธ๋ณด์™„์  ๊ด€๊ณ„

SSR๊ณผ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์„œ๋กœ๋ฅผ ๋Œ€์ฒดํ•˜๋Š” ๊ธฐ์ˆ ์ด ์•„๋‹Œ, ๋ณด์™„ํ•˜๋Š” ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค:

  1. ์—ญํ•  ๋ถ„๋‹ด
    • SSR: ์ดˆ๊ธฐ ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ „์ฒด HTML ์ƒ์„ฑ
    • ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ: ํŠน์ • ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ์„œ๋ฒ„ ์‹คํ–‰ ๋ฐ ๋ฒˆ๋“ค ํฌ๊ธฐ ์ตœ์ ํ™”

 

3. ํ”„๋ ˆ์ž„์›Œํฌ๋“ค์˜ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„

Next.js์˜ ์ ‘๊ทผ

Next.js๋Š” ์ž์ฒด์ ์ธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„์„ ํ†ตํ•ด React์˜ ์‹คํ—˜์  ๊ธฐ๋Šฅ์„ ์•ˆ์ •์ ์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  1. React ์ฝ”์–ด ํŒ€๊ณผ Next.js ํŒ€์˜ ํ˜‘๋ ฅ
    • React ์ฝ”์–ด ํŒ€์˜ ์ฃผ์š” ๋ฉค๋ฒ„๋“ค(Joe Savona, Sebastian Markbåge ๋“ฑ)์ด Vercel(Next.js)์— ํ•ฉ๋ฅ˜
    • ์ด๋ฅผ ํ†ตํ•ด React์˜ ์‹คํ—˜์  ๊ธฐ๋Šฅ์„ Next.js์—์„œ ๋จผ์ € ์•ˆ์ •์ ์œผ๋กœ ๊ตฌํ˜„ ๊ฐ€๋Šฅ
  2. ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐœ์ „ ๊ณผ์ •
    • 2020๋…„: React ํŒ€์ด ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋… ์ฒซ ๋ฐœํ‘œ
    • React 18: ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๊ธฐ์ดˆ ์ธํ”„๋ผ ํฌํ•จ, but ์™„์ „ํ•œ ๊ตฌํ˜„์€ ์•„๋‹˜
    • Next.js 13: App Router์™€ ํ•จ๊ป˜ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์ „๋ฉด ๋„์ž…

๊ธฐ์ˆ ์  ๊ตฌํ˜„ ๋ฐฉ์‹

React์˜ Server Components์— ๋Œ€ํ•œ ๊ณต์‹๋ฌธ์„œ๋ฅผ ๋ณด๋ฉด,

๋ฌธ์„œ์—์„œ๋Š” Server Components ๊ตฌํ˜„์— ๋Œ€ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์–ธ๊ธ‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:

"To support React Server Components as a bundler or framework, we recommend pinning to a specific React version, or using the Canary release. We will continue working with bundlers and frameworks to stabilize the APIs used to implement React Server Components in the future."

์ด๋Š” Server Components ๊ตฌํ˜„์„ ์œ„ํ•œ ๋ฒˆ๋“ค๋Ÿฌ API๊ฐ€ ์•„์ง ์™„์ „ํžˆ ์•ˆ์ •ํ™”๋˜์ง€ ์•Š์•˜์Œ์„ ์‹œ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

 

Next.js๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค:

// next.config.js
{
  webpack: (config, { isServer }) => {
    if (isServer) {
      // ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์ฒ˜๋ฆฌ
      config.plugins.push(new webpack.DefinePlugin({
        // marked, sanitize-html ๊ฐ™์€ ์„œ๋ฒ„ ์ „์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด
        // ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ๋˜์ง€ ์•Š๋„๋ก ์ฒ˜๋ฆฌ
        'process.env.SERVER_ONLY': JSON.stringify(true)
      }));
      
      config.module.rules.push({
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react'],
            plugins: [
              // async ์ปดํฌ๋„ŒํŠธ ์ง€์›์„ ์œ„ํ•œ ๋ณ€ํ™˜
              '@babel/plugin-syntax-top-level-await',
              // Server Component์˜ import ๊ตฌ๋ฌธ ์ฒ˜๋ฆฌ
              ['@babel/plugin-transform-modules-commonjs', {
                importInterop: 'node'
              }]
            ]
          }
        }
      });
    } else {
      // ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ์ฒ˜๋ฆฌ
      config.module.rules.push({
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react'],
            plugins: [
              // "use client" ์ง€์‹œ์–ด๊ฐ€ ์žˆ๋Š” ํŒŒ์ผ๋งŒ ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ
              ['./plugins/client-directive', {}],
              // Server Component์˜ ์ถœ๋ ฅ์„ ํด๋ผ์ด์–ธํŠธ์—์„œ hydrateํ•˜๊ธฐ ์œ„ํ•œ ๋ณ€ํ™˜
              ['./plugins/server-reference', {}]
            ]
          }
        }
      });
    }
    return config;
  }
}

// Server Component ๋ Œ๋”๋ง ํ•จ์ˆ˜
async function renderServerComponent(Component, props) {
  // ๋ฌธ์„œ์— ์–ธ๊ธ‰๋œ ๊ฒƒ์ฒ˜๋Ÿผ build time ๋˜๋Š” request time์— ์‹คํ–‰ ๊ฐ€๋Šฅ
  const stream = await ReactServerDOM.renderToReadableStream(
    <Component {...props} />,
    // ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์˜ ๋ฒˆ๋“ค ์ •๋ณด๋ฅผ ๋‹ด์€ ๋งคํ•‘
    webpackMap
  );
  
  // React์˜ Flight ํฌ๋งท์œผ๋กœ ์ง๋ ฌํ™”
  return encodeRSCPayload(stream);
}

 

์ด ๊ตฌํ˜„์€ ๊ณต์‹ ๋ฌธ์„œ์˜ ๋‹ค์Œ ๊ฐœ๋…๋“ค์„ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค:

  1. "Server Components are a new type of Component that renders ahead of time, before bundling, in an environment separate from your client app or SSR server."
    • webpack ์„ค์ •์—์„œ isServer ํ”Œ๋ž˜๊ทธ๋กœ ํ™˜๊ฒฝ์„ ๋ถ„๋ฆฌ
  2. "Server Components can run once at build time on your CI server, or they can be run for each request using a web server."
    • renderServerComponent ํ•จ์ˆ˜๊ฐ€ ์ด ๋‘ ๊ฐ€์ง€ ์ผ€์ด์Šค๋ฅผ ๋ชจ๋‘ ์ง€์›
  3. "Async Components are a new feature of Server Components that allow you to await in render."
    • ์„œ๋ฒ„ ์ธก babel ์„ค์ •์— @babel/plugin-syntax-top-level-await ํฌํ•จ

ํ•ต์‹ฌ ๊ตฌํ˜„ ์š”์†Œ

  1. RSC ํฌ๋งท
    • ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ๋ Œ๋”๋ง ๊ฒฐ๊ณผ๋ฅผ ํŠน๋ณ„ํ•œ ํฌ๋งท์œผ๋กœ ์ง๋ ฌํ™”
    • JSON์ด ์•„๋‹Œ ํŠน๋ณ„ํ•œ ๋ฐ”์ด๋„ˆ๋ฆฌ ํ˜•์‹ ์‚ฌ์šฉ
    • React Flight ์•„ํ‚คํ…์ฒ˜ ํ™œ์šฉ
  2. ๋ฒˆ๋“ค๋ง ์ตœ์ ํ™”
    • ์„œ๋ฒ„ ์ „์šฉ ์ฝ”๋“œ๋ฅผ ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์—์„œ ์ œ์™ธ
    • ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋งŒ ์„ ํƒ์ ์œผ๋กœ ๋ฒˆ๋“ค๋ง
  3. ์ŠคํŠธ๋ฆฌ๋ฐ ์ง€์›
    • React Suspense์™€ ํ†ตํ•ฉ
    • ์ ์ง„์ ์ธ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ๊ตฌํ˜„

์ดํ•ด๋ฅผ ์œ„ํ•ด ๊ธฐ๋ณธ์ ์ธ ํ‹€์— ๋งž์ถฐ ๊ฐ„์†Œํ™”ํ•ด ๊ตฌํ˜„ํ•ด ๋ดค์Šต๋‹ˆ๋‹ค!
ํ˜น์‹œ ํ‹€๋ฆฐ ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ํŽธํ•˜๊ฒŒ ๋Œ“๊ธ€ ์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค!

Remix์˜ ์ ‘๊ทผ

Remix๋Š” ๋ผ์šฐํ„ฐ ๊ธฐ๋ฐ˜์˜ ์„œ๋ฒ„ ์ค‘์‹ฌ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ฑ„ํƒํ•˜์—ฌ ์œ ์‚ฌํ•œ ์ด์ ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

// Remix์˜ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋กœ์ง ์˜ˆ์‹œ
export async function loader({ request }) {
  const data = await getProducts();
  return json(data);
}

export default function Products() {
  const products = useLoaderData();
  return (
    <div>
      <h1>Products</h1>
      <ProductList products={products} />
    </div>
  );
}

 

4. Next.js ์•ฑ ๋ผ์šฐํ„ฐ์™€ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ๊ด€๊ณ„

Next.js์˜ ์•ฑ ๋ผ์šฐํ„ฐ๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ฑ„ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.  ๋งค์šฐ ์ค‘์š”ํ•œ ์•„ํ‚คํ…์ฒ˜์  ๊ฒฐ์ •์ด์ฃ ?!

๊ธฐ๋ณธ ๊ตฌ์กฐ

// app/layout.jsx (์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ)
export default function RootLayout({ children }) {
  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

// app/page.jsx (์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ)
async function Home() {
  const data = await getData(); // ์„œ๋ฒ„์—์„œ ์ง์ ‘ ๋ฐ์ดํ„ฐ fetch
  
  return (
    <main>
      <h1>Welcome</h1>
      <ClientComponent data={data} />
    </main>
  );
}

// components/client-component.jsx
'use client';

function ClientComponent({ data }) {
  // ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ์ธํ„ฐ๋ž™์…˜์ด ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ
  return <div>{/* ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ UI */}</div>;
}

 

์ฃผ์š” ํŠน์ง•

์ž๋™ ์ฝ”๋“œ ๋ถ„ํ• 

// app/products/page.jsx
import { ProductList } from './components/product-list';

async function ProductsPage() {
  const products = await fetchProducts(); // ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰
  return <ProductList products={products} />;
}

// ์ด ์ปดํฌ๋„ŒํŠธ์™€ ๊ด€๋ จ๋œ ์„œ๋ฒ„ ๋กœ์ง์€ ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ๋˜์ง€ ์•Š์Œ

 

 

์ŠคํŠธ๋ฆฌ๋ฐ๊ณผ Suspense ํ†ตํ•ฉ

// app/dashboard/page.jsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <Suspense fallback={<LoadingUI />}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}
 

๋งˆ์น˜๋ฉฐ

ํ˜„์žฌ ์ œ๊ฐ€ ์ง„ํ–‰ ์ค‘์ธ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” React 18.2.0์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๊ธฐ๋Š” ์–ด๋ ค์šด ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๊ณ ๊ฐ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์€ ํ›„ Next.js๋กœ์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ๊ณ ๋ คํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ์ด์ ์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ๊ธฐํšŒ๊ฐ€ ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๋ฉฐ, ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ตœ์ ํ™”์™€ ์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค ํ™œ์šฉ ์ธก๋ฉด์—์„œ ํฐ ์ด์ ์„ ์ œ๊ณตํ•  ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€๋ฉ๋‹ˆ๋‹ค!

 

 

 

๐Ÿ“ƒ ์ฐธ๊ณ  ๋ฌธํ—Œ  
React 19 RC!!!
React RFC ๊นƒํ—™
Next.js App Router RFC ๊นƒํ—™
Next.js Server Components ๊ณต์‹๋ฌธ์„œ
React CoreํŒ€ Dan Abramov์˜ Server Components ์†Œ๊ฐœ ๋ฐœํ‘œ
์นด์นด์˜คํŽ˜์ด - React Server Components
์š”์ฆ˜ IT - React Server Components
freeCodeCamp - React Server Components

 

+ Recent posts