๊ด€๋ฆฌ ๋ฉ”๋‰ด

UI Code Lab

๐ŸŽฏ Fullpage ์›น์‚ฌ์ดํŠธ๋ฅผ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ• – ์ˆœ์ˆ˜ JavaScript๋กœ ์‹คํ˜„ํ•˜๋Š” ์Šคํฌ๋กค ์ธํ„ฐํŽ˜์ด์Šค ๋ณธ๋ฌธ

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธUI

๐ŸŽฏ Fullpage ์›น์‚ฌ์ดํŠธ๋ฅผ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ• – ์ˆœ์ˆ˜ JavaScript๋กœ ์‹คํ˜„ํ•˜๋Š” ์Šคํฌ๋กค ์ธํ„ฐํŽ˜์ด์Šค

๐Ÿฏ๊ฟ€์ƒ์ด๐Ÿ 2025. 7. 16. 08:34

Fullpage ์›น์‚ฌ์ดํŠธ๋ฅผ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•

 

 

โœ… ๊ฐœ์š”

๋งŽ์€ ์›น์‚ฌ์ดํŠธ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํ’€ํŽ˜์ด์ง€(Fullpage) ์ธํ„ฐํŽ˜์ด์Šค๋Š”

์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ง๊ด€์ ์œผ๋กœ ๋งŒ๋“ค๊ณ , ์ฝ˜ํ…์ธ ์— ๋ชฐ์ž…ํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

 

๋Œ€ํ‘œ์ ์œผ๋กœ fullPage.js์™€ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜์ง€๋งŒ,

์ˆœ์ˆ˜ JavaScript๋งŒ์œผ๋กœ๋„ ์ถฉ๋ถ„ํžˆ ๊ฐ€๋ณ๊ณ  ์œ ์—ฐํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ด ๊ธ€์—์„œ๋Š” ๋ฐ”๋‹๋ผ JS ๊ธฐ๋ฐ˜์˜ fullpage ์›น์‚ฌ์ดํŠธ UI๋ฅผ ์ง์ ‘ ๋งŒ๋“œ๋Š” ๊ณผ์ •์„

์˜ˆ์ œ ์ค‘์‹ฌ์œผ๋กœ ์ž์„ธํžˆ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ“ฆ ์™œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด ์ง์ ‘ ๊ตฌํ˜„ํ• ๊นŒ?

  • ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ถˆํฌํ•จ์œผ๋กœ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์†๋„ ๊ฐœ์„ 
  • ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์ž์œ ๋„: ๋‚ด๊ฐ€ ์ง์ ‘ ๋งŒ๋“  ์ฝ”๋“œ๋กœ ์›ํ•˜๋Š” ๋Œ€๋กœ ์ˆ˜์ • ๊ฐ€๋Šฅ
  • ํ•™์Šต ํšจ๊ณผ: ์Šคํฌ๋กค ์ด๋ฒคํŠธ์™€ UI ํ๋ฆ„ ์ดํ•ด๋„๊ฐ€ ๋†’์•„์ง

 

๐Ÿงฑ HTML ๋ฐ CSS ๊ธฐ๋ณธ ๊ตฌ์กฐ

 
<div class="section-container">
  <section class="section">Section 1</section>
  <section class="section">Section 2</section>
  <section class="section">Section 3</section>
</div>
 
html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
}

.section-container {
  transition: transform 0.7s ease;
}

.section {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 2rem;
}

 

โš™๏ธ JavaScript๋กœ ๋ถ€๋“œ๋Ÿฌ์šด ์„น์…˜ ์ „ํ™˜ ๊ตฌํ˜„ํ•˜๊ธฐ

 
const container = document.querySelector('.section-container');
let currentIndex = 0;
let isScrolling = false;
const sectionCount = document.querySelectorAll('.section').length;

window.addEventListener('wheel', (e) => {
  if (isScrolling) return;
  isScrolling = true;

  const direction = e.deltaY > 0 ? 1 : -1;
  currentIndex = Math.min(Math.max(currentIndex + direction, 0), sectionCount - 1);

  container.style.transform = `translateY(-${currentIndex * 100}vh)`;

  setTimeout(() => {
    isScrolling = false;
  }, 700);
});

 

๐ŸŽ›๏ธ UX ํ–ฅ์ƒ์„ ์œ„ํ•œ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์•„์ด๋””์–ด

  • Dot Navigation: ํ˜„์žฌ ์œ„์น˜๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์‚ฌ์ด๋“œ ์ธ๋””์ผ€์ดํ„ฐ
  • Mobile Touch Event ๋Œ€์‘: touchstart, touchend ์ด๋ฒคํŠธ๋กœ ๋ชจ๋ฐ”์ผ ์นœํ™” UX
  • Active ํด๋ž˜์Šค ๊ด€๋ฆฌ: ํ˜„์žฌ ์„น์…˜์—๋งŒ .active ๋ถ€์—ฌํ•˜์—ฌ ์Šคํƒ€์ผ ๋ถ„๊ธฐ
  • requestAnimationFrame ๊ธฐ๋ฐ˜ ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜

 

๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ๋Œ€์‘ ์˜ˆ์‹œ ์ฝ”๋“œ

 
let touchStartY = 0;

window.addEventListener('touchstart', (e) => {
  touchStartY = e.touches[0].clientY;
});

window.addEventListener('touchend', (e) => {
  const touchEndY = e.changedTouches[0].clientY;
  const deltaY = touchStartY - touchEndY;

  if (Math.abs(deltaY) < 50 || isScrolling) return;

  isScrolling = true;
  currentIndex = deltaY > 0
    ? Math.min(currentIndex + 1, sectionCount - 1)
    : Math.max(currentIndex - 1, 0);

  container.style.transform = `translateY(-${currentIndex * 100}vh)`;

  setTimeout(() => {
    isScrolling = false;
  }, 700);
});

 

๐Ÿง  Fullpage UI์˜ ๋™์ž‘ ์›๋ฆฌ – ์‚ฌ์šฉ์ž์˜ ์Šคํฌ๋กค ์ œ์–ดํ•˜๊ธฐ

 

fullpage ์ธํ„ฐํŽ˜์ด์Šค๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ํ•œ ๋ฒˆ์˜ ์Šคํฌ๋กค ์ž…๋ ฅ์— ๋”ฐ๋ผ

ํ™”๋ฉด ์ „์ฒด๊ฐ€ ํ•œ ์„น์…˜ ๋‹จ์œ„๋กœ ์›€์ง์ด๋Š” ํ˜•ํƒœ์•ผ.

 

์ด๊ฑธ ์œ„ํ•ด ๊ฐ€์žฅ ํ•ต์‹ฌ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•  ๊ฑด:

  • ์‚ฌ์šฉ์ž์˜ ์Šคํฌ๋กค์„ ๊ฐ์ง€ํ•˜๋Š” ๋กœ์ง
  • ํ˜„์žฌ ์„น์…˜์„ ๊ธฐ์ค€์œผ๋กœ ๋‹ค์Œ ์„น์…˜์œผ๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ด๋™
  • ์Šคํฌ๋กค ์ด๋ฒคํŠธ๊ฐ€ ๊ฒน์น˜์ง€ ์•Š๋„๋ก ์ œ์–ด(throttle ๋˜๋Š” flag ์ฒ˜๋ฆฌ)
 
let currentIndex = 0;
let isScrolling = false;

window.addEventListener('wheel', (e) => {
  if (isScrolling) return;
  isScrolling = true;
  const direction = e.deltaY > 0 ? 1 : -1;

  currentIndex = Math.min(Math.max(currentIndex + direction, 0), sectionCount - 1);
  container.style.transform = `translateY(-${currentIndex * 100}vh)`;

  setTimeout(() => isScrolling = false, 700); // ์ „ํ™˜ ์‹œ๊ฐ„๊ณผ ์ผ์น˜
});

โœจ ์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ์ ์€ isScrolling์ด๋ผ๋Š” ํ”Œ๋ž˜๊ทธ๋ฅผ ํ™œ์šฉํ•ด์„œ ์—ฐ์† ์ž…๋ ฅ์„ ๋ง‰๋Š” ๊ฑฐ์•ผ.
์ด ๋ฐฉ์‹์€ Debounce๋‚˜ Throttle๋ณด๋‹ค ์ง๊ด€์ ์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ณ , UX ์•ˆ์ •์„ฑ์„ ๋†’์—ฌ์ค˜.

 

๐Ÿงฉ transition๊ณผ requestAnimationFrame ์ฐจ์ด์ 

 

๋Œ€๋ถ€๋ถ„ CSS์—์„œ transition: transform 0.7s ease;๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ,

์ข€ ๋” ๋ฌผ๋ฆฌ์ ์ด๊ณ  ๋ถ€๋“œ๋Ÿฌ์šด ์›€์ง์ž„์„ ์›ํ•œ๋‹ค๋ฉด

requestAnimationFrame์œผ๋กœ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์–ด:

 
function smoothScroll(targetY) {
  const startY = container.getBoundingClientRect().top;
  const diff = targetY - startY;
  let start;

  function step(timestamp) {
    if (!start) start = timestamp;
    const progress = timestamp - start;
    const easing = Math.min(progress / 700, 1); // 700ms ๊ธฐ์ค€
    container.style.transform = `translateY(${startY + diff * easing}px)`;
    if (easing < 1) requestAnimationFrame(step);
  }

  requestAnimationFrame(step);
}

๐Ÿ” ์ด ๋ฐฉ์‹์€ ์„ฑ๋Šฅ ์ œ์–ด๊ฐ€ ๋” ํ•„์š”ํ•˜์ง€๋งŒ,
๋ณต์žกํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด๋‚˜ ์ปค์Šคํ…€ easing ์ปค๋ธŒ๋ฅผ ์ ์šฉํ•˜๊ธฐ์— ์œ ๋ฆฌํ•ด.

 

๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ํ„ฐ์น˜ ๋Œ€์‘์— ๋”ฐ๋ฅธ UX ๊ฐœ์„ 

 

์Šคํฌ๋กค๊ณผ ํ„ฐ์น˜๋ฅผ ๋ชจ๋‘ ์ง€์›ํ•˜๋ ค๋ฉด ๋ฐ์Šคํฌํƒ‘๊ณผ ๋ชจ๋ฐ”์ผ ์ด๋ฒคํŠธ๋ฅผ ๋ณ‘ํ–‰ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๊ณ ,

๊ฐ๊ฐ์˜ ํŠน์ง•์„ ๋ฐ˜์˜ํ•ด์•ผ UX๊ฐ€ ๊นจ์ง€์ง€ ์•Š์•„:

  • ํ„ฐ์น˜๋Š” touchstart์™€ touchend๋ฅผ ์กฐํ•ฉํ•ด์„œ ์‚ฌ์šฉ์ž์˜ ์Šค์™€์ดํ”„ ๋ฐฉํ–ฅ์„ ํŒ๋‹จ
  • ํ„ฐ์น˜ ๊ฐ๋„ ์กฐ์ ˆ์ด UX์— ํฐ ์˜ํ–ฅ์„ ๋ฏธ์นจ
  • ์ž‘์€ ์›€์ง์ž„(delta < 50)์€ ๋ฌด์‹œํ•˜์—ฌ ์˜ค์ž‘๋™ ๋ฐฉ์ง€
 
let touchStartY = 0;

window.addEventListener('touchstart', (e) => {
  touchStartY = e.touches[0].clientY;
});

window.addEventListener('touchend', (e) => {
  const touchEndY = e.changedTouches[0].clientY;
  const deltaY = touchStartY - touchEndY;

  if (Math.abs(deltaY) < 50 || isScrolling) return;
  isScrolling = true;
  currentIndex = deltaY > 0 ? Math.min(currentIndex + 1, sectionCount - 1) : Math.max(currentIndex - 1, 0);
  container.style.transform = `translateY(-${currentIndex * 100}vh)`;

  setTimeout(() => isScrolling = false, 700);
});

 

๐ŸŽ›๏ธ Dot Navigation์˜ UX์  ๊ฐ€์น˜

 

์  ๋‚ด๋น„๊ฒŒ์ด์…˜์€ ์‚ฌ์šฉ์ž๊ฐ€ ํ˜„์žฌ ์–ด๋А ์„น์…˜์— ์žˆ๋Š”์ง€ ์‹œ๊ฐ์ ์œผ๋กœ ์•Œ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜.

๋˜ํ•œ ํด๋ฆญ์œผ๋กœ ํ•ด๋‹น ์„น์…˜์œผ๋กœ ์ ํ”„ํ•  ์ˆ˜ ์žˆ์–ด:

 
<div class="dot-nav">
  <div class="dot active" data-index="0"></div>
  <div class="dot" data-index="1"></div>
  <div class="dot" data-index="2"></div>
</div>
 
document.querySelectorAll('.dot').forEach(dot => {
  dot.addEventListener('click', (e) => {
    currentIndex = Number(e.target.dataset.index);
    container.style.transform = `translateY(-${currentIndex * 100}vh)`;
    updateActiveDot();
  });
});

function updateActiveDot() {
  document.querySelectorAll('.dot').forEach(dot => dot.classList.remove('active'));
  document.querySelector(`.dot[data-index="${currentIndex}"]`).classList.add('active');
}

๐ŸŽจ ๋‚ด๋น„๊ฒŒ์ด์…˜์€ ์ ‘๊ทผ์„ฑ, ์‚ฌ์šฉ์„ฑ, ์‹œ๊ฐ์  ํ”ผ๋“œ๋ฐฑ๊นŒ์ง€
๋ชจ๋‘ ์ฑ™๊ธธ ์ˆ˜ ์žˆ์–ด์„œ “์ž‘์ง€๋งŒ ํฐ” UX ์š”์†Œ์•ผ.

 

 

๐Ÿš€ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

 

ํ’€ํŽ˜์ด์ง€ ์›น์‚ฌ์ดํŠธ๋Š” ์‹œ๊ฐ์ ์œผ๋กœ ๊ฐ•๋ ฌํ•˜๊ณ 

UX์— ๊ฐ•ํ•œ ์ธ์ƒ์„ ๋‚จ๊ธธ ์ˆ˜ ์žˆ๋Š” UI ํŒจํ„ด์ž…๋‹ˆ๋‹ค.

 

๊ผญ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์˜์กดํ•˜์ง€ ์•Š์•„๋„

์ง์ ‘ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ํ•™์Šต๊ณผ ํšจ์œจ์„ ๋™์‹œ์— ์žก์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์•ž์œผ๋กœ ์ด ๋ฐฉ์‹์„ ํ™•์žฅํ•ด ๋ชจ๋‹ฌ, ์Šฌ๋ผ์ด๋”, ๋‚ด๋น„๊ฒŒ์ด์…˜ ๋“ฑ

๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์™€๋„ ์—ฐ๊ฒฐํ•ด๋ณด์„ธ์š”!