๐ŸŒฑ <groot/>
ํด๋ฆญ์€ ํ†ต๊ณผ์‹œํ‚ค๊ณ , ๊ธ€์”จ์ƒ‰์€ ๋ฐฐ๊ฒฝ์— ๋งž์ถ”๊ธฐ โ€” ์˜ค๋ฒ„๋ ˆ์ด ์œ„์ ฏ์˜ ๋‘ ๊ฐ€์ง€ ํŠธ๋ฆญ | Groot's Tech Blog
๐ŸŒWeb

ํด๋ฆญ์€ ํ†ต๊ณผ์‹œํ‚ค๊ณ , ๊ธ€์”จ์ƒ‰์€ ๋ฐฐ๊ฒฝ์— ๋งž์ถ”๊ธฐ โ€” ์˜ค๋ฒ„๋ ˆ์ด ์œ„์ ฏ์˜ ๋‘ ๊ฐ€์ง€ ํŠธ๋ฆญ

2026.07.02ยท3๋ถ„ ์ฝ๊ธฐ

์‹œ์„ธ ์œ„์ ฏ ํ™•์žฅ์€ "์•„๋ฌด ์›นํŽ˜์ด์ง€ ์œ„์—" ๋–  ์žˆ์–ด์•ผ ํ•œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋‚จ์˜ ํŽ˜์ด์ง€ ์œ„์— ๋ญ˜ ๋ฎ์œผ๋ฉด ๋‘ ๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด๋‹ค. (1) ์œ„์ ฏ์ด ์•„๋ž˜ ํŽ˜์ด์ง€์˜ ํด๋ฆญ์„ ๊ฐ€๋กœ๋ง‰๊ณ , (2) ์œ„์ ฏ ๊ธ€์”จ๊ฐ€ ๋ฐฐ๊ฒฝ์— ๋ฌปํžŒ๋‹ค. ์ด ๋‘˜์„ ์–ด๋–ป๊ฒŒ ํ’€์—ˆ๋Š”์ง€ ์ •๋ฆฌํ•œ๋‹ค.

ํŠธ๋ฆญ 1: ํด๋ฆญ์€ ์•„๋ž˜๋กœ ํ†ต๊ณผ์‹œํ‚ค๊ธฐ (pointer-events)

์œ„์ ฏ์€ ํ™”๋ฉด ์ „์ฒด๋ฅผ ๋ฎ๋Š” ์˜ค๋ฒ„๋ ˆ์ด ์•ˆ์— ๊ทธ๋ฆฐ๋‹ค. ์ด๊ฑธ ๊ทธ๋ƒฅ ๋‘๋ฉด ์˜ค๋ฒ„๋ ˆ์ด๊ฐ€ ํŽ˜์ด์ง€์˜ ๋ชจ๋“  ํด๋ฆญ์„ ์‚ผํ‚จ๋‹ค. ํ•ต์‹ฌ์€ CSS pointer-events๋‹ค.

#stock-widget-root {
  position: fixed;
  inset: 0;
  z-index: 2147483646;
  pointer-events: none; /* ๊ธฐ๋ณธ: ํด๋ฆญ์ด ์•„๋ž˜ ํŽ˜์ด์ง€๋กœ ํ†ต๊ณผ */
}

#stock-widget-root .stock-widget {
  pointer-events: none; /* ์œ„์ ฏ๋„ ํ‰์†Œ์—” ํด๋ฆญ ์•ˆ ๋ฐ›์Œ */
}

/* ํŽธ์ง‘ ๋ชจ๋“œ์ผ ๋•Œ๋งŒ ์œ„์ ฏ์ด ํด๋ฆญยท๋“œ๋ž˜๊ทธ๋ฅผ ๋ฐ›๋Š”๋‹ค */
#stock-widget-root[data-edit-mode="true"] .stock-widget {
  pointer-events: auto;
  cursor: grab;
}

pointer-events: none์ด๋ฉด ๊ทธ ์š”์†Œ๋Š” ๋งˆ์šฐ์Šค ์ด๋ฒคํŠธ์— ํˆฌ๋ช…ํ•ด์ง„๋‹ค. ํด๋ฆญ์ด ์œ„์ ฏ์„ ๊ทธ๋ƒฅ ํ†ต๊ณผํ•ด ์•„๋ž˜ ํŽ˜์ด์ง€๋กœ ๊ฐ„๋‹ค. ์‹œ์„ธ๋Š” ๋ˆˆ์œผ๋กœ ๋ณด๋˜, ํŽ˜์ด์ง€๋Š” ํ‰์†Œ์ฒ˜๋Ÿผ ์“ธ ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋Ÿผ ์œ„์ ฏ์„ ์˜ฎ๊ธฐ๊ณ  ์‹ถ์„ ๋•? Shift๋ฅผ ๋ˆ„๋ฅด๊ฑฐ๋‚˜ ๋“œ๋ž˜๊ทธ ์ค‘์ผ ๋•Œ๋งŒ ํŽธ์ง‘ ๋ชจ๋“œ๋กœ ์ „ํ™˜ํ•œ๋‹ค. ์ƒํƒœ๋ฅผ ๋ฐ์ดํ„ฐ ์†์„ฑ ํ•˜๋‚˜๋กœ ๊ด€๋ฆฌํ–ˆ๋‹ค.

function syncRootState() {
  root.dataset.editMode =
    shiftPressed || cmdPressed || selectionState || dragState || resizeState
      ? "true"
      : "false";
}

data-edit-mode๊ฐ€ true๊ฐ€ ๋˜๋Š” ์ˆœ๊ฐ„ ์œ„์ ฏ์ด pointer-events: auto๋กœ ๋ฐ”๋€Œ์–ด ๋“œ๋ž˜๊ทธยท๋ฆฌ์‚ฌ์ด์ฆˆ๋ฅผ ๋ฐ›๊ณ , ๋†“์œผ๋ฉด ๋‹ค์‹œ ํˆฌ๋ช…ํ•ด์ง„๋‹ค. ์ƒํƒœ๋Š” JS๋กœ, ์‹ค์ œ ๋ฐ˜์‘์€ CSS๋กœ ๋‚˜๋ˆˆ ๊ฒŒ ๊น”๋”ํ–ˆ๋‹ค.

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

ํŠธ๋ฆญ 2: ๊ธ€์”จ์ƒ‰์„ ๋ฐฐ๊ฒฝ์— ๋งž์ถ”๊ธฐ (๋ช…์•”๋น„ ๊ณ„์‚ฐ)

์œ„์ ฏ์€ ๋ฐ˜ํˆฌ๋ช…์ด๋ผ ๋’ค ๋ฐฐ๊ฒฝ์ด ๋น„์นœ๋‹ค. ํฐ ๋ฐฐ๊ฒฝ์ด๋ฉด ๊ฒ€์€ ๊ธ€์”จ, ์–ด๋‘์šด ๋ฐฐ๊ฒฝ์ด๋ฉด ํฐ ๊ธ€์”จ์—ฌ์•ผ ์ฝํžŒ๋‹ค. ๊ทธ๋ž˜์„œ ์œ„์ ฏ ๋’ค์˜ ์‹ค์ œ ๋ฐฐ๊ฒฝ์ƒ‰์„ ์ƒ˜ํ”Œ๋งํ•˜๊ณ , ๊ทธ์— ๋งž๋Š” ๊ธ€์”จ์ƒ‰์„ ๊ณจ๋ž๋‹ค.

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

๊ทธ๋‹ค์Œ์ด ํ•ต์‹ฌ์ธ๋ฐ, "๊ฒ€์ •์ด ๋‚˜์„๊นŒ ํฐ์ƒ‰์ด ๋‚˜์„๊นŒ"๋ฅผ ๊ฐ์ด ์•„๋‹ˆ๋ผ WCAG์˜ ๋ช…์•”๋น„ ๊ณต์‹์œผ๋กœ ๊ณ„์‚ฐํ–ˆ๋‹ค.

// ์ƒ๋Œ€ ํœ˜๋„ (WCAG ์ •์˜)
function getRelativeLuminance(color) {
  const { r, g, b } = parseColor(color);
  const toLinear = (c) => {
    const v = c / 255;
    return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
  };
  return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
}

// ๋‘ ์ƒ‰์˜ ๋ช…์•”๋น„ (1:1 ~ 21:1)
function getContrastRatio(fg, bg) {
  const L1 = getRelativeLuminance(fg);
  const L2 = getRelativeLuminance(bg);
  const [lighter, darker] = [Math.max(L1, L2), Math.min(L1, L2)];
  return (lighter + 0.05) / (darker + 0.05);
}

๋ฐฐ๊ฒฝ์ƒ‰์ด ์ •ํ•ด์ง€๋ฉด ๊ธ€์”จ์ƒ‰์„ ์ด๋ ‡๊ฒŒ ๊ณ ๋ฅธ๋‹ค. ์šฐ์„ ์ˆœ์œ„๊ฐ€ ์žˆ๋‹ค.

  1. ํŽ˜์ด์ง€๊ฐ€ ์›๋ž˜ ์“ฐ๋˜ ํ…์ŠคํŠธ์ƒ‰์„ ์ตœ๋Œ€ํ•œ ์กด์ค‘ํ•œ๋‹ค. ์ƒ˜ํ”Œ ์ง€์ ์˜ ํ…์ŠคํŠธ์ƒ‰, ์ฃผ๋ณ€ ์ปจํ…์ŠคํŠธ ์š”์†Œ์˜ ์ƒ‰, body ์ƒ‰ ์ˆœ์œผ๋กœ ํ›„๋ณด๋ฅผ ํ›‘์œผ๋ฉด์„œ ๋ฐฐ๊ฒฝ๊ณผ์˜ ๋ช…์•”๋น„๊ฐ€ 3.5 ์ด์ƒ์ธ ์ฒซ ํ›„๋ณด๋ฅผ "์„ ํ˜ธ ์ƒ‰"์œผ๋กœ ์žก๋Š”๋‹ค. ์œ„์ ฏ์ด ํŽ˜์ด์ง€์— ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋…น์•„๋“ค๊ฒŒ ํ•˜๋ ค๋Š” ์˜๋„๋‹ค.
  2. ๊ทธ ์„ ํ˜ธ ์ƒ‰์ด ์ตœ์ข… ๊ฒ€์‚ฌ(๋ช…์•”๋น„ 4.5, WCAG AA ๊ธฐ์ค€)๋ฅผ ํ†ต๊ณผํ•˜๋ฉด ๊ทธ๋Œ€๋กœ ์“ด๋‹ค.
  3. ํ†ต๊ณผ ๋ชป ํ•˜๋ฉด ๊ทธ๋•Œ์„œ์•ผ ๋ฐ์€์ƒ‰ยท์–ด๋‘์šด์ƒ‰ ๋‘ ํ›„๋ณด ์ค‘ ๋ฐฐ๊ฒฝ๊ณผ์˜ ๋ช…์•”๋น„๊ฐ€ ๋” ๋†’์€ ์ชฝ์œผ๋กœ ํด๋ฐฑํ•œ๋‹ค.
function pickReadableTextColor(preferredColor, backgroundColor) {
  const light = "rgb(248, 250, 252)";
  const dark = "rgb(15, 23, 42)";
  if (getContrastRatio(preferredColor, backgroundColor) >= 4.5) return preferredColor;
  return getContrastRatio(light, backgroundColor) > getContrastRatio(dark, backgroundColor)
    ? light
    : dark;
}

์‚ฌ๋žŒ์ด "์ด ์ •๋„๋ฉด ์ฝํžˆ๊ฒ ์ง€" ํ•˜๊ณ  ์ฐ๋Š” ๋Œ€์‹ , ์ ‘๊ทผ์„ฑ ํ‘œ์ค€์ด ์ •์˜ํ•œ ์ˆ˜์‹์œผ๋กœ ์ •ํ•˜๋‹ˆ ์–ด๋–ค ์‚ฌ์ดํŠธ ์œ„์— ์˜ฌ๋ ค๋„ ์ผ๊ด€๋˜๊ฒŒ ์ฝํ˜”๋‹ค.

๋ฐฐ์šด ๊ฒƒ

  • pointer-events๋Š” ์˜ค๋ฒ„๋ ˆ์ด UI์˜ ํ•ต์‹ฌ ๋„๊ตฌ๋‹ค. ์ƒํƒœ๋Š” JS, ๋ฐ˜์‘์€ CSS๋กœ ๋‚˜๋ˆ„๋ฉด ๊น”๋”ํ•˜๋‹ค.

  • "๊ธ€์”จ๊ฐ€ ์ž˜ ๋ณด์ธ๋‹ค"๋Š” ์ฃผ๊ด€ ๊ฐ™์ง€๋งŒ, WCAG ์ƒ๋Œ€ ํœ˜๋„ยท๋ช…์•”๋น„๋ผ๋Š” ์ •๋Ÿ‰ ๊ธฐ์ค€์ด ์žˆ๋‹ค. ๊ฐ์œผ๋กœ ์ƒ‰์„ ๊ณ ๋ฅด์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

  • ๋‚จ์˜ ํŽ˜์ด์ง€ ์œ„์— ์–น๋Š” ํ™•์žฅ์€ ๊ฒฐ๊ตญ "๋‚จ์˜ ํ™˜๊ฒฝ์„ ์นจ๋ฒ”ํ•˜์ง€ ์•Š๊ธฐ"๊ฐ€ ์ ˆ๋ฐ˜์ด๋‹ค. ํด๋ฆญ ํ†ต๊ณผ, ์ด๋ฒคํŠธ ์ถฉ๋Œ, ๋ฐฐ๊ฒฝ ์ ์‘ ์ „๋ถ€ ๊ทธ ์—ฐ์žฅ์„ ์ด์—ˆ๋‹ค.

  • ๊ด€๋ จ ํ”„๋กœ์ ํŠธ: Stock Price Widget

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ
ยฉ 2026 ยท Groot's Tech Blog. Built with Gatsby ๐ŸŒฑ