토픽 선택 페이지는 우리가 새로 만들어 보려는 기능이다. 이런 기능은 요즘 왠만한 서비스에는 다 존재한다. 특히 사용자의 취향이나 성향을 파악해 사용자에게 맞는 정보를 제공하는 서비스라면 필수적이다.

내가 참고한 서비스이다. 듀오링고라는 영어 학습 서비스인데 처음 접근하는 사용자에게 선택지를 주어 맞춤 교육 서비스를 제공하는 것이다.

이건 스픽에서 제공하는 주제 선택 페이지이다. 우리도 이러한 토픽을 선택해서 사용자에게 맞춤 리스트를 제공할 예정이다.
우리는 토픽을 선정할때 여러가지를 한번에 선택하는 것이 아니라 반대되는 두개의 토픽중에 선택하는 방향으로 선택했다. 왜냐하면 여행이나 사람의 성향은 중간보다는 극단적인 경우가 많다고 생각했다. 예를들면 외향과 내향, 도시와 자연 등 상반되는 키워드를 줬을때 맞는 정보를 제공할 수 있을 것이라고 생각했다.

그래서 그때 나온 레이아웃 기획안이다. 선택할때마다 게이지가 올라갈 것이다.

그리고 기획을 기반으로 나온 디자인 시안이다. 디자인 시안은 pc버전으로 모바일 버전은 세로로 구성하기로 결정했다.

그리고 우리가 정한 토픽들이다. 7개의 선택지를 줄것이다.
우선 토픽 선택 페이지에서 중요한 점은 모든 토픽을 선택하기 전에는 제출이 안되야하고 제출하는 시점에서는 토픽(문자열) 배열이 제출되어야 한다. 그리고 다시 뒤로 돌아가서 다른 토픽으로 수정했을때 배열이 변경되어야한다.
가장먼저 토픽에 관한 내용을 상수로 지정해뒀다.
const TAGS = [
{ id: 1, color: '#FFD2D2', tag: ['내향', '외향'] },
{ id: 2, color: '#9FE5A1', tag: ['즉흥적', '계획적'] },
{ id: 3, color: '#FFCFA3', tag: ['도시', '자연'] },
{ id: 4, color: '#FEE5AF', tag: ['랜드마크', '숨은 명소'] },
{ id: 5, color: '#E5F3FF', tag: ['홀로', '여럿이'] },
{ id: 6, color: '#C29FE5', tag: ['액티비티', '힐링'] },
{ id: 7, color: '#FDD4FC', tag: ['느긋하게', '바쁘게'] },
];
이렇게 각 토픽과 토픽에 해당하는 색상값을 넣어서 해당 토픽을 다른 페이지에서 태그 형식으로 구현할때 색상을 지정해준 것이다. 그리고 이렇게 만들어둔 토픽을 어떻게 선택하게 할지 고민했다.
처음에는 페이지 경로에 ?page=1이라는 searchParams를 넣어서 페이지를 만들려고 했다. 하지만 그렇게 했을때 이전페이지에서 선택한 토픽을 유지해줘야하는데 그러면 그 값을 다른 곳에 저장해둬야한다. 왜냐하면 경로가 바뀌면서 새로운 페이지가 렌더링되고 그러면 로컬스토리지나 쿠키가 아니면 데이터를 유지시킬 방법이 없다. 그래서 조금 구현해보다가 바로 갈아 엎었다.
내가 제대로 활용하지 못한 것일수도 있지만 더 간편한 방법이 좋을 것 같았다.
그렇게 선택한 방법은 carousel을 사용해서 한 페이지에서 해결하도록 만드는 것이다. 간단하게 말해서 7장의 캐러셀을 만들고 한장의 카드마다 토픽을 선택하게 하고 사용자가 다음 페이지로 넘기도록 하는 것이다. 이렇게 하면 한 페이지에서 토픽을 다른 곳에 저장하지 않고 관리할 수 있으며 복잡한 로직을 구현하지 않아도 된다.
export default function Page() {
const [tagList, setTagList] = useState<string[]>([]);
우선 페이지에 토픽 리스트를 하나 만든다. 그리고 카드를 누르면 해당 값이 추가되도록
const addTag = (tagName: string) => {
setTagList((prev) => [...prev, tagName]);
};
추가하는 함수도 만들어 주었다.
이제 핵심적인 컴포넌트인 캐러셀은 shadcn의 컴포넌트를 사용한다.
shadcn을 사용하는 이유 : 직접 구현해도 충분히 가능하지만 shadcn의 가장 큰 장점은 공통 컴포넌트로서의 기능을 충분히 다할수 있다는 것이다. 만약 우리가 직접 구현한다면 각 페이지에서 작용하는 부분들을 모두 설계해서 공통 컴포넌트를 만들어야하고 그만큼 시간적, 인적 비용이 많이 발생한다. 그래서 효율적인 작업을 위해 사용한다.
간단하게 설치하면 된다.
npx shadcn-ui@latest add carousel
이제 캐러셀을 우리에게 맞게 커스텀해주면 된다.
export default function TagCarousel({ addTag, tagList }: Props) {
return (
<Carousel className="w-full relative">
<CarouselContent>
{TAGS.map((item) => (
<CarouselItem key={item.id}>
<TagContainer tag={item.tag} addTag={addTag} changeTag={changeTag} tagList={tagList} />
</CarouselItem>
))}
</CarouselContent>
<div className="flex flex-col items-center justify-center absolute -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2">
<Image src={Logo} alt="로고 이미지" width={80} />
</div>
<CarouselPrevious className="left-5" />
<CarouselNext className="right-5" />
</Carousel>
);
}
지금 내가 작성한 코드는 위에서 만들어준 토픽 상수를 받아서 그대로 map함수를 사용해 렌더링하고 있다. 그리고 CarouselItem에 담기는 TagContainer는
<div className="flex flex-col items-center justify-center w-full gap-20">
<TagSelectButton isSelected={isSelected} tag={tag[0]} onClick={selectTag} />
<TagSelectButton isSelected={isSelected} tag={tag[1]} onClick={selectTag} />
</div>
토픽 배열의 첫번째 요소와 두번째 요소를 받아서 렌더링해준다.
const [isSelected, setIsSelected] = useState('');
const selectTag = (tagName: string) => {
setIsSelected(tagName);
if (!isSelected) {
addTag(tagName);
} else {
const deleteTagList = tagList.filter((item) => item !== isSelected);
const editList = [...deleteTagList, tagName];
changeTag(editList);
}
};
그리고 선택 여부와 선택했을때 어떤 동작을 할지도 구현해주었다. selectTag를 보면 선택되지 않았다면 바로 페이지에서 만들어준 토픽 배열에 추가를 해준다. 반면에 선택되었을 때에는 토픽 리스트에서 선택된 것과 같은 아이템을 제거하고 맨 뒤에 수정한 토픽을 추가해준다. 이제 사용자가 바로 토픽을 선택했을때와 중간에 다른 토픽을 선택했을때 두가지 상황에서 모두 제어가 가능하다.
이제 하단부에 있는 진행상황을 구현할 것이다. 이것도 shadcn의 progress라는 컴포넌트를 사용하겠다.
npx shadcn-ui@latest add progress
이렇게 설치해주면 된다. 엄청난 컴포넌트는 아니고 0~100사이의 숫자를 props로 주면 숫자만큼 게이지가 올라간다.
const processStatus = page * (100 / TAGS.length);
그래서 진행 상황을 숫자로 만들어주기 위해 계산 식을 삽입해줬다. 여기에서 page라는 값은 페이지에서 관리하는 토픽 배열의 length이다. 그래서 토픽을 선택하면 배열의 길이가 늘어나고 동시에 게이지가 올라가는 구조이다.
<Button className="w-20" disabled={page !== lastTagPage} onClick={() => console.log(tagList)}>
제출하기
</Button>
제출하기 버튼에는 특별한 기능은 없고 disabled옵션을 추가해줬다. 이제 모든 토픽을 선택하기 전까지는 버튼이 비활성화 될것이다.

모바일 우선이기 때문에 이런 화면이다. 이렇게 위 아래의 토픽중에 하나를 고르면
선택한 토픽의 색상이 변경된다. 그리고 progress의 게이지가 상승된 것을 볼수가 있다.

모든 토픽을 선택하고 제출하기버튼이 활성화되고 버튼을 누르면 배열이 잘 출력된다. 여기에서 마지막 토픽 페이지에서 수정을 하면

잘 수정되는 것을 볼수가 있다.
솔직하게 어려운 페이지는 아니다. 어떤 조건에서 어떻게 동작하는게 맞는 것인지 순서만 잘 알고 있다면 쉽게 구현이 가능했다. 나름 이쁘게 나온 것 같아서 뿌듯하다. 이번에 코드를 작성하면서 고민한 점은 컴포넌트가 너무 무겁지 않은가에 대한 고민이였다. 전에는 컴포넌트 자체가 너무 무거워지는 문제가 많았었다. 그리고 setter함수를 직접 전달하는 일도 많았는데 그런 코드도 제거했다. 그러다보니 전보다는 코드 자체가 좋아진 느낌이 든다. 물론 다른 사람이 보기엔 어떤지 모르겠지만 나부터 그게 느껴진다.
이제 뭔가 프로젝트를 하는 느낌이 든다. 누가 시킨것만 그대로 하는게 아니라 어떻게 구현할지, 어떻게 구성할지 고민하는 과정들이 재미있다. 물론 머리는 더 아프겠지만 결과물이 나왔을때 성취감은 더 좋은 것 같다. 아마 다음에는 메인 페이지를 서로 나눠서 구현할 것 같다. 나는 모달을 저번에 해봤기 때문에 질문과 답변에 해당하는 모달을 맡아서 하지 않을까 싶다. 빠르게 구현해보자.