๋ฐ˜๋ณต๋˜๋Š” ๊ธฐ์ค€ ๋ฌธ์„œ๋Š” ์บ์‹œ์— ๋‘๊ณ , ์‚ฌ๋ฃŒ๋ณ„ ์ž…๋ ฅ๋งŒ ์ƒˆ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” 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 ์ „๋žต์„ ๊ฒ€์ฆ ๊ทœ์น™ ์œ„์—์„œ ๋‹ค๋ค„์•ผ ํ•œ๋‹ค.

 

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

 

์ฐธ๊ณ ๋ฌธ์„œ

 

 

AI๋กœ ์ƒ์„ฑํ•ด ๋ดค์Šต๋‹ˆ๋‹ค. ์˜คํƒ€๊ฐ€ ์žˆ์ง€๋งŒ ๋‚˜์˜์ง€ ์•Š๋„ค์š”!

 

์ดํ„ฐ๋Ÿฌ๋ธ”(Iterable)

์ •์˜

  • ๋ฐ˜๋ณต ๊ฐ€๋Šฅํ•œ ๊ฐ์ฒด
  • `for...of`, `์Šคํ”„๋ ˆ๋“œ(...)`, `๋””์ŠคํŠธ๋Ÿญ์ฒ˜๋ง ํ• ๋‹น` ๋“ฑ์— ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  • ์ดํ„ฐ๋Ÿฌ๋ธ”์€ Symbol.iterator ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ, ์ด ๋ฉ”์„œ๋“œ๊ฐ€ ์ดํ„ฐ๋ ˆ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜

 

์˜ˆ์‹œ

const arr = [1, 2, 3]; // ๋ฐฐ์—ด (๋Œ€ํ‘œ์ ์ธ ์ดํ„ฐ๋Ÿฌ๋ธ”) 
const str = "hi"; // ๋ฌธ์ž์—ด๋„ ์ดํ„ฐ๋Ÿฌ๋ธ” 
const set = new Set([1, 2, 3]); // Set๋„ ์ดํ„ฐ๋Ÿฌ๋ธ” 
for (const val of arr) console.log(val); // 1, 2, 3



์ดํ„ฐ๋ ˆ์ดํ„ฐ(Iterator)

์ •์˜

  • ์ดํ„ฐ๋Ÿฌ๋ธ”์„ ์‹ค์ œ๋กœ ์ˆœํšŒํ•˜๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜
  • `next()` ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ `{ done: Boolean, value: any }`์™€ ๊ฐ™์€ ํ˜•ํƒœ์˜ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜
  • `done: true` ๊ฐ€ ๋  ๋•Œ๊นŒ์ง€ ์ˆœํšŒ

 

์˜ˆ์‹œ

const arr = [1, 2, 3]; 
const iterator = arr[Symbol.iterator](); 
console.log(iterator.next()); // { done: false, value: 1 } 
console.log(iterator.next()); // { done: false, value: 2 }
console.log(iterator.next()); // { done: false, value: 3 } 
console.log(iterator.next()); // { done: true, value: undefined }



 

์ดํ„ฐ๋Ÿฌ๋ธ”  ↔  ๋ฐฐ์—ด ๋ฉ”์„œ๋“œ ํ™œ์šฉ

  • ์ดํ„ฐ๋Ÿฌ๋ธ”์„ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ํ•ด `map`, `filter` ๊ฐ™์€ ๊ณ ์ฐจ ํ•จ์ˆ˜ ํ™œ์šฉ ๊ฐ€๋Šฅ
const set = new Set([1, 2, 3]); 
const doubled = [...set].map(x => x * 2); // [2, 4, 6]
 

 

์žฅ์ 

  1. ์ผ๊ด€๋œ ๋ฐ˜๋ณต ํ”„๋กœํ† ์ฝœ ์ œ๊ณต
    → for...of, ์Šคํ”„๋ ˆ๋“œ, ๋””์ŠคํŠธ๋Ÿญ์ฒ˜๋ง ๋“ฑ ์–ด๋””์„œ๋“  ํ™œ์šฉ ๊ฐ€๋Šฅ
  2. ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ํ™•์žฅ ๊ฐ€๋Šฅ
    → ํŠธ๋ฆฌ, ๊ทธ๋ž˜ํ”„ ๊ฐ™์€ ์‚ฌ์šฉ์ž ์ •์˜ ๊ตฌ์กฐ์—์„œ๋„ ์›ํ•˜๋Š” ์ˆœ์„œ(DFS, BFS ๋“ฑ)๋กœ ์ˆœํšŒ ์ •์˜ ๊ฐ€๋Šฅ
  3. ์œ ์—ฐ์„ฑ
    → ์–ด๋–ค ์ˆœ์„œ, ์–ด๋–ค ์กฐ๊ฑด์œผ๋กœ๋“  ๋ฐ˜๋ณต ๋™์ž‘์„ ์ง์ ‘ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Œ

 

 

ํ•œ๊ณ„

  1. ์„ฑ๋Šฅ ๋ฌธ์ œ
    • ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ ์ˆœํšŒ ์‹œ next() ํ˜ธ์ถœ ๋น„์šฉ ๋ˆ„์ 
    • ๋ณต์žกํ•œ ๊ตฌ์กฐ์ผ์ˆ˜๋ก ์ ‘๊ทผ ๋น„์šฉ ์ฆ๊ฐ€
  2. ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€
    • ์ดํ„ฐ๋ ˆ์ดํ„ฐ๋Š” ์ƒํƒœ(state)๋ฅผ ์ €์žฅํ•ด์•ผ ํ•˜๋ฏ€๋กœ ๋™์‹œ์— ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ์“ฐ๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋ถ€๋‹ด
// ์ƒํƒœ ๋ˆ„์  ์˜ˆ์‹œ
  [Symbol.iterator]() {
    this.current = this.from;
    return this;
  },

 

 

  3. ๋ฐฐ์—ด ๋ฉ”์„œ๋“œ ์ง์ ‘ ์‚ฌ์šฉ ๋ถˆ๊ฐ€

  • ์ดํ„ฐ๋Ÿฌ๋ธ” ์ž์ฒด๋Š” map, filter ๊ฐ™์€ ๋ฐฐ์—ด ๋ฉ”์„œ๋“œ๋ฅผ ๋ฐ”๋กœ ์“ธ ์ˆ˜ ์—†์Œ → ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ•จ

 

๊ฐœ์„ ๋ฐฉ์•ˆ

  1. ์ œ๋„ˆ๋ ˆ์ดํ„ฐ(Generator)
    • ์ดํ„ฐ๋Ÿฌ๋ธ”+์ดํ„ฐ๋ ˆ์ดํ„ฐ๋ฅผ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌธ๋ฒ•
    • yield๋ฅผ ์‚ฌ์šฉํ•ด ํ•„์š”ํ•  ๋•Œ๋งŒ ๊ฐ’์„ ์ƒ์„ฑ (lazy evaluation)
    • ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ 
    • `function* numbers() { yield 1; yield 2; yield 3; } for (const n of numbers()) { console.log(n); // 1, 2, 3 }`
  2. Lazy Evaluation ๊ธฐ๋ฒ• ํ™œ์šฉ
    • ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ๊บผ๋ฒˆ์— ๋ฉ”๋ชจ๋ฆฌ์— ์˜ฌ๋ฆฌ์ง€ ์•Š๊ณ , ํ•„์š”ํ•œ ์ˆœ๊ฐ„์—๋งŒ ๊ณ„์‚ฐ/๋ฐ˜ํ™˜

 

์œ ์‚ฌ ๋ฐฐ์—ด(array-like object)

์ •์˜

  • ์ธ๋ฑ์Šค์™€ `length` ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์–ด ๋ฐฐ์—ด์ฒ˜๋Ÿผ ๋ณด์ด์ง€๋งŒ
  • `next()` ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ `{ done: Boolean, value: any }`์™€ ๊ฐ™์€ ํ˜•ํƒœ์˜ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜
  • ๋ฐ˜๋“œ์‹œ `Symbol.iterator`๊ฐ€ ๊ตฌํ˜„๋˜์–ด ์žˆ๋Š” ๊ฒƒ์€ ์•„๋‹˜ → for...of, ์Šคํ”„๋ ˆ๋“œ ๋ถˆ๊ฐ€

์˜ˆ์‹œ

const obj = {0: 'a', 1: 'b', length: 2};
console.log(obj[0]); // 'a'
console.log(obj.length); // 2
// [...obj] // Error! ์œ ์‚ฌ ๋ฐฐ์—ด์€ ์Šคํ”„๋ ˆ๋“œ ๋ถˆ๊ฐ€

 

๋ณ€ํ™˜ ๋ฐฉ๋ฒ•

Array.from(obj); // ['a','b']

 

์ฃผ์˜ํ•  ์ 

  • ์œ ์‚ฌ ๋ฐฐ์—ด๊ณผ ์ดํ„ฐ๋Ÿฌ๋ธ”์„ ํ—ท๊ฐˆ๋ฆฌ๊ธฐ ์‰ฌ์›€
  • ์œ ์‚ฌ ๋ฐฐ์—ด์ด๋ฉด์„œ ์ดํ„ฐ๋Ÿฌ๋ธ”์ธ ๊ฒฝ์šฐ๋„ ์กด์žฌ
    • ์˜ˆ: arguments, NodeList, HTMLCollection ๋“ฑ
    • ์ด๋Ÿฐ ๊ฒฝ์šฐ๋Š” for...of๋‚˜ ์Šคํ”„๋ ˆ๋“œ(...) ์‚ฌ์šฉ ๊ฐ€๋Šฅ

๊ด€๊ณ„ ์ •๋ฆฌ

์ดํ„ฐ๋Ÿฌ๋ธ”(Iterable)
 โ””โ”€ Symbol.iterator ๋ฉ”์„œ๋“œ ์กด์žฌ
      โ””โ”€ ๋ฐ˜ํ™˜ → ์ดํ„ฐ๋ ˆ์ดํ„ฐ(Iterator)
             โ””โ”€ next() ํ˜ธ์ถœ๋กœ ์‹ค์ œ ๋ฐ˜๋ณต ์ˆ˜ํ–‰
                 โ””โ”€ { value, done } ๋ฐ˜ํ™˜

์œ ์‚ฌ ๋ฐฐ์—ด(array-like)
 โ””โ”€ ์ธ๋ฑ์Šค + length๋งŒ ์กด์žฌ
      โ””โ”€ Symbol.iterator ์—†์œผ๋ฉด for...of/์Šคํ”„๋ ˆ๋“œ ๋ถˆ๊ฐ€
      โ””โ”€ Symbol.iterator ์žˆ์œผ๋ฉด ์ดํ„ฐ๋Ÿฌ๋ธ”์ฒ˜๋Ÿผ ์‚ฌ์šฉ ๊ฐ€๋Šฅ (์˜ˆ: arguments, NodeList)

 

 

 

์ •๋ฆฌ

  • ์ดํ„ฐ๋Ÿฌ๋ธ”(Iterable): “๋ฐ˜๋ณตํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด” (๋ฐฐ์—ด, ๋ฌธ์ž์—ด, Set, Map ๋“ฑ)
  • ์ดํ„ฐ๋ ˆ์ดํ„ฐ(Iterator): “์‹ค์ œ๋กœ ๋ฐ˜๋ณต์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋„๊ตฌ” (next() ์‚ฌ์šฉ)
  • ๊ด€๊ณ„: ์ดํ„ฐ๋Ÿฌ๋ธ”์€ Symbol.iterator๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•˜๊ณ , ์ด ๋ฉ”์„œ๋“œ๊ฐ€ ์ดํ„ฐ๋ ˆ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜
  • ๊ฐ•์ : ์ผ๊ด€๋œ ๋ฐ˜๋ณต ํ”„๋กœํ† ์ฝœ, ๋‹ค์–‘ํ•œ ๊ตฌ์กฐ ์ง€์›, ์œ ์—ฐํ•œ ์ˆœํšŒ
  • ํ•œ๊ณ„: ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ์—์„œ ์„ฑ๋Šฅ·๋ฉ”๋ชจ๋ฆฌ ๋ถ€๋‹ด
  • ๋ณด์™„: ์ œ๋„ˆ๋ ˆ์ดํ„ฐ์™€ lazy evaluation ํ™œ์šฉ

 

 

๐Ÿ“ƒ ์ฐธ๊ณ  ๋ฌธํ—Œ  
์ฝ”์–ด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ
Inpa Dev - ์ดํ„ฐ๋Ÿฌ๋ธ” & ์ดํ„ฐ๋ ˆ์ดํ„ฐ
F-Lab - ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ์˜ ๋ฐ˜๋ณต ํ”„๋กœํ† ์ฝœ ์ดํ•ดํ•˜๊ธฐ

 

TOAST UI - ํ„ฐ์น˜์™€ ํด๋ฆญ ์ด๋ฒคํŠธ ๊ธ€์—์„œ ์ƒ๊ธด ์˜๋ฌธ์„ ์ •๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค.

 

ํ„ฐ์น˜ & ํด๋ฆญ ?!

 

ํ„ฐ์น˜ ์ด๋ฒคํŠธ๋ฅผ ๋‹ฌ์•˜๋Š”๋ฐ ์™œ ํด๋ฆญ ์ด๋ฒคํŠธ๊นŒ์ง€ ๋ฐœ์ƒํ•˜๋‚˜์š”?

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

 

ํ„ฐ์น˜ ์ด๋ฒคํŠธ ํ›„ click ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒŒ ์™œ ํŽธ์˜์ผ๊นŒ์š”?

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

 

React ๋“ฑ์—์„œ๋„ ํด๋ฆญ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ• ๊นŒ์š”?

  • ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” React ๊ธฐ์ค€์œผ๋กœ ์—ฌ๊ธฐ์„œ๋„ ๋ธŒ๋ผ์šฐ์ €์˜ ์ด๋ฒคํŠธ ์œ„์ž„ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ๊ทธ๋Œ€๋กœ ๋”ฐ๋ž์Šต๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ €์˜ ๋„ค์ดํ‹ฐ๋ธŒ ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ์„ ๊ทธ๋Œ€๋กœ ํ™œ์šฉํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋™์ผํ•œ ๋™์ž‘์ด ๋ฐœ์ƒํ•ด์š”.
  • ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ„ฐ์น˜ ์ด๋ฒคํŠธ ํ›„ ์ž๋™์œผ๋กœ ๋งˆ์šฐ์Šค์™€ ํด๋ฆญ ์ด๋ฒคํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋Š”๊ฑด React ์ด์ „ ๋‹จ๊ณ„์—์„œ ์ผ์–ด๋‚ฉ๋‹ˆ๋‹ค.
  • React๋Š” ๋ธŒ๋ผ์šฐ์ €์˜ ๋„ค์ดํ‹ฐ๋ธŒ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์‹ธ๋Š” SyntheticEvent๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด์š”. ํ•˜์ง€๋งŒ ๋ธŒ๋ผ์šฐ์ €์˜ ๊ธฐ๋ณธ ์ด๋ฒคํŠธ ๋™์ž‘์€ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  ์ด๋ฒคํŠธ ์œ„์ž„์„ ์‚ฌ์šฉํ•˜์ง€๋งŒ ์ด๊ฑด ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์„ ์ตœ์ ํ™”ํ•˜๋Š” ๊ฒƒ์ด์ง€ ๋ธŒ๋ผ์šฐ์ €์˜ ๊ธฐ๋ณธ ์ด๋ฒคํŠธ ๋™์ž‘์„ ๋ฐ”๊พธ๋Š”๊ฒŒ ์•„๋‹™๋‹ˆ๋‹ค.
  • ๊ด€๋ จ ํ† ๋ก ์ธ GitHub ์ด์Šˆ #9809๋ฅผ ์ •๋ฆฌํ•ด๋ณด๋ฉด,
    • preventDefault() ๋ฌดํšจํ™”: React์˜ ํ•ฉ์„ฑ ์ด๋ฒคํŠธ์—์„œ e.prevnetDefault()๊ฐ€ ํด๋ฆญ ์ด๋ฒคํŠธ ๋ฐฉ์ง€์— ์‹คํŒจ
    • ๋„ค์ดํ‹ฐ๋ธŒ DOM API๋Š” ์ž‘๋™: addEventListener๋กœ ์ง์ ‘ ๋“ฑ๋กํ•˜๋ฉด preventDefault()๊ฐ€ ์ •์ƒ ๋™์ž‘
    • ๋ธŒ๋ผ์šฐ์ €๋ณ„ ์ฐจ์ด: Chrome, Firefox, Safari์—์„œ ๊ฐ๊ฐ ๋‹ค๋ฅด๊ฒŒ ๋™์ž‘ (์ถ”๊ฐ€ ํ™•์ธ์ด ํ•„์š”)
  • ์œ„์˜ ๋‚ด์šฉ์€ ๊ด€๋ จ ์ด์Šˆ๋ฅผ ์ง์ ‘ ๋ณด๊ณ  ์ƒ์„ธ ๋‚ด์šฉ๊ณผ ์ด์œ ๋ฅผ ํ™•์ธํ•ด ๋ณด๋Š”๊ฑธ ์ถ”์ฒœ๋“œ๋ฆฝ๋‹ˆ๋‹ค!
  • ์ด์Šˆ์—์„œ ์ œ์‹œ๋œ ํ•ด๊ฒฐ์ฑ…๋“ค๋„ ์งง๊ฒŒ๋ผ๋„ ์ •๋ฆฌํ•ด ๋ณด๊ฒŸ์Šต๋‹ˆ๋‹ค.
// ๋ฐฉ๋ฒ• 1: ๋„ค์ดํ‹ฐ๋ธŒ DOM API ์‚ฌ์šฉ
componentDidMount() {
  this.elem.addEventListener('touchstart', e => {
    e.preventDefault(); // ์ด๊ฑด ์ž‘๋™ํ•จ
  });
}

// ๋ฐฉ๋ฒ• 2: ํ”Œ๋ž˜๊ทธ ์‚ฌ์šฉ
handleTouchStart(event) {
  this.hasBeenTouchedRecently = true;
  setTimeout(() => { 
    this.hasBeenTouchedRecently = false; 
  }, 500);
}

handleMouseDown(event) {
  if(this.hasBeenTouchedRecently) {
    return; // ํ„ฐ์น˜ ํ›„ ๋งˆ์šฐ์Šค ์ด๋ฒคํŠธ ๋ฌด์‹œ
  }
}

 

 

 

๋ทฐํฌํŠธ ๋ฉ”ํƒ€ ํƒœ๊ทธ์™€ ๋ฐ์Šคํฌํƒ‘/๋ชจ๋ฐ”์ผ ๋™์‹œ ์ง€์›์˜ ๋ฌธ์ œ๋Š” ์—†์„๊นŒ์š”?

 <meta name="viewport" content="width=device-width">
  •  ์œ„ ์ฝ”๋“œ๋ฅผ ๋„ฃ์œผ๋ฉด ๋ชจ๋ฐ”์ผ์—์„œ ํ„ฐ์น˜ ๋”œ๋ ˆ์ด๊ฐ€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.
  • ์ด ํƒœ๊ทธ๋Š” ๋ชจ๋ฐ”์ผ๋งŒ์ด ์•„๋‹ˆ๋ผ ๋ฐ์Šคํฌํƒ‘์— ํฌํ•จํ•ด๋„ ๋ฌด๋ฐฉํ•ด์š”.

 

ํ„ฐ์น˜/ํด๋ฆญ ๋”œ๋ ˆ์ด๋Š” 2025๋…„์—๋„ ๋˜‘๊ฐ™์ด ๋ฐœ์ƒํ• ๊นŒ์š”?

  • ๊ณผ๊ฑฐ์—” 300ms์˜ ํ„ฐ์น˜→ํด๋ฆญ ๋”œ๋ ˆ์ด๊ฐ€ ์žˆ์—ˆ์ง€๋งŒ ํ˜„์žฌ ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ๋ทฐํฌํŠธ ๋ฉ”ํƒ€ ํƒœ๊ทธ๋‚˜ CSS ์„ธํŒ…๋งŒ ์ž˜ ํ•˜๋ฉด ๋”œ๋ ˆ์ด ๋ฌธ์ œ๋Š” ๊ฑฐ์˜ ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

๐Ÿ“ƒ ์ฐธ๊ณ  ๋ฌธํ—Œ  
Chrome ๊ฐœ๋ฐœ์ž ๋ธ”๋กœ๊ทธ - A More Compatible, Smoother Touch
quirksmode

 

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

 

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

 

 

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

 

์˜ค๋Š˜์€ ๋ณ„๋„ ์ปจํ…์ŠคํŠธ๋ฅผ ๊ฐ€์ง„ ์ƒˆ ์ฐฝ์„ ๋„์šฐ๋Š” ๊ฐœ๋ฐœ์„ ํ•˜๋ฉฐ ์•Œ๊ฒŒ๋œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค.

 

window.open

 

์™œ ์•„์ง๋„ window.open์ด ํ•„์š”ํ•œ๊ฐ€?

 

์‹œ์ž‘์ „์— ํŒ์—…๊ณผ ๋ชจ๋‹ฌ์˜ ๋ณธ์งˆ์  ์ฐจ์ด๋ถ€ํ„ฐ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

ํŒ์—…์ฐฝ

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

๋ชจ๋‹ฌ์ฐฝ

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

 

React์—์„œ๋Š” ์ƒํƒœ(state)๋ฅผ ํ™œ์šฉํ•œ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์œผ๋กœ ํŒ์—…์„ ๊ตฌํ˜„ํ•˜๊ฑฐ๋‚˜, Toss์˜ ์˜ค๋ฒ„๋ ˆ์ด ํ‚คํŠธ(Overlay Kit)๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ๋„ ์—ฌ์ „ํžˆ window.open()์ด ํŽธ๋ฆฌํ•˜๊ฑฐ๋‚˜ ํ•„์š”ํ•œ ์ƒํ™ฉ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

 

  1. ์™ธ๋ถ€ ๋„๋ฉ”์ธ๊ณผ์˜ ์—ฐ๋™: OAuth ์ธ์ฆ, ๊ฒฐ์ œ ์‹œ์Šคํ…œ ์—ฐ๋™ ๋“ฑ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์˜ ํŽ˜์ด์ง€๋ฅผ ์—ด์–ด์•ผ ํ•  ๋•Œ
    • OAuth ์ธ์ฆ์ด๋‚˜ ์™ธ๋ถ€ ๊ฒฐ์ œ ์‹œ์Šคํ…œ ์—ฐ๋™ ์‹œ, ๋ณด์•ˆ์ด๋‚˜ ์‚ฌ์šฉ์ž ์ธ์ฆ ํ›„ ์ฝœ๋ฐฑ๋“ฑ์˜ UX ์ ์ธ ์ด์œ ๋กœ ๋ณ„๋„ ์ฐฝ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
    • ์ด๋•Œ window.open()์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ถ€๋ชจ-์ž์‹ ์ฐฝ ๊ฐ„ ํ†ต์‹ ์ด๋‚˜ ์ฐฝ ๋‹ซํž˜ ๊ฐ์ง€ ๋“ฑ์˜ ์ฒ˜๋ฆฌ๊ฐ€ ์‰ฌ์›Œ์ ธ์š”.
    • ๋ชจ๋‹ฌ๋กœ๋Š” ์™ธ๋ถ€ ๋„๋ฉ”์ธ ํŽ˜์ด์ง€๋ฅผ iframe์œผ๋กœ ๋„์šฐ๋Š” ๊ฒƒ์ด CORS, X-Frame-Options ๋˜๋Š” Content-Security-Policy ๋“ฑ์˜ ๋ณด์•ˆ ์ •์ฑ…๋“ฑ์˜ ์ด์œ ๋กœ ๋ถˆํŽธํ•ฉ๋‹ˆ๋‹ค.
  2. ํ”„๋ฆฐํŠธ ๊ธฐ๋Šฅ: ํŠน์ • ์ฝ˜ํ…์ธ ๋งŒ ํ”„๋ฆฐํŠธํ•˜๊ธฐ ์œ„ํ•œ ๋ณ„๋„ ์ฐฝ์ด ํ•„์š”ํ•  ๋•Œ
    • ์ƒˆ ์ฐฝ์— ํ•ด๋‹น ์ฝ˜ํ…์ธ ๋งŒ ๋ Œ๋”๋งํ•˜๊ณ  window.print()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์ด ๋งŽ์ด ์‚ฌ์šฉ๋์Šต๋‹ˆ๋‹ค.
    • ๋ชจ๋‹ฌ์ด๋‚˜ ์˜ค๋ฒ„๋ ˆ์ด๋กœ๋Š” ์ „์ฒด ํŽ˜์ด์ง€์˜ ์ผ๋ถ€๋งŒ ๊น”๋”ํ•˜๊ฒŒ ์ธ์‡„ํ•˜๋Š” ์ธ์‡„ ๋ฒ”์œ„ ์ œ์–ด๊ฐ€ ์–ด๋ ค์›Œ ๊ธฐ์กด ํŽ˜์ด์ง€ ์ „์ฒด๊ฐ€ ์ธ์‡„๋ฉ๋‹ˆ๋‹ค.
    • ๋”ฐ๋ผ์„œ ์ƒˆ ์ฐฝ์ด ๊ฐ€์žฅ ๊ฐ„๋‹จํ•˜๊ณ  ํ™•์‹คํ•œ ๋ฐฉ๋ฒ•์ด๋ผ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.
  3. ๋ฉ€ํ‹ฐํƒœ์Šคํ‚น UX: ์‚ฌ์šฉ์ž๊ฐ€ ๋‘ ํ™”๋ฉด์„ ๋™์‹œ์— ๋ณด๋ฉฐ ์ž‘์—…ํ•ด์•ผ ํ•  ๋•Œ
    • ๋‘ ๊ฐœ์˜ ์ฐฝ์„ ๋™์‹œ์— ํ™”๋ฉด์— ๋„์›Œ ์ƒํ˜ธ ๋น„๊ต ์ž‘์—…ํ•˜๋ ค๋ฉด ์ฐฝ ๋ถ„๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
    • ๋ชจ๋‹ฌ์€ ํ•ญ์ƒ ๋ถ€๋ชจ ์ฐฝ ์œ„์— ๋–  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‘ ํ™”๋ฉด์„ ๋™์‹œ์— ๋ณผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
    • ๋”ฐ๋ผ์„œ ์ƒˆ ์ฐฝ์„ ๋„์šฐ๋Š” ๊ฒƒ์ด ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
  4. ๋ ˆ๊ฑฐ์‹œ ์‹œ์Šคํ…œ ํ†ตํ•ฉ: ๊ธฐ์กด ์‹œ์Šคํ…œ๊ณผ์˜ ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ
    • ๊ธฐ์กด ์‹œ์Šคํ…œ์—์„œ ํŒ์—… ์ฐฝ์„ ํ†ตํ•œ ๋ฐ์ดํ„ฐ ๊ตํ™˜์ด๋‚˜ ์ธ์ฆ ๋“ฑ์˜ ๊ตฌ์กฐ๊ฐ€ ์ด๋ฏธ window.open()์— ๋งž์ถฐ์ ธ ์žˆ๋‹ค๋ฉด, ์ด๋ฅผ ๋ชจ๋‹ฌ๋กœ ๋Œ€์ฒดํ•˜๋Š” ๊ฒฝ์šฐ ํ˜ธํ™˜์„ฑ ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  5. ๋ณ„๋„ ์œˆ๋„์šฐ ๊ธฐ๋Šฅ: ๋ถ€๋ชจ ์ฐฝ์˜ ์ƒˆ๋กœ๊ณ ์นจ์ด๋‚˜ ์ด๋™์—๋„ ์œ ์ง€๋˜์–ด์•ผ ํ•˜๋Š” ๋…๋ฆฝ์  ๊ธฐ๋Šฅ
    • ๋ชจ๋‹ฌ์ด๋‚˜ SPA ์ปดํฌ๋„ŒํŠธ๋กœ๋Š” ๊ตฌํ˜„์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
    • ์ƒˆ๋กœ์šด ํŒ์—…์ฐฝ๋งŒ์ด ์™„์ „ํžˆ ๋ณ„๋„์˜ ๋ธŒ๋ผ์šฐ์ € ์ปจํ…์ŠคํŠธ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

 

์ด๋Ÿฐ ๊ฒฝ์šฐ์—๋Š” window.open()์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์•ˆ์ „ํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

window.open('javascript:focus()', 'myPopup', 'width=800,height=600');

 

๊ฒ‰๋ณด๊ธฐ์—” ์ž˜ ์ž‘๋™ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ด ๋ฐฉ์‹์€ ๋ณด์•ˆ์ƒ ๋ฌธ์ œ๊ฐ€ ์žˆ์œผ๋ฉฐ ๊ณต์‹์ ์œผ๋กœ ๊ถŒ์žฅ๋˜์ง€ ์•Š๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

์ด๋ฒˆ ๊ธ€์—์„  ๊ทธ ์ด์œ ์— ๋Œ€ํ•ด ์ ์–ด๋ณด๋ คํ•ฉ๋‹ˆ๋‹ค! ๋˜ํ•œ ์–ด๋–ค ๋ฐฉ์‹์ด ์•ˆ์ „ํ•˜๊ณ  ํ‘œ์ค€์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€๋„ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

javascript:focus() ๋ฐฉ์‹์˜ ์œ„ํ—˜์„ฑ

์ด ๋ฐฉ์‹์€ javascript: URL ์Šคํ‚ด์„ ์ด์šฉํ•ด ์ƒˆ ์ฐฝ์„ ์—ด๋ฉด์„œ focus() ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋Š” ์˜๋„์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ๋ฐฉ์‹์€ ๋ณด์•ˆ์ƒ ์ทจ์•ฝํ•ฉ๋‹ˆ๋‹ค.

 

MDN ๊ณต์‹ ๋ฌธ์„œ์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ช…ํ™•ํžˆ ๊ฒฝ๊ณ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

"Use of javascript: URLs on the web is discouraged as it may lead to execution of arbitrary code, similar to the ramifications of using eval()."

 

๋˜ํ•œ, ESLint์—์„œ๋„ no-script-url ๊ทœ์น™์„ ํ†ตํ•ด javascript: URL ์Šคํ‚ด ์‚ฌ์šฉ์„ ๊ธˆ์ง€ํ•˜๊ฑฐ๋‚˜ ๊ฒฝ๊ณ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ฝ”๋“œ๊ฐ€ ์™ธ๋ถ€ ์ž…๋ ฅ์— ์˜ํ•ด ์กฐ์ž‘๋˜๊ฑฐ๋‚˜ ์•…์šฉ๋  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

 

์š”์•ฝํ•˜์ž๋ฉด

  • ๋ณด์•ˆ์ƒ ์œ„ํ—˜ (์ž„์˜ ์ฝ”๋“œ ์‹คํ–‰ ๊ฐ€๋Šฅ์„ฑ)
  • XSS(ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ ์Šคํฌ๋ฆฝํŒ…) ๊ณต๊ฒฉ์˜ ๋ฒกํ„ฐ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Œ
  • ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ € ๋ฐ ๋ฆฐํ„ฐ ์ฐจ๋‹จ ๋Œ€์ƒ
  • ์œ ์ง€๋ณด์ˆ˜์™€ ์˜๋„ ํŒŒ์•…์ด ์–ด๋ ค์›€

 

ํ‘œ์ค€์ ์ด๊ณ  ์•ˆ์ „ํ•œ ๋ฐฉ๋ฒ•: about:blank ๋˜๋Š” ๋นˆ ๋ฌธ์ž์—ด

ํŒ์—…์„ ์•ˆ์ „ํ•˜๊ฒŒ ์—ด๊ณ  ์‹ถ๋‹ค๋ฉด, ๊ถŒ์žฅ๋˜๋Š” ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

const popup = window.open('about:blank', 'myPopup', 'width=800,height=600');
if (popup) {
  popup.location.href = '/your-target-page';
  popup.focus(); // ํ•„์š” ์‹œ ํฌ์ปค์Šค ์ด๋™
}

 

๋˜๋Š” ์•„์˜ˆ ๋นˆ ๋ฌธ์ž์—ด๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const popup = window.open('', 'myPopup', 'width=800,height=600');

 

์ด ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๋ณด์•ˆ์ƒ ์•ˆ์ „ํ•จ
  • ํ‘œ์ค€ ์›น API ์‚ฌ์šฉ๋ฒ•์„ ์ค€์ˆ˜
  • ์ฝ”๋“œ ์˜๋„๊ฐ€ ๋ช…ํ™•ํ•จ
  • ๊ณต์‹ ๋ฌธ์„œ(MDN, W3C ๋“ฑ)์—์„œ๋„ ๊ถŒ์žฅํ•˜๋Š” ๋ฐฉ์‹

 

javascript:focus()์˜ ๋ชฉ์ ๊ณผ ์˜คํ•ด

๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด javascript:focus()๋ฅผ ์ดˆ๊ธฐ URL๋กœ ๋„ฃ๋Š” ์ด์œ ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • ๋นˆ ์ฐฝ์„ ์—ด ๋•Œ ํŒ์—…์ด ์ฐจ๋‹จ๋˜๋Š” ํ˜„์ƒ์„ ์šฐํšŒํ•˜๋ ค๊ณ 
  • ์ฐฝ์„ ์—ฐ ์งํ›„ ์ž๋™์œผ๋กœ ํฌ์ปค์Šค๋ฅผ ์ฃผ๋ ค๊ณ 

ํ•˜์ง€๋งŒ ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠน์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ์‚ฌ์šฉ์ž ํด๋ฆญ ์ด๋ฒคํŠธ ๋‚ด๋ถ€์—์„œ window.open()์„ ํ˜ธ์ถœํ•˜๋ฉด ๋Œ€๋ถ€๋ถ„ ์ฐจ๋‹จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค
  2. ํฌ์ปค์Šค๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด popup.focus()๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ด ํ›จ์”ฌ ๋” ๋ช…ํ™•ํ•˜๊ณ  ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค
  3. ์ผ๋ถ€ ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ๋ณด์•ˆ ์ •์ฑ…์œผ๋กœ ์ธํ•ด javascript: URL์ด ์ž‘๋™ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

์ฆ‰, javascript:focus()๋Š” ๊ณผ๊ฑฐ์—๋Š” ํ†ตํ–ˆ์„์ง€ ๋ชฐ๋ผ๋„, ํ˜„๋Œ€ ์›น ๋ณด์•ˆ ํ™˜๊ฒฝ์—์„œ๋Š” ์ ์ ˆํ•˜์ง€ ์•Š์€ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

 

URL ๊ฐ์ฒด์™€ javascript: URL์€ ์™„์ „ํžˆ ๋‹ค๋ฆ…๋‹ˆ๋‹ค

ํ˜น์‹œ URL์ด๋ผ๋Š” ๋‹จ์–ด ๋•Œ๋ฌธ์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ๋ฒ•์„ ๋– ์˜ฌ๋ฆฌ์‹  ๋ถ„๋„ ๊ณ„์‹ค ํ…๋ฐ์š”?!

const url = new URL(window.location.href);
const tag = url.searchParams.get('tag');

 

์ด๊ฑด URL ์ •๋ณด๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์กฐ์ž‘ํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ์ฒด๋กœ, ์ฃผ์†Œ ํŒŒ์‹ฑ์„ ์œ„ํ•ด ์•„์ฃผ ์œ ์šฉํ•˜๊ฒŒ ์“ฐ์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”์ถœ, ๊ฒฝ๋กœ ์กฐ์ž‘ ๋“ฑ์— ์ž์ฃผ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

 

๋ฐ˜๋ฉด 'javascript:...'๋Š” ๋ธŒ๋ผ์šฐ์ €์— ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ง์ ‘ ์‹คํ–‰ํ•˜๋ผ๊ณ  ๋ช…๋ นํ•˜๋Š” ์Šคํ‚ด์ด๊ธฐ ๋•Œ๋ฌธ์—, ๋ชฉ์ ๋„ ์“ฐ์ž„๋„ ์™„์ „ํžˆ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

 

ํ•ญ๋ชฉ์„ค๋ช… - ์‚ฌ์šฉ ์šฉ๋„

new URL(...) ์ฃผ์†Œ ํŒŒ์‹ฑ์„ ์œ„ํ•œ URL ๊ฐ์ฒด ์•ˆ์ „ํ•˜๊ณ  ํ‘œ์ค€์ ์ธ ๋ฐฉ๋ฒ•
'javascript:focus()' ์‹คํ–‰ ์ฝ”๋“œ๋ฅผ URL์— ๋„ฃ๋Š” ๋ฐฉ์‹ ๋ณด์•ˆ์ƒ ์œ„ํ—˜, ๋น„๊ถŒ์žฅ ๋ฐฉ์‹

 

๋ถ€๋ชจ-์ž์‹ ์ฐฝ ๊ด€๊ณ„์™€ ์ œ์–ด

window.open()์œผ๋กœ ์—ด๋ฆฐ ํŒ์—…์€ ๋ถ€๋ชจ ํŽ˜์ด์ง€์™€ ๋ฐ€์ ‘ํ•œ ๊ด€๊ณ„๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค. ์ด๋ฅผ ํ™œ์šฉํ•˜๊ฑฐ๋‚˜ ์ฃผ์˜ํ•ด์•ผ ํ•  ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

 

๋ถ€๋ชจ ํŽ˜์ด์ง€์—์„œ ์ž์‹ ์ฐฝ ์ œ์–ดํ•˜๊ธฐ

ํŒ์—…์„ ์—ฐ ๋ถ€๋ชจ ํŽ˜์ด์ง€๋Š” ๋ฐ˜ํ™˜๋œ ์ฐธ์กฐ๋ฅผ ํ†ตํ•ด ์ž์‹ ์ฐฝ์„ ์™„์ „ํžˆ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const popup = window.open('about:blank', 'myPopup');
// ์ž์‹ ์ฐฝ ๋‚ด์šฉ ๋ณ€๊ฒฝ
popup.document.body.innerHTML = '<h1>๋ถ€๋ชจ๊ฐ€ ๋ณ€๊ฒฝํ•œ ๋‚ด์šฉ</h1>';
// ์ž์‹ ์ฐฝ ํฌ๊ธฐ/์œ„์น˜ ์กฐ์ •
popup.resizeTo(500, 300);
popup.moveTo(100, 100);
// ์ž์‹ ์ฐฝ ์ข…๋ฃŒ
popup.close();

 

์ž์‹ ์ฐฝ ์ข…๋ฃŒ ์‹œ๋‚˜๋ฆฌ์˜ค

์—ฌ๋Ÿฌ ์ƒํ™ฉ์—์„œ ์ž์‹ ์ฐฝ์ด ์ž๋™์œผ๋กœ ์ข…๋ฃŒ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ๋ถ€๋ชจ ํŽ˜์ด์ง€ ์ด๋™ ๋˜๋Š” ์ƒˆ๋กœ๊ณ ์นจ ์‹œ
    // ํŽ˜์ด์ง€ ์ด๋™ ์ „์— ์ž์‹ ์ฐฝ ๋‹ซ๊ธฐ
    window.addEventListener('beforeunload', () => {
      if (popup && !popup.closed) {
        popup.close();
      }
    });

     

  2. ๋‹ค๋ฅธ SPA ๋ผ์šฐํŠธ๋กœ ์ด๋™ ์‹œ
     
    // React Router ๋“ฑ์˜ ๋ผ์šฐํŠธ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ์— ์—ฐ๊ฒฐ
    router.beforeEach((to, from, next) => {
      if (window.myPopup && !window.myPopup.closed) {
        window.myPopup.close();
      }
      next();
    });

 

์ž์‹ ์ฐฝ์ด ๋ถ€๋ชจ ํŽ˜์ด์ง€ ๊ฐ์ง€ํ•˜๊ธฐ

์ž์‹ ์ฐฝ์—์„œ๋Š” ๋ถ€๋ชจ ์ฐฝ์˜ ์กด์žฌ์™€ ์ƒํƒœ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// ์ž์‹ ์ฐฝ์—์„œ ๋ถ€๋ชจ ์ฐฝ ํ™•์ธ ๋ฐ ๋Œ€์‘ํ•˜๊ธฐ
if (window.opener && !window.opener.closed) {
  // ๋ถ€๋ชจ ์ฐฝ์ด ์กด์žฌํ•˜๊ณ  ์—ด๋ ค์žˆ์Œ
  console.log('๋ถ€๋ชจ ์ฐฝ URL:', window.opener.location.href);
} else {
  // ๋ถ€๋ชจ ์ฐฝ์ด ๋‹ซํ˜”๊ฑฐ๋‚˜ ์—†์Œ
  console.log('๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰ ์ค‘');
}

 

๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ๊ณผ ์ถ”๊ฐ€ ๊ณ ๋ ค์‚ฌํ•ญ

์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €๋“ค์€ window.open()์— ๋Œ€ํ•ด ์ ์  ๋” ์—„๊ฒฉํ•œ ์ •์ฑ…์„ ์ ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

  • Chrome, Safari: ์‚ฌ์šฉ์ž ์ œ์Šค์ฒ˜(ํด๋ฆญ ๋“ฑ) ์—†์ด window.open()์„ ํ˜ธ์ถœํ•˜๋ฉด ์ฐจ๋‹จ๋ฉ๋‹ˆ๋‹ค
  • Firefox: ํŒ์—… ์ฐจ๋‹จ ์„ค์ •์— ๋”ฐ๋ผ ๋™์ž‘์ด ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค
  • ๋ชจ๋ฐ”์ผ ๋ธŒ๋ผ์šฐ์ €: ๋งŽ์€ ๋ชจ๋ฐ”์ผ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํŒ์—… ์ž์ฒด๋ฅผ ์ œํ•œ์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค

๋”ฐ๋ผ์„œ ํŒ์—… ๋Œ€์‹  ๋ชจ๋‹ฌ์ด๋‚˜ ์ธ๋ผ์ธ ์ฝ˜ํ…์ธ ๋ฅผ ๊ณ ๋ คํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋Œ€์•ˆ์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

// ํŠน๋ณ„ํ•œ ์ด์œ ๊ฐ€ ์—†๋‹ค๋ฉด ํŒ์—… ๋Œ€์‹  ๋ชจ๋‹ฌ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”
document.getElementById('openModal').addEventListener('click', () => {
  document.getElementById('myModal').style.display = 'block';
});

 

์š”์•ฝ ๋ฐ ๊ฒฐ๋ก 

  • ๋ชจ๋˜ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ฃผ๋ฅ˜์ธ ์ง€๊ธˆ๋„ OAuth, ๊ฒฐ์ œ ์—ฐ๋™, ํ”„๋ฆฐํŠธ ๊ธฐ๋Šฅ ๋“ฑ์„ ์œ„ํ•ด window.open()์ด ํ•„์š”ํ•œ ์ƒํ™ฉ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • javascript:focus() ๋ฐฉ์‹์€ ๋ณด์•ˆ์ƒ ์œ„ํ—˜ํ•˜๊ณ , MDN ๋ฐ ESLint์—์„œ๋„ ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ํŒ์—…์„ ์—ด ๋•Œ๋Š” about:blank๋‚˜ ๋นˆ ๋ฌธ์ž์—ด์„ ์‚ฌ์šฉํ•˜๊ณ , ๊ทธ ์ดํ›„์— popup.location.href์™€ popup.focus()๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด ๊ฐ€์žฅ ํ‘œ์ค€์ ์ด๊ณ  ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ถ€๋ชจ ํŽ˜์ด์ง€์—์„œ๋Š” ํŒ์—… ์ฐฝ์„ ์™„์ „ํžˆ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํŽ˜์ด์ง€ ์ „ํ™˜ ์‹œ ํŒ์—… ์ฐฝ์„ ์ ์ ˆํžˆ ๊ด€๋ฆฌ(์ข…๋ฃŒ)ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • React ๊ฐ™์€ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ๋Š” useRef์™€ useEffect๋ฅผ ํ™œ์šฉํ•ด ํŒ์—…์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์ปดํฌ๋„ŒํŠธ์™€ ์—ฐ๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • URL ๊ฐ์ฒด์™€ javascript: URL์€ ๊ฐœ๋…์ด ์™„์ „ํžˆ ๋‹ค๋ฅด๋ฏ€๋กœ ํ˜ผ๋™ํ•˜์ง€ ๋งˆ์„ธ์š”.

ํŠน์ • ์ƒํ™ฉ์—์„œ๋Š” window.open์ด ์—ฌ์ „ํžˆ ์ ์ ˆํ•œ ์„ ํƒ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ์—๋„ ๋” ์•ˆ์ „ํ•œ ๋ฐฉ์‹์œผ๋กœ ์ฝ”๋”ฉํ•ด ๋ณด์‹œ๋Š” ๊ฑด ์–ด๋–จ๊นŒ์š”?!

๊ถ๊ธˆํ•˜์‹  ์ ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“ ์ง€ ๋Œ“๊ธ€๋กœ ๋‚จ๊ฒจ์ฃผ์„ธ์š”! 

 

 

๐Ÿ“ƒ ์ฐธ๊ณ  ๋ฌธํ—Œ  
JAVASCRIPT.INFO - Popups and window methods
W3 Schools - Window open()
eslint/no-script-url
MDN Web Docs
Popup or Modal

 

์›น๊ฐœ๋ฐœ์ž๋กœ์„œ AI๋ฅผ ํ•™์Šตํ•˜๊ณ  ํ™œ์šฉํ•˜๋˜ ์ค‘ ๋ฌธ๋“ ๊ถ๊ธˆํ•ด์กŒ์Šต๋‹ˆ๋‹ค.

 

"AI/ML ์—”์ง€๋‹ˆ์–ด๋“ค์€ ์ž์‹ ๋“ค์ด ๋งŒ๋“  ๋ณต์žกํ•œ ๋ชจ๋ธ์˜ ๊ฒฐ๊ณผ๋ฅผ ์–ด๋–ป๊ฒŒ ํ™•์ธํ•˜๊ณ  ๊ณต์œ ํ• ๊นŒ?" ๊ทธ๋ฆฌ๊ณ  "๋น„๊ฐœ๋ฐœ์ž๋“ค์€ ์–ด๋–ป๊ฒŒ AI๋ฅผ ํ™œ์šฉํ•ด ์›ํ•˜๋Š” ๊ฒƒ์„ ๋งŒ๋“ค์–ด๋‚ผ๊นŒ?"

 

์ด ๊ถ๊ธˆ์ฆ์œผ๋กœ '์ŠคํŒŒ๋ฅดํƒ€์—์„œ ์ฃผ๊ด€ํ•˜๋Š” AI์™€ 100์ธ์˜ ์šฉ์‚ฌ๋“ค - ํ•ด์ปคํ†ค'์— ์ฐธ๊ฐ€ํ•ด ๋ดค์–ด์š”.
ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋กœ์„œ AI ์ƒํƒœ๊ณ„์— ๋ฐœ์„ ๋‹ด๊ฐ€๋ณด๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค.

 

 

AI/ML ์—”์ง€๋‹ˆ์–ด๋ฅผ ์œ„ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ!

ํ•ด์ปคํ†ค์—์„œ ๋งŒ๋‚œ Streamlit

ํ•ด์ปคํ†ค์— ์ฐธ๊ฐ€ํ•˜๋ฉด์„œ ์ฒ˜์Œ์œผ๋กœ Streamlit์ด๋ผ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ ‘ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

AI/ML ๊ฐœ๋ฐœ์ž๋“ค ์‚ฌ์ด์—์„œ๋Š” ๊ฝค ์œ ๋ช…ํ•œ ๋„๊ตฌ๋”๋ผ๊ตฌ์š”?!

์ฒ˜์Œ์—๋Š” 'Django๋‚˜ React, Next ๊ฐ™์€ ์›น ํ”„๋ ˆ์ž„์›Œํฌ์˜ ์ผ์ข…์ธ๊ฐ€?' ํ•˜๊ณ  ์ƒ๊ฐํ–ˆ์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” ์ข€ ๋‹ค๋ฅธ ์šฉ๋„๋กœ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

Streamlit์€ ํŒŒ์ด์ฌ ๊ฐœ๋ฐœ์ž, ํŠนํžˆ ๋ฐ์ดํ„ฐ๋‚˜ ML ์—”์ง€๋‹ˆ์–ด๊ฐ€ ๋ณต์žกํ•œ ์›น ๊ฐœ๋ฐœ ์ง€์‹ ์—†์ด๋„ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋น ๋ฅด๊ฒŒ ์ œ์ž‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ฃผ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์˜€์Šต๋‹ˆ๋‹ค.

 

ํŒŒ์ด์ฌ ์ฝ”๋“œ๋งŒ์œผ๋กœ ์›น ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋‹ค๋‹ˆ, ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋กœ์„œ ์ƒ๋‹นํžˆ ํฅ๋ฏธ๋กœ์šด ๊ฐœ๋…์ด์—ˆ์–ด์š”.

๋ฌธ์„œ ๋งํฌ์ž…๋‹ˆ๋‹ค!

 

 

AI ์—”์ง€๋‹ˆ์–ด์™€ ๋น„๊ฐœ๋ฐœ์ž์˜ ํ™œ์šฉ๋ฒ•

ํ•ด์ปคํ†ค์—์„œ ๊ด€์ฐฐํ•œ ๋ฐ”๋กœ๋Š”, AI/ML ์—”์ง€๋‹ˆ์–ด๋“ค์ด Streamlit์„ ์‚ฌ์šฉํ•˜๋Š” ์ฃผ๋œ ์ด์œ ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•˜์Šต๋‹ˆ๋‹ค.

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

๊ฐ€์žฅ ๋†€๋ผ์› ๋˜ ์ ์€ ๋น„๊ฐœ๋ฐœ์ž๋“ค๋„ ์ด ๋„๊ตฌ๋ฅผ ํ™œ์šฉํ•ด ์ž์‹ ๋งŒ์˜ AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค์–ด๋‚ด์—ˆ๋‹ค๋Š” ์‚ฌ์‹ค์ž…๋‹ˆ๋‹ค..!!

๋งˆ์ผ€ํ„ฐ, ๋””์ž์ด๋„ˆ, ์‹ฌ์ง€์–ด ์˜์‚ฌ๋‚˜ ๊ต์‚ฌ์™€ ๊ฐ™์€ ์ „ํ˜€ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์˜ ์ „๋ฌธ๊ฐ€๋ถ„๋“ค๋„ ๊ฐ„๋‹จํ•œ ํŒŒ์ด์ฌ ์ฝ”๋“œ๋ฅผ ๋ฐฐ์›Œ ์ž์‹ ์˜ ์•„์ด๋””์–ด๋ฅผ ๊ตฌํ˜„ํ•˜์…จ์Šต๋‹ˆ๋‹ค!!

 

 

๋‚ด ๊ฒฝํ—˜! Streamlit์œผ๋กœ ์„œ๋น„์Šค ๊ตฌ์ถ•ํ•˜๊ธฐ

์ €๋„ ํ‰์†Œ ์‚ฌ์šฉํ•˜๋˜ React, Vue ์ง„์˜์ด ์•„๋‹Œ Streamlit์„ ์‚ฌ์šฉํ•ด ๊ฐ„๋‹จํžˆ ์•„์ด๋””์–ด ๊ตฌํ˜„์„ ๋ณด๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋กœ์„œ HTML, CSS, JavaScript์— ์ต์ˆ™ํ•œ ์ €์—๊ฒŒ๋Š” ๊ต‰์žฅํžˆ ์žฌ๋ฐŒ์—ˆ์–ด์š”!

 

๊ธฐ๋ณธ์ ์ธ ์„ค์ •์€ ๋†€๋ผ์šธ ์ •๋„๋กœ ๊ฐ„๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค. (์„ค๋ช…์„ ์œ„ํ•œ ์ฝ”๋“œ๋กœ ์‹ค์ œ Github์— ๊ฒŒ์‹œํ•œ ์ฝ”๋“œ์™€๋Š” ์ „ํ˜€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค!)

import streamlit as st
import pandas as pd
import custom_nlp_model  # ๋‚ด ์ปค์Šคํ…€ ๋ชจ๋ธ

st.title("ํ…์ŠคํŠธ ๊ฐ์ • ๋ถ„์„๊ธฐ")
user_input = st.text_area("๋ถ„์„ํ•  ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”:")

if st.button("๋ถ„์„ํ•˜๊ธฐ"):
    result = custom_nlp_model.analyze_sentiment(user_input)
    st.write(f"ํ˜„์žฌ ์ƒํ™ฉ ๋ถ„์„: {result['sentiment']}")
    st.bar_chart(result['confidence_scores'])

 

์ด ์ฝ”๋“œ๋งŒ์œผ๋กœ ์ž‘๋™ํ•˜๋Š” ์›น ์•ฑ์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! React ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ , CSS๋ฅผ ์ž‘์„ฑํ•˜๊ณ , ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์„ค์ •ํ•˜๋Š” ๋“ฑ์˜ ์ž‘์—… ์—†์ด ๋ง์ด์ฃ . ์ž๋™ ๋ฐฐํฌ๋Š” ๋”๋”์šฑ ์‰ฌ์› ์Šต๋‹ˆ๋‹ค!!

 

 

์›น๊ฐœ๋ฐœ์ž๋กœ์จ ๋А๋‚€ ๋ถˆํŽธํ•œ ์ 

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

 

Streamlit์€ ๊ธฐ๋ณธ์ ์ธ ์Šคํƒ€์ผ๋ง ์˜ต์…˜์„ ์ œ๊ณตํ•˜์ง€๋งŒ, ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ์ˆ˜์ค€์˜ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์€ ์–ด๋ ค์› ์Šต๋‹ˆ๋‹ค. ๊ฒฐ๊ตญ HTML๊ณผ CSS๋ฅผ ์ง์ ‘ ์‚ฝ์ž…ํ•˜๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค. (์• ์ดˆ์— ๋ชฉ์ ์ด ์ด์˜๊ฒŒ ๊พธ๋ฏธ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค!! Streamlit์ด ์ž˜๋ชป๋œ๊ฒŒ ์•„๋‹ˆ์—์š”!!)

 

custom_html = """
<div class="result-card" style="border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); padding: 20px; margin: 20px 0;">
    <h3 style="color: #333; font-family: 'Segoe UI', sans-serif;">๋ถ„์„ ๊ฒฐ๊ณผ</h3>
    <div class="sentiment-meter" style="background: linear-gradient(to right, #ff4b4b, #ffff4b, #4bff4b); height: 20px; border-radius: 10px;">
        <div class="marker" style="position: relative; left: {}%; transform: translateX(-50%); width: 10px; height: 30px; background-color: #333; border-radius: 5px;"></div>
    </div>
</div>
"""

st.markdown(custom_html.format(sentiment_score * 100), unsafe_allow_html=True)

 

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

 

 

ํŽ˜์ด์ง€๋„ค์ด์…˜๊ณผ DOM ์ปจํŠธ๋กค

ํŠนํžˆ ๊ณ ์ „ํ–ˆ๋˜ ๋ถ€๋ถ„์€ ํŽ˜์ด์ง€๋„ค์ด์…˜๊ณผ DOM ์ปจํŠธ๋กค์ด์—ˆ์Šต๋‹ˆ๋‹ค.

React๋‚˜ Vue์—์„œ๋Š” ์ƒํƒœ ๊ด€๋ฆฌ์™€ ์ปดํฌ๋„ŒํŠธ ๋ผ์ดํ”„์‚ฌ์ดํด์„ ํ†ตํ•ด ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ๋“ค์ด Streamlit์—์„œ๋Š” ์ƒ๋‹นํžˆ ๋ฒˆ๊ฑฐ๋กœ์› ์Šต๋‹ˆ๋‹ค.

(๋ฌผ๋ก  ์ œ๊ฐ€ ์ œ๋Œ€๋กœ ์ฐพ์•„๋ณด์ง€ ๋ชปํ•œ ๊ฒƒ์ผ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค!!)

 

์˜ˆ๋ฅผ ๋“ค์–ด, ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ํŽ˜์ด์ง€๋กœ ๋‚˜๋ˆ„์–ด ํ‘œ์‹œํ•˜๋ ค๋ฉด ์ด๋Ÿฐ ์‹์œผ๋กœ ์ ‘๊ทผํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

(๋ฐฐํฌํ•œ ์ฝ”๋“œ์™€๋Š” ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค! - ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ œ๊ฑฐ)

 

import streamlit as st
import pandas as pd

# ์„ธ์…˜ ์ƒํƒœ๋ฅผ ์‚ฌ์šฉํ•œ ํŽ˜์ด์ง€๋„ค์ด์…˜
if 'page_number' not in st.session_state:
    st.session_state.page_number = 0
    
# ๋ฐ์ดํ„ฐ ๋กœ๋“œ
data = pd.read_csv("large_results.csv")
items_per_page = 10
total_pages = len(data) // items_per_page + (1 if len(data) % items_per_page > 0 else 0)

# ํ˜„์žฌ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ
start_idx = st.session_state.page_number * items_per_page
end_idx = min(start_idx + items_per_page, len(data))
current_page_data = data.iloc[start_idx:end_idx]

# ํ˜„์žฌ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ
st.write(f"๊ฒฐ๊ณผ {start_idx+1}–{end_idx} / ์ด {len(data)}๊ฐœ")
st.table(current_page_data)

# ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ปจํŠธ๋กค
col1, col2 = st.columns(2)
if col1.button("์ด์ „ ํŽ˜์ด์ง€") and st.session_state.page_number > 0:
    st.session_state.page_number -= 1
    st.experimental_rerun()
    
if col2.button("๋‹ค์Œ ํŽ˜์ด์ง€") and st.session_state.page_number < total_pages - 1:
    st.session_state.page_number += 1
    st.experimental_rerun()

 

์ด ๋ฐฉ์‹์€ ์ž‘๋™์€ ํ•˜์ง€๋งŒ, ํŽ˜์ด์ง€ ์ „ํ™˜ ์‹œ ์ „์ฒด ์•ฑ์ด ์ƒˆ๋กœ๊ณ ์นจ๋˜์–ด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๋งค๋„๋Ÿฝ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

React์—์„œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋”๋ผ๋„ useState์™€ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์œผ๋กœ ๊ฐ„๋‹จํžˆ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌธ์ œ๊ฐ€ ํ›จ์”ฌ ๋ณต์žกํ•ด์ง„ ๋А๋‚Œ์ด์—ˆ์ฃ .

 

 

์›น๊ณผ AI์˜ ์ ‘์ 

ํ•ด์ปคํ†ค๊ณผ ํŒ€ ํ”„๋กœ์ ํŠธ๋ฅผ ํ†ตํ•ด ๋А๋‚€ ์ ์€, Streamlit์ด '์›น ๊ฐœ๋ฐœ'๊ณผ 'AI/ML ๊ฐœ๋ฐœ' ์‚ฌ์ด์˜ ๊ฐ„๊ทน์„ ๋ฉ”์šฐ๊ธฐ ์œ„ํ•œ ๋„๊ตฌ๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

์™„๋ฒฝํ•œ ํ”„๋ก ํŠธ์—”๋“œ ์†”๋ฃจ์…˜์€ ์•„๋‹ˆ์ง€๋งŒ, AI/ML ์—”์ง€๋‹ˆ์–ด์™€ ๋น„๊ฐœ๋ฐœ์ž๋“ค์ด ์ž์‹ ์˜ ์•„์ด๋””์–ด๋ฅผ ๋น ๋ฅด๊ฒŒ ์‹œ๊ฐํ™”ํ•˜๊ณ  ๊ณต์œ ํ•˜๋Š” ๋ฐ๋Š” ํƒ์›”ํ•œ ๋„๊ตฌ์˜€์–ด์š”.

 

์œ„์—์„œ๋„ ์–ธ๊ธ‰ํ–ˆ์ง€๋งŒ ์ €์—๊ฒŒ ๊ฐ€์žฅ ๋†€๋ผ์› ๋˜ ์ ์€ ๋น„๊ฐœ๋ฐœ์ž๋“ค๋„ Streamlit์„ ํ†ตํ•ด ์ž์‹ ์˜ ์•„์ด๋””์–ด๋ฅผ AI ์ƒํƒœ๊ณ„ ์•ˆ์—์„œ ๊ตฌํ˜„ํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์•„์ด๋””์–ด๊ฐ€ ์žˆ์ง€๋งŒ ๊ธฐ์ˆ ์  ์žฅ๋ฒฝ ๋•Œ๋ฌธ์— ์‹คํ˜„ํ•˜์ง€ ๋ชปํ–ˆ๋˜ ์‚ฌ๋žŒ๋“ค์ด, ์ด์ œ๋Š” ๊ฐ„๋‹จํ•œ ํŒŒ์ด์ฌ ์ฝ”๋“œ๋งŒ์œผ๋กœ๋„ ์ž์‹ ์˜ ์•„์ด๋””์–ด๋ฅผ ํ”„๋กœํ† ํƒ€์ž…์œผ๋กœ ๋งŒ๋“ค์–ด ์ดˆ๊ธฐ ๊ฒ€์ฆ์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ ๊ฒƒ์ด์ฃ .

 

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

 

 

Streamlit์˜ ์ง„์งœ ๋ชฉ์ 

์ด๋ฒˆ ํ•ด์ปคํ†ค์„ ๊ฒฝํ—˜ํ•˜๋ฉฐ ์ƒ๊ฐํ•œ๊ฑด, Streamlit์„ ํ”„๋ก ํŠธ์—”๋“œ, ์›น ๊ฐœ๋ฐœ ๋„๊ตฌ์˜ ๊ด€์ ์—์„œ ๋ฐ”๋ผ๋ณด๋Š” ๊ฒƒ ์ž์ฒด๊ฐ€ ์ž˜๋ชป๋œ ์ ‘๊ทผ์ด๋ผ๋Š” ์ ์ž…๋‹ˆ๋‹ค! Streamlit์€ ์• ์ดˆ์— ํ™”๋ คํ•œ UI/UX๋‚˜ ๋ณต์žกํ•œ ์ธํ„ฐ๋ž™์…˜์„ ์œ„ํ•œ ๋„๊ตฌ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.

 

์‚ฌ์‹ค, ์Šคํƒ€์ผ๋ง์ด๋‚˜ ํŽ˜์ด์ง€๋„ค์ด์…˜์˜ ํ•œ๊ณ„๋Š” Streamlit์˜ '๋‹จ์ '์ด๋ผ๊ธฐ๋ณด๋‹ค๋Š” ๊ทธ๋ƒฅ ์ด ๋„๊ตฌ์˜ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚œ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.

์ด๊ฑด ๋งˆ์น˜ ๋ง์น˜๋กœ ๋‚˜์‚ฌ๋ฅผ ๋ฐ•์œผ๋ ค๊ณ  ํ•˜๋ฉด์„œ ๋ง์น˜๊ฐ€ ๋‚˜์˜๋‹ค๊ณ  ๋งํ•˜๋Š” ๊ฒƒ๊ณผ ๋น„์Šทํ•˜๋‹ค ์ƒ๊ฐํ•ด์š”.

 

Streamlit์˜ ์ง„์งœ ๋ชฉ์ ์€ ML ์—”์ง€๋‹ˆ์–ด๋“ค์ด ์ž์‹ ์ด ๋งŒ๋“  ๋ชจ๋ธ์„ ์‰ฝ๊ณ  ๋น ๋ฅด๊ฒŒ ์‹œ๊ฐํ™”ํ•˜๊ณ  ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

ML ์—”์ง€๋‹ˆ์–ด๋“ค์—๊ฒ ์™„๋ฒฝํ•œ UI๋ณด๋‹ค ๋ชจ๋ธ์˜ ์„ฑ๋Šฅ๊ณผ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ํ›จ์”ฌ ์ค‘์š”ํ•˜๋‹ˆ๊นŒ์š”.

 

ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž์ธ ์ €๋กœ์„œ๋Š” ์ฒ˜์Œ์— ์ด ์ ์„ ๋†“๊ธฐ๊ฐ€ ํž˜๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ฒƒ์„ '์ข‹์€ UI/UX'์˜ ๊ด€์ ์—์„œ๋งŒ ๋ฐ”๋ผ๋ดค๊ฑฐ๋“ ์š”. ํ•˜์ง€๋งŒ ํ•ด์ปคํ†ค์„ ํ†ตํ•ด AI/ML ์—”์ง€๋‹ˆ์–ด์˜ ์ด์•ผ๊ธฐ๋‚˜ ๋‹ค์–‘ํ•œ ์‚ฌ๋žŒ๋“ค๊ณผ ๋Œ€ํ™”ํ•˜๋ฉด์„œ, ๊ทธ๋“ค์—๊ฒŒ ํ•„์š”ํ•œ๊ฒŒ ํ™”๋ คํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด๋‚˜ ์™„๋ฒฝํ•œ ๋ฐ˜์‘ํ˜• ๋””์ž์ธ์ด ์•„๋‹ˆ๋ผ '๋ชจ๋ธ์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ๋น ๋ฅด๊ฒŒ ํ™•์ธํ•˜๊ณ  ํŒ€์›๋“ค๊ณผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•'์ด๋ผ๋Š” ๊ฑธ ๊นจ๋‹ฌ์•˜์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ž˜์„œ Streamlit์€ ML ์—”์ง€๋‹ˆ์–ด๋“ค์ด ์›น ๊ฐœ๋ฐœ ์—†์ด๋„ ์ž์‹ ์˜ ์ž‘์—… ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๊ณ  ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋Š”, ์ •ํ™•ํžˆ ๊ทธ ๋ชฉ์ ์— ์ตœ์ ํ™”๋œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค!!

+ Recent posts