
현재 개발하고 있는 프리비는 프리랜서 사진 작가를 위한 예약 관리 도구다. 사용자가 이 서비스를 정말 자신의 업무에서 활용하려면, 우선 우리가 기획 단계에서 설계한 대로 자신의 촬영 상품을 등록하고 프로필을 설정하는 등의 세팅을 해 줘야 했다. 사실상 서비스를 통해 편리함을 얻기 전에 해야 할 일부터 생기는 셈이다.

(초기 메인 페이지 UI는 위와 같았다. 처음 가입한 사용자가 이런 페이지를 제일 먼저 마주치면 과연 계속해서 서비스를 사용할 수 있을까 🥹)
사용자가 우리의 의도대로 움직이게 하려면 어떻게 할 수 있을까? 처음 가입한 이후 무엇부터 해야 할지를 알려 줄 필요도 있고, 서비스를 통해 얻게 될 기대 효과를 상기시켜 줄 수도 있을 것이다. 우선 전자가 존재하지 않는 건 가입 이후의 사용자 유지를 어렵게 만들 요인으로 보였기 때문에, 서비스 튜토리얼을 통해 세팅 방법을 좀 더 쉽게 소개하기로 했다.
우선 튜토리얼이 진행될 메인 페이지의 UI를 개선했다. 메인 페이지에서 접근할 수 있는 기능을 한눈에 보여주고, 튜토리얼을 통해 각각의 기능을 어떤 순서로, 어떻게 사용해야 할지 설명하고자 했다.
기존에 메인 페이지에서 기능에 접근하려면 헤더의 메뉴를 열어야 했고, 마이페이지에 들어갔을 때만 사이드 메뉴로 각각의 탭을 보여주고 있었다. (이렇다 보니 메인 페이지가 더 허전해 보이기도 했다 😂) 모든 페이지에서 사이드 메뉴를 통해 이동할 수 있고, 필요할 때는 메뉴를 닫아 작업 영역을 넓힐 수 있는 방식으로 수정했다.

(수정된 현재 UI. 아직 예약이 없어도 조금은 덜 비어 보인다!)
각 컴포넌트에서 지금 튜토리얼을 띄워 줘야 하는지, 튜토리얼의 어느 단계에 위치해 있는지를 어떻게 공유할 수 있을까?
전역 상태 관리가 가장 먼저 떠올랐지만, 현재 프로젝트에서 tanstack query와 react-hook-form의 useFormContext를 쓰다 보니 전역 상태의 필요성이 없었어서 이걸 위해 도입하기에는 고민이 됐다. 구현하다 보니 튜토리얼 단계는 하나의 컴포넌트에서 관리해도 문제가 없어서, url에 search parameter를 추가해 튜토리얼 진행 여부만 구별하기로 했다. 클라이언트 컴포넌트일 경우 Next.js의 useSearchParam을 사용해 확인이 가능하다. 이 경우 가입 직후 이동하는 메인 페이지 url에 파라미터를 추가해 주기만 하면 되고, 사용자가 직접 '튜토리얼 보기' 버튼을 눌러 튜토리얼을 시작했을 때도 동일한 url로 이동시켜 주기만 하면 된다.
처음에는 진행 여부를 알아야 하는 각 컴포넌트에서(크게는 튜토리얼 진행시 열려 있어야 하는 사이드 메뉴와, 실제 데이터 대신 더미 데이터를 보여 줘야 하는 신청 목록이 있었다) 개별적으로 파라미터를 확인해 진행 상태를 추적하려 했지만, 관리 포인트가 지나치게 늘어나는 문제가 있었다. 또 기존에 이용하던 다른 파라미터에 대한 로직과 충돌이 생길 수도 있는 상황이었다.
이 문제를 해결하기 위해서는 다음과 같이 접근 방식을 수정했다.
이 방식은 쿼리 파라미터를 통한 상태 관리와 상태 전달 방식에 대해 고려할 때 유용하다. 특히, 페이지 간 상태 공유가 필요한 경우, 중앙에서 관리하여 상태 변경의 일관성을 유지하는 것이 관리 효율성을 높인다.
튜토리얼 과정에서 단순히 텍스트나 이미지를 보여 줘서 기능을 소개할 수도 있겠지만, 기왕이면 실제로 페이지 위에 각 영역을 강조해서 보여 주면 더 좋을 것이다. 나는 튜토리얼에서 원래 쓰고 있던 @Mantine의 모달을 사용했는데, 이렇게 하면 페이지 위에 오버레이, 오버레이 위에 모달이 올라가는 UI가 만들어진다. 여기서 특정 영역만 강조하는 방법은 두 가지가 떠올랐다.
사실 처음에는 z-index를 사용하면 쉽게 구현할 수 있을 것으로 예상했지만, 모달이 포탈(portal)에서 렌더링되어 기존 DOM 계층을 벗어나기 때문에 이 방법은 불가능했다. 🥲
포탈은 React에서 모달을 구현할 때 자주 사용된다. 포탈은 DOM 구조 외부에 위치하여 모달이 다른 요소에 의해 가려지지 않도록 도와주지만, 계층 구조의 제약이 발생할 수 있다.
그 외에도 z-index는 상위 컴포넌트에서부터 위계적으로 적용되니, 첫 번째 방법으로 구현하려면 강조해야 할 각각의 컴포넌트와 오버레이 컴포넌트가 모두 형제 컴포넌트 관계에 있어야만 할 것 같다. 결국 두 번째 방법을 통해 제외시켜야 할 영역을 오버레이 컴포넌트에 직접 지정했다. mask-composite 속성을 add로 두고, mask-image를 다음과 같이 설정하면 원하는 위치에 사각형으로 오버레이를 뚫을 수 있었다.
linear-gradient(to right,
black ${maskX.start}px,
transparent ${maskX.start}px ${maskX.end}px,
black ${maskX.end}px),
linear-gradient(black ${maskY.start}px,
transparent ${maskY.start}px ${maskY.end}px,
black ${maskY.end}px)
오버레이에 전달할 CSS 속성 하나만 수정하면 되고, 영역을 완전히 원하는 대로 조정할 수 있어서 좋기도 했지만...! 직접 컴포넌트를 기준으로 하는 게 아니라, 각 컴포넌트가 위치하게 될 좌표를 기준점으로 삼으면서 한계를 동반하기도 했다. 바로 이걸로 인한 추가적인 문제가 다음에... 👇
강조할 요소가 오버레이에서 제외하는 위치에 알맞게 보이게 하기 위해서는 튜토리얼 시작 시 무조건 스크롤을 최상단으로 조정해야 하는 이슈도 발생했다. 튜토리얼이 트리거될 때 scrollTop을 활용하여 스크롤을 최상단으로 이동시키려고 했지만, 일부 상황에서는 정확히 최상단으로 이동하지 않는 문제가 있었다. 고정된 헤더가 있어 위치가 맞지 않았던 것이 원인이었다.
이 문제는 CSS의 scroll-margin-top 속성을 활용해 해결할 수 있었다. 고정된 헤더가 있을 때 적절한 마진을 설정해 줌으로써, 튜토리얼에 따라 스크롤 위치를 조정해도 요소가 제대로 강조되는 화면 구성을 만들 수 있었다.
scroll-margin-top은 특히 고정 헤더나 상단에 공간이 필요한 페이지 디자인에서 유용한 속성이다. 이 속성은 스크롤로 특정 요소가 스크롤 위치에 맞춰 노출될 때 추가적인 여백을 줘서 정확한 위치 조정을 가능하게 한다.
요런 시행착오를 거쳐 결과적으로는 다음과 같이 튜토리얼이 완성됐다! 👏

구현하는 동안 많이 헤매기도 했지만 완성해 놓고 보니 마음에 들게 잘 나오기도 했고 나름 재미있었다 😆 이제 이 튜토리얼이 우리 서비스를 더 많이 사용하게 해 줬으면... 🙏