๐จ Tailwind Advanced 2์ฅ: ์คํฌ๋กค ๋ง์คํฐ โ Scroll Snap & ๋ถ๋๋ฌ์ด UX
๐ ๊ฐ์
CSS Scroll Snap, overscroll-behavior, scroll-smooth๋ฅผ ์์ ํ ์ดํดํ๊ณ JavaScript ์์ด ์นด๋ ์บ๋ฌ์ ๊ณผ ์น์ ์ค๋ ์คํฌ๋กค์ ๊ตฌํํฉ๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐ฏ 1๋จ๊ณ: Scroll Snap ํต์ฌ ๊ตฌ์กฐ โ ๋ถ๋ชจ์ ์์์ ๊ณ์ฝ
- ๐ 2๋จ๊ณ: snap-mandatory vs snap-proximity โ ๊ฐ์ ๋ ์์ ๋
- ๐ 3๋จ๊ณ: snap-start, snap-center, snap-end โ ์ด๋์ ๊ฑธ๋ฆด๊น
- ๐ 4๋จ๊ณ: scroll-smooth โ ๋ถ๋๋ฌ์ด ์ต์ปค ์ด๋
- ๐ 5๋จ๊ณ: overscroll-behavior โ ์คํฌ๋กค ์ ํ ๋ง๊ธฐ
- ๐ 6๋จ๊ณ: scroll-margin & scroll-padding โ sticky ํค๋ ์คํ์ ํด๊ฒฐ
- ๐ ์ค์ : ์์๋ค ์ปค๋ฎค๋ํฐ ์คํฐ๋ ์นด๋ ์บ๋ฌ์ + ์น์ ์ค๋
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 22๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 10๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์(๋์์ด๋): "์์ฒ ๋, ์คํฐ๋ ์นด๋ ๋ชฉ๋ก์ ์ข์ฐ ์คํฌ๋กค๋ก ๋๊ธฐ๋ ์บ๋ฌ์ ๋ก ๋ง๋ค๊ณ ์ถ์ด์. ๊ทผ๋ฐ ๊ทธ๋ฅ ์คํฌ๋กค์ด ์๋๋ผ, ์นด๋ ๋จ์๋ก ๋ฑ๋ฑ ๋ฉ์ถ๊ฒ ํด์ฃผ์ธ์! ์์ฆ ๋ชจ๋ฐ์ผ ์ฑ ๋ณด๋ฉด ๋ค ๊ทธ๋ ๊ฒ ๋์์์."
- ์์ฒ (์ ์
): "์! ์บ๋ฌ์
์ด์? ์ ๊ฐ
touchstart,touchend์ด๋ฒคํธ ๊ฐ์งํด์ ์ค์์ดํ ๊ฑฐ๋ฆฌ ๊ณ์ฐํ๊ณ , requestAnimationFrame์ผ๋ก ๋ถ๋๋ฝ๊ฒ ์ด๋ํ๋ ์ฝ๋ ์ง๋ฉด ๋๊ฒ ๋๋ฐ์? 200์ค ์ ๋๋ฉด..." - ์ํธ(๋ฆฌ๋): "์์ฒ ๋,
snap-x snap-mandatory๋snap-center๋ ํด๋์ค๋ฉด ๋ฉ๋๋ค. JavaScript ํ ์ค๋ ํ์ ์์ด์."
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
-
snap-x,snap-y,snap-mandatory,snap-proximity์ ์กฐํฉ ์๋ฆฌ๋ฅผ ์ดํดํ๋ค. -
snap-start,snap-center,snap-end๋ฅผ ์ํฉ์ ๋ง๊ฒ ์ ํํ ์ ์๋ค. -
overscroll-contain์ผ๋ก ๋ชจ๋ฌ/๋๋ก์ด ๋ด๋ถ ์คํฌ๋กค ์ ํ๋ฅผ ๋ง์ ์ ์๋ค. -
scroll-mt-*๋ก sticky ํค๋์ ๊ฐ๋ ค์ง๋ ์ต์ปค ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
์์์ด์ ์ค๋
์บ๋ฌ์
์์ฒญ โ Scroll Snap ๋ถ๋ชจ-์์ ๊ตฌ์กฐ โ mandatory vs proximity โ ๋ฐฉํฅ๋ณ ์ ๋ ฌ โ smooth scroll โ overscroll ์ ์ด โ ์ค์ ๊ตฌํ
๐ค ์ ์์์ผ ํ๋๊ฐ
์บ๋ฌ์ ์ฌ๋ผ์ด๋. ์น ๊ฐ๋ฐ์๋ผ๋ฉด ๋๊ตฌ๋ ํ๋ฒ์ฏค ๊ตฌํํด๋ดค๊ฑฐ๋, ๊ตฌํ ์์ฒญ์ ๋ฐ์๋ณธ ๊ฒฝํ์ด ์์ ๊ฑฐ์ผ.
์ ํต์ ์ธ ๋ฐฉ์์ ์ด๋ฌ์ด:
- ์ปจํ ์ด๋ ๋๋น ์ธก์
- ํ์ฌ ์คํฌ๋กค ์์น ์ถ์
- ์ค์์ดํ/ํด๋ฆญ ์ด๋ฒคํธ ๊ฐ์ง
- ๊ฐ์ฅ ๊ฐ๊น์ด ์นด๋ ์์น ๊ณ์ฐ
requestAnimationFrame์ผ๋ก ์ ๋๋ฉ์ด์ ์คํ
์ด๊ฒ boilerplate๋ง 100์ค์ด ๋์ด. ๊ทธ๋ฐ๋ฐ ์๋ง์ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ์ด ๋ณต์ก์ฑ์ ์จ๊ธฐ๋ ๋ฐ ์ฑ๊ณตํ๋ฉด์ swiper.js, embla-carousel ๊ฐ์ ๊ฒ๋ค์ด ์๊ฒจ๋ฌ์ง.
๊ทผ๋ฐ ์ ๊น. CSS ํ์ค ์์ํ๊ฐ 2019๋ ์ ์ด ๋ชจ๋ ๊ฑธ CSS๋ก ํด๊ฒฐํ๋ ๋ช ์ธ๋ฅผ ์ ์์ผ๋ก ํ์ ํ์ด. ์ด๋ฆํ์ฌ CSS Scroll Snap. ๊ทธ๋ฆฌ๊ณ ํ์ฌ ๋ชจ๋ ์ฃผ์ ๋ธ๋ผ์ฐ์ ์์ ์๋ฒฝํ ์ง์๋ผ.
๋ชจ๋ฅด๋ฉด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ ํํ๊ณ , ๋ฒ๋ค ํฌ๊ธฐ๋ฅผ ๊ฑฑ์ ํ๊ณ , ์ ๋ฐ์ดํธ๋ฅผ ๊ด๋ฆฌํ๊ณ , ๋ฒ๊ทธ๋ฅผ ์ง์ ๊ณ ์ณ์ผ ํด. ์๋ฉด ๊ทธ๋ฅ ํด๋์ค ๋ ๊ฐ๋ฉด ๋์ด์ผ.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ ๊ธฐ์ฐจ ์น๊ฐ์ฅ์ ์์ ์ ์์คํ
CSS Scroll Snap์ ๊ธฐ์ฐจ์ญ์ ์น๊ฐ์ฅ๊ณผ ๋น์ทํด.
- ์คํฌ๋กค ์ปจํ ์ด๋ (๋ถ๋ชจ): ๊ธฐ์ฐจ ์ ๋ก. "์ด ์์์ ๊ธฐ์ฐจ๊ฐ ๋ค๋๋ฉฐ ์ค๋ ๊ท์น์ ์ ํด."
- ์ค๋ ์์ดํ (์์๋ค): ์น๊ฐ์ฅ. "๊ธฐ์ฐจ๊ฐ ์ฌ๊ธฐ์ ์ ์ฐจํด์ผ ํด."
- snap-mandatory: ๊ฐ์ ์ ์ฐจ. "๊ธฐ์ฐจ๋ ๋ฐ๋์ ์น๊ฐ์ฅ์์๋ง ๋ฉ์ถฐ์ผ ํด. ์ค๊ฐ์์ ๋ฉ์ถ๋ ๊ฒ์ ํ์ฉ ์ ํด."
- snap-proximity: ์์ ์ ์ฐจ. "์น๊ฐ์ฅ ๊ฐ๊น์ด ์์ ๋๋ง ์๋์ผ๋ก ๋์ด๋น๊ฒจ. ๋ฉ๋ฆฌ ์์ผ๋ฉด ๊ทธ๋ฅ ๋์ด."
- snap-center: ์น๊ฐ์ฅ ์ค์ ํ์. "๊ธฐ์ฐจ ๋ฌธ์ด ์ด ๋งํฌ์ ๋ฑ ๋ง๊ฒ ์."
๋ถ๋ชจ๋ "์ด๋ ๋ฐฉํฅ์ผ๋ก, ์ผ๋ง๋ ์๊ฒฉํ๊ฒ" ์ค๋ ํ ์ง ์ ํ๊ณ ,
์์์ "๋ด ์ด๋ ์ง์ ์ ๊ฑธ๋ฆด์ง" ์ ํด. ์ด ๊ณ์ฝ์ด Scroll Snap์ด์ผ.
๐ฏ 1๋จ๊ณ: Scroll Snap ํต์ฌ ๊ตฌ์กฐ โ ๋ถ๋ชจ์ ์์์ ๊ณ์ฝ
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Scroll Snap์์ ๋ถ๋ชจ์ ์์์ด ๊ฐ๊ฐ ๋ฌด์จ ์ญํ ์ ๋ด๋นํ๋์ง ๊ตฌ๋ถํ ์ ์๋ค.
- ์ต์ํ์ ํด๋์ค๋ก ์๋ํ๋ ์ค๋ ์ปจํ ์ด๋๋ฅผ ๋ง๋ค ์ ์๋ค.
Scroll Snap์ ๊ตฌ์กฐ๋ ํญ์ 2๊ฐ์ ๋ ์ด์ด๋ก ๋๋์ด.
<!-- ๐ฃ ์์ฒ : "snap-x๋ง ๋ฃ์๋๋ฐ ์ ์ ๊ฑธ๋ ค์?" -->
<!-- snap-x ํผ์์๋ ์๋ฌด๊ฒ๋ ์ ํด. ์์์ align์ด ์์ด์ผ ํจ! -->
<!-- โ ์์์ snap align์ด ์์ผ๋ฉด ์ค๋
ํฌ์ธํธ๊ฐ ์์ด์ ๋์ ์ ํจ -->
<div class="flex overflow-x-auto snap-x snap-mandatory gap-4">
<div class="w-80 h-48 bg-gray-200 shrink-0">์นด๋ 1</div> <!-- snap ํฌ์ธํธ ์์! -->
<div class="w-80 h-48 bg-gray-200 shrink-0">์นด๋ 2</div>
</div>
<!-- โ
๋ถ๋ชจ: ์ปจํ
์ด๋ ์ค์ / ์์: ์ ๋ ฌ ํฌ์ธํธ ์ค์ -->
<div class="
flex overflow-x-auto /* ์คํฌ๋กค ํ์ฑํ */
snap-x /* X์ถ ๋ฐฉํฅ ์ค๋
*/
snap-mandatory /* ์๊ฒฉํ ์ค๋
*/
gap-4
">
<!-- ๊ฐ ์์์ snap-center ๋๋ snap-start ์ถ๊ฐ -->
<div class="w-80 h-48 bg-gray-200 shrink-0 snap-center">์นด๋ 1</div>
<div class="w-80 h-48 bg-gray-200 shrink-0 snap-center">์นด๋ 2</div>
<div class="w-80 h-48 bg-gray-200 shrink-0 snap-center">์นด๋ 3</div>
</div>๋ถ๋ชจ๊ฐ ๋ด๋นํ๋ ๊ฒ:
<!-- 1. ์ค๋
๋ฐฉํฅ -->
snap-x <!-- ๊ฐ๋ก ์คํฌ๋กค ์ค๋
-->
snap-y <!-- ์ธ๋ก ์คํฌ๋กค ์ค๋
-->
snap-both <!-- ์๋ฐฉํฅ ์ค๋
-->
snap-none <!-- ์ค๋
ํด์ -->
<!-- 2. ์ค๋
์๊ฒฉ๋ -->
snap-mandatory <!-- ํญ์ ์ค๋
ํฌ์ธํธ์์ ๋ฉ์ถค -->
snap-proximity <!-- ๊ฐ๊น์ธ ๋๋ง ์ค๋
ํฌ์ธํธ์ ๋๋ฆผ -->์์์ด ๋ด๋นํ๋ ๊ฒ:
<!-- ์ค๋
์ ๋ ฌ ์์น -->
snap-start <!-- ์์ดํ
์ ์์์ ์ด ์ค๋
ํฌ์ธํธ -->
snap-center <!-- ์์ดํ
์ ์ค์์ด ์ค๋
ํฌ์ธํธ -->
snap-end <!-- ์์ดํ
์ ๋์ ์ด ์ค๋
ํฌ์ธํธ -->
snap-align-none <!-- ์ด ์์ดํ
์ ์ค๋
๋์์์ ์ ์ธ -->๐ 2๋จ๊ณ: snap-mandatory vs snap-proximity โ ๊ฐ์ ๋ ์์ ๋
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
snap-mandatory์snap-proximity์ค ์ด๋ค ์ํฉ์ ์ด๋ ๊ฒ์ ์จ์ผ ํ๋์ง ํ๋จํ ์ ์๋ค.- ๊ฐ๊ฐ์ UX ํน์ฑ๊ณผ ์ฃผ์์ฌํญ์ ์ ์ ์๋ค.
snap-mandatory โ ์ ๋ ๋ณต์ข ํ
์คํฌ๋กค์ด ๋ฉ์ถ๋ฉด ๋ฌด์กฐ๊ฑด ๊ฐ์ฅ ๊ฐ๊น์ด ์ค๋ ํฌ์ธํธ๋ก ์ด๋ํด. ์ค๊ฐ ์ด์ ์ฉกํ ์์น์ ๋ฉ์ถ๋ ๊ฒ์ด ํ์ฉ๋์ง ์์.
<!-- snap-mandatory: ํญ์ ์นด๋ ๋จ์๋ก ๋ฑ๋ฑ ๋ฉ์ถค -->
<div class="flex overflow-x-auto snap-x snap-mandatory gap-6 px-6">
<div class="snap-center shrink-0 w-72">์คํฐ๋ ์นด๋ 1</div>
<div class="snap-center shrink-0 w-72">์คํฐ๋ ์นด๋ 2</div>
<div class="snap-center shrink-0 w-72">์คํฐ๋ ์นด๋ 3</div>
</div>snap-mandatory ์ฃผ์์ฌํญ: ์์ดํ ํฌ๊ธฐ๊ฐ ์ปจํ ์ด๋๋ณด๋ค ํฌ๋ฉด ์คํฌ๋กค์ด ๋ถ๊ฐ๋ฅํด์ง ์ ์์ด.
<!-- ๐จ ์ํํ ํจํด: ์์ดํ
์ด ์ปจํ
์ด๋๋ณด๋ค ํฌ๋ฉด ๊ฑด๋๋ ํ์ ๋ฐ์ -->
<!-- ์ปจํ
์ด๋: 400px, ์์ดํ
: 600px -->
<div class="w-[400px] overflow-x-auto snap-x snap-mandatory">
<div class="w-[600px] snap-start">์ด ์์ดํ
์ 100% ๋ณผ ์ ์์ด!</div>
<!-- mandatory๋ฉด ํญ์ ๋ค์ ์ค๋
ํฌ์ธํธ๋ก ์ด๋ โ ๊ธด ์ฝํ
์ธ ์ฝ๊ธฐ ๋ถ๊ฐ -->
</div>
<!-- โ
์ด๋ฐ ๊ฒฝ์ฐ์ snap-proximity๊ฐ ๋ ์ ํฉ -->
<div class="w-[400px] overflow-x-auto snap-x snap-proximity">
<div class="w-[600px] snap-start">์ด ์์ดํ
์ ์ฒ์ฒํ ์ฝ์ ์ ์์ด</div>
</div>snap-proximity โ ์ ๋นํ ํ์กฐํ
์ค๋ ํฌ์ธํธ์ ๊ฐ๊น์ด ์์ ๋๋ง ๋์ด๋น๊ฒจ. ๋ฉ๋ฆฌ ์์ผ๋ฉด ๊ทธ๋ฅ ์คํฌ๋กค ๊ด์ฑ๋๋ก ๋ฉ์ถฐ.
<!-- snap-proximity: ๊ฐ๊น์ธ ๋๋ง ์ค๋
, ๊ฐ์ ์ด๋ ์์ -->
<div class="flex overflow-x-auto snap-x snap-proximity gap-6">
<div class="snap-center shrink-0 w-72">์คํฐ๋ ์นด๋ 1</div>
<div class="snap-center shrink-0 w-72">์คํฐ๋ ์นด๋ 2</div>
<div class="snap-center shrink-0 w-72">์คํฐ๋ ์นด๋ 3</div>
</div>์ธ์ ๋ฌด์์ ์ธ๊น:
| ์ํฉ | ์ถ์ฒ |
|---|---|
| ์นด๋ ์บ๋ฌ์ (์นด๋๊ฐ ํ๋ฉด๋ณด๋ค ์์) | snap-mandatory |
| ํํ์ด์ง ์น์ ์คํฌ๋กค (์น์ ์ด ์ ํํ ํ๋ฉด ํฌ๊ธฐ) | snap-mandatory |
| ๊ธด ๊ธฐ์ฌ/๋ธ๋ก๊ทธ ํฌ์คํธ์ ์น์ ์ด๋ | snap-proximity |
| ์์ดํ ํฌ๊ธฐ๊ฐ ์ปจํ ์ด๋๋ณด๋ค ํฐ ๊ฒฝ์ฐ | snap-proximity |
๐ 3๋จ๊ณ: snap-start, snap-center, snap-end โ ์ด๋์ ๊ฑธ๋ฆด๊น
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
snap-start,snap-center,snap-end์ ์๊ฐ์ ์ฐจ์ด๋ฅผ ์ดํดํ๋ค.- ์นด๋ ์บ๋ฌ์ vs ํํ์ด์ง ์น์ ๋ฑ ์ฉ๋์ ๋ง๋ ์ ๋ ฌ์ ์ ํํ ์ ์๋ค.
snap-start โ ์์ดํ ์ ์ผ์ชฝ(์)์ด ๊ธฐ์ค์
<!-- snap-start: ์์ดํ
์ ์์(left)์ด ์ปจํ
์ด๋์ ์์(left)์ ๋ง์ถฐ์ง -->
<!--
[์นด๋1][์นด๋2][์นด๋3]
^
์ปจํ
์ด๋ ์ผ์ชฝ๊ณผ ์นด๋ ์ผ์ชฝ์ด ๋ง์ถฐ์ง
-->
<div class="flex overflow-x-auto snap-x snap-mandatory">
<div class="snap-start shrink-0 w-72 mr-4">์นด๋ 1</div>
<div class="snap-start shrink-0 w-72 mr-4">์นด๋ 2</div>
<div class="snap-start shrink-0 w-72 mr-4">์นด๋ 3</div>
</div>snap-center โ ์์ดํ ์ ์ค์์ด ๊ธฐ์ค์
<!-- snap-center: ์์ดํ
์ ์ค์์ด ์ปจํ
์ด๋์ ์ค์์ ๋ง์ถฐ์ง -->
<!--
[ ์นด๋1 ][ ์นด๋2 ][ ์นด๋3 ]
^
์ปจํ
์ด๋ ์ค์๊ณผ ์นด๋ ์ค์์ด ๋ง์ถฐ์ง
-->
<div class="flex overflow-x-auto snap-x snap-mandatory">
<!-- ๐ ์ ๋ ์ฌ๋ฐฑ ์์ดํ
: ์ค์ ์ ๋ ฌ ์ ์ฒ์/๋ง์ง๋ง ์นด๋๊ฐ ์ค์์ ์ค๋๋ก -->
<div class="shrink-0 w-[calc(50%-9rem)]"></div> {/* ์นด๋๊ฐ 144px(w-72/2)์ผ ๋ ์ฌ๋ฐฑ */}
<div class="snap-center shrink-0 w-72 mr-4">์นด๋ 1</div>
<div class="snap-center shrink-0 w-72 mr-4">์นด๋ 2</div>
<div class="snap-center shrink-0 w-72 mr-4">์นด๋ 3</div>
<div class="shrink-0 w-[calc(50%-9rem)]"></div>
</div>snap-end โ ์์ดํ ์ ์ค๋ฅธ์ชฝ(์๋)์ด ๊ธฐ์ค์
<!-- snap-end: ์์ดํ
์ ๋(right)์ด ์ปจํ
์ด๋์ ๋(right)์ ๋ง์ถฐ์ง -->
<div class="flex overflow-x-auto snap-x snap-mandatory">
<div class="snap-end shrink-0 w-72 mr-4">์นด๋ 1</div>
<div class="snap-end shrink-0 w-72 mr-4">์นด๋ 2</div>
<div class="snap-end shrink-0 w-72">์นด๋ 3</div> {/* ๋ง์ง๋ง์ mr ์์ */}
</div>์ค๋ฌด ํ โ ์ด๋ align์ ์ธ๊น:
์นด๋ ์บ๋ฌ์
(์ผ๋ฐ) โ snap-start (์์ฐ์ค๋ฌ์ด ์ผ์ชฝ ์ ๋ ฌ)
์นด๋ ์บ๋ฌ์
(์ค์ ๊ฐ์กฐ) โ snap-center (์๋ ์ฌ๋ฐฑ ์์ดํ
์ถ๊ฐ ํ์)
ํํ์ด์ง ์น์
์ค๋
โ snap-start (๊ฐ ์น์
์ ์์์ด ํ๋ฉด ์๋จ์ ๋ง์ถฐ์ง)
์ธ๋ก ์ฌ๋ผ์ด๋์ผ โ snap-center (์ฝํ
์ธ ๊ฐ ์ค์์ ์ค๊ฒ)
๐ 4๋จ๊ณ: scroll-smooth โ ๋ถ๋๋ฌ์ด ์ต์ปค ์ด๋
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
scroll-smooth๋ก ์ต์ปค ๋งํฌ ํด๋ฆญ ์ ๋ถ๋๋ฌ์ด ์คํฌ๋กค ์ด๋์ ๊ตฌํํ ์ ์๋ค.prefers-reduced-motion์ ๊ทผ์ฑ ๊ณ ๋ ค์ ์ค์์ฑ์ ์ดํดํ๋ค.
scroll-behavior: smooth ๋ ๊ฐ์ฅ ๋จ์ํ์ง๋ง UX๋ฅผ ๊ทน์ ์ผ๋ก ๊ฐ์ ํ๋ ์์ฑ์ด์ผ. ์ต์ปค ๋งํฌ(#section)๋ฅผ ํด๋ฆญํ ๋ ์๊ฐ ์ด๋์ด ์๋๋ผ ๋ถ๋๋ฝ๊ฒ ์คํฌ๋กค๋ผ.
<!-- โ ์์ฒ ์ด์ ์ฝ๋: JavaScript๋ก smooth scroll ๊ตฌํ -->
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', (e) => {
e.preventDefault();
const target = document.querySelector(anchor.getAttribute('href'));
target.scrollIntoView({ behavior: 'smooth' });
});
});
<!-- โ
์ํธ์ ๋ฐฉ์: html ํ๊ทธ์ ํด๋์ค ํ๋ -->
<html class="scroll-smooth">
<!-- ์ด์ ๋ชจ๋ ์ต์ปค ๋งํฌ๊ฐ ๋ถ๋๋ฝ๊ฒ ์ด๋ -->
</html><!-- ์ค์ : ๋ค๋น๊ฒ์ด์
๋ฉ๋ด์์ ์น์
์ผ๋ก ์ด๋ -->
<html class="scroll-smooth">
<head>...</head>
<body>
<!-- ๋ค๋น๊ฒ์ด์
-->
<nav class="fixed top-0 ...">
<a href="#studies">์คํฐ๋ ์ฐพ๊ธฐ</a> <!-- ํด๋ฆญ ์ ๋ถ๋๋ฝ๊ฒ ์คํฌ๋กค -->
<a href="#how-it-works">์ด์ฉ ๋ฐฉ๋ฒ</a>
<a href="#testimonials">ํ๊ธฐ</a>
</nav>
<!-- ์น์
๋ค -->
<section id="studies">...</section>
<section id="how-it-works">...</section>
<section id="testimonials">...</section>
</body>
</html>์ ๊ทผ์ฑ ์ฃผ์์ฌํญ:
/* ๋์ ๊ฐ์ ์ค์ ์ ํ ์ฌ์ฉ์์๊ฒ๋ ์ฆ์ ์ด๋์ด ๋์ ์ ์์ด */
/* Tailwind๋ก๋ ์๋์ฒ๋ผ ๋ฐ์ํ ๋ณํ ํ์ฉ */<!-- motion-reduce: ์ ๊ทผ์ฑ ๋ฐฐ๋ ค -->
<html class="scroll-smooth motion-reduce:scroll-auto">
<!-- ์ ๋๋ฉ์ด์
๊ฐ์ ์ ํธ ์ฌ์ฉ์์๊ฒ ์ฆ์ ์ด๋ -->
</html>๐ 5๋จ๊ณ: overscroll-behavior โ ์คํฌ๋กค ์ ํ ๋ง๊ธฐ
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- "์คํฌ๋กค ์ฒด์ธ(scroll chaining)"์ด ๋ฌด์์ธ์ง ์ดํดํ๊ณ , ์ด๋ฅผ ๋ง์ ์ ์๋ค.
- ๋ชจ๋ฌ, ๋๋ก์ด, ์ฌ์ด๋ํจ๋์ ์คํฌ๋กค ๋ ๋ฆฝ์ฑ์ ๊ตฌํํ ์ ์๋ค.
์คํฌ๋กค ์ฒด์ธ์ด๋?
์คํฌ๋กค ๊ฐ๋ฅํ ์์(์: ๋ชจ๋ฌ)๋ฅผ ๋๊น์ง ์คํฌ๋กคํ๋ฉด, ๊ทธ ์คํฌ๋กค ์ด๋ฒคํธ๊ฐ ๋ถ๋ชจ(body)์๊ฒ๋ ์ ํ๋๋ ํ์์ด์ผ.
์ฌ์ฉ์๊ฐ ๋ชจ๋ฌ ๋ด๋ถ๋ฅผ ์คํฌ๋กค โ
๋ชจ๋ฌ ์ฝํ
์ธ ๋ฅผ ๋ค ์ฌ๋ฆผ โ
์คํฌ๋กค์ด ๋จ์ ์์ผ๋ฉด body๊ฐ ์คํฌ๋กค๋จ โ ์ด๊ฒ ์คํฌ๋กค ์ฒด์ธ ๋ฒ๊ทธ!
<!-- โ ์์ฒ ์ด์ JavaScript ํด๊ฒฐ์ฑ
: ์ด๋ฒคํธ ๋ง๊ธฐ -->
modal.addEventListener('wheel', (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
if (
(e.deltaY < 0 && scrollTop === 0) ||
(e.deltaY > 0 && scrollTop + clientHeight === scrollHeight)
) {
e.preventDefault();
}
}, { passive: false });
// ๐ฃ passive event ๋ฌธ์ , iOS Safari ๋ฒ๊ทธ, ์ฑ๋ฅ ์ด์... ๊ณจ์น ์ํ
<!-- โ
์ํธ์ CSS ํ ๋ฐฉ ํด๊ฒฐ -->
<div class="overflow-y-auto overscroll-contain max-h-screen">
<!-- ๋ชจ๋ฌ ๋ด๋ถ ์ฝํ
์ธ -->
<!-- overscroll-contain: ์ด ์์์ ์คํฌ๋กค์ด ๋ถ๋ชจ๋ก ์ ํ๋์ง ์์ -->
</div>overscroll 3๊ฐ์ง ๋ชจ๋
<!-- overscroll-auto (๊ธฐ๋ณธ๊ฐ): ์คํฌ๋กค ์ฒด์ธ ํ์ฉ -->
<div class="overflow-y-auto overscroll-auto">
<!-- ๋์์ ์คํฌ๋กคํ๋ฉด ๋ถ๋ชจ ์คํฌ๋กค ๋ฐ์ (๊ธฐ๋ณธ ๋ธ๋ผ์ฐ์ ๋์) -->
</div>
<!-- overscroll-contain: ์คํฌ๋กค ์ฒด์ธ ์ฐจ๋จ + ๋ฐ์ด์ค ํ์ฉ -->
<div class="overflow-y-auto overscroll-contain">
<!-- ๋์์ ์คํฌ๋กค์ด ๋ถ๋ชจ๋ก ์ ํ ์ ๋จ -->
<!-- iOS Safari์ ๊ณ ๋ฌด์ค ๋ฐ์ด์ค ํจ๊ณผ๋ ์ ์ง๋จ -->
</div>
<!-- overscroll-none: ์คํฌ๋กค ์ฒด์ธ + ๋ฐ์ด์ค ๋ชจ๋ ์ฐจ๋จ -->
<div class="overflow-y-auto overscroll-none">
<!-- ๋์์ ์คํฌ๋กค์ด ๋ถ๋ชจ๋ก๋ ์ ํ ์ ๋๊ณ ๋ฐ์ด์ค๋ ์์ -->
<!-- ์ฑ์ฒ๋ผ ๋ฑ๋ฑํ ๋๋ -->
</div>์ธ์ ๋ฌด์์ ์ธ๊น:
| ์ํฉ | ์ถ์ฒ |
|---|---|
| ๋ชจ๋ฌ/๋๋ก์ด ๋ด๋ถ ์คํฌ๋กค | overscroll-contain |
| ์ฑํ ์ฐฝ ๋ฉ์์ง ์์ญ | overscroll-contain |
| ์๋ฒ ๋๋ ์คํฌ๋กค ์ปจํ ์ด๋ | overscroll-contain |
| ๋ชจ๋ฐ์ผ ์ฑ์ฒ๋ผ ๋ฑ๋ฑํ UX | overscroll-none |
| ํํ์ด์ง ์คํฌ๋กค ์น์ | overscroll-none |
์ค์ : ๋ชจ๋ฌ ์คํฌ๋กค ๋ ๋ฆฝ์ฑ
// components/StudyDetailModal.tsx
// ๐ฆ ์ํธ: "๋ชจ๋ฌ ์์์ ์คํฌ๋กคํ ๋ ๋ฐฐ๊ฒฝ์ด ๊ฐ์ด ์์ง์ด๋ ๋ฒ๊ทธ ์์ง? overscroll-contain ํ๋๋ฉด ๋์ด์ผ"
export default function StudyDetailModal({ isOpen, onClose, study }) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* ๋ฐฐ๊ฒฝ ์ค๋ฒ๋ ์ด */}
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
{/* ๋ชจ๋ฌ ํจ๋ */}
<div className="
relative z-10
bg-white rounded-2xl
w-full max-w-lg
max-h-[85vh] /* ์ต๋ ๋์ด ์ ํ */
overflow-y-auto /* ๋ด๋ถ ์คํฌ๋กค ํ์ฑํ */
overscroll-contain /* ์คํฌ๋กค ์ ํ ์ฐจ๋จ */
shadow-2xl
">
{/* ๋ชจ๋ฌ ํค๋ (sticky) */}
<div className="sticky top-0 bg-white border-b px-6 py-4 flex justify-between items-center">
<h2 className="text-lg font-semibold">{study.title}</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">โ</button>
</div>
{/* ๋ชจ๋ฌ ์ฝํ
์ธ - ์๋ฌด๋ฆฌ ์คํฌ๋กคํด๋ ๋ฐฐ๊ฒฝ body๋ ์ ์์ง์ */}
<div className="px-6 py-4 space-y-4">
<p className="text-gray-700 leading-relaxed">{study.description}</p>
{/* ... ๋ ๋ง์ ์ฝํ
์ธ ... */}
</div>
</div>
</div>
);
}๐ 6๋จ๊ณ: scroll-margin & scroll-padding โ sticky ํค๋ ์คํ์ ํด๊ฒฐ
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- sticky ํค๋๊ฐ ์์ ๋ ์ต์ปค ์ด๋ ํ ์ฝํ ์ธ ๊ฐ ํค๋์ ๊ฐ๋ ค์ง๋ ๋ฒ๊ทธ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
scroll-mt-*์scroll-pt-*์ ์ฐจ์ด๋ฅผ ์ดํดํ๋ค.
๋ฌธ์ ์ํฉ: ํค๋ ์๋๋ก ์จ๋ ์ฝํ ์ธ
<!-- ๐ฃ ์์ฒ : "nav์ fixed ์ฃผ๊ณ ์ต์ปค ์ด๋ํ๋ฉด ์น์
์ ๋ชฉ์ด nav ๋ค์ ์จ์ด์!" -->
<!--
ํ๋ฉด:
โโโโโโโโโโโโโโโโโโโ
โ Sticky Nav โ โ height: 64px (h-16)
โโโโโโโโโโโโโโโโโโโค
โ (์ด ๋ถ๋ถ์ด ๊ฐ๋ ค์ง) โ ์น์
์ ๋ชฉ์ด ์ฌ๊ธฐ ์์ด์ ์ ๋ณด์!
โ โ
โ ์น์
๋ด์ฉ ... โ
โโโโโโโโโโโโโโโโโโโ
-->ํด๊ฒฐ์ฑ : scroll-margin-top
<!-- โ ์์ฒ ์ด์ ๋ฐฉ๋ฒ: ์น์
์ padding-top ์ฃผ๊ธฐ -->
<section id="studies" class="pt-16"> <!-- ํ์ง๋ง ์น์
๋ด๋ถ ๋ ์ด์์์ด ๋ง๊ฐ์ง -->
<!-- โ
์ํธ์ ๋ฐฉ๋ฒ: scroll-mt-16 -->
<section id="studies" class="scroll-mt-16">
<!-- scroll-mt-16 = scroll-margin-top: 4rem -->
<!-- ์ต์ปค๋ก ์ด๋ ์ 64px ์๋์์ ๋ฉ์ถค โ ํค๋ ๋ฐ์ ์ ๊ฐ๋ ค์ง -->
<h2>์คํฐ๋ ๋ชฉ๋ก</h2>
</section>scroll-padding vs scroll-margin
<!-- scroll-padding: ์ปจํ
์ด๋(๋ถ๋ชจ)์ ์ ์ฉ -->
<!-- ์ปจํ
์ด๋ ์์ ์ค๋
/์ต์ปค ์ด๋ ์ ์์ ์คํ์
-->
<div class="snap-y snap-mandatory scroll-pt-16 overflow-y-auto h-screen">
<!-- ๋ชจ๋ ์์์ ์ค๋
ํฌ์ธํธ์ 16(4rem) ์คํ์
์ ์ฉ -->
<section class="snap-start h-screen">์น์
1</section>
<section class="snap-start h-screen">์น์
2</section>
</div>
<!-- scroll-margin: ์์ดํ
(์์)์ ์ ์ฉ -->
<!-- ์ด ์์ดํ
์ด ์ค๋
/์ต์ปค ์ด๋ ๋์์ด ๋ ๋ ์ฌ๋ฐฑ -->
<section id="studies" class="snap-start scroll-mt-16">
<!-- ์ด ์น์
์ผ๋ก ์ด๋ ์ 16(4rem) ์์์ ๋ฉ์ถค -->
</section>์ค์ ์ ๋ต - sticky ํค๋ ์คํ์ :
// tailwind.config.ts - ํค๋ ๋์ด๋ฅผ CSS ๋ณ์๋ก ๊ด๋ฆฌ
// ๋ชจ๋ ์น์
์ ๋์ผํ scroll-mt ์ ์ฉ์ ์ํด
// JSX์์
const HEADER_HEIGHT = "h-16"; // 64px
const SCROLL_OFFSET = "scroll-mt-16"; // ๋์ผํ๊ฒ ๋ง์ถค
// layout.tsx
<html className="scroll-smooth">
<body>
<nav className={`sticky top-0 z-50 ${HEADER_HEIGHT}`}>
...๋ค๋น
</nav>
<main>
<section id="hero">...</section>
<section id="studies" className={SCROLL_OFFSET}>...</section>
<section id="how-it-works" className={SCROLL_OFFSET}>...</section>
</main>
</body>
</html>๐ ์ค์ : ์์๋ค ์ปค๋ฎค๋ํฐ ์คํฐ๋ ์นด๋ ์บ๋ฌ์ + ์น์ ์ค๋
์ํ ์นด๋ ์บ๋ฌ์
// components/StudyCarousel.tsx
// ๐ฆ ์ํธ: "JavaScript ํ ์ค ์์ด ์ค์์ดํ ์บ๋ฌ์
์์ฑ์ด์์"
interface Study {
id: string;
title: string;
topic: string;
memberCount: number;
thumbnail: string;
}
export default function StudyCarousel({ studies }: { studies: Study[] }) {
return (
<div className="relative">
<h2 className="text-2xl font-bold mb-4 px-4">์ถ์ฒ ์คํฐ๋</h2>
{/* ๐ฎ ํต์ฌ: ์คํฌ๋กค ์ปจํ
์ด๋ */}
<div className="
flex /* ๊ฐ๋ก ์ ๋ ฌ */
overflow-x-auto /* ๊ฐ๋ก ์คํฌ๋กค ํ์ฑํ */
snap-x /* X์ถ ์ค๋
*/
snap-mandatory /* ๊ฐ์ ์ค๋
*/
gap-4 /* ์นด๋ ๊ฐ๊ฒฉ */
px-4 /* ์์ชฝ ์ฌ๋ฐฑ */
pb-4 /* ์คํฌ๋กค๋ฐ ๊ณต๊ฐ */
-mx-4 /* ๋ถ๋ชจ padding ์์ */
/* ์คํฌ๋กค๋ฐ ์จ๊ธฐ๊ธฐ (ํฌ๋ก์ค ๋ธ๋ผ์ฐ์ ) */
scrollbar-hide /* tailwind-scrollbar-hide ํ๋ฌ๊ทธ์ธ ํ์ */
[&::-webkit-scrollbar]:hidden /* ๋๋ ์ด ๋ฐฉ์ */
">
{/* ์ฒซ ๋ฒ์งธ ์ฌ๋ฐฑ ์์ดํ
- snap-center๋ฅผ ์ํด ํ์ */}
{/* snap-center ์ฐ์ง ์๋๋ค๋ฉด ์ด๊ฑด ์๋ต ๊ฐ๋ฅ */}
{studies.map((study) => (
<div
key={study.id}
className="
snap-start /* ์นด๋ ์ผ์ชฝ์ด ์ค๋
ํฌ์ธํธ */
shrink-0 /* flex ์์ดํ
์ถ์ ๋ฐฉ์ง */
w-[280px] /* ์นด๋ ๋๋น ๊ณ ์ */
rounded-2xl overflow-hidden
bg-white shadow-md
hover:shadow-xl
transition-shadow duration-200
cursor-pointer
"
>
{/* ์ธ๋ค์ผ */}
<div className="h-40 overflow-hidden">
<img
src={study.thumbnail}
alt={study.title}
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
/>
</div>
{/* ์นด๋ ๋ด์ฉ */}
<div className="p-4">
<span className="text-xs font-medium text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-full">
{study.topic}
</span>
<h3 className="font-semibold text-gray-900 mt-2 line-clamp-2">{study.title}</h3>
<p className="text-sm text-gray-500 mt-1">๋ฉค๋ฒ {study.memberCount}๋ช
</p>
</div>
</div>
))}
</div>
{/* ์ธ๋์ผ์ดํฐ (CSS only - overflow ๋ค์ gradient fade) */}
<div className="absolute top-0 right-0 bottom-4 w-16 bg-linear-to-l from-gray-50 to-transparent pointer-events-none" />
</div>
);
}ํํ์ด์ง ์น์ ์ค๋
// app/landing/page.tsx
// ๐ฆ ์ํธ: "๊ฐ ์น์
์ด ๋ฑ๋ฑ ๋ง๊ฒ ์ค๋
๋๋ ๋๋ฉํ์ด์ง์ผ"
// ๐ฃ ์์ฒ : "์ด๊ฑฐ ๊ตฌํํ๋ ค๊ณ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฐพ๊ณ ์์๋๋ฐ..."
export default function LandingPage() {
return (
// ํํ์ด์ง ์ค๋
์ปจํ
์ด๋
<div className="
h-screen /* ๋ทฐํฌํธ ๋์ด๋ก ์ ํ */
overflow-y-auto /* ์ธ๋ก ์คํฌ๋กค */
snap-y /* Y์ถ ์ค๋
*/
snap-mandatory /* ๊ฐ์ ์ค๋
*/
overscroll-none /* ํํ์ด์ง ํน์ฑ์ ๋ฐ์ด์ค ์ ๊ฑฐ */
scroll-smooth /* ๋ถ๋๋ฌ์ด ์ค๋
์ด๋ */
">
{/* ์น์
1: ํ์ด๋ก */}
<section className="
snap-start /* ์น์
์๋จ์ด ๋ทฐํฌํธ ์๋จ์ ๋ง์ถฐ์ง */
h-screen /* ์ ํํ 1 ํ๋ฉด ๋์ด */
flex items-center justify-center
bg-linear-to-br from-violet-900 to-indigo-900
">
<div className="text-center text-white">
<h1 className="text-6xl font-black mb-6
bg-linear-to-r from-violet-300 to-indigo-300
bg-clip-text text-transparent">
์์๋ค ์ปค๋ฎค๋ํฐ
</h1>
<p className="text-xl text-white/70">
๊ฐ๋ฐ์ ์คํฐ๋ ๋งค์นญ ํ๋ซํผ
</p>
</div>
</section>
{/* ์น์
2: ๊ธฐ๋ฅ ์๊ฐ */}
<section className="
snap-start
h-screen
flex items-center justify-center
bg-white
">
<div className="max-w-4xl mx-auto px-4 text-center">
<h2 className="text-4xl font-bold text-gray-900 mb-12">์ด๋ป๊ฒ ์ฌ์ฉํ๋์?</h2>
<div className="grid grid-cols-3 gap-8">
{/* 3๋จ๊ณ ์๊ฐ ์นด๋ */}
<div className="p-6 rounded-2xl bg-indigo-50">
<div className="text-3xl mb-3">๐</div>
<h3 className="font-bold mb-2">์คํฐ๋ ํ์</h3>
<p className="text-gray-600 text-sm">๊ด์ฌ ๊ธฐ์ ์คํ์ผ๋ก ํํฐ๋ง</p>
</div>
<div className="p-6 rounded-2xl bg-purple-50">
<div className="text-3xl mb-3">๐ค</div>
<h3 className="font-bold mb-2">๋งค์นญ ์ ์ฒญ</h3>
<p className="text-gray-600 text-sm">์ํด๋ฆญ์ผ๋ก ์ฐธ์ฌ ์ ์ฒญ</p>
</div>
<div className="p-6 rounded-2xl bg-pink-50">
<div className="text-3xl mb-3">๐</div>
<h3 className="font-bold mb-2">ํจ๊ป ์ฑ์ฅ</h3>
<p className="text-gray-600 text-sm">์ค์๊ฐ ์ฑํ
์ผ๋ก ์ํต</p>
</div>
</div>
</div>
</section>
{/* ์น์
3: ์คํฐ๋ ๋ชฉ๋ก ๋ฏธ๋ฆฌ๋ณด๊ธฐ */}
<section className="
snap-start
h-screen
flex flex-col items-center justify-center
bg-gray-50
px-4
">
<h2 className="text-4xl font-bold text-gray-900 mb-8">์ธ๊ธฐ ์คํฐ๋</h2>
{/* ์น์
๋ด๋ถ์ ๊ฐ๋ก ์บ๋ฌ์
์ฝ์
*/}
{/* overscroll-contain์ผ๋ก ๊ฐ๋ก ์คํฌ๋กค์ด ์ธ๋ก ํํ์ด์ง์ ๊ฐ์ญ ์ ํจ */}
<div className="
w-full max-w-4xl
flex overflow-x-auto
snap-x snap-mandatory
overscroll-x-contain /* ๊ฐ๋ก ์คํฌ๋กค์ด ์ธ๋ก ์ค๋
์ ์ ํ ์ ๋จ */
gap-4 pb-4
">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="snap-center shrink-0 w-64 bg-white rounded-xl shadow p-4">
<div className="h-32 bg-indigo-100 rounded-lg mb-3" />
<h3 className="font-semibold">React ์ฌํ ์คํฐ๋ {i}</h3>
<p className="text-sm text-gray-500">4๋ช
์ฐธ์ฌ ์ค</p>
</div>
))}
</div>
</section>
</div>
);
}๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
| ์ ํธ๋ฆฌํฐ | ์ญํ | ์ ์ฉ ๋์ |
|---|---|---|
snap-x | ๊ฐ๋ก ์ค๋ ํ์ฑํ | ๋ถ๋ชจ ์ปจํ ์ด๋ |
snap-y | ์ธ๋ก ์ค๋ ํ์ฑํ | ๋ถ๋ชจ ์ปจํ ์ด๋ |
snap-mandatory | ํญ์ ์ค๋ ํฌ์ธํธ์์ ๋ฉ์ถค | ๋ถ๋ชจ ์ปจํ ์ด๋ |
snap-proximity | ๊ฐ๊น์ธ ๋๋ง ์ค๋ | ๋ถ๋ชจ ์ปจํ ์ด๋ |
snap-start | ์์ดํ ์์์ ์ด ์ค๋ ํฌ์ธํธ | ์์ ์์ดํ |
snap-center | ์์ดํ ์ค์์ด ์ค๋ ํฌ์ธํธ | ์์ ์์ดํ |
snap-end | ์์ดํ ๋์ ์ด ์ค๋ ํฌ์ธํธ | ์์ ์์ดํ |
scroll-smooth | ์ต์ปค ์ด๋ ์ ๋ถ๋๋ฌ์ด ์คํฌ๋กค | <html> ๋๋ ์ปจํ
์ด๋ |
overscroll-contain | ์คํฌ๋กค ์ ํ(์ฒด์ธ) ์ฐจ๋จ | ์คํฌ๋กค ๊ฐ๋ฅํ ์์ |
overscroll-none | ์คํฌ๋กค ์ ํ + ๋ฐ์ด์ค ๋ชจ๋ ์ฐจ๋จ | ํํ์ด์ง ์คํฌ๋กค |
scroll-mt-* | ์ต์ปค ์ด๋ ์ ์์ชฝ ์ฌ๋ฐฑ | ์ต์ปค ๋์ ์์ |
scroll-pt-* | ์ค๋ /์ต์ปค ์ด๋ ์ ์ปจํ ์ด๋ ์์ ์คํ์ | ์ค๋ ์ปจํ ์ด๋ |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ์์์ด๊ฐ ์์ฒญํ "์นด๋ ๋จ์๋ก ๋ฑ๋ฑ ๋ฉ์ถ๋ ๊ฐ๋ก ์บ๋ฌ์ "์ ๊ตฌํํ๊ธฐ ์ํด ์ปจํ ์ด๋(๋ถ๋ชจ)์ ํ์ํ ์ต์ ํด๋์ค ์กฐํฉ์?
โ
์ ๋ต: overflow-x-auto snap-x snap-mandatory
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: Scroll Snap์ ํญ์ ๋ถ๋ชจ + ์์ ๋ ๋ ์ด์ด๊ฐ ํ์ํด. ๋ถ๋ชจ์๋ โ
overflow-x-auto(์คํฌ๋กค ํ์ฑํ), โกsnap-x(๊ฐ๋ก ๋ฐฉํฅ ์ค๋ ํ์ฑํ), โขsnap-mandatory(์๊ฒฉํ ์ค๋ ) ์ธ ๊ฐ์ง๊ฐ ํ์ํ๊ณ , ๊ฐ ์์ ์นด๋์๋snap-start๋๋snap-center๊ฐ ์์ด์ผ ์ค์ ๋ก ๋์ํด. - ์ค๋ต ํผ๋๋ฐฑ:
snap-x๋ง ์ค๋ ์ ๋ผ. ์์์snap-center์์ผ๋ฉด ์ค๋ ํฌ์ธํธ๊ฐ ์์ด์ ํจ๊ณผ ์์ด. ๋ฐ๋์ ๋ถ๋ชจ + ์์์ ๋ ๋ ์ด์ด ์กฐํฉ์ด์ผ. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: ๋ถ๋ชจ๋ "๋ฐฉํฅ + ์๊ฒฉ๋", ์์์ "์ ๋ ฌ ์์น". ๋ถ๋ชจ๊ฐ "์ด๋์ ์ค๋ ํ ์ง", ์์์ด "๋ด ์ด๋์ ๊ฑธ๋ฆด์ง"๋ฅผ ๊ฐ๊ฐ ๋ด๋นํด.
Q2. ์์๋ค ์ปค๋ฎค๋ํฐ ์ฑ์์ ์ฑํ ์ฐฝ ๋ด๋ถ๋ฅผ ์คํฌ๋กคํ๋ค ๋์ ๋๋ฌํ๋ฉด ๋ฐฐ๊ฒฝ ํ์ด์ง๋ ํจ๊ป ์คํฌ๋กค๋๋ ๋ฒ๊ทธ๊ฐ ๋ฐ์ํ๋ค. ์ด๋ค Tailwind ํด๋์ค๋ก ํด๊ฒฐํ ์ ์๋๊ฐ?
โ
์ ๋ต: ์ฑํ
์ปจํ
์ด๋์ overscroll-contain ์ถ๊ฐ
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: ์ด ํ์์ "์คํฌ๋กค ์ฒด์ธ(scroll chaining)" ์ด๋ผ ํด. ๋ด๋ถ ์คํฌ๋กค์ด ๋๋๋ฉด ์ด๋ฒคํธ๊ฐ ๋ถ๋ชจ๋ก ์ ํ๋๋ ๋ธ๋ผ์ฐ์ ๊ธฐ๋ณธ ๋์์ด์ผ.
overscroll-contain์ CSS์overscroll-behavior: contain์ ์ ์ฉํด ์คํฌ๋กค ์ด๋ฒคํธ๊ฐ ์ด ์์๋ฅผ ๋ฒ์ด๋์ง ๋ชปํ๊ฒ ์ฐจ๋จํด. iOS Safari์ ๋ฐ์ด์ค ํจ๊ณผ๋ ์ ์ง๋ผ์ ์์ฐ์ค๋ฌ์ด ๋๋๋ ์ด์์์ด. - ์ค๋ต ํผ๋๋ฐฑ: JavaScript
preventDefault()๋ก ๋ง์ผ๋ฉดpassive event listener๊ฒฝ๊ณ ์ ์ฑ๋ฅ ์ด์๊ฐ ๋ฐ์ํ ์ ์์ด. CSS ํ ์ค์ด ํจ์ฌ ์์ ํ๊ณ ํจ์จ์ ์ด์ผ. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: ๋ชจ๋ฌ/์ฑํ
์ฐฝ/๋๋ก์ด์ฒ๋ผ "์คํฌ๋กค ๋
๋ฆฝ ๊ณต๊ฐ"์ด ํ์ํ ๊ณณ์ ํญ์
overscroll-contain์ธํธ.
Q3. ์์๋ค ์ปค๋ฎค๋ํฐ์ sticky ๋ค๋น๊ฒ์ด์
(๋์ด 64px)์ด ์๋ค. ์ต์ปค ๋งํฌ๋ก #studies ์น์
์ผ๋ก ์ด๋ ์ ์น์
์ ๋ชฉ์ด ๋ค๋น ์๋์ ์จ๋ ๋ฒ๊ทธ๋ฅผ CSS๋ง์ผ๋ก ํด๊ฒฐํ๋ ค๋ฉด?
โ
์ ๋ต: #studies ์น์
์ scroll-mt-16 ์ถ๊ฐ (16 = 64px)
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
:
scroll-margin-top์์ฑ์ ์ต์ปค ์ด๋ ์ ํด๋น ์์๊ฐ ๋ทฐํฌํธ ์๋จ์ผ๋ก๋ถํฐ ์ผ๋ง๋ ๋จ์ด์ง ๊ณณ์ ์์นํ ์ง๋ฅผ ์ค์ ํด.scroll-mt-16= 64px์ด๋ฏ๋ก, 64px ๋์ด์ sticky ํค๋ ์๋์์ ๋ฑ ์์๋ผ.padding-top๊ณผ ๋ฌ๋ฆฌ ๋ ์ด์์์ ์ํฅ์ ์ฃผ์ง ์๋๋ค๋ ๊ฒ์ด ํต์ฌ ์ฅ์ ์ด์ผ. - ์ค๋ต ํผ๋๋ฐฑ:
pt-16์ผ๋ก ํด๊ฒฐํ๋ฉด ์น์ ๋ด๋ถ ๊ฐ๊ฒฉ์ด ํ์ด์ ธ์ ๋ ์ด์์์ด ๋ง๊ฐ์ ธ.scroll-mt-*๋ ์คํฌ๋กค ์์น๋ง ์กฐ์ ํ๊ณ ์ค์ ๋ ์ด์์์ ๊ฑด๋๋ฆฌ์ง ์์. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: sticky ํค๋ ๋์ด = scroll-mt ๊ฐ.
h-16nav โscroll-mt-16sections. ํญ์ ์ง์ ๋ง์ถฐ.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋ ์์ ๋๋๊ฐ "์นด๋๋ฅผ ๋ฑ๋ฑ ๋ฉ์ถ๋ ์ค์์ดํ ์บ๋ฌ์
"์ ์์ฒญํ์ ๋, ๋๋ ์ต๊ด์ฒ๋ผ ์บ๋ฌ์
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ถํฐ ์ฐพ์๋ค. ๊ทธ๋ฐ๋ฐ ์ํธ ํ์ด overflow-x-auto snap-x snap-mandatory์ ์์์ snap-center๋ง์ผ๋ก ๋จผ์ ๊ตฌํํด๋ณด์๊ณ ํ๋ค.
์ค์ ๋ก ๋ง๋ค์ด ๋ณด๋ ๋ ์ค์ํ ๋ฌธ์ ๋ ์บ๋ฌ์
์์ฒด๋ณด๋ค ์คํฌ๋กค ๊ฒฝ๊ณ์๋ค. ์ฑํ
์ฐฝ ๋์์ ๋ฐฐ๊ฒฝ ํ์ด์ง๊น์ง ๊ฐ์ด ๋ฐ๋ฆฌ๋ ๋ฌธ์ ๋ overscroll-contain์ผ๋ก ๋ง์ ์ ์์๊ณ , sticky ํค๋์ ๊ฐ๋ ค์ง๋ ์ต์ปค ์ ๋ชฉ์ scroll-mt-16์ผ๋ก ๋ ์ด์์์ ๊ฑด๋๋ฆฌ์ง ์๊ณ ํด๊ฒฐ๋๋ค.
๐ก "์คํฌ๋กค UX๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ํ ๋ฌธ์ ๊ฐ ์๋๋ผ ์ค๋ ์ง์ , ์คํฌ๋กค ์ ํ, ๊ณ ์ ํค๋ ์คํ์ ์ ๋ถ๋ฆฌํด์ ์ค๊ณํ๋ ๋ฌธ์ ๋ค."
์ด์ ์คํฌ๋กค ์๊ตฌ์ฌํญ์ ๋ฐ์ผ๋ฉด "ํจํค์ง ๋ญ ์ฐ์ง?"๋ณด๋ค ๋จผ์ ๋ถ๋ชจ/์์ ์ค๋ ์ญํ , ๋ด๋ถ ์คํฌ๋กค ๋ ๋ฆฝ์ฑ, ์ต์ปค ์ด๋ ์์น๋ฅผ ํ๋ก ์ ์ด๋ณผ ์๊ฐ์ด๋ค. ์ฌ์ฉ์๊ฐ ์๊ฐ๋ฝ์ผ๋ก ๋๋ผ๋ UX๋ ๊ฒฐ๊ตญ CSS์ ์์ ๊ณ์ฝ๋ค์ด ๋ชจ์ฌ ๋ง๋ค์ด์ง๋ค๋ ๊ฑธ ์๊ฒ ๋๋ค.