gocamp - mobile swipe 흉내내기

ryan·2022년 9월 14일
0
post-custom-banner

개요

현재 react-device-detect를 통해 브라우저와 모바일의 UI/UX를 조금씩 다르게 구현하고 있다. 아래 이미지를 통해 브라우저와 모바일의 UI를 비교해볼 수 있다.

상단부터 브라우저, 모바일

  • 모바일의 경우, 캠핑장 마커를 터치했을 때 상세 정보 안내로 인해 화면 전체가 가려지기 때문에 캠핑장 이름만 간단하게 보려는 유저에게는 불편함을 초래할 수 있다.
  • 그렇기 때문에, 네이버 지도앱을 레퍼런스 삼아서 마커를 터치했을 때는 간단한 정보만 보여주고, swipe up했을 때 상세 정보로 확장되는 컴포넌트로 변환되게 하는 작업을 해보려고 한다.

swipe 예시

구현 과정

컴포넌트 구조

  • 기본적으로 모든 정보(야영장 정보, 내 정보 등)는 Drawer라는 공통 컴포넌트에서 띄우게 된다.
  • 빨간색으로 테두리 친 부분이 Drawer Component이며, 접속 기기에 따라 다른 css를 가지게 된다. (좌 모바일, 우 브라우저)

Drawer 컴포넌트

  • 마커를 클릭한 경우, Drawer에는 ResSpotInfo라는 컴포넌트를 띄우게 되며, ResSpotInfo는 단순히 모바일인지, 브라우저인지 구분해서 다른 컴포넌트를 띄우는 분기처리만 담당하는 컴포넌트이다.
  • Drawer 컴포넌트에서 조건문으로 분기 처리해도 되지만, 굳이 depth를 한 단계 더 둔 이유는 Drawer가 담당하는 역할이 많아지게 되어 코드가 길고 복잡해지기 때문이다.
    • Drawer는 state에 따라 '내 정보'/'캠핑장 정보'를 띄우는 분기처리 컴포넌트이기도 하다.
    • 그렇기 때문에 하나의 컴포넌트가 너무 많은 역할을 담당하면 컴포넌트 간 의존성도 더 커지게 되고, 코드 유지 보수/개선이 어려워질 것 같아 Drawer는 위 bullet에서 설명한 하나의 역할만을 가지게 했다.

ResSpotInfo 컴포넌트

  • ResSpotInfo는 위에서 설명한 것처럼, 전역 상태에 따라 어떤 컴포넌트를 띄울지 결정하는 분기 처리 컴포넌트이며 코드는 아래와 같다.
  • mDrawer라는 전역 상태가 expand(확장) 상태이거나, 브라우저인 경우 확장된 야영장 정보 컴포넌트를 띄우며, 그게 아닌 경우에는 축소된 야영장 정보 컴포넌트를 띄운다.
  • 야영장 정보는 비동기 selector를 활용하기 때문에, Suspense로 각각 다른 스켈레톤UI를 띄우도록 처리했다.
const ResSpotInfo = () => {
  const mDrawer = useRecoilValue(mDrawerState);

  const SpotInfoUI = () => {
    if (mDrawer === "expand" || isBrowser) {
      return (
        <Suspense fallback={<DrawerSkeleton />}>
          <SpotInfo />
        </Suspense>
      );
    } else {
      return (
        <Suspense fallback={<MDrawerSkeleton />}>
          <MSpotInfo />
        </Suspense>
      );
    }
  };
  return <SpotInfoUI />;
};

SpotInfo, mSpotInfo

  • SpotInfo는 모바일과 브라우저에서 공통으로 사용하는 야영장 상세 정보 컴포넌트이며, mSpotInfo는 아래 이미지와 같이 약식 정보를 제공하는 축소된 컴포넌트이다.

Swipe 구현

mSpotInfo swipe 적용하기

  • 우선 touch event(start,move,end)에 touchHandler라는 switch문으로 event를 구분하고 그에 맞는 함수를 실행하는 함수를 등록했다.

  • touchstart는 touchmove와 값을 비교하여 움직인 거리를 계산하기 위해 달았으며, touchend는 움직인 값을 비교해서 일정 값을 만족하지 못하는 경우, 초기화하기 위해 달았다.

export const touchHandler = (evt, state, setState, scroll) => {
  const { clientY } = evt.changedTouches[0]; // 마지막으로 이벤트가 발생한 위치
  const { startY, swipeUp, swipeDown } = state; 
  const swipe = startY - clientY; // swipe한 거리
  
  switch (evt.type) {
    case "touchstart":
      // startY, endY 초기화(startY=터치 이벤트가 처음 발생한 좌표)
      setState({ ...state, startY: clientY, endY: null });
      break;

    case "touchmove":
      // swipe Up,Down 구분
      // Up할 경우 컴포넌트 확장 / Down할 경우 컴포넌트 축소
      if (swipe > 0) { 
        setState({ ...state, swipeUp: swipe });
      } else if (scroll && swipe < 0) {
        setState({ ...state, swipeDown: swipe });
      }
      break;

      
    case "touchend":
      if (swipeUp && swipeUp >= 250) {
        setState({
          ...state,
          endY: clientY,
          swipeDown: null,
          swipeUp: null,
          moveY: swipe,
        });
      } else if (swipeUp && swipeUp < 250) {
        // 일정한 값을 못 넘길 경우, 초기화
        setState({ ...state, swipeUp: null });
      }
      if (swipeDown && swipeDown >= -50) {
       // 일정한 값을 못 넘길 경우, 초기화
        setState({ ...state, endY: clientY, swipeDown: null, swipeUp: null });
      } else if (swipeDown && swipeDown < -50) {
        setState({ ...state, endY: clientY, swipeUp: null, moveY: swipe, swipeDown: null });
      }
      break;
    default:
      return;
  }
};

swipe 값에 따라 컴포넌트 변형시키기

  • 위 이미지에서 MSpotInfo컴포넌트에 css 속성으로 등록되어 있는 mDrawerContent 함수에 전역 상태의 swipeUp값을 인수로 전달하고 swipe값에 따라 컴포넌트가 위로 확장될 수 있게 했다.
export const mDrawerContent = (swipe) => {
  return css`
    display: flex;
    flex-direction: column;
    justify-content: start;
    align-items: flex-start;
    margin-bottom: ${3 + swipe / 10}vh;
}
  • swipe 값이 일정한 값을 충족시키면 moveY값이 null에서 이동거리값으로 변경되며 selector에 의해 'expand'또는 'collapse'를 반환하게 되고, ResSpotInfo컴포넌트에서 이 selector에 의해 컴포넌트가 변경된다.
export default selector({
  key: "mDrawerState",
  get: ({ get }) => {
    const mDrawer = get(mDrawerQuery);
    const { moveY } = mDrawer;

    if (moveY < -50) {
      return "collapse";
    }
    if (moveY >= 250) {
      return "expand";
    }
    return;
  },
});
  • 반대로 확장된 컴포넌트에서 swipe down을 하는 경우, 컴포넌트의 scroll이 최상단에 위치했을 때 swipe down이 되도록 해야 했다. 확장 컴포넌트에서는 scroll이 발생하기 때문이다.
    • Drawer 컴포넌트에 useRef, onScroll eventHandler를 등록하여, scrollTop이 0경우, true의 값을 가지도록 boolean 형태의 전역 상태를 관리하도록 했다.

결과

  • touchend 되었을 때, 일정 조건을 충족하지 못하면 값이 초기화되어, 원래의 컴포넌트 상태로 돌아가게 되며 일정 값을 충족하게 되면 컴포넌트의 변형이 발생하게 된다.

profile
프론트엔드 개발자
post-custom-banner

0개의 댓글