July 22, 2025

๐Ÿ•ธ๏ธ ์˜ค๋ž˜๋œ ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ ๋ฌด์‹œ: ์›น ์ ‘๊ทผ์„ฑ์˜ ์žฅ๋ฒฝ๊ณผ ์‚ฌ์šฉ์ž ์ดํƒˆ

JavaScript
CSS
HTML
UX
์›นํ‘œ์ค€
ํ˜ธํ™˜์„ฑ
๋นŒ๋“œ&๋ฒˆ๋“ค๋ง
์—๋Ÿฌ์ฒ˜๋ฆฌ
SEO/์ ‘๊ทผ์„ฑ

Summary

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

Why Wrong?

๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ  ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•˜๋Š” ๊ฒƒ์€ ์‹ฌ๊ฐํ•œ ์•ˆํ‹ฐํŒจํ„ด์ž…๋‹ˆ๋‹ค. ํŠน์ • ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์—๋งŒ ์กด์žฌํ•˜๋Š” ์ตœ์‹  JavaScript API(์˜ˆ: IntersectionObserver, ResizeObserver)๋‚˜ CSS ์†์„ฑ(์˜ˆ: gap in Flexbox, scroll-behavior)์„ ํด๋ฆฌํ•„(Polyfill)์ด๋‚˜ ํด๋ฐฑ(Fallback) ์—†์ด ๋ฌด๋ถ„๋ณ„ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด, ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €๋‚˜ ํŠน์ • ํ™˜๊ฒฝ(์˜ˆ: ๊ธฐ์—… ๋‚ด๋ถ€๋ง์˜ ์—…๋ฐ์ดํŠธ๊ฐ€ ๋А๋ฆฐ ๋ธŒ๋ผ์šฐ์ €)์—์„œ ์›น์‚ฌ์ดํŠธ์˜ UI๊ฐ€ ๊นจ์ง€๊ฑฐ๋‚˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ด ์ „ํ˜€ ์ž‘๋™ํ•˜์ง€ ์•Š๋Š” ์น˜๋ช…์ ์ธ ๊ฒฐ๊ณผ๋ฅผ ์ดˆ๋ž˜ํ•ฉ๋‹ˆ๋‹ค.

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

How to Fix?

๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” '์ ์ง„์  ํ–ฅ์ƒ(Progressive Enhancement)' ์ „๋žต์„ ์ฑ„ํƒํ•˜๊ณ , '๊ธฐ๋Šฅ ๊ฐ์ง€(Feature Detection)'๋ฅผ ํ†ตํ•ด ๋ธŒ๋ผ์šฐ์ € ์ง€์› ์—ฌ๋ถ€๋ฅผ ๋Ÿฐํƒ€์ž„์— ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ ๋‹จ๊ณ„์—์„œ caniuse.com๊ณผ ๊ฐ™์€ ๋„๊ตฌ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํƒ€๊ฒŸ ์‚ฌ์šฉ์ž์ธต์˜ ๋ธŒ๋ผ์šฐ์ € ์ง€์› ๋ฒ”์œ„๋ฅผ ๋ฉด๋ฐ€ํžˆ ํŒŒ์•…ํ•˜๊ณ , ๋‹ค์Œ ๊ธฐ๋ฒ•๋“ค์„ ์ ๊ทน์ ์œผ๋กœ ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

  1. ๊ธฐ๋Šฅ ๊ฐ์ง€(Feature Detection): ํŠน์ • JavaScript API๋‚˜ CSS ๊ธฐ๋Šฅ์ด ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ง€์›๋˜๋Š”์ง€ ๋จผ์ € ํ™•์ธํ•œ ํ›„ ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์ง€์›๋˜์ง€ ์•Š๋Š” ํ™˜๊ฒฝ์—์„œ๋Š” ๋Œ€์ฒด ๋กœ์ง์ด๋‚˜ ํด๋ฐฑ UI๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, JavaScript์—์„œ๋Š” if ('IntersectionObserver' in window)์™€ ๊ฐ™์ด ๊ฐ์ฒด์˜ ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ณ , CSS์—์„œ๋Š” @supports ์ฟผ๋ฆฌ๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.
  2. ํด๋ฆฌํ•„(Polyfill) ์ ์šฉ: ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ตœ์‹  JavaScript ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํด๋ฆฌํ•„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(์˜ˆ: core-js, polyfill.io)๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ํด๋ฆฌํ•„์€ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ๋ชจ๋ฐฉํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ œ๊ณตํ•˜์—ฌ ๋งˆ์น˜ ํ•ด๋‹น ๊ธฐ๋Šฅ์ด ๋‚ด์žฅ๋œ ๊ฒƒ์ฒ˜๋Ÿผ ์ž‘๋™ํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ๋นŒ๋“œ ์‹œ์Šคํ…œ(์˜ˆ: Webpack, Rollup)๊ณผ Babel์„ ํ†ตํ•ด ํ•„์š”ํ•œ ํด๋ฆฌํ•„๋งŒ ๋ฒˆ๋“ค์— ํฌํ•จํ•˜๋„๋ก ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.
  3. ํด๋ฐฑ(Fallback) CSS: display: grid๋‚˜ gap ์†์„ฑ ๋“ฑ ์ตœ์‹  CSS ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ, ์ด๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์œ„ํ•ด float, inline-block, flexbox (๊ตฌํ˜• Flexbox ๋ฌธ๋ฒ• ํฌํ•จ) ๋˜๋Š” ๋งˆ์ง„/ํŒจ๋”ฉ์„ ํ™œ์šฉํ•œ ๋Œ€์ฒด ๋ ˆ์ด์•„์›ƒ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. @supports ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŠน์ • CSS ์†์„ฑ ์ง€์› ์—ฌ๋ถ€์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์Šคํƒ€์ผ์„ ์กฐ๊ฑด๋ถ€๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  4. ํŠธ๋žœ์ŠคํŒŒ์ผ๋ง(Transpiling): Babel๊ณผ ๊ฐ™์€ ํŠธ๋žœ์ŠคํŒŒ์ผ๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ตœ์‹  JavaScript(ES6+) ๋ฌธ๋ฒ•์„ ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €์—์„œ๋„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š” ES5 ๋ฌธ๋ฒ•์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๊ฐœ๋ฐœ ํŽธ์˜์„ฑ๊ณผ ๋„“์€ ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ์„ ๋™์‹œ์— ํ™•๋ณดํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
  5. ์ ๊ทน์ ์ธ ํ…Œ์ŠคํŠธ: ๊ฐœ๋ฐœ ์ค‘์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋‹ค์–‘ํ•œ ๋ธŒ๋ผ์šฐ์ €(ํฌ๋กฌ, ํŒŒ์ด์–ดํญ์Šค, ์‚ฌํŒŒ๋ฆฌ, ์—ฃ์ง€ ๋“ฑ)์™€ ๋‹ค์–‘ํ•œ ๋””๋ฐ”์ด์Šค(๋ฐ์Šคํฌํƒ‘, ๋ชจ๋ฐ”์ผ, ํƒœ๋ธ”๋ฆฟ)์—์„œ ํ…Œ์ŠคํŠธํ•˜์—ฌ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ๋ฅผ ์‚ฌ์ „์— ๋ฐœ๊ฒฌํ•˜๊ณ  ํ•ด๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. CI/CD ํŒŒ์ดํ”„๋ผ์ธ์— ๋ธŒ๋ผ์šฐ์ € ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ฉํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

Before Code (Bad)

// Before: IntersectionObserver์™€ Flexbox gap์„ ๊ธฐ๋Šฅ ๊ฐ์ง€ ์—†์ด ์‚ฌ์šฉ
// ์ด ์ฝ”๋“œ๋Š” IntersectionObserver๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ธŒ๋ผ์šฐ์ €๋‚˜,
// Flexbox gap์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €(์˜ˆ: IE, ์ผ๋ถ€ ๊ตฌํ˜• Safari)์—์„œ
// ์Šคํฌ๋กค ๊ฐ์ง€ ๊ธฐ๋Šฅ์ด ์ž‘๋™ํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋ ˆ์ด์•„์›ƒ์ด ๊นจ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import React, { useRef, useEffect } from 'react';

const StickyHeader = () => {
  const headerRef = useRef(null);

  useEffect(() => {
    // IntersectionObserver๊ฐ€ ์ „์—ญ ๊ฐ์ฒด์— ์—†์œผ๋ฉด ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (!entry.isIntersecting) {
          headerRef.current.classList.add('fixed-header');
        } else {
          headerRef.current.classList.remove('fixed-header');
        }
      },
      { threshold: 0.01 } // ํ—ค๋”๊ฐ€ ๊ฑฐ์˜ ๋ณด์ด์ง€ ์•Š์„ ๋•Œ ๊ณ ์ •
    );

    if (headerRef.current) {
      observer.observe(headerRef.current);
    }

    return () => {
      if (headerRef.current) {
        observer.unobserve(headerRef.current);
      }
    };
  }, []);

  return (
    <header ref={headerRef} className="app-header">
      <div className="nav-links">
        <a href="#">Home</a>
        <a href="#">About</a>
        <a href="#">Contact</a>
      </div>
    </header>
  );
};

export default StickyHeader;

/* CSS */
// .app-header {
//   background: #f0f0f0;
//   padding: 15px;
//   position: sticky; /* IE, ์ผ๋ถ€ ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ฏธ์ง€์› */
//   top: 0;
//   z-index: 100;
// }
// .nav-links {
//   display: flex;
//   gap: 20px; /* IE, ์ผ๋ถ€ ๊ตฌํ˜• Safari์—์„œ ๋ฏธ์ง€์› */
// }
// .fixed-header {
//   box-shadow: 0 2px 5px rgba(0,0,0,0.2);
// }

After Code (Good)

// After: ๊ธฐ๋Šฅ ๊ฐ์ง€ ๋ฐ ํด๋ฐฑ/ํด๋ฆฌํ•„ ์ ์šฉ

import React, { useRef, useEffect } from 'react';

const StickyHeader = () => {
  const headerRef = useRef(null);

  useEffect(() => {
    // IntersectionObserver ๊ธฐ๋Šฅ ๊ฐ์ง€ ๋ฐ ํด๋ฐฑ ๋กœ์ง
    if ('IntersectionObserver' in window) {
      const observer = new IntersectionObserver(
        ([entry]) => {
          if (!entry.isIntersecting) {
            headerRef.current.classList.add('fixed-header');
          } else {
            headerRef.current.classList.remove('fixed-header');
          }
        },
        { threshold: 0.01 }
      );

      if (headerRef.current) {
        observer.observe(headerRef.current);
      }

      return () => {
        if (headerRef.current) {
          observer.unobserve(headerRef.current);
        }
      };
    } else {
      // IntersectionObserver ๋ฏธ์ง€์› ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์œ„ํ•œ ํด๋ฐฑ (์˜ˆ: ์Šคํฌ๋กค ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜)
      console.warn('IntersectionObserver is not supported. Using scroll event fallback.');
      const handleScroll = () => {
        if (headerRef.current) {
          const rect = headerRef.current.getBoundingClientRect();
          if (rect.top <= 0) {
            headerRef.current.classList.add('fixed-header');
          } else {
            headerRef.current.classList.remove('fixed-header');
          }
        }
      };
      window.addEventListener('scroll', handleScroll);
      return () => window.removeEventListener('scroll', handleScroll);
    }
  }, []);

  return (
    <header ref={headerRef} className="app-header">
      <div className="nav-links">
        <a href="#">Home</a>
        <a href="#">About</a>
        <a href="#">Contact</a>
      </div>
    </header>
  );
};

export default StickyHeader;

/* CSS */
// .app-header {
//   background: #f0f0f0;
//   padding: 15px;
//   /* `position: sticky` ํด๋ฐฑ: IE์—์„œ๋Š” `position: fixed` ๋˜๋Š” JavaScript๋กœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ */
//   position: -webkit-sticky; /* ๊ตฌํ˜• ์›นํ‚ท ๋ธŒ๋ผ์šฐ์ € ์ง€์› */
//   position: sticky;
//   top: 0;
//   z-index: 100;
// }

// .nav-links {
//   display: flex;
//   /* flex-gap ๋ฏธ์ง€์› ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์œ„ํ•œ ํด๋ฐฑ ๋งˆ์ง„ */
//   margin-left: -20px;
// }
// .nav-links > a {
//   margin-left: 20px;
// }

// /* @supports๋ฅผ ์ด์šฉํ•œ Flexbox gap ์กฐ๊ฑด๋ถ€ ์ ์šฉ */
// @supports (gap: 20px) {
//   .nav-links {
//     gap: 20px;
//     margin-left: 0; /* gap ์ง€์› ์‹œ ํด๋ฐฑ ๋งˆ์ง„ ์ œ๊ฑฐ */
//   }
//   .nav-links > a {
//     margin-left: 0; /* gap ์ง€์› ์‹œ ํด๋ฐฑ ๋งˆ์ง„ ์ œ๊ฑฐ */
//   }
// }

// .fixed-header {
//   box-shadow: 0 2px 5px rgba(0,0,0,0.2);
// }

You Might Also Like

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฐ ์›น API์˜ ๊ธฐ๋Šฅ ์ง€์› ์—ฌ๋ถ€๋ฅผ ๋Ÿฐํƒ€์ž„์— ํ™•์ธํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ํŠน์ • ๊ธฐ๋Šฅ์ด ์—†๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ํด๋ฐฑ์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ๊ธฐ๋ณธ์ ์ธ ๊ธฐ๋ฒ•์ž…๋‹ˆ๋‹ค.
https://developer.mozilla.org/en-US/docs/Web/API/Feature_detection
CSS ์†์„ฑ ์ง€์› ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜์—ฌ ๋‹ค๋ฅธ ์Šคํƒ€์ผ์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” `@supports` ๊ทœ์น™์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์ตœ์‹  CSS ์†์„ฑ์„ ์‚ฌ์šฉํ•˜๋ฉด์„œ๋„ ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์œ„ํ•œ ํด๋ฐฑ ์Šคํƒ€์ผ์„ ํšจ๊ณผ์ ์œผ๋กœ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
https://developer.mozilla.org/en-US/docs/Web/CSS/@supports
ํ•ต์‹ฌ ์ฝ˜ํ…์ธ ์™€ ๊ธฐ๋Šฅ์€ ๋ชจ๋“  ์‚ฌ์šฉ์ž์—๊ฒŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๊ณ , ์ตœ์‹  ๋ธŒ๋ผ์šฐ์ € ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ๋” ํ–ฅ์ƒ๋œ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๋Š” ์›น ๊ฐœ๋ฐœ ์ „๋žต์ธ ์ ์ง„์  ํ–ฅ์ƒ(Progressive Enhancement)์— ๋Œ€ํ•œ ๊ฐœ๋…์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.
https://developer.mozilla.org/en-US/docs/Glossary/Progressive_enhancement