4. NavBar 애니메이션 추가 / 모바일 화면

박준혁·2023년 6월 5일
0

NavBar에서 적용되는 애니메이션을 추가하고, 지금까지 제작한 NavBar은 PC 용이기 때문에 모바일에서 적용가능한 NavBar도 제작한 과정에 대하여 정리해보고자 한다.

NavBar 애니메이션

NavBar에는 지금까지는 흰색으로만 글자들이 나타났지만, 애니메이션을 추가해 조금 더 동적인 모션이 나타나게 구현하고자 한다. 가장 먼저 특정 글자에 마우스를 올리면 밑줄이 그어지는 모션이 나타나게 하고, 현재 보고 있는 section에 대응되는 글자의 색을 검은색으로 변경시키려고 한다. 또한, 현재는 글자를 누르면 해당 section으로 바로 이동했는데 이 또한 부드럽게 이동할 수 있도록 구현하고자 한다.

마우스 Hover 효과

마우스 hover 효과는 가장 간단하게 hover: 로 시작하는 css를 추가하면 된다. css를 추가한 것은 아래와 같다. 기본 높이를 지정하고(3px), 너비(width)를 0으로 설정한다. 그러면 밑줄의 너비가 0으로 되어 나타나지 않지만, 마우스 hover시 width를 full로 바꾸어주면 밑줄이 생긴다는 것을 확인할 수 있다. 이때 transition 효과를 같이 부여해 자연스러운 밑줄이 형성되는 것을 확인할 수 있다. (밑줄 색상 역시 light로 글씨 색상과 맞추었다.)

<span 
	className={`h-[3px] inline-block bg-light
    absolute left-0 -bottom-0.5 group-hover:w-full
    transition-all ease-in-out duration-[300ms] w-0`}>
                &nbsp;
</span>

구현 결과

Section 위치에 따라 색상 변경

다음은 현재 화면에 나타난 sectioin에 따라서 활성화된 글자의 색상이 변경되는 효과를 구현해야 한다. 아마도 메인 페이지를 구성하는 과정에서 가장 헤맸던 부분이었던 것 같다. 크게 2가지 방법을 사용했다. 첫 번째 방법은 Intersection Observer API를 사용하는 방식과, 두 번째 방법은 기본 javascript를 이용해 구현하는 방식이다. 결과적으로는 두 번째 방법을 이용했지만 그 과정을 설명해보고자 한다.

Intersection Observer API

위 API는 자동으로 타겟 요소가 화면에 나타나는지 인지한 다음, 그에 따른 작동을 수행하는 방식으로 진행된다. 공식 문서는 아래 링크를 참고하면 된다.
Intersection Observer API Document

예시 코드는 아래와 같다. observer를 아래와 같은 방식으로 정의하고, target을 querySelector로 지정하고, observe 함수를 통해 관찰을 시작할 수 있다. 만약 감지가 되면 callback function을 실행한다. Option은 관찰하고자 하는 요소의 여러 인자들을 설정할 수 있는데, threshold는 관찰하고자 하는 요소의 어느 정도의 비율이 화면에 나타났을 때 감지할 것인지 등을 정할 수 있다.

let observer = new IntersectionObserver(callback, options);

let target = document.querySelector('#target');
observer.observe(target);

여기까지는 굉장히 좋았다. 나도 해당 내용을 참고해 감지가 시작되면 색상을 변경하고, 이전까지 감지되었던 항목의 색상을 다시 원래로 변경하면 될 것이라고 생각했다. 그러나 크게 2가지의 문제점이 발생한다는 것을 확인할 수 있었다.

  • 첫 번째 문제는 thresehold의 ratio 기준이다. thresehold에 지정한 숫자는 앞서 언급한 것처럼 비율을 의미한다. 그러나 비율의 의미가 target의 height 값 중 현재 화면에 나타나는 높이의 비율을 의미한다는 것이다. (사용자 화면 높이 무관)
    예를 들어 해당 target이 원래 800px인데 200px만 화면에 나타난다면 ratio는 0.25가 되는 것이다. 그러나 지금 제작하고자 하는 기능은 대략 화면 절반에 target이 나타났을 때 observe 될 수 있돌록 하는 것이다. 이를 해결하기 위해 생각한 방식이 여러 target마다 서로 ratio를 다르게 설정하는 것이었지만, 사용자의 화면 높이에 따라서 해당 ratio 값도 달라질 수 있으며 더 중요한 mobile 환경에서는 target들의 높이가 매우 커질 것이기 때문에 상대적으로 ratio가 줄어들어야 하는 등의 문제점이 생긴 것이다.

  • 두 번째 문제는 여러 target이 동시에 observe 될 수 있다는 것이다. 위에서 언급한 문제의 연장선에 있는 문제점으로, 현재 제작하는 메인 페이지의 요소들이 높이가 모두 동일하지는 않다. 그렇기 때문에 특정 비율 이상 나타나면 모두 observe하고 있다고 코드에서는 인지하기 때문에 active한 요소를 찾는데 어려움이 발생한다. 또한, contact 섹션은 내용이 매우 적어 높이가 낮은 반면 what 섹션은 높이가 매우 높다. 이러한 차이로 실제로 observe 되는 요소를 찾는 것이 문제가 되었다.

위 문제점들을 해결하기 위해 많은 방법들을 사용해보았지만, 결과적으로는 모두 실패하였다. 따라서 다른 방법을 고안해 아래와 같은 방식을 이용하여 구현해보고자 하였다.

javascript scroll event 이용

scroll event가 매우 자주 호출되기 때문에 위 기능을 이용해보고자 하였지만 여러 문제점이 발생해 직접 scroll event를 이용해 기능을 구현해보고자 한다. scroll이 되었을 때 실행하는 scrollFunction은 아래와 같다. 코드를 하나씩 설명해보도록 하자.
가장 먼저 스크롤 하면서 새로운 section이 나타났을 때 새로운 section으로 active된 항목이 이동하는 조건은 아래 2가지 중 하나를 만족하면 된다.

  • 새로운 section의 위치가 현재 사용자 화면 높이의 절반보다 높을 경우
  • section의 가장 아래 위치가 높이보다 낮거나 같을 때 (section이 화면 하단에 위치할 때)

이 작업을 위해 if문 내부에 위치 관계를 확인하는 코드를 작성하였다. 코드에서 일부 내용들이 의미하는 내용을 먼저 정리해보자.

window.pageYOffset //현재 관찰 중인 화면의 위치
target.getBoundingClientRect() //target의 현재 화면 상 위치
window.innerHeight //사용자 화면 높이

따라서 앞서 이야기한 두 가지 조건 중 하나라도 맞는 section에 대해서 색상을 dark로 바꾸는 작업을 진행하고, 동시에 여러개의 section이 dark로 남아있으면 안되기 때문에 dark로 바뀐 section이 존재할 때 flag=0을 설정해 하나만 적용될 수 있도록 하였다. 나머지 section은 light 속성을 유지하게 된다. (tailwind css를 활용해 className을 추가하고 제거하는 방식을 이용함)
마지막으로 throttle을 추가하였다. throttle은 일정 주기마다 함수가 실행되도록 하는 것으로, scroll이 상대적으로 많이 호출되는 event이기 때문에 특정 시간 주기를 부여하여 과도한 함수 호출을 방지하였다. 시각적으로 문제가 없는 시간을 조절하여 300ms로 설정하였다.
scrollFunction의 최종 형태는 아래와 같다.

const scrollFunction = throttle(()=>{
    let flag = 1;
    let arr = ['home','about','what','question','contact'];
    for(let i = 4; i >=0; i--){
        if((window.pageYOffset > window.pageYOffset + document.querySelector(`#${arr[i]}`).getBoundingClientRect().top - window.innerHeight/2 || 
        document.querySelector(`#${arr[i]}`).getBoundingClientRect().bottom <= window.innerHeight ) && flag)
        {
          //light 속성 제거 후 dark 속성 추가 (일부 class만 작성)
            document.querySelector(`.${arr[i]} `).classList.remove('text-light')
            document.querySelector(`.${arr[i]} > span`).classList.add('bg-dark')
            flag = 0;
        }
        else{
          //dark 속성 제거 후 light 속성 추가 (일부 class만 작성)
            document.querySelector(`.${arr[i]} `).classList.remove('text-dark')
            document.querySelector(`.${arr[i]} `).classList.add('text-light')
        }
    }
},300)

위에서 제작한 함수를 바탕으로 아래와 같이 useEffect를 작성하였다. Next.js 기반에서 위 함수와 같이 작성하는 방식의 가장 큰 문제점은 window라는 객체가 처음에 존재하지 않는다는 것이다. 흔히 'window is not defined' 에러를 마주치게 된다. Next.js가 처음에 서버에서 렌더링할 때는 window 객체가 없기 때문에 발생하는 문제이다. 따라서 client side에서 렌더링한 이후에 window를 호출한다면 이러한 문제를 해결할 수 있다. 이때 사용하는 것이 useEffect이다. useEffect는 client side rendering이 끝난 이후에 호출되기 때문에 문제를 해결할 수 있는 것이다.
useEffect 내부에는 먼저 초기 화면에서도 NavBar의 Home 글자의 색상이 dark로 바뀌어야 하기 때문에 해당 작업을 진행하기 위해 scrollFunction을 강제로 1회 실행하고, 이후 addEventListener를 통해 scroll할때 감지하여 함수를 실행시키게 하였다.

useEffect(()=>{
        scrollFunction();
        window.addEventListener('scroll',scrollFunction)
        return () => {
            window.removeEventListener('scroll',scrollFunction)
        }
    },[])

구현 결과


부드러운 Section 이동

NavBar의 항목을 눌렀을 때 Section간 부드러운 이동이 가능하게 하는 방식을 구현하였다. 이는 비교적 간단하게 구현할 수 있었다. 먼저 현재 NavBar에 위치한 Link 태그 내부에 onClick function을 새로 구현하면 된다.
아래와 같은 clickFunction을 추가할 예정이다. 먼저 클릭 했을 때 이동하는 default event를 제거하고, 목적지(y 좌표)를 구한 뒤, behavior:"smooth"의 scrollTo 함수를 이용해 부드러운 이동이 가능하게 하였다.

const clickFunction = (e) => {
    e.preventDefault();
    let y_var = document.querySelector(e.target.hash).getBoundingClientRect().y;
    window.scrollTo({top:window.scrollY +y_var,behavior:"smooth"})
}

PC / Mobile NavBar

이번에는 환경에 따른 NavBar를 다르게 나타내기 위한 작업을 진행하였다. 우선 사용자의 화면 너비에 따라서 NavBar의 종류를 변화시키고, PC NavBar는 지금까지 구현한 NavBar를, Mobile NavBar는 좌측 상단에 버튼을 고정시킨 뒤, 해당 버튼을 누르면 NavBar 내용이 나오도록 제작하였다.

Mobile NavBar

모바일 전용 NavBar를 따로 제작할 계획이다. 기존에 만들어 둔 NavBar 양식을 NavBarForPC로 변경하고, 새롭게 NavBarForMobile을 만들었다.
가장 중요하게 menu가 활성화 되어있는지 아닌지 확인하기 위한 menuOn을 useState로 만들었다. 해당 값은 menu 모양으로 생긴 버튼을 누르면 상태가 반전되고, true 일때 높이가 설정되고, 화면에 나타나도록 설정하였다. false인 상태에는 높이를 0으로 설정하고, display:none을 통해 화면에서 제거하는 방식이다. 또한 transition을 설정하여 menu가 나타나고 사라지는 것을 자연스럽게 하였다. 코드는 아래와 같다. (CustomMobileLink는 기존 CustomLink와 동일하고 css 성질만 다른 것이다.)

const NavBarForMobile = ({className = ""}) => {
    const [menuOn, setMenuOn] = useState(true);
    const MobileLinkClass = ""
    return (
        <header> 
                <button onClick={()=>{setMenuOn(!menuOn)}}>
                    <MenuImage />
                </button>
                <div className={`${menuOn ? ' opacity-0 h-[0px]' : ' opacity-100 h-auto'}`} >
                    <nav className={`${menuOn ? ' hidden' : ''}`}>
                        <CustomMobileLink href="/#home" title="Home" className="Mhome" />
                        <CustomMobileLink href="/#about" title="About Us" className="Mabout"/>
                        <CustomMobileLink href="/#what" title="What We Do" className="Mwhat"/>
                        <CustomMobileLink href="/#question" title="Q&A" className="Mquestion"/>
                        <CustomMobileLink href="/#contact" title="Contact" className="Mcontact"/>
                    </nav>
                </div>
        </header>

    )
}

이후 PC 와 Mobile NavBar를 동시에 return 시킨다. 아래 과정에서 너비에 따라서 선택적으로 NavBar를 나타낼 것이다.

return (
        <>
            <NavBarForPC className="NavBarPC"/>
            <NavBarForMobile className="NavBarMobile"/>
        </>
    );

CSS Width

앞서서 Mobile NavBar를 구현한 뒤에 PC NavBar와 같이 나타나게 했기 때문에 화면 너비에 맞추어 하나씩만 활성화 시켜야 한다. 따라서 CSS 코드를 아래와 같이 작성했다.
767px 기준으로 최대 너비가 767px일때는 Mobile 활성화, 최소 너비가 768px일때는 PC 활성화 하는 코드이다.

/* global.css */
@media (max-width : 767px) {
    .NavBarPC{
        display : none;
    }
}
@media (min-width : 768px) {
    .NavBarMobile{
        display: none;
    }
}

구현 결과

모바일 환경에서 아래와 같이 나타남을 확인할 수 있었다.

0개의 댓글