GSAP Trick / TIL ~11.01 #1

leitmotif·2021년 11월 1일
0

Frontend 개인 공부

목록 보기
6/27
post-thumbnail

개요

어느새 11월이 됐습니다!

10월의 4, 5째주에 진행한 것은 아래와 같습니다.

1. 자기소개 페이지 만들기 with HTML, CSS, JavaScript
2. 개인 프로젝트 관리자 페이지 만들기 with React, Node.js

그 가운데 가장 유용하게 써먹은 것이 GSAP입니다.

머리 속에 그려진 그림을 어거지로 구현한다는 불편한 느낌이 있었는데

GSAP 덕분에 어느정도 해소된 것 같네요!

오늘 포스팅은 1번인 자기소개 페이지에서 써먹은(?) Loading Trick입니다.

Loading Trick

자기소개 페이지에 적용된 로딩 페이지입니다.
딱히... 서버에서 받아와야할 데이터는 없는 가짜이긴 하지만요 😂
눈 가리고 아웅!

웹 사이트에선 DB 응답이 늦거나하는 경우에 대비해

Loading.... 이라는 페이지를 먼저 렌더링한 뒤 응답이 완료되면 그 때 메인 페이지를 보여주곤 합니다.

HTML에는 이와 관련된 태그가 있습니다.

출처 : https://developer.mozilla.org/ko/docs/Web/HTML/Element/progress

예를 들면...

<progress value='0' max='100' id='progress_bar'></progress>
 // html

gsap.to('#progress_bar',{value:'100', duration:3})
 // JS

이렇게 하면 0부터 100까지 차오르는 animation을 구현가능합니다만

역동적이지도 않고, 개인적으로 너무 심심해 원으로 표현하기로 생각했습니다.


SVG Circle

SVG는 Scalable Vector Graphics, 확장 가능한 벡터 그래픽입니다.
어려운 해석은 제쳐둡시다. 쉽게 설명하면 능동적으로 변화가능한 이미지 형식입니다.

사각형을 표현하는 'rect', 타원인 'ellipse', 직선인 'line' 등등의 태그가 있습니다.

당연하게도, 여기에 쓰인 태그는 'circle' 입니다.


HTML


<div>
  <svg class='circle_progress' width='120' height='120' viewBox='0 0 120 120'>
      <circle class="frame" cx="60" cy="60" r="54" stroke-width="12" />
          <circle class="bar" cx="60" cy="60" r="54" stroke-width="12" />
          <strong class='value'>0%</strong>
  </svg>
</div>

생소한 태그들이 많이 섞여있지만

svg와 circle 태그를 나누어 보겠습니다.

<svg class='circle_progress' width='120' height='120' viewBox='0 0 120 120'>
	~
</svg>
	// SVG 도형은 <svg> 태그로 감싸야한다.
    	// 120의 너비, 120의 높이를 가진 svg 도형을 그릴 것이고
        // viewBox는 120*120의 svg 벡터 공간을 정의한다.
<circle class="frame" cx="60" cy="60" r="54" stroke-width="12" />
<circle class="bar" cx="60" cy="60" r="54" stroke-width="12" />
<strong class='value'>0%</strong>
	// 2개의 circle, %를 보여줄 강조 태그를 선언한다.
    	// cx : x 중심축, cy : y 중심축. viewBox가 120*120 이므로 60으로 설정한다.
        // r : 원의 반지름. 
        // stroke-width: 선의 굵기. 

특이 사항이랄 것은 없고, 반지름은 대충 하나하나 찍어보면서 마음에 드는 것으로 기입했습니다.

stroke-width는 테두리의 굵기라고 생각하면 됩니다. border-width처럼요.


CSS


SVG의 백미는 CSS 설정에 있습니다.

.circle_progress { transform: rotate(-90deg); margin:0; }
	/// 1
.frame, .bar { fill: none; }
	/// 2
.frame { stroke: #e6e6e6; } // 옅은 회색
	/// 3
.bar {
  stroke: #03c75a;			// 청록색
  stroke-linecap: round;
}
	/// 4

.value{
    position:absolute;
    left:0;
    right:0;
    bottom:0;
    top:37%;
    text-align: center;
    line-height: 120px;
}
	/// 5
  1. SVG에서 원은 +90도가 시작점입니다. 따라서 rotate(-90deg)로 12시를 맞추ㅝ줍니다.
  2. fill:none을 하면 테두리 제외 빈 원이 그려집니다.
  3. stroke는 선의 색상입니다. 여기선 테두리의 backgroundColor 역할입니다.
  4. stroke-linecap은 선의 양쪽 끝 모양을 의미합니다.
    a. butt은 끝에서 칼같이 잘라냅니다.
    b. round은 border-radius처럼 둥글게 마감합니다.
    c. square는 선 끝을 네모로 마감해줍니다.
  5. %를 보여줄 텍스트부분입니다. line-height를 통해 태그를 부모 태그에 꽉 채우고, 중앙 정렬 후 top을 적절히 부여하여 정중앙에 있는 듯이 보이도록 처리했습니다.

JavaScript


먼저 사전 작업부터 정리합니다.

1. 사전 작업
    a. GSAP Animation을 적용하기 위한 변수 선언 문단입니다.
    b. bar, value : animate가 실행될 element를 가져옵니다.
    c. RAD : 원의 반지름입니다.
    d. CIRCUMFERENCE : 원의 둘레입니다.
    e. strokeDasharray 
    	* svg에서 점선을 표시하는 속성.
        * stroke-dasharray='5,5' 라고 하면 5만큼의 길이로 반복될 점선 사이의 간격으로 5로 지정한다.
    f. strokeDashoffset
    	* svg에서 시작지점을 알립니다.

여기에서 주목해야될 부분은 frame circle에 대해서는 따로 속성을 지정하지 않은 것입니다.

즉, frame circle은 옅은 회색의 테두리 원이 되고

JS를 활용하여 청록색인 bar의 offset 값을 조정하여 채워지는 듯한 움직임을 구현합니다.

const CIRCUMFERENCE = 2* Math.PI * RAD;
bar.style.strokeDasharray = CIRCUMFERENCE;
bar.style.strokeDashoffset = 339.292;

제가 임의로 지정한 원의 넓이는 약 339.292006587...... 로 이어져나가니
대충 339.292로 잡습니다.
물론 Math.floor로 계산해도 되겠습니다.


두 번째, 상태 변화 체크입니다.

GSAP에는 TextPlugin 이라는 것이 있어서, 단순히 CSS 뿐만 아니라 innerHTML을 특정 조건에 따라 변형시킬 수도 있습니다.

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/TextPlugin.min.js"></script>
	// HTML CDN
    
var box = $("#redBox")
var tl = new TimelineLite()

tl.set(box, {text:"right"})
  .to(box, 2, {left:400})
  	// left:400을 통해 2초간 box element를 오른쪽으로 이동시킴.
    	// 오른쪽으로 이동되는 동안 innerHTML은 right로 표시된다.

위와 같은 방법이 있긴 하지만, 로딩 페이지는 결국 element의 상태에 따라 %가 달라지기에

임의로 n초 동안 어떠한 작업을 해라!와 같은 동작은 적합하지 않다고 생각해 찾아보다가

MutationObserver라는 것을 알게 되었습니다.

MutationObserver 는 개발자들에게 DOM 변경 감시를 제공합니다. DOM3 이벤트 기술 설명서에 정의된 Mutation Events 를 대체합니다.
참고 링크 : https://developer.mozilla.org/ko/docs/Web/API/MutationObserver

저는 bar의 strokeDashoffset 속성을 339.292에서 0으로 변경시키는 animate를 적용했습니다.

즉, style 요소의 변경이 발생하는 겁니다.

var observer = new MutationObserver(function(mutations){	// 1
    mutations.forEach(function(mutationRecord){			// 2
        value.innerHTML = Math.floor((Math.abs(Math.floor(bar.style.strokeDashoffset-340)/340))*100)+'%'
    								// 3
    })		
})


observer.observe(bar,{attributes:true,attributeFilter:['style']})	4
  1. MutationObserver를 선언합니다.
  2. forEach문을 통해 모든 요소 변경에 대한 콜백 함수를 작성합니다.
  3. 변경이 일어날 때마다 소수점 첫째자리를 반올림한 백분율을 표시합니다.
    a. 0~100%까지 변경됩니다.
  4. 앞서 선언해둔 bar element를 param으로 넣고, style 요소의 변경점에 대해 모니터링합니다.

후술할 내용에 있을 bar의 animation은 2초 동안 실행되며
style 요소 또한 2초 동안 mutation된다는 뜻이므로 실행 시점 / 종료 시점 등은 신경 쓸 이유가 없습니다.


세 번째, async / await 작업입니다.

const worker = async () =>{
	await gsap.to('.bar',{strokeDashoffset:0,duration:2})
        await gsap.to('.loadingDiv',{display:'none',autoAlpha:0,duration:1})
        await gsap.to('.rootDiv',{display:'block',autoAlpha:1, duration:1})
}

worker()

	// Loading Trick을 위한 Async / Await #3
  1. bar의 strokeDashoffset을 2초의 범위동안 0으로 줄입니다.
  2. loadingDiv는 로딩과 관련된 요소를 한 데 묶은 부모 div입니다.
    a. display 속성을 none으로 하여 다른 div의 position이 원활하게 되도록 합니다.
    b. autoAlpha는 gsap에서 사용하는 것으로, opacity와 같습니다. 서서히 사라집니다.
  3. rootDiv는 메인 컨텐츠를 담은 부모 div입니다.
    a. display:'none'에서 display:'block'으로 전환됩니다.
    b. autoAlpha:1을 통해 서서히 드러냅니다.

모든 JavaScript입니다.

const bar = document.querySelector('.bar')
const value = document.querySelector('.value')

const RAD = 54;

const CIRCUMFERENCE = 2* Math.PI * RAD;

bar.style.strokeDasharray = CIRCUMFERENCE;
bar.style.strokeDashoffset = 339.292;

	// 사전 작업 #1

// MutationObserver monitors attribute's status.

var observer = new MutationObserver(function(mutations){
    mutations.forEach(function(mutationRecord){
        value.innerHTML = Math.floor((Math.abs(Math.floor(bar.style.strokeDashoffset-340)/340))*100)+'%'
    })
})


observer.observe(bar,{attributes:true,attributeFilter:['style']})

	// DOM Element 상태 변화 체크 #2

const worker = async () =>{
	await gsap.to('.bar',{strokeDashoffset:0,duration:2})
        await gsap.to('.loadingDiv',{opacity:0,display:'none',autoAlpha:1,duration:1})
        await gsap.to('.rootDiv',{display:'block',autoAlpha:1, duration:1})
}

worker()

	// Loading Trick을 위한 Async / Await #3

마무리

위의 것을 구현하다가 문득 생각난 것이 있었습니다.

추후에 무거운 서버 응답 작업이 있는 경우, async / await와 MutationObserver를 활용해서

const work1 = () =>{
	get first data....
    	if observer find mutations, gsap.to(value,{progress:33%})
const work2 = () =>{
	get second data....
    	if observer find mutations, gsap.to(value,{progress:66%})
}
const work3 = () =>{
	get final data....
    	if observer find mutations, gsap.to(value,{progress:99.9%})
}

const totalWork = async () =>{
	await work1()
    	await work2()
        await work3()
}
totalWork()
	// 작동되지 않는 유사 코드입니다.

이런 식으로 Loading bar를 구성하면 될 것 같다는 생각이 들었습니다.

당장은 딱히 뭔가 있진 않지만요.

다음 포스트는 관리자 페이지입니다.

Frontend는 GSAP와 컴포넌트 분리를 활용해 불필요한 전체 재 렌더링을 막은 것이 있고

Backend는 MongoDB에서 ref를 활용한 외래키 구현이 주 내용입니다.

profile
제가 그린 것으로 하여금, 동기를 일으키는 개발자가 되고 싶습니다.

0개의 댓글