튜토리얼 페이지 구현하기

HR·2023년 1월 9일
2

프로그래머스(https://programmers.co.kr/)의 알고리즘 문제를 풀기 위해 초기에 접속하게 되면, 아래와 같은 튜토리얼 가이드가 나온다.

이 글에서는, 위의 튜토리얼을 구현하는 방식에 대해 다룬다.

튜토리얼은 언제 띄울까?

대충 구현 로직을 정리해보자.

1. 페이지에 접속하면 튜토리얼을 진행했는지 확인한다.
2. 진행하지 않았다면, 튜토리얼 화면을 보여준다.
3. 튜토리얼을 모두 진행했다면, 로컬 스토리지에 완료했다는 정보를 저장한다.

위와 같은 방식을 이용하면, 같은 유저가 다른 컴퓨터에서 접속했을 경우 매번 튜토리얼을 봐야 한다는 단점이 있다.

다른 방법이 있다면, user테이블에 계정별로 튜토리얼을 진행했는지 컬럼을 생성해 저장하는 방법이 있다.

두 가지 방법을 팀원들과 논의해본 결과, 전자의 방식으로 진행하기로 했다.

어떻게 만들까?

두 가지 방식중에 고민이 되기 시작했다.

  1. page마다 튜토리얼을 위한 코드를 심는다.
  2. 따로 tutorial 페이지를 만들고, 해당 페이지로 라우팅 되도록 한다.

1번 방식으로 진행하면, 튜토리얼을 보여줄 모든 페이지에 튜토리얼과 관련된 뷰가 만들어져야 하고, 매 페이지 접속 시마다 튜토리얼을 진행했는지 확인하는 로직을 거쳐야 한다.

2번 방식을 사용하면, 초기 서비스 접속 시 튜토리얼 페이지로 라우팅만 해주면 되지만, 튜토리얼 페이지 내부의 코드가 굉장히 읽기 힘들어질 수 있다.

1번 방식으로 진행하면 하나의 페이지가 가지고 있는 책임이 너무 많아질 것이라고 생각했고,
우선 2번 방식으로 진행한 뒤, 코드 퀄리티의 한계가 느껴지게 된다면 1번 방식을 고려해보기로 했다.

구현 전에 고려한 점

기존 페이지들의 로직이 아닌 뷰만 가져오면 된다. 하지만 튜토리얼로 보여줘야 할 페이지가 6개, 포커스 해야 할 컴포넌트가 11개나 됐다. 또한 기존 페이지들은 네트워크 통신을 통해 데이터를 가져와야 하는 부분, 동적으로 변경되는 페이지 등이 많았고, 이를 튜토리얼 페이지를 위해 하나의 파일로 다시 작성해야 했다.

하지만 튜토리얼에서는 해당 유저의 맞춤 데이터나 동적인 화면을 보여줄 필요는 없었고, 그러려면

  1. 네트워크 통신은 mocking을 이용
  2. props를 통해 뷰를 그리던 컴포넌트들은 풀어서 작성
  3. 튜토리얼에는 기존의 이벤트들 (클릭 등)이 모두 동작하지 않도록 방지

을 해야 할 것이라고 생각했다.

고민했던 점들

어떻게 일부분만 강조해서 보여줄까?

배경을 흐리게 하는건 쉽다. 하지만 일부분만 제외하고 나머지 부분을 흐리게 하는건 어떻게 구현해야 할까?

찾아보니 react native 용으로 구현된 react-native-hole-view 라는 라이브러리가 있었다.

이 라이브러리와 동일한 방식으로 구현해 보았는데, 뭔가 요소가 강조되는 느낌이 제대로 들지 않았다.

따라서

포커스 될 요소만 남기고 나머지는 다 dim 처리 한다.

이 방법으로 구현했다.

구현 과정

하이라이트 할 요소 보여주기

대충 구현한 로직은 아래와 같다.

const TUTORIAL_DONE = 4; //해당 페이지에서 강조할 요소의 개수

const [count, setCount] = useState(0); //현재 튜토리얼 단계를 가진 상태

useEffect(() => {
  if (count === TUTORIAL_DONE) {
    tutorialCounter((prev) => ++prev);
    return;
  }
}, [count]);


//View
<div>
  ...
  <div //처음 보여줄 요소
    className = {
      count === 0
        ? '하이라이트 용 css'
      	: '일반 css'
    }    
  >
    ...
  </div>
  
  ...
  
  <div //두 번째 보여줄 요소
    className = {
      count === 1
        ? '하이라이트 용 css'
      	: '일반 css'
    }    
  >
    ...
  </div>
  
  ...
  
</div>

누가 봐도 팻말을 강조하고 있는게 보인다!

이제 여기다가 보조 텍스트만 말풍선 형태로 달아주면 될 것 같다.

설명 말풍선 제작

말풍선은 튜토리얼 페이지 모두에서 사용할 것이므로 컴포넌트로 분리하는게 좋겠다고 생각했다.
하지만 말풍선에서 매번 달라져야 하는 요소가 있다면,

위와 같이 말풍선 꼬리의 방향이 말풍선이 설명할 요소의 위치에 따라 달라진다는 것이다.

따라서 아래와 같이 말풍선이 받을 props를 설계했다.

interface HighlightDescriptionProps {
  direction: 'top' | 'right' | 'bottom' | 'left';
  message: string;
}

또한 코드의 가독성을 위해 index.css에 direction에 따라 달라지는 css를 따로 분리해서 작성해놓았다.
tailwind에서 css 속성들을 하나의 변수로 생성할 수 있는 @apply 태그를 이용하였다.

.popover-bottom {
  @apply after:border-[10px] after:border-b-transparent after:border-r-transparent after:border-l-transparent after:border-t-white;
}
.popover-top {
  @apply before:border-[10px] before:border-b-white before:border-r-transparent before:border-l-transparent before:border-t-transparent;
}
.popover-left {
  @apply before:border-[10px] before:border-b-transparent before:border-r-white before:border-l-transparent before:border-t-transparent;
}
.popover-right {
  @apply after:border-[10px] after:border-b-transparent after:border-r-transparent after:border-l-white after:border-t-transparent;
}

말풍선의 꼬리는 삼각형 모양으로 만들었는데, border를 이용해 css로 삼각형을 만드는 방법에 대해서는 이 블로그를 참고했다.

위의 속성들을 이용해 아래와 같이 컴포넌트를 생성했다.

const HighlightDescription: React.FC<HighlightDescriptionProps> = ({ direction, content }) => {
  return (
    <div
      className={
        direction === 'left' || direction === 'right'
          ? direction === 'left'
            ? 'flex justify-center items-center z-[200] max-w-[250px] popover-left relative'
            : 'flex justify-center items-center z-[200] max-w-[250px] popover-right relative'
          : direction === 'bottom'
          ? 'flex flex-col justify-center items-center z-[200] max-w-[320px] popover-bottom relative'
          : 'flex flex-col justify-center items-center z-[200] max-w-[320px] popover-top relative'
      }
    >
      <div className='z-[200] flex items-center px-3 py-2 text-xl rounded-xl bg-bgColor-100 relative'>
        {content}
      </div>
    </div>
  );
};

3개의 삼항 연산자를 통해 방향에 따른 말풍선 꼬리의 위치를 지정해줬다.

말풍선 컴포넌트에 붙여주기

기존의 튜토리얼 파일에, 말풍선이 들어갈 위치를 지정해주었다. 최종 코드는 아래와 같이 작성되었다.

const TUTORIAL_DONE = 4; //해당 페이지에서 강조할 요소의 개수

const [count, setCount] = useState(0); //현재 튜토리얼 단계를 가진 상태

useEffect(() => {
  if (count === TUTORIAL_DONE) {
    tutorialCounter((prev) => ++prev);
    return;
  }
}, [count]);


//View
<div>
  ...
  
  {count === 0 && ( //처음 보여줄 요소의 말풍선
    <HighlightDescription
      direction='bottom'
      content='친구가 심은 씨앗을 클릭해 물을 줄 수 있어요'
      />
  )}
  <div //처음 보여줄 요소
    className = {
      count === 0
        ? '하이라이트 용 css'
      	: '일반 css'
    }    
  >
    ...
  </div>
  
  ...
  
  {count === 1 && ( //두 번째 보여줄 요소의 말풍선
    <HighlightDescription
      direction='right'
      content='내 텃밭을 공유하면 친구가 씨앗을 심으러 올 수 있어요'
      />
  )}
  <div //두 번째 보여줄 요소
    className = {
      count === 1
        ? '하이라이트 용 css'
      	: '일반 css'
    }    
  >
    ...
  </div>
  
  ...
  
</div>

백그라운드 요소들이 클릭되는 현상

현재 하이라이트 된 요소 이외의 화면을 dim 처리 하기 위해서 BackgroundHider라는 컴포넌트를 사용하고 있다.

//BackgroundHider.tsx
interface BackgroundHiderProps {
  tutorialCounter: React.Dispatch<React.SetStateAction<number>>;
}

const BackgroundHider: React.FC<BackgroundHiderProps> = ({ tutorialCounter }) => {
  const clickBackground = () => {
    tutorialCounter((prev) => ++prev);
  };

  return (
    <div
      className='absolute top-0 bottom-0 left-0 right-0 z-50 h-screen bg-black/80'
      onClick={clickBackground}
    />
  );
};

구현 전 고민에 적어놨던 튜토리얼에는 기존의 이벤트들 (클릭 등)이 모두 동작하지 않도록 방지를 위해 기존 컴포넌트의 event function들을 모두 제거하는 것이 아닌, 페이지 컴포넌트에 pointer-events-none 속성을 선언해줌으로서 클릭을 방지했다.

따라서 위처럼 BackgroundHider에 클릭 이벤트를 심고, 해당 배경이 클릭된 경우에는 다음 튜토리얼로 넘어가도록 컴포넌트를 설계했다.

결과

0개의 댓글