본문 바로가기

UI 디자인 애니메이션 연구소

gsap flip 을 이용한 인터렉티브 카드 스위칭

반응형

GSAP 에는 뒤짚는 녀석이 있습니다.

GSAP 를 공부하면서 아주 아주 흥미로운 친구를 발견했습니다. 바로 Flip 이라는 친구인데요. 이 친구가 왜 흥미롭냐면 이름 그대로 판을 뒤짚기 때문입니다. 
 
해당 기능은 사용해보지 않으면 알수가 없기에 오늘은 해당 녀석을 가지고 재밌는 기능을 하나 구현해볼까 합니다.

 

아마 해당 기능은 GSAP 를 자주 사용하셨다면, 튜토리얼 영상에서 한 번 보셨을 수도 있습니다. 모르셨다면, 이번에 알고 가셔서 보다 재밌는 인터렉티브한 웹 사이트를 만드는데 활용해보시면 좋을 것 같습니다. 보면 알기에 굳이 어떤 기능이다 설명하지 않겠습니다. 직접 따라해보시면서 느껴보시길 바랍니다.

 

우선 준비물을 챙겨오세요.

이 친구는 당연히 GSAP 이라는 라이브러리의 가족이므로 이들을 대여해 와야 합니다. 따라서 다음 사이트에 접속해서 GSAP 코어와 Flip 플로그인 두 가지의 CDN 을 가져와야 합니다.

https://gsap.com/docs/v3/Installation/?tab=cdn&module=esm&method=private+registry&tier=free&club=false&require=false&trial=true&plugins=Flip

 

<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/Flip.min.js"></script>

 

HTML 을 다음과 같이 작성하세요

그 다음에는 HTML 토대를 구성할 겁니다. 제가 본 튜토리얼에서는 CSS Grid Generator 라는 사이트에서 다음과 같이 그리드를 생성하여 붙여넣기 했었는데, 현 포스트를 따라하시는 분들도 쉽게 만들어서 가져옵시다.

 
https://cssgrid-generator.netlify.app/

    <div class="parent">
      <div class="div1"></div>
      <div class="div2"></div>
      <div class="div3"></div>
      <div class="div4"></div>
    </div>

https://cssgrid-generator.netlify.app/

 

여기서 잠깐 건드려야 하는 부분이 있습니다. data-grid 속성을 각 div 요소에 다음과 같이 넣어서 수정해주셔야 합니다. 또한 각 div 박스가 무엇인지 인지하기 위해 TextNode 로 1 ~ 4를 순서대로 추가해줍시다.

    <div class="parent">
      <div data-grid="1" class="div1">1</div>
      <div data-grid="2" class="div2">2</div>
      <div data-grid="3" class="div3">3</div>
      <div data-grid="4" class="div4">4</div>
    </div

 

CSS 를 준비합시다.

앞서 grid 생성 사이트에서 HTML 을 생성하셨다면, CSS SHOW 를 통해서 생성된 CSS 속성들도 확인하실 수 있을 겁니다. 그것을 복사해서 붙여넣어 주세요. 그러면 다음과 같은 코드가 보일 겁니다.

      .parent {
        display: grid;
        grid-template-columns: repeat(5, 1fr);
        grid-template-rows: repeat(5, 1fr);
        grid-column-gap: 0px;
        grid-row-gap: 0px;
      }

      .div1 {
        grid-area: 1 / 1 / 4 / 4;
      }
      .div2 {
        grid-area: 1 / 4 / 4 / 6;
      }
      .div3 {
        grid-area: 4 / 4 / 6 / 6;
      }
      .div4 {
        grid-area: 4 / 1 / 6 / 4;
      }

 
 

그 다음 CSS 의 경우도 마찬가지로 앞서 HTML 태그에서 data-grid 넣었던 것을 사용해서 접근할 수 있도록 다음과 같이 수정해 줍시다. 이렇게 수정하는 이유는 나중에 자바스크립트로 조작할 일이 있어서 그렇습니다. 일단은 따라와 주세요.

      .parent {
        display: grid;
        grid-template-columns: repeat(5, 1fr);
        grid-template-rows: repeat(5, 1fr);
        grid-column-gap: 0px;
        grid-row-gap: 0px;
      }

      div[data-grid='1'] {
        grid-area: 1 / 1 / 4 / 4;
      }
      div[data-grid='2'] {
        grid-area: 1 / 4 / 4 / 6;
      }
      div[data-grid='3'] {
        grid-area: 4 / 4 / 6 / 6;
      }
      div[data-grid='4'] {
        grid-area: 4 / 1 / 6 / 4;
      }

 
여기 까지 오셨다면, 브라우저 화면에는 다음처럼 보일지도 모릅니다. 하지만, 이렇게 두면 구분하기가 어려우니 여러분의 입맛에 맡게 구분하기 쉽도록 꾸며보도록 합시다
 

 

저 같은 경우에는 다음과 같이  글자 크기가 커지고 명확한 선이 생겼습니다. 아래 부분은 이미지가 잘린게 아니라 제가 만든 박스가 실제 뷰 포트를 벗어나서 그렇습니다.  어떤 모양이 되던지 상관 없으니 여러분의 창의성을 최대한 발휘해보세요.

 
 

자바스크립트로 마크업에 생명을 불어넣어 줍시다.

이제 본격적으로 GSAP Flip  이라는 친구를 활용해보겠습니다.
 

 Flip 플로그인을 등록해줍시다.

우선 GSAP 의 Flip 플로그인을 등록해주어야 합니다. 아래처럼 DOMContentLoaded 이벤트를 사용하면, 콜백으로 등록된 함수를 브라우저에서 DOM 콘텐츠가 렌더링되면 즉시 내부 코드가 실행되도록 해줍니다. 이 부분은 다들 아실 것이라 생각하지만, 만일 script 태그 내에 defer 나 async 과 같은 비동기적 다운로드를 처리하는 속성을 포함시키셨다면 굳이 아래 이벤트를 사용하지 않고 바로 gsap 에 플로그인 등록 처리를 해도 무방합니다.

    document.addEventListener("DOMContentLoaded", (event) => {
        gsap.registerPlugin(Flip);
        
      });

 

 parent 클래스의 자식들을 데려 옵시다.

원래 gsap 가 없는 경우에는 div1, div2, div3,..이 친구들을 가져오려면, document 객체의 querySelectorAll 메서드를 사용해야 했습니다. 하지만 gsap 에서는 이를 쉽게 가져올 수 있는 유틸함수를 제공해줍니다.
 
다음과 같이 입력하셔서 div 친구들을 데려 옵시다. 아래와 같이 입력하게 되면 .parent 클래스의 자식 요소인 모든 div를 가져와서 배열로 만든 후 boxes 변수에 할당해줍니다.
 

      document.addEventListener("DOMContentLoaded", (event) => {
        gsap.registerPlugin(Flip);

        const boxes = gsap.utils.toArray('.parent div')
        
      });

 

데려온 친구들이 담긴 boxes 를 Flip 에 등록해 줍시다.

그 다음 해야할 것은 앞서 생성한 div 배열을 Flip 플로그인의 getState 를 사용하여 넘겨주어야 합니다.  이렇게 되면 state 라는 변수에는 getState 메소드를 통해 반환된 boxes 배열를 구성하는 각 요소에 대한 상태정보들이 담겨 있게 됩니다.

      document.addEventListener("DOMContentLoaded", (event) => {
        gsap.registerPlugin(Flip);

        const boxes = gsap.utils.toArray('.parent div')

        const state = Flip.getState(boxes)
        
      });

 

만일 어떤 정보가 들어있는지 궁금하시다면 직접 state 을 콘솔로 출력해보면 됩니다. 출력해보시면, 앞서 배열로 만든 요소들의 상태 정보가 들어있는 것을 볼 수 있습니다. 건드릴 요소는 없으니까 참고만 해두도록 합시다.
 

 

parent 클래스가 부여한 부모 요소에 이벤트 리스너를 등록해줍시다.

그 다음 해야할 것은 앞서 div 요소들을 감싸고 있는 parent 요소에 이벤트를 등록해야 합니다. 사실 앞서 생성했던 boxes 배열을 forEach 등으로 순회하여 접근해도 되지만,  이벤트 위임의 이점을 누리려면 parent 요소에 이벤트 리스너를 등록하고, 버블링을 통해 자식 요소의 이벤트가 부모 요소가 처리하도록 위임하는 것이 좋습니다.

    document.addEventListener("DOMContentLoaded", (event) => {
        gsap.registerPlugin(Flip);

        const boxes = gsap.utils.toArray(".parent div");
        const firstBox = boxes[0];
        const state = Flip.getState(boxes);

        document.querySelector(".parent").addEventListener("click", (e) => {
          const div = e.target;
          cardFlip(div, firstBox);
        });
      });

 

눈이 좋으신 분은 바로 아셨겟지만, cardFlip 이라는 함수를 콜백으로 등록한 것을 볼 수 있습니다. 그리고 firstBox 변수와 div 변수를 넘겨주고 있는데, 이를 cardFlip 으로 전달하여 재사용할 것이므로 준비해 줍시다. 
 
준비가 끝나셨다면,  이제 이 함수를 구현해보러 가시죠.

cardFlip 함수 구현하기

함수 이름은 무엇으로 지을지 고민하다가 심플하게 cardFlip 으로 지어보았습니다. 암튼, 이 함수의 역할은 사용자가 카드를 클릭하면 클릭을 당한 카드의 data-* 속성의 값에 변동을 주어서 요소 간에 자리를 변경하는 듯한 애니메이션을 적용하는 기능을 수행합니다.
 

element.dataset 속성으로 data-grid 속성 읽어오기 및 이른 반환 처리

지금 바로 해야하는 작업은 현재 사용자가 선택한 요소가 Flip 의 기준이 되는 요소와 같은지 아닌지를 판단하여 같다면 함수를 탈출하고, 같지 않다면 다음 작업을 수행하는 코드를 작성하는 겁니다.
 
이렇게 코드를 작성하는 이유는 자기 자신을 클릭해봤자 애니메이션이 동작하더라도 아무런 변화가 없으니 굳이 애니메이션을 적용할 필요가 없기 때문입니다. 그러므로 다음과 같이 조건문을 작성하여 이른 반환 처리를 해줍니다.

     function cardFlip(div, firstDiv) {
        if (div.getAttribute("data-grid") === "1") return;
      }

 

firstDiv 요소의 data-grid 에 div 의 data-grid 속성의 값으로 덮어줍시다.

그 다음 작업은 매개변수로 전달받은 firstDiv 요소의 data-grid 속성을 타겟 요소의 data-grid 값으로 대체하는 것입니다. 그 후 타겟 요소인 div 의 dataset.grid 에는 firstDiv 를 카피한 요소의 dataset.grid 값을 할당해주면 됩니다.

      function cardFlip(div, firstDiv) {
        if (firstDiv === div) return; // 선택한 요소가 자기자신이면 애니메이션을 실행하지 않습니다.

        let copy =  firstBox.dataset.grid // firstDiv의 dataset 속성을 복사합니다.
        firstDiv.dataset.grid = div.dataset.grid // firstDiv 의 dataset.grid 에 div의 dataset 속성을 할당합니다.
        div.dataset.grid = copy // 마지막으로  카피한 값을 div 요소의 dataset.grid 에 할당합니다.
      }

 
위 로직의 목적은 사용자가 클릭한 요소와 firstDiv 에 있었던 기존 요소의 위치를 서로 바꿔주기 위해서입니다. 이것이 가능한 이유는 display 속성을 grid 로 지정하고, 각 그리드 셀의 영역을 data-grid의 속성값과 일치하는 경우에 스타일로 적용해두었기 때문입니다.

쉽게 말해서, data-grid 의 각 할당값과 일치하는 요소 마다 그리드에 배치를 달리 하였기 때문에, 이 값을 변경하게 되면, 그리드 위에서 해당 요소의 배치 위치도 달라지게 됩니다. 이러한 원리를 사용하여 그리드 셀 간에 위치를 변경시키는 것이 이번 애니메이션의 핵심입니다.

 

grid-area 는 그리드 라인을 기준으로 각 셀의 위치와 차지하는 공간을 정의할 수 있습니다.

 

마지막으로 Flip 효과를 적용해봅시다.

마지막으로 다시 이벤트 핸들러 내부로 돌아와서 Flip.from  메서드를 등록해야 합니다.

        document.querySelector(".parent").addEventListener("click", (e) => {

          const state = Flip.getState(boxes);
          const div = e.target;
          cardFlip(div, firstBox);

          Flip.from(state, {
            duration:2,
            ease:'power2.inOut',
            absolute:true
          })
        });

 

여기서 첫 번째 인자에는 앞서 Flip.getState 로 생성한 각 요소에 대한 상태 정보를 담은 state 변수를 입력합니다.
 
그 다음 두 번째 매개변수는 적용할 추가적인 옵션을 지정하는데, duration 은 애니메이션 실행시간으로 ms 단위가 아니라 s 단위로 입력합니다.
 
ease 는 애니메이션의 저항력을 설정하는 옵션으로 power2.inOut 을 적용하면 처음에는 천천히 마지막에는 빠르게 진행되는 속도로 애니메이션이 진행됩니다.
 
마지막으로 absolute 는 각 div 요소 간에 Flip 애니메이션이 적용되는 동안 position 을 absolute 로 두도록 설정합니다. 만일 설정하지 않는 경우에는 주변 레이아웃을 깨뜨리는 등의 문제가 발생할 수 있습니다.
 

그럼 구현한 결과를 체험해 봅시다. 마우스를 각 요소에 클릭해서 확인해보세요!

1
2
3
4

 
 






 

이건 기초 사용법에 불과합니다. 이 기능을 복잡하게 꼬아서 사용한다면, 이게 구현이 가능한거 였어 싶은 기능들을 많이 만들어 낼 수 있습니다. 이후 부터는 본인만의 컨셉에 따른 창의성에 달려 있는 것 같습니다.

반응형