Lumpy Space Princess - Adventure Time

JAVASCRIPT

Parallax Effect 01 - 메뉴 효과에 대해서 알아보자

jongyung 2023. 4. 18. 21:58

“ 지연되는 프로젝트에 인력을 더 투입하면 오히려 더 늦어진다. ”

- Frederick Philips Brooks
Mythical Man-Month 저자
728x90

설명:

Parallax effect(피럴랙스 효과)는 물체의 움직임에 따라 시차가 발생하여, 같은 물체를 보는 관찰자들이 서로 다른 시각적인 경험을 하는 현상을 말합니다. 

쉽게 말해, 우리 눈 앞에 있는 두 개의 물체가 서로 다른 거리에 위치해 있을 때, 더 가까이 있는 물체는 더 빨리 움직이는 것처럼 보이고, 더 멀리 있는 물체는 더 느리게 움직이는 것처럼 보입니다.

이 효과를 적용해보겠습니다.

 

body 의 main 과 좌측 하단에 있는 aside 그리고 script 로 나눠서 설명하겠습니다.

body 의 main 

<main id="main">
    <div id="parallax__wrap">
        <section id="section1" class="parallax__item">
            <span class="parallax__item__num">01</span>
            <h2 class="parallax__item__title">Section1</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc">결과도 중요하지만, 과정을 더 중요하게 생각한다.</p>
        </section>
        <!-- section1 -->

        <section id="section2" class="parallax__item">
            <span class="parallax__item__num">02</span>
            <h2 class="parallax__item__title">Section2</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc">산을 움직이려 하는 이는 작은 돌을 들어내는 일로 시작한다.</p>
        </section>
        <!-- section2 -->

        <section id="section3" class="parallax__item">
            <span class="parallax__item__num">03</span>
            <h2 class="parallax__item__title">Section3</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc">높은 목표를 세우고 스스로 채찍질을 한다.</p>
        </section>
        <!-- section3 -->

        <section id="section4" class="parallax__item">
            <span class="parallax__item__num">04</span>
            <h2 class="parallax__item__title">Section4</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc">꿈이 있다면, 그 꿈을 잡고 절대 놓아 주지마라.</p>
        </section>
        <!-- section4 -->

        <section id="section5" class="parallax__item">
            <span class="parallax__item__num">05</span>
            <h2 class="parallax__item__title">Section5</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc">나는 내가 더 노력할 수록 운이 더 좋아진다는 걸 발견했다.</p>
        </section>
        <!-- section5 -->

        <section id="section6" class="parallax__item">
            <span class="parallax__item__num">06</span>
            <h2 class="parallax__item__title">Section6</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc">조그만 성공에 만족하지 않으며, 방심을 경계한다.</p>
        </section>
        <!-- section6 -->

        <section id="section7" class="parallax__item">
            <span class="parallax__item__num">07</span>
            <h2 class="parallax__item__title">Section7</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc">천 마디 말보단 하나의 행동이 더 값지다.</p>
        </section>
        <!-- section7 -->

        <section id="section8" class="parallax__item">
            <span class="parallax__item__num">08</span>
            <h2 class="parallax__item__title">Section8</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc">열정을 잃지 않고 실패에서 실패로 걸어가는 것이 성공이다.</p>
        </section>
        <!-- section8 -->

        <section id="section9" class="parallax__item">
            <span class="parallax__item__num">09</span>
            <h2 class="parallax__item__title">Section9</h2>
            <figure class="parallax__item__imgWrap">
                <div class="parallax__item__img"></div>
            </figure>
            <p class="parallax__item__desc">매 순간에 최선을 다하고, 끊임없이 변화한다.</p>
        </section>
        <!-- section9 -->

    </div>
</main>

명언이 적힌 부분입니다.

각각을 section 으로 설정해서 영역을 나눴습니다.

 

class 를 설정해서 css 로 원하는 디자인을 적용했습니다.

body 의 aside

<aside class="parallax__info">
    <div class="scroll">scrollTop : <span>0</span>px</div>
    <div class="info">
        <ul>                
            <li>#section1 offset() : <span class="offset1">0</span>px</li>
            <li>#section2 offset() : <span class="offset2">0</span>px</li>
            <li>#section3 offset() : <span class="offset3">0</span>px</li>
            <li>#section4 offset() : <span class="offset4">0</span>px</li>
            <li>#section5 offset() : <span class="offset5">0</span>px</li>
            <li>#section6 offset() : <span class="offset6">0</span>px</li>
            <li>#section7 offset() : <span class="offset7">0</span>px</li>
            <li>#section8 offset() : <span class="offset8">0</span>px</li>
            <li>#section9 offset() : <span class="offset9">0</span>px</li>
        </ul>
    </div>
</aside>

aside 를 만들어서 영역별로 높이 값을 구하고, 그 값이 scroll 의 값과 일치하면 상단에 있는 메뉴가 활성화 되도록 하는 script 를 만들었습니다.

script 

 window.addEventListener("scroll", () => {
    let scrollTop = window.pageYOffset || window.scrollY || document.documentElement.scrollTop;

    document.querySelectorAll(".parallax__item").forEach((item, index) => {
        if(scrollTop > item.offsetTop){
            document.querySelectorAll(".parallax__nav li").forEach((li) => {
                li.classList.remove("active");
            });
            document.querySelector(".parallax__nav li:nth-child("+(index+1)+")").classList.add("active");
        }
    });

    document.querySelectorAll(".parallax__nav li a"). forEach(li => {
        li.addEventListener("click", (e) => {
            e.preventDefault();
            document.querySelector(li.getAttribute("href")).scrollIntoView({
                behavior: "smooth"
            })
        });
    });

// info
document.querySelector(".scroll span").innerHTML = parseInt(scrollTop);

let scroll 의 값 들 3 가지(window.pageYOffset || window.scrollY || document.documentElement.scrollTop) 중에서 한 가지를 적용해도 됩니다.

 

scroll 을 움직이면 변하는 값을 화면에 나오도록 하는 함수입니다.

 

parseInt()는 문자열을 정수로 변환하는 JavaScript 내장 함수입니다. 이 함수는 문자열 인수를 받아서 그 문자열이 나타내는 정수를 반환합니다.

parseInt() 함수는 다음과 같은 방식으로 작동합니다:
- 문자열의 앞부분에서부터 숫자를 읽어 정수를 만듭니다.
- 문자열에서 숫자 이외의 다른 문자를 만나면, 그 문자 이후의 문자는 무시됩니다.
- 첫 번째 인수로 문자열을 받아서, 두 번째 인수로는 선택적으로 진법을 지정할 수 있습니다. 기본값은 10진수입니다.

예를 들어, parseInt("123")은 123을 반환합니다. parseInt("101", 2)는 5를 반환합니다.
parseInt("0xf", 16)은 15를 반환합니다. 하지만 주의할 점은 parseInt() 함수는 첫 번째 인수가 문자열이 아닌 경우, 자동으로 문자열로 변환한 후에 처리합니다.
따라서 예기치 않은 결과를 초래할 수 있습니다.

예를 들어, parseInt(012)는 10진수에서 10을 반환합니다.
이를 방지하기 위해서는 parseInt() 함수를 사용할 때 항상 문자열을 인수로 전달해야 합니다.

 

다음은 section 으로 나눈 각 영역의 높이 값들을 구하는 함수입니다.

document.querySelector(".info .offset1").innerText = document.getElementById("section1").offsetTop;
document.querySelector(".info .offset2").innerText = document.getElementById("section2").offsetTop;
document.querySelector(".info .offset3").innerText = document.getElementById("section3").offsetTop;
document.querySelector(".info .offset4").innerText = document.getElementById("section4").offsetTop;
document.querySelector(".info .offset5").innerText = document.getElementById("section5").offsetTop;
document.querySelector(".info .offset6").innerText = document.getElementById("section6").offsetTop;
document.querySelector(".info .offset7").innerText = document.getElementById("section7").offsetTop;
document.querySelector(".info .offset8").innerText = document.getElementById("section8").offsetTop;
document.querySelector(".info .offset9").innerText = document.getElementById("section9").offsetTop;

이렇게 하면 구할 수 있지만, 이 값들을 for 문으로 간략하게 쓸 수 있습니다.

🧋for()

for(let i=1; i<=9; i++){
    document.querySelector(".info .offset"+i).innerText = document.getElementById("section"+i).offsetTop;
}

이 식을 또 다르게 표현할 수도 있습니다.

🧃forEach()

Array.from({length: 9}, (_, i) => i + 1).forEach(i => {
document.querySelector(`.info .offset${i}`).innerText = document.getElementById(`section${i}`).offsetTop;
});

forEach 메소드를 사용하여 for 루프를 대체할 수 있습니다.


이 코드는 Array.from을 사용하여 길이가 9이고 각 요소가 1부터 9까지의 정수인 배열을 생성합니다. 

그리고 배열의 각 요소를 처리하기 위해 forEach를 사용합니다. 

이 때, querySelector와 getElementById의 인자로 전달하는 문자열에서 템플릿 리터럴을 사용하여 ${i} 부분을 변수로 대체합니다.

Array.from()은 유사 배열 혹은 이터러블 객체를 배열로 변환하는 JavaScript 내장 함수입니다.

이 함수는 배열의 형태를 띄는 객체(유사 배열) 또는 이터러블 객체(예: 문자열, Set, Map, NodeList 등)를 받아서 새로운 배열을 반환합니다.
Array.from() 함수는 다음과 같은 방식으로 작동합니다:
- 첫 번째 인수로 변환하고자 하는 유사 배열 또는 이터러블 객체를 받습니다.
- 두 번째 인수로 선택적으로 콜백 함수를 지정할 수 있습니다. 이 콜백 함수는 각 요소에 대해 호출되며, 요소의 값을 변환할 수 있습니다.
- 마지막으로, 새로운 배열을 반환합니다.
querySelector()는 CSS 선택자를 사용하여 DOM에서 요소를 선택하는 JavaScript 내장 함수입니다.
이 함수는 문서에서 첫 번째로 일치하는 요소를 반환합니다.

querySelector() 함수는 다음과 같은 방식으로 작동합니다:
- 첫 번째 인수로 CSS 선택자 문자열을 받습니다.
- CSS 선택자에 해당하는 첫 번째 요소를 찾아서 반환합니다. 일치하는 요소가 없으면 null을 반환합니다.

querySelector() 함수는 CSS 선택자를 사용하기 때문에, CSS 선택자를 사용하는 방법에 익숙하다면 쉽게 요소를 선택할 수 있습니다.
이 함수를 사용하면 특정 요소에 대한 참조를 쉽게 얻을 수 있기 때문에, 이 요소에 대한 조작 및 이벤트 처리 등을 수행할 수 있습니다.
getElementById()는 문서에서 특정 ID를 가진 요소를 선택하는 JavaScript 내장 함수입니다.
이 함수는 ID를 기준으로 문서에서 첫 번째로 일치하는 요소를 반환합니다.

etElementById() 함수는 다음과 같은 방식으로 작동합니다:
- 첫 번째 인수로 요소의 ID를 문자열로 받습니다.
- ID가 일치하는 첫 번째 요소를 찾아서 반환합니다. 일치하는 요소가 없으면 null을 반환합니다.

getElementById() 함수는 ID를 사용하여 요소를 선택하기 때문에, ID가 고유한 값이기 때문에 페이지에서 하나의 요소만 선택할 수 있습니다. 따라서 getElementById() 함수는 다른 선택자를 사용하는 함수보다 더 빠르게 동작합니다.

getElementById() 함수를 사용하면 특정 요소에 대한 참조를 쉽게 얻을 수 있기 때문에, 이 요소에 대한 조작 및 이벤트 처리 등을 수행할 수 있습니다.

🍨for in

for (let i in Array.from({length: 9}, (_, i) => i + 1)) {
    document.querySelector(`.info .offset${parseInt(i)+1}`).innerText = document.getElementById(`section${parseInt(i)+1}`).offsetTop;
}

Array.from과 forEach를 사용한 코드를 for...in으로 변환했습니다.


Array.from으로 생성한 배열의 인덱스를 for...in문에서 직접 사용할 수 있습니다. 

이 경우에는 인덱스가 문자열 형태이므로, parseInt를 사용하여 숫자로 변환합니다. 

또한, offset 클래스의 숫자는 1부터 시작하지만, for...in문은 0부터 시작하므로, 두 번째 줄에서 parseInt(i)+1로 조정해주어야 합니다.

🍵for of

for (let i of Array.from({length: 9}, (_, i) => i + 1)) {
    document.querySelector(`.info .offset${i}`).innerText = document.getElementById(`section${i}`).offsetTop;
}

for...in문은 객체의 속성을 반복할 때 사용되며, for...of문은 이터러블(iterable)한 객체를 반복할 때 사용됩니다. 

이 경우에는 Array.from({length: 9}, (_, i) => i + 1)를 이터러블 객체로 사용하여 for...of문을 사용할 수 있습니다. 

Iterable은 ES6(ECMAScript 2015)에서 도입된 개념으로, 반복 가능한 객체를 의미합니다.
이터러블 객체는 for...of 루프를 사용하여 요소를 반복할 수 있습니다.

이터러블 객체는 Symbol.iterator 속성을 가지고 있으며, 해당 속성은 이터레이터(Iterator)를 반환하는 메소드입니다.
이터레이터는 next() 메소드를 가지고 있으며, 이 메소드를 호출하면 이터러블 객체에서 다음 요소를 반환합니다. next() 메소드가 호출될 때마다 이터러블 객체의 내부 상태가 변경됩니다.

ES6에서는 다음과 같은 객체가 이터러블 객체입니다.
Array, Map, Set, String, TypedArray (Int8Array, Uint8Array, 등등), arguments 객체, NodeList, DOM 컬렉션

이터러블 객체를 만들기 위해서는 Symbol.iterator 속성을 구현해야 합니다.

이 메소드는 이터레이터를 반환하는 메소드이며, 반환되는 이터레이터는 next() 메소드를 가지고 있어야 합니다.
next() 메소드는 done과 value를 속성으로 가진 객체를 반환해야 합니다.
done은 이터러블 객체의 요소를 모두 반환했는지를 나타내며, value는 다음 요소의 값입니다.


위 코드에서 Array.from으로 생성한 배열을 for...of문의 반복 대상으로 지정하고, 반복될 때마다 i 변수에 배열의 요소값이 할당됩니다. 

 

이후, querySelector와 getElementById의 인자로 전달하는 문자열에서 템플릿 리터럴을 사용하여 ${i} 부분을 변수로 대체합니다.