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

FSD ์•„ํ‚คํ…์ฒ˜์™€ ํ•จ๊ป˜ ์ƒ๊ฐํ•ด๋ณด๊ธฐ

FSD ์™€ ๋ฐฐ๋Ÿด ํŒŒ์ผ

 

Vue3์—์„œ React.js๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๋ฉด์„œ Feature-Sliced Design(FSD) ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋„์ž…ํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ๊ณผ์ •์—์„œ ๋ฐฐ๋Ÿด ํŒŒ์ผ(barrel files)์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์ด ์ƒ๊ฒผ๊ณ , ์ตœ๊ทผ ๋ช‡ ๋…„ ์‚ฌ์ด์— ์ด์— ๋Œ€ํ•œ ๊ด€์ ์ด ๋งŽ์ด ๋ฐ”๋€Œ์—ˆ๋‹ค๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๋ฐฐ๋Ÿด ํŒŒ์ผ์ด๋ž€?

๋ฐฐ๋Ÿด ํŒŒ์ผ์€ ์—ฌ๋Ÿฌ ๋ชจ๋“ˆ์„ ํ•˜๋‚˜์˜ ํŒŒ์ผ(์ฃผ๋กœ index.js ๋˜๋Š” index.ts)์—์„œ ๋‹ค์‹œ ๋‚ด๋ณด๋‚ด๋Š” ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ์ด ํŒจํ„ด์€ ์ฝ”๋“œ ์ •๋ฆฌ์™€ import ๋ฌธ์„ ๋‹จ์ˆœํ™”ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.

 

๋ฐฐ๋Ÿด ํŒŒ์ผ ์ „ ํ›„

 

๋ฐฐ๋Ÿด ํŒŒ์ผ ์—†์ด:

// ๊ฐ ํŒŒ์ผ์—์„œ ๊ฐœ๋ณ„์ ์œผ๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ 
import { Button } from "../components/Button"; 
import { TextField } from "../components/TextField"; 
import { Checkbox } from "../components/Checkbox";

 

๋ฐฐ๋Ÿด ํŒŒ์ผ ์‚ฌ์šฉ ์‹œ:

// ํ•˜๋‚˜์˜ ๊ฒฝ๋กœ์—์„œ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ 
import { Button, TextField, Checkbox } from "../components";

 

๋ฐฐ๋Ÿด ํŒŒ์ผ์˜ ์‹ค์ œ ๊ตฌํ˜„

์ผ๋ฐ˜์ ์ธ ๋ฐฐ๋Ÿด ํŒŒ์ผ (components/index.ts):

// ์™€์ผ๋“œ์นด๋“œ ๋‚ด๋ณด๋‚ด๊ธฐ (๊ถŒ์žฅํ•˜์ง€ ์•Š์Œ) 
export * from './Button'; 
export * from './TextField'; 
export * from './Checkbox'; 

// ๋˜๋Š” ๋ช…์‹œ์  ๋‚ด๋ณด๋‚ด๊ธฐ (๊ถŒ์žฅ) 
export { Button } from './Button'; 
export { TextField } from './TextField'; 
export { Checkbox } from './Checkbox';

 

๋ฐฐ๋Ÿด ํŒŒ์ผ์˜ ์„ฑ๋Šฅ ๋ฌธ์ œ

Marvin์˜ JavaScript ์ƒํƒœ๊ณ„ ์†๋„ ๊ฐœ์„ : ๋ฐฐ๋Ÿด ํŒŒ์ผ ๋ฌธ์ œ ๊ธ€์—์„œ ์ง€์ ํ•œ ๋ฐ”์™€ ๊ฐ™์ด, ๋ฐฐ๋Ÿด ํŒŒ์ผ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์„ฑ๋Šฅ ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. ๋ชจ๋“ˆ ๊ทธ๋ž˜ํ”„ ๋ณต์žก์„ฑ ์ฆ๊ฐ€: ๋ฐฐ๋Ÿด ํŒŒ์ผ์ด ์ฆ๊ฐ€ํ• ์ˆ˜๋ก ๋ชจ๋“ˆ ๊ฐ„ ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„๊ฐ€ ๋ณต์žกํ•ด์ง‘๋‹ˆ๋‹ค.
  2. ๋ถˆํ•„์š”ํ•œ ๋ชจ๋“ˆ ๋กœ๋”ฉ: ํŠนํžˆ ์™€์ผ๋“œ์นด๋“œ ๋‚ด๋ณด๋‚ด๊ธฐ(export * from)๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, ์‹ค์ œ๋กœ ํ•„์š”ํ•˜์ง€ ์•Š์€ ๋ชจ๋“ˆ๊นŒ์ง€ ๋กœ๋“œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๋ชจ๋“ˆ ๊ทธ๋ž˜ํ”„ ๋น„๊ต

๋ฐฐ๋Ÿด ํŒŒ์ผ ์—†์ด:

App.js
โ”œโ”€โ”€ Button.js
โ”œโ”€โ”€ TextField.js
โ””โ”€โ”€ Checkbox.js

 

๋ฐฐ๋Ÿด ํŒŒ์ผ ์‚ฌ์šฉ ์‹œ:

App.js
โ””โ”€โ”€ components/index.js
    โ”œโ”€โ”€ Button.js
    โ”œโ”€โ”€ TextField.js
    โ””โ”€โ”€ Checkbox.js

 

์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ์˜ˆ์‹œ

๋ชจ๋“ˆ ์ˆ˜๋ฐฐ๋Ÿด ํŒŒ์ผ ์—†์Œ๋ฐฐ๋Ÿด ํŒŒ์ผ ์‚ฌ์šฉ์ฐจ์ด

๋ชจ๋“ˆ ์ˆ˜ ๋ฐฐ๋Ÿด ํŒŒ์ผ ์—†์Œ ๋ฐฐ๋Ÿด ํŒŒ์ผ ์‚ฌ์šฉ ์ฐจ์ด
10๊ฐœ 150ms 180ms +20%
50๊ฐœ 450ms 630ms +40%
100๊ฐœ 820ms 1250ms +52%

 

์ฐธ๊ณ : ์ด ์ˆ˜์น˜๋Š” ์˜ˆ์‹œ์šฉ์ด๋ฉฐ, ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋‹ค์–‘ํ•œ ์š”์ธ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

FSD ์•„ํ‚คํ…์ฒ˜์—์„œ์˜ ๋ฐฐ๋Ÿด ํŒŒ์ผ

Feature-Sliced Design(FSD)์€ ๋ฐฐ๋Ÿด ํŒŒ์ผ๊ณผ ์œ ์‚ฌํ•œ ํŒจํ„ด์„ ์˜๋„์ ์œผ๋กœ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋‹จ์ˆœํžˆ ์ฝ”๋“œ ์ž‘์„ฑ์˜ ํŽธ์˜์„ฑ์„ ์œ„ํ•œ ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์•„ํ‚คํ…์ฒ˜์˜ ๊ตฌ์กฐ์™€ ๋ชฉ์ ์„ ๋ฐ˜์˜ํ•œ ๊ฒฐ์ •์ž…๋‹ˆ๋‹ค.

FSD๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ธฐ๋Šฅ ๋‹จ์œ„๋กœ ๋ถ„ํ• ํ•˜๊ณ  ๊ณ„์ธตํ™”๋œ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ตฌ์กฐ์—์„œ ๊ฐ ๋ ˆ์ด์–ด(layer)์™€ ์Šฌ๋ผ์ด์Šค(slice)๋Š” ๋ช…ํ™•ํ•œ ์ฑ…์ž„๊ณผ ์—ญํ• ์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค. ๋ฐฐ๋Ÿด ํŒŒ์ผ๊ณผ ์œ ์‚ฌํ•œ ํŒจํ„ด์„ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ, FSD๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด์ ์„ ์–ป์Šต๋‹ˆ๋‹ค:

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

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

 

FSD์—์„œ์˜ ๋ฐฐ๋Ÿด ํŒŒ์ผ ํ™œ์šฉ - ์‹ค์ œ ๊ฒฝํ—˜๊ณผ ํŒ

FSD ์•„ํ‚คํ…์ฒ˜๋ฅผ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด๋ณธ ๊ฒฝํ—˜์— ๋”ฐ๋ฅด๋ฉด, ๋ฐฐ๋Ÿด ํŒŒ์ผ(Public API)์˜ ์ฃผ์š” ์ด์  ์ค‘ ํ•˜๋‚˜๋Š” ์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ์ž…๋‹ˆ๋‹ค. ์ด์— ๋Œ€ํ•ด ์ด๋„ˆ์„œํด ์žฌ์ง์ž ๊ณผ์ •์—์„œ ๋งŒ๋‚œ ๋™๋ฃŒ ๊ฐœ๋ฐœ์ž๋ถ„๊ป˜์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ธ์‚ฌ์ดํŠธ๋ฅผ ๊ณต์œ ํ•ด์ฃผ์…จ์–ด์š”:

"FSD๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๋А๋‚€ ๋ฐ”๋กœ๋Š”, Public API๋ฅผ ํ™œ์šฉํ•˜๋Š” ์ฃผ๋œ ์ด์œ  ์ค‘ ํ•˜๋‚˜๊ฐ€ ์œ ์ง€๋ณด์ˆ˜์— ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, entities/user์˜ ์Šคํ‚ค๋งˆ๊ฐ€ ์ผ๋ถ€ ๋ณ€๊ฒฝ๋˜๋”๋ผ๋„ features/loginUser, features/registerUser ๋“ฑ์—์„œ import ๊ตฌ๋ฌธ์„ ๋ณ€๊ฒฝํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง‘๋‹ˆ๋‹ค."


์ด๋Š” FSD์˜ ํ•ต์‹ฌ ์›์น™ ์ค‘ ํ•˜๋‚˜์ธ ๋ชจ๋“ˆ ๊ฐ„ ์˜์กด์„ฑ ๊ด€๋ฆฌ์™€ ์ผ์น˜ํ•˜๋Š” ๊ด€์ ์ž…๋‹ˆ๋‹ค.

 

๋ฐฐ๋Ÿด ํŒŒ์ผ ์ƒ์„ฑ ์ž๋™ํ™”

๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์—์„œ ๋งค๋ฒˆ ์ˆ˜๋™์œผ๋กœ ๋ฐฐ๋Ÿด ํŒŒ์ผ(Public API)์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์€ ๋ฒˆ๊ฑฐ๋กœ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ์‹ค์šฉ์ ์ธ ์ ‘๊ทผ ๋ฐฉ๋ฒ•์œผ๋กœ, ๋ฐฐ๋Ÿด ํŒŒ์ผ์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด:

pnpm api features/loginUser


์ด๋Ÿฌํ•œ CLI ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด features/loginUser ๋‚ด๋ถ€์˜ ๋ชจ๋“ˆ์„ ์ž๋™์œผ๋กœ ๋ถ„์„ํ•˜๊ณ , ๋ช…์‹œ์ ์œผ๋กœ ๋‚ด๋ณด๋‚ด๋Š” ๋ฐฐ๋Ÿด ํŒŒ์ผ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋•Œ _๋กœ ์‹œ์ž‘ํ•˜๋Š” ํŒŒ์ผ์ด๋‚˜ ํด๋”๋Š” ๋‚ด๋ถ€์šฉ์œผ๋กœ ๊ฐ„์ฃผํ•˜์—ฌ ์ž๋™์œผ๋กœ ์ œ์™ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๋ช…์‹œ์  ๋‚ด๋ณด๋‚ด๊ธฐ์˜ ์ค‘์š”์„ฑ

์•ž์„œ ์–ธ๊ธ‰ํ–ˆ๋“ฏ์ด, ์™€์ผ๋“œ์นด๋“œ ๋‚ด๋ณด๋‚ด๊ธฐ(export * from './module')๋Š” ๊ถŒ์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋Œ€์‹ , ๋ช…์‹œ์  ๋‚ด๋ณด๋‚ด๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์„ฑ๋Šฅ ์ด์Šˆ๋ฅผ ์ตœ์†Œํ™”ํ•˜๋ฉด์„œ๋„ FSD์˜ ์ด์ ์„ ์ถฉ๋ถ„ํžˆ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// Good: ๋ช…์‹œ์  ๋‚ด๋ณด๋‚ด๊ธฐ
export { LoginForm } from './LoginForm';
export { useAuth } from './useAuth';
export type { User } from './types';

์ด๋Ÿฌํ•œ ์ ‘๊ทผ ๋ฐฉ์‹์€ ๋ชจ๋“ˆ์˜ ๊ณต๊ฐœ API๋ฅผ ๋ช…ํ™•ํžˆ ์ •์˜ํ•˜๊ณ , ๋ถˆํ•„์š”ํ•œ ์˜์กด์„ฑ์„ ๋ฐฉ์ง€ํ•˜๋ฉฐ, ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ์ข€ ๋” ์ƒ์„ธํ•œ ์˜ˆ์‹œ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ์š”.

์Šคํฌ๋ฆฝํŠธ์— ๋Œ€ํ•œ ํด๋” ๊ตฌ์กฐ ์˜ˆ์‹œ:

project-root/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ features/
โ”‚   โ”‚   โ””โ”€โ”€ loginUser/
โ”‚   โ”‚       โ”œโ”€โ”€ ui/
โ”‚   โ”‚       โ”œโ”€โ”€ model/
โ”‚   โ”‚       โ””โ”€โ”€ index.ts (์ž๋™ ์ƒ์„ฑ๋  ํŒŒ์ผ)
โ”‚   โ””โ”€โ”€ ... (๋‹ค๋ฅธ FSD ํด๋”๋“ค)
โ”œโ”€โ”€ scripts/
โ”‚   โ””โ”€โ”€ generate-barrel.ts
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ tsconfig.json

 

scripts/generate-barrel.ts ํŒŒ์ผ์˜ ๋‚ด์šฉ:

import fs from 'fs';
import path from 'path';

function generateBarrel(directory: string) {
  const files = fs.readdirSync(directory);
  const exports: string[] = [];

  files.forEach(file => {
    if (file.startsWith('_') || file === 'index.ts') return;

    const filePath = path.join(directory, file);
    const stats = fs.statSync(filePath);

    if (stats.isDirectory()) {
      exports.push(`export * from './${file}';`);
    } else if (file.endsWith('.ts') || file.endsWith('.tsx')) {
      const name = path.parse(file).name;
      exports.push(`export { ${name} } from './${name}';`);
    }
  });

  const content = exports.join('\n') + '\n';
  fs.writeFileSync(path.join(directory, 'index.ts'), content);
}

const targetDir = process.argv[2];
if (!targetDir) {
  console.error('Please specify a target directory');
  process.exit(1);
}

const fullPath = path.resolve(process.cwd(), targetDir);
generateBarrel(fullPath);
console.log(`Barrel file generated for ${fullPath}`);

 

package.json์— ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€:

{
  "scripts": {
    "api": "ts-node scripts/generate-barrel.ts"
  }
}

์ด์ œ pnpm api features/loginUser ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜๋ฉด, src/features/loginUser/index.ts ํŒŒ์ผ์ด ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜๊ฑฐ๋‚˜ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค! ์•„๋ž˜๋ถ€ํ„ฐ๋Š” ์ข€ ๋” ์ผ๋ฐ˜์ ์ธ ์˜ˆ์‹œ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ์š”.

 

FSD ๊ธฐ๋ณธ ํด๋” ๊ตฌ์กฐ:

src/
โ”œโ”€โ”€ app/        # ๊ธ€๋กœ๋ฒŒ ์„ค์ •, ์Šคํƒ€์ผ, ํ”„๋กœ๋ฐ”์ด๋”
โ”œโ”€โ”€ processes/  # ๋น„์ฆˆ๋‹ˆ์Šค ํ”„๋กœ์„ธ์Šค
โ”œโ”€โ”€ pages/      # ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ
โ”œโ”€โ”€ widgets/    # ๋ณตํ•ฉ UI ๋ธ”๋ก
โ”œโ”€โ”€ features/   # ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜
โ”œโ”€โ”€ entities/   # ๋น„์ฆˆ๋‹ˆ์Šค ์—”ํ‹ฐํ‹ฐ
โ””โ”€โ”€ shared/     # ๊ณต์œ  ์œ ํ‹ธ๋ฆฌํ‹ฐ, UI ํ‚คํŠธ

 

FSD์—์„œ์˜ ๋ฐฐ๋Ÿด ํŒŒ์ผ ์‚ฌ์šฉ ์˜ˆ์‹œ

features/auth/index.ts (์ ์ ˆํ•œ ๋ฐฐ๋Ÿด ํŒŒ์ผ ์‚ฌ์šฉ):

// ๋ช…์‹œ์  ๋‚ด๋ณด๋‚ด๊ธฐ - ๊ณต๊ฐœ API๋ฅผ ๋ช…ํ™•ํžˆ ์ •์˜
export { LoginForm } from './ui/LoginForm';
export { useAuth } from './model/useAuth';
export type { AuthUser } from './model/types';

 

๊ท ํ˜• ์žกํžŒ ์ ‘๊ทผ๋ฒ•: ์‹ค์ œ ๊ตฌํ˜„ ์˜ˆ์‹œ

1. ๊ตฌ์กฐํ™”๋œ ๋ฐฐ๋Ÿด ํŒŒ์ผ ์‚ฌ์šฉ:

features/
โ”œโ”€โ”€ auth/
โ”‚   โ”œโ”€โ”€ ui/
โ”‚   โ”‚   โ”œโ”€โ”€ LoginForm.tsx
โ”‚   โ”‚   โ””โ”€โ”€ RegisterForm.tsx
โ”‚   โ”œโ”€โ”€ model/
โ”‚   โ”‚   โ”œโ”€โ”€ useAuth.ts
โ”‚   โ”‚   โ””โ”€โ”€ types.ts
โ”‚   โ””โ”€โ”€ index.ts  # ์•„ํ‚คํ…์ฒ˜ ๊ฒฝ๊ณ„์—๋งŒ ๋ฐฐ๋Ÿด ํŒŒ์ผ ์‚ฌ์šฉ
โ””โ”€โ”€ userProfile/
    โ”œโ”€โ”€ ui/
    โ”œโ”€โ”€ model/
    โ””โ”€โ”€ index.ts

 

2. ๋ช…์‹œ์  ๋‚ด๋ณด๋‚ด๊ธฐ:

// features/auth/index.ts
// ๋ช…์‹œ์  ๋‚ด๋ณด๋‚ด๊ธฐ๋กœ ๊ณต๊ฐœ API ์ •์˜
export { LoginForm } from './ui/LoginForm';
export { RegisterForm } from './ui/RegisterForm';
export { useAuth } from './model/useAuth';
export type { User, AuthCredentials } from './model/types';

 

3. FSD ์•„ํ‚คํ…์ฒ˜ ๊ทœ์น™์— ๋”ฐ๋ฅธ ๊ฐ€์ ธ์˜ค๊ธฐ:

// ์ž˜๋ชป๋œ ๋ฐฉ์‹: ๋ ˆ์ด์–ด ์šฐํšŒ
import { Button } from '@/shared/ui/Button';

// ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ์‹: ์•„ํ‚คํ…์ฒ˜ ๊ฒฝ๊ณ„ ์กด์ค‘
import { Button } from '@/shared/ui';

 

4. ์‹ค์ œ React ์ปดํฌ๋„ŒํŠธ ์˜ˆ์‹œ:

// features/auth/ui/LoginForm.tsx
import { useState } from 'react';
import { Button, TextField } from '@/shared/ui';
import { useAuth } from '../model/useAuth';
import type { AuthCredentials } from '../model/types';

export const LoginForm = () => {
  const [credentials, setCredentials] = useState({
    email: '',
    password: ''
  });
  const { login, isLoading } = useAuth();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    login(credentials);
  };

  return (
    
      <TextField
        label="Email"
        value={credentials.email}
        onChange={(e) => setCredentials(prev => ({
          ...prev,
          email: e.target.value
        }))}
      />
      <TextField
        type="password"
        label="Password"
        value={credentials.password}
        onChange={(e) => setCredentials(prev => ({
          ...prev,
          password: e.target.value
        }))}
      />
      
        {isLoading ? 'Logging in...' : 'Login'}
      
    
  );
};

 

ํ˜„๋Œ€์  ๋„๊ตฌ์˜ ์˜ํ–ฅ

์ตœ์‹  ๋นŒ๋“œ ๋„๊ตฌ๋“ค์€ ๋ฐฐ๋Ÿด ํŒŒ์ผ์˜ ์„ฑ๋Šฅ ๋ฌธ์ œ๋ฅผ ์ผ๋ถ€ ์™„ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

 

๋ฒˆ๋“ค๋Ÿฌ๋ณ„ ์ตœ์ ํ™” ์„ค์ •:

Vite ์„ค์ • ์˜ˆ์‹œ:

// vite.config.js
export default {
  build: {
    target: 'esnext',
    minify: 'esbuild',
    rollupOptions: {
      output: {
        manualChunks(id) {
          // ์ตœ์ ํ™”๋œ ์ฒญํฌ ์„ค์ •
        }
      }
    }
  }
}

 

Webpack ์„ค์ • ์˜ˆ์‹œ:

// webpack.config.js
module.exports = {
  optimization: {
    usedExports: true, // ํŠธ๋ฆฌ ์‰์ดํ‚น ํ™œ์„ฑํ™”
    moduleIds: 'deterministic',
    splitChunks: {
      chunks: 'all',
    }
  }
}

 

์„ฑ๋Šฅ ์ธก์ • - ์‹ค์‹œ๊ฐ„ ํ”„๋กœ์ ํŠธ ๋ชจ๋‹ˆํ„ฐ๋ง

// build-analyzer.ts
import { performance } from 'perf_hooks';

export function measureBuildTime(taskName: string, task: () => void) {
  const startTime = performance.now();
  task();
  const endTime = performance.now();
  console.log(`Task "${taskName}" took ${endTime - startTime}ms`);
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
measureBuildTime('Module compilation', () => {
  // ๋นŒ๋“œ ๋กœ์ง
});

 

๊ฒฐ๋ก  - ํ”„๋กœ์ ํŠธ ๊ทœ๋ชจ๋ณ„ ๊ถŒ์žฅ ์ ‘๊ทผ๋ฒ•

 

์†Œ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ (10,000์ค„ ์ดํ•˜)

src/
โ”œโ”€โ”€ components/
โ”‚   โ””โ”€โ”€ index.ts  # ๋ฐฐ๋Ÿด ํŒŒ์ผ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
โ”œโ”€โ”€ hooks/
โ”‚   โ””โ”€โ”€ index.ts  # ๋ฐฐ๋Ÿด ํŒŒ์ผ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
โ””โ”€โ”€ utils/
    โ””โ”€โ”€ index.ts  # ๋ฐฐ๋Ÿด ํŒŒ์ผ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

 

์ค‘๊ฐ„ ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ (10,000~50,000์ค„)

src/
โ”œโ”€โ”€ features/
โ”‚   โ”œโ”€โ”€ feature1/
โ”‚   โ”‚   โ””โ”€โ”€ index.ts  # ์•„ํ‚คํ…์ฒ˜ ๊ฒฝ๊ณ„์—๋งŒ ๋ฐฐ๋Ÿด ํŒŒ์ผ
โ”‚   โ””โ”€โ”€ feature2/
โ”‚       โ””โ”€โ”€ index.ts
โ””โ”€โ”€ shared/
    โ”œโ”€โ”€ ui/
    โ”‚   โ””โ”€โ”€ index.ts  # ๋ช…์‹œ์  ๋‚ด๋ณด๋‚ด๊ธฐ ์‚ฌ์šฉ
    โ””โ”€โ”€ lib/
        โ””โ”€โ”€ index.ts  # ๋ช…์‹œ์  ๋‚ด๋ณด๋‚ด๊ธฐ ์‚ฌ์šฉ

 

๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ (50,000์ค„ ์ด์ƒ)

src/
โ”œโ”€โ”€ app/
โ”œโ”€โ”€ processes/
โ”œโ”€โ”€ pages/
โ”‚   โ””โ”€โ”€ index.ts  # ์—„๊ฒฉํ•˜๊ฒŒ ์ œํ•œ๋œ ๋ฐฐ๋Ÿด ํŒŒ์ผ
โ”œโ”€โ”€ widgets/
โ”‚   โ””โ”€โ”€ index.ts  # ์—„๊ฒฉํ•˜๊ฒŒ ์ œํ•œ๋œ ๋ฐฐ๋Ÿด ํŒŒ์ผ
โ”œโ”€โ”€ features/
โ”‚   โ””โ”€โ”€ [๊ฐ ๊ธฐ๋Šฅ๋ณ„]/
โ”‚       โ””โ”€โ”€ index.ts  # ๊ณต๊ฐœ API๋งŒ ๋ช…์‹œ์  ๋‚ด๋ณด๋‚ด๊ธฐ
โ”œโ”€โ”€ entities/
โ”‚   โ””โ”€โ”€ [๊ฐ ์—”ํ‹ฐํ‹ฐ๋ณ„]/
โ”‚       โ””โ”€โ”€ index.ts  # ๊ณต๊ฐœ API๋งŒ ๋ช…์‹œ์  ๋‚ด๋ณด๋‚ด๊ธฐ
โ””โ”€โ”€ shared/
    โ””โ”€โ”€ [๊ฐ ๋ชจ๋“ˆ๋ณ„]/
        โ””โ”€โ”€ index.ts  # ์„ฑ๋Šฅ ์ธก์ • ํ›„ ๊ฒฐ์ •
 

 

๋ฐฐ๋Ÿด ํŒŒ์ผ์˜ ์‚ฌ์šฉ์€ ๊ฒฐ๊ตญ ๊ฐœ๋ฐœ ๊ฒฝํ—˜๊ณผ ์„ฑ๋Šฅ ์‚ฌ์ด์˜ ๊ท ํ˜•์„ ์ฐพ๋Š” ๋ฌธ์ œ๋ผ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

FSD ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋„์ž…ํ•œ๋‹ค๋ฉด, ๋ฐฐ๋Ÿด ํŒŒ์ผ์˜ ์žฅ๋‹จ์ ์„ ์ธ์‹ํ•˜๊ณ  ํ”„๋กœ์ ํŠธ์˜ ๊ทœ๋ชจ์™€ ํŒ€์˜ ํ•„์š”์— ๋งž๊ฒŒ ์ ์ ˆํžˆ ์กฐ์ ˆํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•  ๊ฒƒ ๊ฐ™์•„์š”.

๋ช…ํ™•ํ•œ ์•„ํ‚คํ…์ฒ˜ ๊ฒฝ๊ณ„, ๋ช…์‹œ์ ์ธ ๋‚ด๋ณด๋‚ด๊ธฐ, ๊ทธ๋ฆฌ๊ณ  ํ˜„๋Œ€์ ์ธ ๋นŒ๋“œ ๋„๊ตฌ๋ฅผ ํ™œ์šฉํ•œ๋‹ค๋ฉด, ๋ฐฐ๋Ÿด ํŒŒ์ผ์˜ ์ด์ ์„ ์ทจํ•˜๋ฉด์„œ๋„ ์„ฑ๋Šฅ ๋ฌธ์ œ๋ฅผ ์ตœ์†Œํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

+ Recent posts