본문 바로가기

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

바닐라 JS/HTML/CSS 로 재미있는 애니메이션 구현하기(0탄)

반응형

광원 마우스호버란?

사실 그냥 마우스호버 애니메이션이라 하면 멋 없어보이니까 광원이라는 단어를 붙여서 만들어 보았습니다. hover 시 특정 애니메이션(빛의 퍼짐을 표현한 애니메이션)이 마우스 포인터를 따라서 움직이는 아주 간단한 애니메이션 입니다. 다만 해당 마우스 포인터가 활성화되는 지점이 카드 내부라는 점에서 일반적인 포인터를 이용한 애니메이션과 차이가 있습니다.

 

실제로 구현 해보시면, 요소의 중앙으로 갈수록 빛이 줄어들었다가 외곽으로 가면 커지는 것을 볼 수 있습니다. 

 

해당 애니메이션의 원조?는 유튜브에 보면 재밌는 효과들을 구현하시는 외국 개발자분의 유튜브를 참고해서 만들어졌습니다. 흥미롭고, 고난이도의 효과들도 많으니 유튜브 많이 애용하시면 좋을 것 같아요. 그럼 시작하겠습니다.

 

필요한 준비물

해당 애니메이션을 구현하기 위해서 필요한 준비물은 HTML, CSS, JS 를 실행할 수 있는 간편한 에디터만 있으면 됩니다. VSCODE 를 일일이 설치하여 사용하기 불편하시다면 코드팬(CodePen) 이라는 사이트를 이용하시면 HTML, CSS, JS 뿐만 아니라 다양한 언어를 한 페이지에서 쉽게 바꿔가며 사용할 수 있습니다.

 

완성된 HTML, CSS 미리보기

잘 보이시는지 모르겠지만, HTML, CSS 를 작성하시면 아래와 같은 형태와 색상 배치가 될겁니다. 

 

 

HTML 코드 구성

현재 표현하고자 하는 이미지는 위에서 언급했으므로, HTML 코드는 다음과 같은 형태를 따라주세요.

    <div id="cards">
        <div class="card"></div>
        <div class="card"></div>
        <div class="card"></div>
        <div class="card"></div>
        <div class="card"></div>
    </div>

 

CSS 구성

그 다음에는 CSS 인데요, 이 부분도 아래와 같이 작성해주시면 됩니다. 참고로 var(--bg-color) 부분은 CSS 에서 사용하는 변수 입니다. var 는 변수에 담겨 있는 값을 반환하는 함수이구요. --bg-color 는 사용자가 직접 정의한 사용자 정의 값을 담고 있는 변수입니다.

:root {
  --bg-color : #333;  
  

}

        body {
            background-color: var(--bg-color);
            height:100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            margin: 0;
            overflow:hidden;
            padding:0px;
        }

        #cards {
            display: flex;
            
            flex-wrap: wrap;
            gap: 8px;
            max-width: 1100px;
            width: calc(100%-20px);
        }

        .card {
            height: 260px;
            border: 1px solid rgba(255,255,255,0.1);
            background-color: rgba(255,255,255,0.02);
            border-radius: 10px;
            position: relative;
            cursor: pointer;
            width:360px;
        }

 

 

JS 코드 작성

querySelectorAll 을 사용해서 .card 목록 가져오기

본격적으로 자바스크립트 작업을 시작하겠습니다. 우선, 해당 애니메이션은 각 카드 요소 내부에서 별도의 위치를 기억 해두었다가 호버가 되는 순간 애니메이션이 동작하게 됩니다.

 

따라서 제일 먼저 .card 클래스가 부여된 모든 div 요소들을 querySelectorAll 을 사용하여 가져와야 합니다. 

        for(const card of document.querySelectorAll(".card")) {
          
        }

 

여기서,  querySelectorAll 을 통해서 가져온 요소들은 NodeList  객체에 할당되어 반환됩니다. NodeList 는 유사배열과 유사하게 동작하므로 일반적인 자바스크립트 배열과 같이 forEach 나 for ~ of 를 사용할 수 있습니다.

 

for ~ of 반복문으로 NodeList 순회 후 mousemove 이벤트 리스너 등록하

제가 준비한 예제에서는 해당 NodeList 를 별도의 변수에 할당하여 재사용하지 않고, 즉시 for ~ of 반복문으로 순회 후 각 카드 요소를 각 변수에 할당하여 이벤트 리스너에 즉시 등록합니다. 바로 아래 처럼요.

        for(const card of document.querySelectorAll(".card")) {
            card.addEventListener('mousemove', handleOnMouseMove)
        }

 

여기서 우리가 구현해야 하는 함수는 handleOnMouseMove 함수입니다. 해당 함수 내에서는 event 객체를 전달받아 해당 객체 내에 마우스 이벤트와 관련한 몇 몇 속성을 사용할 예정입니다.

 

getBoundingClientRect 함수를 사용하여 타겟 요소의  left, top 속성 읽어오기

이제 handleOnMouseMove 함수를 구현할 것인데요, 그 첫 번째로 이벤트 객체로 부터 currentTarget 는 읽어 옵니다. currentTarget 은 현재 이벤트가 등록된 요소를 가리키므로 (즉, .card 클래스 가 부여된 요소를 가리킴) 이를 이용해서 실제 .card 가 부여된 요소로 부터 getBoundingClientRect 함수를 호출 할 수 있습니다.

 

그리고 getBoundingClientRect 함수는 반환값으로 해당 요소(currentTarget)의 레이아웃 정보인 x, y, left, top, width, height 등의 정보를 반환하는데요. 여기서 우리가 사용할 값은 left 와 top 이 두가지 입니다. 따라서 아래와 같이 left 와 top 을 객체구조분해 할당을 통해 가져와 줍니다.

        const handleOnMouseMove = e => {
            const {currentTarget: target} = e;

            const rect= target.getBoundingClientRect();
            const {left, top} = rect
            
        }

 

잠깐 알아보고 가는 사전 지식 4 가지

left 와 top 속성을 구했다면, 이제는 e.clientX 와 e.clientY 를 사용해서 x 와 y 좌표 값을 구할 차레입니다.

 

다만, 몇 가지 사전 지식이 필요하므로 이에 대한 설명 부터 간략하게 하고 넘어 가겠습니다. 

 

left 와 top 이 뭘까요?

여기서 left 와 top 가 무엇인지 궁금하실 수 있습니다. 이를 위해 MDN 에 있는 이미지 자료를 가져와 봤습니다.

 

 

보시면 left 는 브라우저 전체 화면 의 좌측 끝 지점 기준(x=0) 으로 요소의 좌측 모서리 까지 길이를 의미하고,

 

top 의 경우에는 브라우저 전체 화면의 상단 측 지점 기준(y=0)으로 요소의 상단 모서리 까지의 길이를 의미합니다.

 

그럼, e.clientX 와 e.clientY 는 뭐죠?

그 다음에는 이벤트 객체의 clientX 와 clientY 에 대한 부분입니다. 여기서 이벤트 객체는 앞서 말했듯 Event 를 의미하는데, 해당 이벤트 객체는 이벤트 리스너(addEventListener)를 등록한 대상(const card)  을 기준으로 다양한 이벤트 호출과 관련한 정보들을 담고 있습니다.

 

그 중에서 clientX 와 clientY 는 뷰포트 넓이의 가로축(x 위치)과 높이를 나타내는 세로축(y 위치) 을 기준으로 마우스가 이동한 위치를 관찰하는데, 이 때 추적한 위치(좌표)를 저장하고 있는 친구들입니다.

 

글로는 설명이 참 애매하고 길어질 것 같아서 MDN 공식문서에서 설명하고 있는 예제를 링크로 첨부해보니 얼마 걸리지 않으니 꼭 보시길 권장합니다. 

https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX#result

 

MouseEvent: clientX property - Web APIs | MDN

The clientX read-only property of the MouseEvent interface provides the horizontal coordinate within the application's viewport at which the event occurred (as opposed to the coordinate within the page).

developer.mozilla.org

 

 

앞서 4 가지 속성을 사용하여 각 card 내 에서 마우스가 이동하는  좌표값(x, y) 구하기

앞서 살펴본 속성들을 사용해서 card 내 에서 mousemove 이벤트가 호출될 당시의 좌표값인 x 와 y 를 구해야 합니다. 

 

구하는 공식은 x = e.clientX - left , y = e.clientY - top 인데요. 여기서 left 와 top 을 각각 빼주는 이유는 현재 마우스 이벤트가 호출되는 좌표의 위치를 추적 할 때 clientX 와 clientY 는 해당 card 요소가 아닌 전체 브라우저의 넓이와 높이를 기준으로 위치를 반영하기 때문입니다. 

 

즉, left 와 top 의 경우에는 각각 좌측 모서리와 상단 모서리 사이의 여백을 나타내기 때문에 전체 뷰포트에서 각각의 여백을 빼주는 작업을 수행하는 것이죠. 

 

요약하면, clientX 와 Y 는 뷰포트 기준의 좌표축(0,0)을 계산하므로,  card 요소 내에서 좌표축을 (0,0) 으로 계산할 수 있도록 요소와 뷰포트 끝지점 사이의 여백에 해당하는 left 와 top 을 각 clientX, Y 값에서 빼주는 겁니다.

 

원래 쉬운 것도 글로만 보면 정리가 안 되는 법이죠. 그래서 위 설명을 반영한 이미지를 만들어 보았습니다. 

 

 

결국 좌표축의 기준을 요소 내부로 설정하는 단순한 원리를 설명한 것인데, 글이 엄청 길어졌네요.

 

아무튼 이러한 원리를 기반으로 구현하면 다음과 같이 x 와 y 좌표를 구할 수 있습니다. 즉, 이 값들은 card 요소 기준으로 (0, 0)  좌표를 가지고 시작됩니다.

        const handleOnMouseMove = e => {
            const {currentTarget: target} = e;

            const rect= target.getBoundingClientRect();
            const {left, top} = rect
            const x = e.clientX - left
            const y = e.clientY - top
            
       }

 

setProperty 함수를 사용해서 CSS 변수 등록하기

그 다음에 추가적으로 해주어야 하는 것은, 카드 내에서 애니메이션 포인터가 따라다닐 수 있도록 해야겠죠. 따라서 각 x, y 좌표 값을 setProperty를 사용해서 CSS 변수인 --mouse-x 와 --mouse-y 에 담아줄겁니다. 이렇게 되면, CSS 내에서 --mouse-x 와 --mouse-y 라는 변수에 담긴 값을 var(--mouse-x) 와 같이 호출하여 사용할 수 있게 됩니다.

 

즉, --mouse-y 에 할당된 y 값이 50px 이라면, css 내에서 left : var(--mouse-y); 로 할당 시 left : 50px 로 남게 된다는 소리입니다. 이를 원리로 다음과 같이 설정해주면 모든 준비는 끝났습니다.

 

        const handleOnMouseMove = e => {
            const {currentTarget: target} = e;

            const rect= target.getBoundingClientRect();
            const {left, top} = rect
            const x = e.clientX - left
            const y = e.clientY - top
            
            target.style.setProperty("--mouse-x",`${x}px`);
            target.style.setProperty("--mouse-y",`${y}px`);
        }

        for(const card of document.querySelectorAll(".card")) {
            card.addEventListener('mousemove', handleOnMouseMove)
        }

 

 

마지막 CSS 변수를 사용하여 .card 내에 background 셋팅하기

마지막으로 CSS 변수를 사용해서 background 속성에 적용하기만 하면 됩니다.

 

단, 광원 이라는 닉값을 하기 위해서는 실제 빛이 주위로 퍼져나가는 듯한 느낌을 주어야 하는데요, 이를 위해서 background 속성에서 사용 가능한 radial-gradient 함수를 사용할 것입니다.

 

사실 이 함수에 대한 설명만 한다고 해도 하나의 포스트 분량을 뚝딱 채울 수 있기 때문에, 아쉽지만 모두 설명하지는 않을 겁니다. 일단 이렇게 사용할 수 있다고만 참고하시고 별도로 공부해보시길 권장합니다.

 


 

다시 앞서 CSS 파트에서 작성했던 코드로 돌아옵니다. 여기서 .card 요소의 위로 움직이는 광원을 만들 것이기 때문에, .card::before { } 을 사용하여 덧 씌울 겁니다. 따라서 position 은 absolute 로 지정하고, 부모 영역 전체에 가득 차도록 left:0, top:0 을 설정 후 width 와 height 를 100% 으로 지정 합니다.

 

마지막으로 하이라이트 역할을 하는 background 를 설정합니다. 여기서 사용하는 함수는 radial-gradient 이고, 앞서 JS 파트에서 설정한 --mouse-x 와 y 변수를 var() 함수를 사용해서 읽어오기만 하면 됩니다. 이렇게 되면 JS 에서  mousemove 이벤트 호출 때 마다  setProperty 로 매번 설정된 값이 업데이트 되면서 .card 요소의 내에서만 광원이 생성되어 마우스 포인터를 따라오게 됩니다.

        .card::before {
           content:"";
           position:absolute;
           left:0;
           top:0;
           width:100%;
           height:100%;
           background: radial-gradient(circle at var(--mouse-x) var(--mouse-y),  rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.2) 5%, transparent 40%);
        
        }

 

 

구현 결과

지금 구현된 결과에서는 호버 애니메이션이 실행된 후 마우스 포인트가 요소를 벗어나도 사라지지 않고 잔상이 남아 있습니다. 이 문제를 어떻게 해결할 수 있을까요? 한 번 도전해 보세요!

 

크기는 0.5 배 사이즈로 보시면 잘 보입니다.

 

 

 

참고자료

mdn 공식문서 전반 https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/radial-gradient

반응형