Image Slider / TIL ~10.22

jh_leitmotif·2021년 10월 22일
0

Frontend 개인 공부

목록 보기
5/24
post-thumbnail

🧐 개요

뭔가 이것저것하고 있지만 블로그 포스팅에 약간 게으른 것 같습니다 😓

저번 포스팅 이후 한 내용은 다음과 같습니다.

1. 공지사항 게시판 Delete 구현
2. 개인별 Q&A, Review 게시판 구현 (In MyPage Component)
3. 1:1 문의 메뉴 구현 (nodemailer)
4. unused variables and component clear
5. state setting function optimize
6. GSAP 공부
7. Image Slider 구현 

우선 아직 기억에 남아있는(?) Image Slider부터 정리하려 합니다.


LandingPage는 사용자로 하여금 눈에 띄면서도 사이트의 컨셉과 맞아야합니다.

현재 임시로 만든 페이지는 단순히 Navbar와 메인 이미지의 구성으로

정말 심심합니다.

어딘가 익숙한데..!?

그래서 좀 더 다이나믹한 NewLandingPage를 만들기로 결심했고,

여기에 적용한 것이 GSAP와 Image Slider 입니다.

GSAP는 추후에 정리합니다.

📝 What I Need?

화면 설계

무작정 코딩하기 전에 어떤 화면으로 구성될지 생각해보았습니다.

  1. When a client visits
    • 1-1 : Navigation bar + background video will play
    • 1-2 : Something Text will be floated on left side
    • 1-3 : Image Slider (auto) will be floated on the opposite side of text
  2. The other contents will be appeared by scrolling down.

첫 번째 칸에는 nav, video, text, slider 4가지 요소가 공존합니다.
즉 position과 z-index를 적절히 정의해야합니다.

How To Slide?

1. 기본적으로 setInterval과 같은 동작으로 이미지가 슬라이드된다.
2. 이미지 위에 마우스 커서를 올리면 prev, next 버튼이 나타난다.
3. prev, next버튼을 통해 직접 수동 슬라이드를 할 수 있다.

📺 화면 구성

   #background-video{
      width:100%;
      height:100%;
      position:relative;
      z-index:1;
    }
    #textWrapping{
      width:100%;
      height:100%;
      position:absolute;
      background-color:black;
      opacity:0.2;
      top:0px;
      z-index:2;
    }    
    #wrappingImageArea{
      position:absolute;
      width:500px;
      height:650px;
      left:55%;
      top:10%;
      bottom:0%;
      border:1px solid lightgrey;
      opacity:0;
      z-index:2;
      overflow:hidden;
    }
  1. background-video
    • 비디오는 첫 문단의 배경이므로 relative position
    • text, slider를 위해 z-Index를 1로 맨 뒤로 배치
  2. textWrapping
    • absolute position을 통해 video 영역과 겹치게 배치
    • z-index를 2로 배치하여 위로 얹음
  3. wrappingImageArea
    • absolute position을 통해 video 영역과 겹치게 배치
    • z-index를 2로 배치하여 위로 얹음

text와 slider는 서로 영역을 침범하지 않으므로 z-index를 통일했습니다.


⚒ Slide 뼈대 만들기

n초의 주기로 부드럽게 이미지가 넘어가야 한다.

구현에 앞서 가장 핵심은 책을 넘기듯 부드럽게 넘어가는 동작입니다.

즉 FadeIn/Out 같은 꼼수(?!)는 쓰지 않을 겁니다.

대략적인 슬라이더 구조도입니다.

예를 들어

const imgList = [img1,img2,img3,img4,img5]

이 이미지 리스트에서 img3이 보여질 때

img1, img2, img4, img5는 overflow되어 보이지 않도록 처리해야됩니다.

만약 img4를 볼 차례가 된다면

위와 같은 동작이 되어야합니다.

<div id="wrappingImageArea" className="wrappingImage" >
  <div id="imageArea" className="imageMap" ref={imageRef}>
    	{
          clothMap.map((img,i)=> 
           <img src={img} key={i} style={{width:'100%', height:'100%', float:'left'}}/>
          )
         }
  </div>
  <PrevBtn id="prevBtn" style={{cursor:'pointer'}} onClick={prevBtnClick}/>
  <NextBtn id="nextBtn" style={{cursor:'pointer'}} onClick={nextBtnClick}/>
 </div>

Overflow되는 공간을 잡아내기 위해 wrappingArea를 크게 잡습니다.

그 안에 image가 배치되는 div를 배치했고, img element는 float:left를 통해 일렬로 붙입니다.

( 이미지들은 이미 dispatch action 동작으로 DB로부터 경로를 읽어온 상태입니다. )

그리고 Prev, NextBtn element를 absolute position으로 적절히 위치를 잡습니다.

#wrappingImageArea{
    position:absolute;
    width:500px;
    height:650px;
    left:55%;
    top:10%;
    bottom:0%;
    border:1px solid lightgrey;
    opacity:0;
    z-index:2;
    overflow:hidden;
}
#imageArea{
    display:flex;
    transition:all 1s ease-in-out;
}
#prevBtn{
    position:absolute;
    width:50px;
    height:50px;
    bottom:50%;
    opacity:0;
}
#nextBtn{
    position:absolute;
    width:50px;
    height:50px;
    left:90%;
    bottom:50%;
    opacity:0;
}

최상위 요소인 wrappingImageArea의 overflow 속성을 hidden으로 하여

범위를 벗어난 하위 요소인 이미지들은 보이지 않도록 합니다.

display:flex를 이용해 불러온 이미지를 가로로 정렬하고,
ease-in-out으로 transition이 부드럽게 움직이도록 설정했습니다.


🏃‍♂️ Slide Transform

움직여야하는 태그는 imageArea div tag입니다.

가로로 나열되어 있고, 부모 요소의 범위에 넘치는 이미지는 hidden 상태가 되므로

단순히 x축 좌표를 이동시킵니다.

<div id="imageArea" className="imageMap" ref={imageRef} 
  style={{transform:`translate(-${imgLoc-2}00%)`}}>
  {clothMap.map((img,i)=> 
     <img src={img} key={i} style={{width:'100%', height:'100%', float:'left'}}/>
  )}
</div>
transform:`translate(-${imgLoc-2}00%)`

translate(100%)는 오른쪽으로, translate(-100%)는 왼쪽으로 움직인다.
따라서 Prev 동작은 + 방향, Next 동작은 음수 방향으로 진행되어야한다.

특이사항이라면 DB에 저장된 Image의 인덱스가 2부터 시작되기에
imgLoc-2 를 해야 0%로 첫 번째 이미지가 출력됩니다.

보편적인 경우라면

-${imgLoc}00%

로 사용될 것입니다.


⛸ Slide 움직임 만들기

React는 상태가 업데이트되면 리렌더링이 진행된다.

앞서 서술했던 것 중 setInterval과 같은 동작을 구현한다고 해두었습니다.

하지만 React의 특성을 고려했을 때,
딱 한 번 선언된 이후 계속 주기를 도는 setInterval은 맞지 않습니다.

setInterval을 통해 image의 index를 변화시킬 때마다

<App/>

은 지속적으로 다시 렌더링되면서 결국 index는 초기값을 벗어나지 못합니다.


첫 번째 접근

불러왔다가 죽이면 되지 않을까?

setTimeout을 통해 상태를 업데이트하고 바로 clearTimeout을 하면 되지 않을까? 라는 생각이었습니다.

const tick = () =>{
	return setTimeout(()=>{
          if (imageRef.current!=null){
              imgLoc+1<=clothMap.length ? setImgLoc(imgLoc+1) : setImgLoc(2)
          }    
        },4000)
}

useEffect(()=>{
        tick()
        clearTimeout(tick())
},[imgLoc])

const nextBtnClick = (event) =>{
	event.preventDefault();
	clearTimeout(tick());
	if (imgLoc+1<=clothMap.length){
		setImgLoc(imgLoc+1)
	}else{
		setImgLoc(2)
	}
}

const prevBtnClick = (event) =>{
	event.preventDefault();
	clearTimeout(tick());
	if (imgLoc-1>=2){
		setImgLoc(imgLoc-1)
	}else{
		setImgLoc(clothMap.length)
	}
}

결론은 잘못된 방법입니다.

가만히 두면 정상적으로 자동 슬라이딩이 적용되지만

Timeout이 되는 시점과 prev/next btn을 클릭하는 시점에 따라

이미지의 인덱스를 의미하는 imgLoc의 상태가 정상적으로 업데이트되지 않았고

뒤로 갔다 앞으로 갔다하는 기괴한 동작이 출력되었습니다.


두 번째 접근

직전의 imgLoc 상태 값이 어떤 경우이더라도 보존되어야 한다.

위의 전제와 완벽하게 부합하는 것이 useRef() 였습니다.

useRef()로 관리되는 변수는 변경점이 있더라도 리렌더링이 되지 않는다

즉 그저 ref 변수에 setInterval을 넣으면 그만인 것입니다.

const tickRef = useRef();

useEffect(()=>{
	if (slideReady){
		clearInterval(tickRef.current)
		tickRef.current = setInterval(()=>{
			if (imageRef.current!=null){
				imgLoc+1<=clothMap.length ? setImgLoc(imgLoc+1) : setImgLoc(2)
			}
                },4000)
            
            
         return ()=>clearInterval(tickRef.current);
         }
},[slideReady,imgLoc])

const nextBtnClick = (event) =>{
	event.preventDefault();
	clearInterval(tickRef.current);
	if (imgLoc+1<=clothMap.length){
		setImgLoc(imgLoc+1)
	}else{
		setImgLoc(2)
	}
}

const prevBtnClick = (event) =>{
	event.preventDefault();
	clearInterval(tickRef.current);
	if (imgLoc-1>=2){
		setImgLoc(imgLoc-1)
	}else{
		setImgLoc(clothMap.length)
	}
}
1. tickRef.current에 setInterval을 저장해 imgLoc이 변경되도 렌더링이 다시 일어나지 않는다.
2. Prev/Next Btn 클릭시 clearInterval을 통해 작업을 멈췄다가 다시 실행시켜 동작을 유연하게 한다.
3. 첫 번째 이미지에서 PrevBtn을 클릭했을 때
  * imgLoc을 이미지 배열의 마지막 인덱스로 세팅한다.
4. 마지막 이미지에서 NextBtn을 클릭했을 때
  * imgLoc을 이미지 배열의 첫 번째 인덱스로 세팅한다.

🎯 결과

용량 제한에 맞춰 올려 첫 번째, 마지막 이미지에서의 동작이 다소 삐그덕대긴하지만

실제로 보면 매끄럽게 움직이고 있습니다 😅😅😅

이미지 자체 크기로 인해 div가 모두 메꿔지지 않는 것이 보이지만

그것은 각각 이미지의 크기를 균일하게 맞추면 해결될 부분입니다.


😅 마무리

useRef()를 DOM Element를 조작하는 걸로만 알고 있었지만

라이브러리 없이 Image Slider를 만들어보며 더 배우게 되었습니다.

더불어 리액트의 리렌더링 특성에 주의를 요해야겠다는 생각을 또 하게 되었습니다.


📋 참고 링크

https://react.vlpt.us/basic/12-variable-with-useRef.html
// useRef에 대한 설명. velopert님의 post입니다.
https://erikmartinjordan.com/clear-timeout-react-hooks
// clearTimeout(Interval) 와 관련된 post입니다.
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
// Dan abramov 님의 post입니다.
// https://velog.io/@jakeseo_me 님의 벨로그에 번역 포스트가 있습니다.


📋 npm package

https://www.npmjs.com/package/react-simple-image-slider

이미 npm 패키지로 다운받아 사용가능한 슬라이더가 있습니다.

사용법도 매우 간편합니다.


profile
Define the undefined.

0개의 댓글