인프런 커뮤니티 질문&답변

김민식님의 프로필 이미지
김민식

작성한 질문수

웹 애니메이션을 위한 GSAP 가이드 Part.03

OneScroll Layout (1)

OneScroll Layout (1) 강의중 질문 드립니다.

해결된 질문

작성

·

98

·

수정됨

1

안녕하세요, 쉬운 강의 덕분에 gsap 을 쉽게 접하고 사용할 수 있어서 감사합니다.
근데 해당 부분 구현중에 범샘께서 작성하신 코드(ScrollTrigger-finished)에서 확인해보면

1) section3 도달 후, section2로 스크롤을 올리는 경우
2) section4 도달 후, section3에서 다시 section4 로 스크롤을 내리는 경우
해당 2가지 경우에서 section03을 기준으로 스크롤이 위아래로 작동을 잘 하지 않습니다.

if(currentPageIndex === 3) return;
때문인 것 같은데, 스크롤을 자유롭게 올렸다 내렸다 하면서 페이지를 확인할 수 있도록 하려고 하는데 잘 안되네요.
코드를 어떤 방식으로 수정해야할지 질문 남깁니다.

감사합니다.

답변 1

1

범쌤 님의 프로필 이미지
범쌤
지식공유자

안녕하세요 김민식님 😀

 

질문주신 문제를 확인 했으며, 코드에 대한 설명은 아래와 같습니다.

 

  1. section3 도달 후, section2로 스크롤을 올리는 경우

원인 : section03 진입시 state.isPlaying값이 true로 변경되어 wheel 이벤트는 작동되지만
handleWheel 이벤트 안에서의 조건 처리 인 if(state.isPlaying) 부분에 조건이 적용되지 않아 바로 올라가지 않는 것 입니다.

 

해결방안 :

일반 스크롤을 조금이라도 내린 후 올리면 다시 isPlaying의 상태 변수가 true로 되기 때문에 제대로 동작할 수 있습니다.

우선, 언급해 주신 if(currentPageIndex === 3) return 조건을 else 안으로 넣어 애니메이션이 작동되도록 설정합니다.

 if(state.isPlaying){
    
    state.isPlaying = false;


    if(direction === 'up'){
      if(currentPageIndex <= 1) return;
      --currentPageIndex
      
  
    }else{
      if(currentPageIndex === 3) return;  // 해당 부분 입니다.
      if(currentPageIndex >= sections.length) return;
      ++currentPageIndex
    }
    transition(currentPageIndex,direction)
    
  }

그리고 다음 문제를 해결하기 위해선 isPlaying이 false인과 동시에 휠의 방향이 up인 조건이 필요하며,
handleWheel 안에 조건을 더 만들어 처리해줄 수 있습니다.

 

  if(!state.isPlaying && direction === 'up'){
    
    if(!state.isGoingUp){
      transition(2,'up')
      state.isGoingUp = true;
    }
    return;
  }

이후 해당 조건 발동시 아래의 코드가 읽히지 않도록 return을 넣어 주고(아래의 코드는 isPlaying이 true인 경우 애니메이션이 실행되는 조건며, transition함수는 실행 즉시 isPlaying을 true로 만들어 버립니다.) 애니메이션이 1회만 실행할 수 있도록(wheel 이벤트는 여러번 실행되기 때문에 1번만 실행될 수 있는 변수가 필요합니다.) isGoingUp변수를 만들고 제어해줍니다.

이어서 transition 함수 안의 scrollTrigger onComplete callback 부분에 section03 영역에 다시 진입시 isGoingUp 변수를 false로 만들어주어 다음번에 같은 섹션 재진입시 (section02 → section03) 동일한 애니메이션을 가져갈 수 있도록 처리해줍니다.

 

    onComplete:()=>{
      state.isPlaying = true;
      
      globalEnter()

      switch (index) {
        case 1: page01.enter(); return;
        case 2: page02.enter(); return;
        case 3: 
          page03.enter(); 
          state.isGoingUp = false;
          return
        ;
        case 4: page04.enter(); return;
      
      }
    }

 

  1. section4 도달 후, section3에서 다시 section4 로 스크롤을 내리는 경우

원인 : section04 도달 후 지속적인 down wheel을 할 경우 isPlaying의 상태가 false이기 때문에 더이상 애니메이션이 동작하지 않음.

 

해결방안 :

section04에 도착할 경우 휠을 계속 진행하여도 isPlaying의 상태를 true로 유지시켜 주어야 합니다.
그리고 section01에서도 위로 계속 휠을 할 경우 동일한 문제가 발생하기 때문에 handleWheel 함수 안에서 같이 조건처리를 진행합니다.


function handleWheel(e){

  let direction = e.deltaY < 0 ? 'up' : 'down'

  // 아래 코드의 2줄의 조건 처리 부분 입니다.
  if(direction === 'up' && currentPageIndex === 1) return; 
  if(direction === 'down' && currentPageIndex === sections.length) return;

  if(!state.isPlaying && !state.isScrolling && direction === 'up'){
    
    if(!state.isGoingUp){
      transition(2,'up')
      state.isGoingUp = true;
    }
    return;
  }

  if(state.isPlaying){
    
    state.isPlaying = false;


    if(direction === 'up'){
      if(currentPageIndex <= 1) return;
      --currentPageIndex
      
      
  
    }else{
      if(currentPageIndex === 3) return;
      if(currentPageIndex >= sections.length) return;
      ++currentPageIndex
    }
    transition(currentPageIndex,direction)
    
  }
}

위에서 return을 해주셔야 아래의 playing을 변경하는 코드까지 도달하지 않으므로 위에서 조건처리를 진행합니다.

 

이후 section03에서도 스크롤을 할 경우 자잘하게 section의 이동이 생기는 문제를 해결하기 위해 확실하게 스크롤 중인 상태냐 아니냐에 따라 추가 조건을 넣어 문제를 해결합니다.

 

전역 상태는 다음과 같습니다.

const state = {
  isPlaying: true,
  isScrolling: false,
  isGoingUp: false
}

 

 

scrollTrigger onUpdate callback 부분에 progress를 가져와 0인 상태에서만 isScrolling이 false가 될 수 있도록 (스크롤 제일 최상단 부분) 설정해줍니다.

  page03:{
    enter:()=>{
      // console.log('enter page03');

      if(!ScrollTrigger.getById('section03')){

        ScrollTrigger.create({
          trigger: '.depth_wrapper',
          start: 'top top',
          end: 'bottom bottom',
          markers: true,
          id:'section03',
          onLeaveBack:()=> {
            if(!state.isScrolling){
              transition(2,'up')
            }
          },
          onLeave:()=> transition(4,'down'),
          onUpdate:({progress}) => {
            // 해당 부분 입니다.
            // onUpdate 함수는 매개변수가 있기 때문에 progress만 구조분해로 가져옵니다.
            if(progress === 0) {
              state.isScrolling = false;
            }else{
              state.isScrolling = true;
            }
            
          }
        })
  
        markers()
      }
     

    },
    leave:()=>{
      // console.log('leave page03');
    }
  },

 

 

이렇게 3가지 옵션을 설정하면 보다 안정적으로 oneScroll 이벤트와 중간에 연결된 normal scroll의 레이아웃까지 mixin하여 사용할 수 있습니다.

 

완성된 코드는 다음과 같습니다.


const state = {
  isPlaying: true,
  isScrolling: false,
  isGoingUp: false
}


let currentPageIndex = 1;


const sections = gsap.utils.toArray('.section');


const pages = {
  page01:{
    enter:()=>{
      // console.log('enter page01');
    },
    leave:()=>{
      // console.log('leave page01');
    },
  },
  page02:{
    enter:()=>{
      // console.log('enter page02');
    },
    leave:()=>{
      // console.log('leave page02');
    }
  },
  page03:{
    enter:()=>{
      // console.log('enter page03');

      if(!ScrollTrigger.getById('section03')){

        ScrollTrigger.create({
          trigger: '.depth_wrapper',
          start: 'top top',
          end: 'bottom bottom',
          markers: true,
          id:'section03',
          onLeaveBack:()=> {
            if(!state.isScrolling){
              transition(2,'up')
            }
          },
          onLeave:()=> transition(4,'down'),
          onUpdate:({progress}) => {

            
            if(progress === 0) {
              state.isScrolling = false;
            }else{
              state.isScrolling = true;
            }
            
          }
        })
  
        markers()
      }
     

    },
    leave:()=>{
      // console.log('leave page03');
    }
  },
  page04:{
    enter:()=>{
      // console.log('enter page04');
    },
    leave:()=>{
      // console.log('leave page04');
    }
  },
}


function globalEnter(){
  // console.log('globalEnter');
  gsap.to('h2',{opacity:1,y:0})
  
}

function globalLeave(){
  // console.log('globalLeave');
  gsap.to('h2',{opacity:0,y:30})
}

function transition(index,dir){

  const {page01,page02,page03,page04} = pages;

  currentPageIndex = index;


  gsap.to('.wrapper',{
    y: -innerHeight * (index - 1),
    duration:1.5,
    ease:'expo.inOut',
    onStart:()=>{

      globalLeave()

      switch (dir === 'up' ? index + 1 : index - 1) {
        case 1: page01.leave(); return;
        case 2: page02.leave(); return;
        case 3: page03.leave(); return;
        case 4: 
          page04.leave(); 
          state.isPlaying = false;
          return;
      }
    },
    onComplete:()=>{
      state.isPlaying = true;
      
      globalEnter()

      switch (index) {
        case 1: page01.enter(); return;
        case 2: page02.enter(); return;
        case 3: 
          page03.enter(); 
          state.isPlaying = false;
          state.isGoingUp = false;
          return
        ;
        case 4: page04.enter(); return;
      
      }
    }
  })
}


function handleWheel(e){

  let direction = e.deltaY < 0 ? 'up' : 'down'


  if(direction === 'up' && currentPageIndex === 1) return;
  if(direction === 'down' && currentPageIndex === sections.length) return;

  if(!state.isPlaying && !state.isScrolling && direction === 'up'){
    
    if(!state.isGoingUp){
      transition(2,'up')
      state.isGoingUp = true;
    }
    return;
  }

  if(state.isPlaying){
    
    state.isPlaying = false;


    if(direction === 'up'){
      if(currentPageIndex <= 1) return;
      --currentPageIndex
      
      
  
    }else{
      if(currentPageIndex === 3) return;
      if(currentPageIndex >= sections.length) return;
      ++currentPageIndex
    }
    transition(currentPageIndex,direction)
    
  }
}


container.addEventListener('wheel',handleWheel)





markers()

 

자세하게 확인해볼 수 있도록 수업 자료에 (scrollTrigger-finished) 업데이트 해두겠습니다.

image.png

 

 

 

열정적으로 수업을 들어주셔서 감사합니다 :)

 

 

 

김민식님의 프로필 이미지
김민식

작성한 질문수

질문하기