Javascript Mouseover Effect

자바스크립트를 활용하여 재미있는 마우스 오버 효과를 구현해보자


들어가며

Javascript 언어를 공부하고 익히는 데에 있어 늘 어려움이 생기지만, 재미있는 예제를 통해서 학습 하다 보면 단순히 공부로만 느껴지는 부담감이 덜하더라고요.
Codepen에는 다양한 예제가 있는데, 그 예제 속 코드를 정확히 이해하고, 저만의 방법으로 조금 더 간단하고, 쉽게 다시 구현해내는 방식의 학습을 선택했습니다.


구현하게 될 예제 살펴보기


제가 구현하고자 하는 Effect는 바로 이거예요!
화면의 크기가 바뀔 때마다 그에 비례하도록 글자가 복제되고, 무한 루프 됩니다.
또! 마우스 오버 시 무한 루프 중이던 글자가 멈추게 돼요.

Javascript에 익숙하지 않은 분들은 코드만 봤을 때 ‘이거 내가 할 수 있을까?’ 하실 수 있지만,
오늘 저는 이 코드를 최-대한 필요한 부분만 살리고, 또 그 코드를 하나하나 파악하고 학습하는 게 목표입니다.
코드를 어느 정도 살펴보셨다면, 이제 차근차근 풀어나가볼까요?


마크업 & CSS

<div class="menu">
  <div class="menu-item">
    <div class="menu-word">Heewon -&nbsp;</div>
  </div>
</div>

저와 무조건 동일하게 구현하실 필요는 없습니다. 다만 우리가 공통적으로 갖추어야 할 조건이 있어요, 글자를 감싸는 부모 요소가 있어야 합니다!

CSS에서 필수 조건은 글자를 감싸는 부모 요소에 ‘display: flex’,‘flex-wrap: nowrap’ 두 가지 기능을 주어야 합니다.
지금은 글자가 하나뿐이지만, 우리는 무한 루프를 만들 것이기 때문이지요! 글자가 복제되면 바로 옆에 붙어서 진행될 수 있도록 사전 작업을 해준다고 생각하시면 좋을 것 같아요 :)


Script (1)복제하기

const item = document.querySelector('.menu-item');
const word = item.querySelector('.menu-word');

let wordWidth = 0;
let width = 0;
let cloned = [];

const calculate = () => {
  // 불필요한 값 지우기
  cloned.forEach((i) => {
    i.parentNode.removeChild(i);
  });
  cloned = [];

  // 변수 값 설정
  wordWidth = Math.ceil(word.clientWidth);
  width = Math.ceil(window.innerWidth / wordWidth);

  // 반복 값 실행
  for (let i = 0; i < width + 1; i++) {
    const clone = word.cloneNode(true); // 브라우저 width값 만큼 노드 복제
    // 복제된 값 붙이기
    word.parentNode.appendChild(clone);
    cloned.push(clone);
  }
};

window.addEventListener('resize', calculate);
window.addEventListener('load', calculate);

가장 먼저, 현재 한 개뿐인 글자를 화면 크기에 맞춰 계속해서 복제되는 script를 만들어줄 거예요.


- 변수 값 설정

변수로 설정한 wordWidth는 글자의 width 값입니다.
width는 현재 브라우저 창의 width 크기에서 글자의 width를 나눈 값인데, 유추할 수 있듯이 브라우저 크기에 맞는 개수만큼 복제를 하려고 합니다.
*Math.ceil는 소수점 이하를 올림 해주는 메서드로 복제하기 위해 정수가 필요함으로 넣어줍니다.

- 브라우저 width 값만큼 노드 복제

width 만큼 복제해 줘야 하기 때문에, for문을 사용해 반복문을 실행시켜 줍니다.
*A.cloneNode(B)는 A에 복제되어야 할 node를, B는 true또는 false를 넣을 수 있는데, true일 경우 해당 노드의 children까지, false일 경우 해당 노드만 복제하게 됩니다.

우리는 그러니 당연히 true를 넣어줘야 하겠죠?

- 복제된 값 붙이기

*A.appendChild(B)는 B값 안에 들어가는 노드를 A노드의 자식 중 가장 마지막 자식으로 붙여주는 메서드 입니다.
빈 객체로 선언해 준 cloned에 push를 사용해 복제 값을 넣어줍니다.
*push(): 배열 끝에 하나 이상의 요소를 추가하고, 배열의 새로운 길이를 반환합니다.

- 불필요한 값 지우기

사실 위의 과정들만 해도 화면상에서는 문제가 없이 진행되는 것으로 보입니다.
하지만 코드를 살펴보면, 브라우저의 width값이 변경될 때마다 노드는 계속 복제에 복제를 반복해요.
console로 한번 출력해 볼까요?

image01

이것이야말로 무한 loop의 지옥이네요..!
해서 우리는 리사이징될 때마다 객체를 비우고 새로 복제하는 코드가 필요합니다.

*A.parentNode.removeChild(A) : A 노드를 삭제하기 위해서 이와 같은 코드를 작성해 주어야 됩니다.
복제된 값이 들어있는 cloned을 foreach를 사용해 돌려주면 코드상 i는 글자 element가 되기 때문에 리사이징 될 때마다 복제되는 i를 지우고, 반복문이 끝나면 다시 빈 객체를 선언해 주는 코드를 작성해 줍니다.


Script (2)Animation

// 변수 기본값 설정
let speed = 0;
let acc = 0;

const animate = () => {
  // 가속
  acc += 0.1;
  if (hover) {
    acc -= 0.35;
  }

  // 최댓값 제한 걸기
  acc = Math.min(13, Math.max(0, acc));

  // 가속도 주기
  speed += acc;

  if (speed >= wordWidth) {
    speed = 0;
  }

  // CSS Text
  item.style.transform = `translateX(${-speed}px) skewX(${-2 * acc}deg)`;

  // RaF
  requestAnimationFrame(animate);
};
animate();

- 가속

0.1씩 가속 되는 코드를 작성해 주고, 마우스 오버 될 때는 점점 속도가 떨어져야 하므로 조건문을 넣어줍니다.

- 최댓값 제한 걸기

가속 값에 최댓값을 지정해 줍니다. Mate.min을 사용해서 13보다 커지면 13으로 출력되도록 코드를 입력해 줍니다.

- 가속도 주기

속도 값 변수로 설정해둔 speed에 가속도를 줍니다.
단, 조건문을 걸지 않으면 가속은 끝없이 더해지므로 wordWidth보다 클 때 0으로 리셋해주는 조건문을 걸어주겠습니다.

image02 조건문 없이 진행했을 때 console로 speed값을 출력한 결과
(가속이 계속 증가해서 화면상 글자도 사라집니다.)


- CSS Text

속도 값을 설정해 주었으니, 이제 복제된 글자에 스피드 값을 주는 코드를 짜주어야겠죠?
translateX로 speed값을, skewX 기울기는 가속 값만큼 설정해 줍니다.

- requestAnimationFrame

Javascript가 프레임 시작 시 실행되도록 보장하는 방법.
브라우저가 프레임 생성 초기 단계에 맞춰 애니메이션 코드를 실행시키므로, 애니메이션이 더 부드럽게 작동합니다.


Script (3)이벤트 리스너

let hover = false;

// 이벤트 리스너(true값은 멈추게 한다.)
const hadleMouse = (bool) => (hover = bool);
item.addEventListener('mouseenter', () => {
  hadleMouse(true);
});
item.addEventListener('touchstart', () => {
  hadleMouse(true);
});
item.addEventListener('mouseleave', () => {
  hadleMouse(false);
});
item.addEventListener('touchend', () => {
  hadleMouse(false);
});

변수 및 애니메이션 코드를 작성해 주었으니, 이제 이벤트 리스너 설정만 해주면 완성!

마우스를 대거나, 터치 시(모바일) hadleMouse값을 true로 바꿔 동작이 멈추도록 설정해 주고,
마우스를 떼거나, 터치가 끝날 시 hadleMouse값을 false로 바꿔 동작이 실행되도록 설정해 주면서 코드를 완성합니다.


마치며

스크립트를 구현하는 데에 여러 방법이 있지만,
해당 방법으로 코드를 풀어나가고 구현하면서 재미있게 학습을 하게 된 것 같습니다.

이 글을 읽으시면서 같은 기분을 느끼셨으면 좋겠네요!




참고문서