[코드 리뷰] PP Fragment 클론 코딩

Carrie·2024년 1월 23일
0
post-thumbnail

1. 다이나믹 줄바꿈 처리 구현

이번 프로젝트에서는 아래와 같은 문단에서 각 텍스트 라인이 한 라인씩 순차적으로 아래에서 스르륵 올라오는 애니메이션을 구현하려고 한다. 중요 포인트는 한 라인씩 나타나게 하는 것인데, 다양한 화면 크기에서 문단이 한 라인씩 배치되려면 화면 크기에 따라 동적으로 텍스트의 줄바꿈이 일어나야 한다.

It comes in 4 preset cuts, Sans, Serif, Glare, and Text, each with unique personalities and quirks. Each weight counts 581 glyphs with plenty of alternate symbols to achieve the best-desired result for your next design.

그리고 애니메이션이 실행될 때, 각 라인이 해당 영역을 벗어나지 않도록 overflow: hidden 속성을 적용해야 하기 때문에 각 라인을 div로 한번 더 감싸주어야 한다. 여기서는 animation-wrapper라는 클래스를 사용하여 각 라인을 감싸주려고 한다.

코드를 살펴보자!

코드 살펴보기

function dynamicWrapLines() {
  const elementArray = document.querySelectorAll(".dynamic-paragraph");

  elementArray.forEach((element) => {
    const content = element.innerText; // 문단의 내용
    const wordArray = content.split(" "); // 공백을 기준으로 단어를 잘라서 배열 생성
    let line = "";
    element.innerHTML = "";

    wordArray.forEach((word) => {
      const testLine = line + word + " "; // 가로너비 비교를 위한 테스트 라인 생성
      const tempSpan = document.createElement("span"); // 임시 span 생성
      tempSpan.style.position = "absolute";
      tempSpan.style.left = "-9999px";
      tempSpan.style.visibility = "hidden";
      tempSpan.innerText = testLine;
      document.body.appendChild(tempSpan); // body에 tempSpan 추가

      // tempSpan의 가로너비와 해당 요소의 가로너비 비교
      if (tempSpan.offsetWidth > element.offsetWidth) {
        const newLine = document.createElement("span"); // tempSpan의 너비가 크다면 새로운 라인 생성
        const animationWrapper = document.createElement("span"); // 애니메이션 적용을 위한 부모 wrapper 생성
        animationWrapper.className = "animation-wrapper";
        newLine.className = "line";
        newLine.innerText = line; // line에는 현재 단어 이전까지의 텍스트가 담겨 있음
        element.appendChild(animationWrapper); // 해당 요소에 부모 wrapper를 먼저 추가
        animationWrapper.appendChild(newLine); // 최종적으로 부모 wrapper에 라인을 자식으로 추가
        line = word + " "; // 라인에 현재 단어를 넣어서 초기화
      } else {
        line = testLine;
      }

      document.body.removeChild(tempSpan); // body에서 tempSpan 제거
    });

    const lastLine = document.createElement("span");
    const lastWrapper = document.createElement("span"); // 애니메이션 적용을 위한 부모 wrapper 생성
    lastWrapper.className = "animation-wrapper";
    lastLine.className = "line";
    lastLine.innerText = line;
    element.appendChild(lastWrapper); // 해당 요소에 부모 wrapper를 먼저 추가
    lastWrapper.appendChild(lastLine);
  });
}

코드를 간단하게 설명하자면 먼저,
.dynamic-paragraph 클래스를 가진 모든 요소를 선택하고, 각 문단의 내용을 공백을 기준으로 분리하여 배열을 생성한다. 이 배열은 문단의 각 단어를 순회하며 라인을 구성하는 데 사용된다.

줄바꿈이 필요한 지점을 체크하기 위해 임시 span을 생성하고, 단어를 추가할 때마다 가로 너비를 비교한다. 임시 span의 너비가 해당 요소(문단)의 가로 너비보다 크면 현재 단어 이전까지의 텍스트를 newLine에 담고 그 라인을 문단에 추가한다.

이제 이 함수의 호출 시점이 중요한데, 여기서 문제가 발생했다.

문제 발생🧨

tempSpan.offsetWidth가 새로고침할 때마다 다르게 측정되는 문제가 발생했다. 원인을 추측해보자면,
1) tempSpan이 생성되고 측정되는 시점이 페이지의 로딩 상태에 따라 달라질 수 있다.
2) css 스타일이 tempSpan에 적용되기 전에 크기를 측정하면, 잘못된 너비값을 얻을 수 있다.
3) tempSpan을 추가하고 바로 크기를 측정하는 경우, 브라우저가 아직 레이아웃 계산을 완료하지 않았을 수 있다.

해결🥳

웹페이지 초기 진입 시에 tempSpan의 너비가 제대로 측정되지 않는 점, 해당 페이지에서 새로고침을 한 후에는 tempSpan의 너비가 올바르게 측정된다는 점으로 미루어보아 페이지 로딩 상태에 따른 문제일 것으로 추측, window.onload를 사용하여 웹페이지가 완전히 로드되었을 때 해당 함수를 실행하도록 수정하여 문제를 해결하였다.

window.addEventListener("load", function () {
  dynamicWrapLines();
});

DOMContentLoaded VS window.onload

  • DOMContentLoaded: HTML 문서가 완전히 로드되고 파싱됐을 때 발생한다. 이 시점에는 모든 HTML이 로드되고, DOM 트리가 구축되었지만, 스타일시트, 이미지, 프레임 등 다른 리소스의 로딩은 완료되지 않았을 수 있다.
  • window.onload: 페이지의 모든 리소스(HTML, 스타일시트, 스크립트, 이미지, 프레임 등)가 로드되고, 페이지 로딩이 완전히 끝난 후에 발생한다.

2. GSAP 라이브러리를 이용한 Text 애니메이션

2-1. 랜덤하게 등장하는 텍스트

opacity를 이용해 텍스트가 서서히 나타나는 애니메이션을 구현하고자 하는데, 텍스트가 랜덤하게 나타나게 하려면 어떻게 할 수 있을까?
gsap의 sttagerrandom 옵션을 사용하면 가능하다. 사용 방법은 아래와 같다.

const introMotion = gsap.timeline(); // gsap 타임라인 생성
introMotion.from(".sc-main .scatter-letters .scatter-item", {
    opacity: 0, // opacity 0에서 시작한다.
    stagger: {
      from: "random", // 애니메이션의 순서를 무작위로 지정한다.
      each: 0.05, // 각 요소 사이에 0.05초 지연 시간 적용
    },
    delay: 1, // 애니메이션의 지연 시간(1초 대기 후 시작)
    duration: 3, // 애니메이션의 총 지속 시간
    ease: "power2.out", // 부드러운 ease 효과 적용
  })

여기서 sttager에 대해 좀 더 알아보자!

💡 sttager란?
stagger는 여러 요소에 대한 애니메이션을 순차적으로 지연시켜 실행하는 기능이다. stagger를 사용하면 동일한 애니메이션을 여러 요소에 적용하면서 각 요소마다 약간의 지연을 두어 순차적으로 애니메이션을 실행할 수 있다.

stagger의 옵션
each: 각 요소 사이의 지연 시간
예를 들어, each: 0.1은 한 요소가 애니메이션된 후 다음 요소가 0.1초 뒤에 애니메이션되도록 설정한다.
from: 애니메이션이 시작될 요소의 위치
예를 들어, from: "start"는 첫 번째 요소에서 시작하여 순차적으로 애니메이션하고, from: "end"는 마지막 요소에서 시작한다. 내가 사용한 from: "random"은 무작위 순서로 애니메이션한다.
amount: 전체 stagger 애니메이션의 총 지속 시간
이 옵션을 사용하면 개별 지연 시간(each) 대신 전체 애니메이션 시간을 기반으로 각 요소의 지연 시간이 계산된다.

2-2. 흩어지는 텍스트 애니메이션

다음으로 텍스트가 깨져서 흩어지는 듯한 애니메이션을 구현하고자 한다. gsap 라이브러리와 css로 해당 애니메이션을 구현할 수 있다.

원하는 시점에 해당 요소에 class를 추가하여 css와 결합한다. 이후 설정은 css에서 해주면 된다.

javascript

introMotion.add(() => {
    $(".sc-main .scatter-letters .scatter-item").addClass("on"); // 클래스 추가
  })

각각의 텍스트 조각이 특정한 위치로 이동하도록 transform: translate(X, Y) 값을 설정해주면 된다. 필요하다면 rotate() 값도 추가한다.

css

@keyframes scaleAndMoveAnimation { /* keyframes 애니메이션 선언 */
  10% {
    transform: translate(0, 0) rotate(0) scale(0.9);
  }
  15% {
    transform: translate(0, 0) rotate(0) scale(1);
  }
  100% {
    transform: var(--move-transform);
  }
}
.sc-main .scatter-letters .scatter-item.on { /* 해당 요소에 애니메이션 설정 */
  animation: scaleAndMoveAnimation cubic-bezier(0.165, 0.84, 0.44, 1) forwards,
    floatAnimation cubic-bezier(0.445, 0.05, 0.55, 0.95) infinite;
}

그런데 깨진 텍스트 조각이 많기 때문에 각 요소마다 애니메이션을 별도로 설정하는 것은 번거롭고 비효율적이다. 이 때 css 변수를 선언해서 변수에 각 요소의 이동 지점을 담아준다. 아래와 같이 하면 중복 코드를 줄일 수 있다.

.sc-main .scatter-letters .f1.on {
/* 이동할 지점의 위치값을 변수에 담아준다. */
  --move-transform: translateX(-50vw) translateY(-40vh) rotate(10deg);
  animation-duration: 3s, 4.68s; /* 두 개의 애니메이션에 대한 각각의 지속 시간 */
  animation-delay: 0s, 3s; /* 두 개의 애니메이션에 대한 각각의 지연 시간*/
}
.sc-main .scatter-letters .f2.on {
  --move-transform: translateX(-50vw) translateY(-20vh) rotate(45deg);
  animation-duration: 3s, 5.34s;
  animation-delay: 0s, 3s;
}
.sc-main .scatter-letters .f3.on {
  --move-transform: translateX(-20vw) translateY(-50vh) rotate(-45deg);
  animation-duration: 3s, 5.82s;
  animation-delay: 0s, 3s;
}
.
.
.
생략

2-3. 둥둥 떠다니는 텍스트 애니메이션

1-2와 이어서 이동한 지점에서 둥둥 떠다니는 듯한 애니메이션 효과를 주려면 어떻게 할까? 포인트는 기존의 위치에서가 아닌, 이동한 지점에서 해당 애니메이션을 적용해야 하는 것이다. 아래와 같이 할 수 있다.

@keyframes floatAnimation {
  0% {
    transform: var(--move-transform);
  }
  50% {
    transform: var(--move-transform) translateY(10%);
  }
  100% {
    transform: var(--move-transform);
  }
}

이동한 지점의 좌표값을 담은 변수를 넣어주기만 하면 간단하게 구현할 수 있다.

2-4. 텍스트 롤링 애니메이션

여러개의 텍스트가 그 자리에서 롤링되는 애니메이션은 어떻게 구현할 수 있을까?
먼저 html 구조를 살펴보자.

html

<div class="first-line">
  <div class="letter-p1">
    <span class="default-letter sans">P</span>
    <div class="rolling-letters">
      <span class="sans">P</span>
      <span class="glare">P</span>
      <span class="serif">P</span>
      <span class="text">P</span>
    </div>
  </div>
  <div class="letter-p2">
    <span class="default-letter serif">P</span>
    <div class="rolling-letters">
      <span class="serif">P</span>
      <span class="text">P</span>
      <span class="glare">P</span>
      <span class="sans">P</span>
    </div>
  </div>
</div>
<div class="second-line">
  <div class="letter-f">
    <div class="default-letter sans">F</div>
    <div class="rolling-letters">
      <span class="sans">F</span>
      <span class="text">F</span>
      <span class="serif">F</span>
      <span class="glare">F</span>
    </div>
  </div>
  .
  .
  .
  생략

한글자 한글자마다 div로 묶어주어야 한다. 그리고 자식 요소로 초기 위치를 잡기 위한 default-letter와 롤링될 텍스트 rolling-letters가 필요하다. rolling-letters는 y축 기준으로 아래로 내려가면서 애니메이션이 진행될 예정이다.

css

.sc-main .main-title .first-line {
  display: flex;
  justify-content: center;
  overflow: hidden; /* 원래의 자리에서 롤링되어야 하기 때문에 벗어나는 요소는 숨겨준다. */
}
.sc-main .main-title .default-letter {
  display: inline-block;
  visibility: hidden;
}
.sc-main .main-title .rolling-letters {
  position: absolute; /* 롤링될 텍스트들을 절대 위치를 잡아준다. */
  left: 0;
  bottom: 0;
}
.sc-main .main-title .rolling-letters span {
  display: flex;
}

rolling-letters의 초기 위치가 중요하다 부모를 기준으로 bottom: 0에 위치하도록 잡아준다. 그리고 gsap 라이브러리의 to()로 위치를 이동시켜 준다. 아래로 이동해야 하기 때문에 y축 기준 100% 이동한다. y축 기준 아래로 100% 이동하면 현재 default-letter의 아래 rolling-letters가 위치해 있기 때문에 다시 위로 bottom: 100%만큼 올려주어야 한다.

javascript

introMotion.to(
    ".sc-main .rolling-letters",
    2, // 애니메이션 진행 시간
    {
      yPercent: 100,
      bottom: "100%",
      stagger: {
        from: "random",
        each: 0.1,
      },
    },
    "a"
  )

3. Swiper 라이브러리 커스터마이징

3-1. Swiper와 css의 조합

Swiper는 다양한 옵션과 유연한 커스터마이징이 가능한 라이브러리이다. Swiper와 css를 조합하면 내가 원하는 효과를 자유롭게 구현할 수 있다. 이 프로젝트에서는 direction 옵션을 사용하여 수직 방향으로 슬라이드되는 배경 슬라이드를 구현하였다.

javascript

const bgSwiper = new Swiper(".bg-swiper-container", {
  direction: "vertical", // 슬라이드 방향을 수직으로 설정
  slidesPerView: 1,
  spaceBetween: 20,
  loop: true,
  speed: 500,
  navigation: {
    nextEl: ".random-btn",
  },
  autoplay: {
    delay: 3000,
    disableOnInteraction: false,
  },
});

Swiper에서 제공하는 클래스명을 사용하면 css를 통해 스타일을 쉽게 조정할 수 있다. .swiper-slide-prev 클래스를 커스터마이징하여 이전 슬라이드에 특별한 효과를 적용하였다.

css

.sc-poster .bg-swiper-wrapper .swiper-slide {
  width: 100%;
  height: 100%;
  border-radius: 48px;
  transition: 0.3s ease-in-out;
}
.sc-poster .bg-swiper-wrapper .swiper-slide-prev {
  opacity: 0.75;
  transform: scale(0.8);
}

3-2. creativeEffect를 활용한 슬라이더 애니메이션 구현

Swiper의 creativeEffect 옵션을 활용하면 다양한 창의적인 애니메이션을 구현할 수 있다. 이번엔 creativeEffect를 활용하여 슬라이더에 독특한 효과를 추가했다.

const posterSwiper = new Swiper(".poster-swiper-container", {
  direction: "vertical",
  slidesPerView: 1,
  spaceBetween: 20,
  loop: true,
  speed: 500,
  effect: "creative", // creative effect 적용
  creativeEffect: {
    prev: {
      shadow: true, // 이전 슬라이드에 그림자 효과 적용
      translate: [0, 0, -400], // z축으로 -400px 이동
    },
    next: {
      translate: [0, "100%", 0], // 다음 슬라이드를 y축으로 100% 이동
    },
  },
  navigation: {
    nextEl: ".random-btn",
  },
  autoplay: {
    delay: 3000,
    disableOnInteraction: false,
  },
});

이전 슬라이드에는 그림자 효과와 z축 이동으로 공간감을 주고, 다음 슬라이드는 아래로 내려가도록 하여 마치 페이지를 넘기는 듯한 효과를 주었다.

📌 참고
Swiper 라이브러리의 공식 데모페이지에서 다양한 효과를 확인할 수 있으며, 원하는 효과를 선택하여 자유롭게 커스터마이징할 수 있다.

profile
Markup Developer🧑‍💻

0개의 댓글