카카오클론, 기억하고픈 코드 2 - 애니메이션

hyounglee·2020년 10월 1일
0

Project

목록 보기
4/7

지난 검색창에 이어 이번에는 3일에 걸쳐 겨우 성공한! 애니메이션 코드를 기록해보고자 한다. 카테고리 및 유저 아이콘 버튼을 클릭시 나오는 모달창의 transition 효과, 메인에서 탭이 바뀔때 나타나는 효과 이렇게 두가지 애니메이션을 구현했다. 컴포넌트가 마운트 될때 트렌지션 효과가 나타나게 해야하는거라 좀 까다롭게 느껴졌다.

1.모달창 opacity transition

유저와 관련된 마이페이지의 내용이 뜨는 모달창과 카테고리 모달창은 동일하게 opacity가 변화하며 은근하게 나타나는 효과가 적용되어 있다. 카테고리창은 형우님께서 먼저 구현을 하셨고 이를 메인화면에 합치면서 트랜지션을 적용해봤다.

코드 분석

상대적으로 코드가 간단한 user Modal을 기준으로 분석해보자.

class UserModal extends React.Component {
  constructor() {
    super();
    this.state = {
      hoverOn: false,
      opacity: 0,
    };
  }

  componentDidMount() {
    setTimeout(() => {
      this.setState({ opacity: 1 });
    }, 0);
  }
  
  ...
  
<ul
  style={{
    opacity: this.state.opacity,
  }}
  className="UserModal"
  onMouseOver={() => {
    this.setState({ hoverOn: true });
  }}
>

CDM가 일어났을때 setTimeout() 메소드로 opacity state를 0에서 1로 바꿔준다. 모달창의 style 속성에 직접 접근해서 opacity 값을 변경시켜주었다.

.UserModal {
  transition: opacity 0.5s ease-in-out;
}

scss에는 transition 이 자연스럽도록 미리 설정을 해두었다.

전체 코드 : UserModal.js

import React from "react";
import "./UserModal.scss";

class UserModal extends React.Component {
  constructor() {
    super();
    this.state = {
      hoverOn: false,
      opacity: 0,
    };
  }

  componentDidMount() {
    setTimeout(() => {
      this.setState({ opacity: 1 });
    }, 0);
  }

  LoginInOut = () => {
    if (!this.props.isLogin) {
      this.props.history.push("/signin");
      return;
    }
    window.localStorage.removeItem("token");
    alert("로그아웃 했습니다!");
    this.props.history.push("/main");
  };

  render() {
    const { isLogin } = this.props;
    return (
      <ul
        style={{
          opacity: this.state.opacity,
        }}
        className="UserModal"
        onMouseOver={() => {
          this.setState({ hoverOn: true });
        }}
      >
        <li>
          <p onClick={this.LoginInOut}>{!isLogin ? "로그인" : "로그아웃"} </p>
        </li>
        <li className={isLogin ? "allowed" : "notAllowed"}>주문내역</li>
        <li className={isLogin ? "allowed" : "notAllowed"}></li>
        <li className={isLogin ? "allowed" : "notAllowed"}>취소 및 교환</li>
        <li className={isLogin ? "allowed" : "notAllowed"}>포인트</li>
        <li className={isLogin ? "allowed" : "notAllowed"}>1:1 문의</li>
        <li>비회원 주문조회</li>
        <li>기프트카드 조회•환불</li>
      </ul>
    );
  }
}

export default UserModal;

2. 탭이동 animation

실제 페이지를 참고해보면, 탭이 바뀔때마다 마치 슬라이드가 지나가는 것처럼 하단 리스트의 박스의 위치가 바뀌는 것을 볼 수 있다. 이전의 탭과 위치를 비교해서 왼쪽이나 오른쪽에서 컴포넌트가 나타난다.

개발자도구에서 보면 실제로 위치값도 변화하고, 슬라이드처럼 div 들이 연결되어있는 것을 확인할 수 있다. 위와 동일한 방식으로는 애니메이션을 구현할 수 없었던 것이 나의 코드는 active Tab 으로 메뉴가 구성되어 있어 그때그때 다른 component가 render 되도록 코드를 짰기 때문이었다.

컴포넌트가 mount 될때, 이전에 있었던 컴포넌트와 비교해서 시작위치도 다르게 해줘야하고, 바로 이동을 시켜줘야하여서 오랜 시간을 씨름했다. 리액트는 컴포넌트로 재사용성이 뛰어난 반면 라이프사이클을 잘 이해하지 못하면 이런 트랜지션 효과를 주기가 까다로운 것 같다..

코드 분석

componentDidMount()

  state = {
    activeTab: 0,
    positionChange: 0,
    footerPosition: 0,
    isLeft: true,
    posX: "-100%",
    opacity: 1,
  };

  comeToCenter = () => {
    this.setState({ posX: "0", opacity: 1 });
  };

  componentDidMount() {
    this.comeToCenter();
  }
<div
  className="tabBox"
  style={{
    transform: `translateX(${this.state.posX})`,
    opacity: this.state.opacity,
  }}
>
  {SHOWTAB[activeTab]}
</div>

Main.js 컴포넌트는 다섯가지 탭의 부모컴포넌트라고 할 수있다. 해당 컴포넌트가 마운트 될 때, 즉 사용자가 메인페이지에 들어왔을 때는 디폴트 탭인 '홈' 탭이 마운트되고, comeToCenter() 라는 함수가 실행된다. x의 포지션 값이 -100% 에서 0으로 이동한다. opacity는 1에서 1로 유지된다. 이러한 위치값을 주기 위해서 {SHOWTAB[activeTab]}을 감싸는 div 박스를 만들었고 다음과 같은 css를 적용했다.

.tabContainer {         // tabBox의 부모 요소
      width: 1126px;
      margin: 0 auto;
      position: relative;
      .tabBox {
        transition: transform 0.5s;
      }
    }

onClick()

<ul className="menuTab">
  {TAB_ARR.map((el, idx) => {
    const isActive = activeTab === idx;
    return (
      <li key={el} onClick={() => this.clickHandler(idx)}>
        <button className={`${isActive && "highlightedBtn"}`}>
          {el}
        </button>
        <hr
          className={`highlightedTab ${isActive ? "on" : "off"}`}
        />
      </li>
    );
  })}
</ul>

메뉴의 버튼을 클릭하면 clickHandler() 함수가 실행되고 인덱스가 인자로 전달된다.

  clickHandler = (idx) => {
    const { activeTab } = this.state;
    this.setState({
      isLeft: idx < activeTab,
    });
    this.setState({ activeTab: idx });
  };

activeTab state가 인자로 넘어온 인덱스로 변화하고, 이전의 인덱스와 값을 비교하여 이전의 탭과 바뀌는 탭의 위치를 비교하는 isLeft boolean state가 설정된다.

componentDidUpdate()

componentDidUpdate(prevProps, prevState) {
  if (prevState.activeTab !== this.state.activeTab) {
    this.setState(
      { posX: this.state.isLeft ? "-100%" : "100%", opacity: 0 },
      () =>
        setTimeout(() => {
          this.comeToCenter();
        }, 500)
    );
  }
}

activeTab 값이 변하면 CDU이 실행된다. 바로 여기서 애니메이션 효과를 적용했다. posX의 값을 isLeft boolean에 따라 -100% 즉 왼쪽에서 가운데로 오거나 100% 오른쪽에서 가운데로 올 수 있도록 state 값을 변경시켜준다.

transition 효과 때문에 -100%100% 로 이동할때도 천천히 이동하는게 보이는 문제가 있어서 opacity0으로 주는 방법으로 사용자에게 눈속임 (ㅋㅋㅋ)을 했다. setTimeout 메소드를 사용해서 이동후 0.5초 뒤에 comeToCenter() 함수를 실행한다.

별의 별 방법을 다 써보고 거의 포기하다 싶을 즈음에 승현님이랑 겨우 만들어낸 코드 ㅋㅋㅋㅋㅋ 왠지 더 쉽고 깔끔한 방법이 있지 않을까 싶지만, 다른 기능 구현이 더 중요하다 생각되어 여기서 끝냈다.

전체 코드 : Main.js

export default class Main extends React.Component {
  state = {
    activeTab: 0,
    positionChange: 0,
    footerPosition: 0,
    isLeft: true,
    posX: "-100%",
    opacity: 1,
  };

  comeToCenter = () => {
    this.setState({ posX: "0", opacity: 1 });
  };

  componentDidMount() {
    this.comeToCenter();
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.activeTab !== this.state.activeTab) {
      this.setState(
        { posX: this.state.isLeft ? "-100%" : "100%", opacity: 0 },
        () =>
          setTimeout(() => {
            this.comeToCenter();
          }, 500)
      );
    }
  }

  clickHandler = (idx) => {
    const { activeTab } = this.state;
    this.setState({
      isLeft: idx < activeTab,
    });
    this.setState({ activeTab: idx });
  };

  render() {
    const { activeTab } = this.state;

    return (
      <>
        <Nav />
        <div className="Main">
          <main>
            <div className="Maintab">
              <ul className="menuTab">
                {TAB_ARR.map((el, idx) => {
                  const isActive = activeTab === idx;
                  return (
                    <li key={el} onClick={() => this.clickHandler(idx)}>
                      <button className={`${isActive && "highlightedBtn"}`}>
                        {el}
                      </button>
                      <hr
                        className={`highlightedTab ${isActive ? "on" : "off"}`}
                      />
                    </li>
                  );
                })}
              </ul>
            </div>
            <div className="tabContainer">
              <div
                className="tabBox"
                style={{
                  transform: `translateX(${this.state.posX})`,
                  opacity: this.state.opacity,
                }}
              >
                {SHOWTAB[activeTab]}
              </div>
            </div>
            <TopBtn />
          </main>
        </div>
        <Footer />
      </>
    );
  }
}

const TAB_ARR = ["홈", "신규", "인기", "세일", "전체"];

const SHOWTAB = {
  0: <Homeitem />,
  1: <Newitem />,
  2: <Hotitem />,
  3: <Saleitem />,
  4: <Allitem />,
};
profile
(~˘▾˘)~♫❝ 쉽게만 살아가면 재미없어 빙고 .ᐟ ❞•*¨*•.¸¸♪

0개의 댓글