๐ธ๏ธ ์ค๋๋ ๋ธ๋ผ์ฐ์ ํธํ์ฑ ๋ฌด์: ์น ์ ๊ทผ์ฑ์ ์ฅ๋ฒฝ๊ณผ ์ฌ์ฉ์ ์ดํ
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
๊ณผ ๊ฐ์ ๋๊ตฌ๋ฅผ ํ์ฉํ์ฌ ํ๊ฒ ์ฌ์ฉ์์ธต์ ๋ธ๋ผ์ฐ์ ์ง์ ๋ฒ์๋ฅผ ๋ฉด๋ฐํ ํ์
ํ๊ณ , ๋ค์ ๊ธฐ๋ฒ๋ค์ ์ ๊ทน์ ์ผ๋ก ์ ์ฉํด์ผ ํฉ๋๋ค:
- ๊ธฐ๋ฅ ๊ฐ์ง(Feature Detection): ํน์ JavaScript API๋ CSS ๊ธฐ๋ฅ์ด ๋ธ๋ผ์ฐ์ ์์ ์ง์๋๋์ง ๋จผ์ ํ์ธํ ํ ํด๋น ๊ธฐ๋ฅ์ ์ฌ์ฉํฉ๋๋ค. ์ด๋ฅผ ํตํด ์ง์๋์ง ์๋ ํ๊ฒฝ์์๋ ๋์ฒด ๋ก์ง์ด๋ ํด๋ฐฑ UI๋ฅผ ์ ๊ณตํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, JavaScript์์๋
if ('IntersectionObserver' in window)
์ ๊ฐ์ด ๊ฐ์ฒด์ ์กด์ฌ ์ฌ๋ถ๋ฅผ ํ์ธํ๊ณ , CSS์์๋@supports
์ฟผ๋ฆฌ๋ฅผ ํ์ฉํฉ๋๋ค. - ํด๋ฆฌํ(Polyfill) ์ ์ฉ: ๊ตฌํ ๋ธ๋ผ์ฐ์ ์์ ์ต์ JavaScript ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์๋๋ก ํด๋ฆฌํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(์:
core-js
,polyfill.io
)๋ฅผ ์ ์ฉํฉ๋๋ค. ํด๋ฆฌํ์ ์๋ก์ด ๊ธฐ๋ฅ์ ๋ชจ๋ฐฉํ๋ ์ฝ๋๋ฅผ ์ ๊ณตํ์ฌ ๋ง์น ํด๋น ๊ธฐ๋ฅ์ด ๋ด์ฅ๋ ๊ฒ์ฒ๋ผ ์๋ํ๊ฒ ๋ง๋ญ๋๋ค. ๋น๋ ์์คํ (์: Webpack, Rollup)๊ณผ Babel์ ํตํด ํ์ํ ํด๋ฆฌํ๋ง ๋ฒ๋ค์ ํฌํจํ๋๋ก ์ค์ ํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. - ํด๋ฐฑ(Fallback) CSS:
display: grid
๋gap
์์ฑ ๋ฑ ์ต์ CSS ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ, ์ด๋ฅผ ์ง์ํ์ง ์๋ ๋ธ๋ผ์ฐ์ ๋ฅผ ์ํดfloat
,inline-block
,flexbox
(๊ตฌํ Flexbox ๋ฌธ๋ฒ ํฌํจ) ๋๋ ๋ง์ง/ํจ๋ฉ์ ํ์ฉํ ๋์ฒด ๋ ์ด์์์ ์ ๊ณตํฉ๋๋ค.@supports
์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ํน์ CSS ์์ฑ ์ง์ ์ฌ๋ถ์ ๋ฐ๋ผ ๋ค๋ฅธ ์คํ์ผ์ ์กฐ๊ฑด๋ถ๋ก ์ ์ฉํ ์ ์์ต๋๋ค. - ํธ๋์คํ์ผ๋ง(Transpiling): Babel๊ณผ ๊ฐ์ ํธ๋์คํ์ผ๋ฌ๋ฅผ ์ฌ์ฉํ์ฌ ์ต์ JavaScript(ES6+) ๋ฌธ๋ฒ์ ๊ตฌํ ๋ธ๋ผ์ฐ์ ์์๋ ์ดํดํ ์ ์๋ ES5 ๋ฌธ๋ฒ์ผ๋ก ๋ณํํฉ๋๋ค. ์ด๋ ๊ฐ๋ฐ ํธ์์ฑ๊ณผ ๋์ ๋ธ๋ผ์ฐ์ ํธํ์ฑ์ ๋์์ ํ๋ณดํ๋ ๋ฐฉ๋ฒ์ ๋๋ค.
- ์ ๊ทน์ ์ธ ํ ์คํธ: ๊ฐ๋ฐ ์ค์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค์ํ ๋ธ๋ผ์ฐ์ (ํฌ๋กฌ, ํ์ด์ดํญ์ค, ์ฌํ๋ฆฌ, ์ฃ์ง ๋ฑ)์ ๋ค์ํ ๋๋ฐ์ด์ค(๋ฐ์คํฌํ, ๋ชจ๋ฐ์ผ, ํ๋ธ๋ฆฟ)์์ ํ ์คํธํ์ฌ ํธํ์ฑ ๋ฌธ์ ๋ฅผ ์ฌ์ ์ ๋ฐ๊ฒฌํ๊ณ ํด๊ฒฐํด์ผ ํฉ๋๋ค. 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);
// }