halang-log
💻 Frontend

[React] Stepper를 직접 구현해보자

date
Aug 11, 2023
slug
make-stepper
author
status
Public
tags
React
Typescript
Stepper
summary
모여모여 프로젝트에서 사용할 Stepper 컴포넌트를 만들어보자
type
Post
thumbnail
category
💻 Frontend
updatedAt
Aug 10, 2023 07:36 PM
언어

개요

현재 넥스터즈에서 모여모여 라는 프로젝트를 개발중이다. 모여모여는 넥스터즈에서 사용할 팀빌딩 서비스이다. 기존에는 엑셀 파일로 팀빌딩 진행 과정을 일일히 기록했는데 이를 좀 더 게임스럽게 풀어내고자 이 프로젝트를 진행하게 됐다.
우리 서비스에는 아래와 같은 Stepper가 필요하다.
💬
팀빌딩은 아래와 같은 순서대로 진행된다. 각 PM은 본인을 1지망으로 선택한 팀원들을 먼저 선택할 수 있게 된다. 이렇게 순차적으로 4지망까지 진행되면 마지막은 팀 구성 조정 라운드이다. 이때에는 아직 어느 팀에도 속하지 못한 팀원들을 배정하는 라운드이다.
우리의 금자이너가 만들어준 스태퍼
우리의 금자이너가 만들어준 스태퍼
 
사용한 기술 스택은 아래와 같다.
🛠️
React, Typescript, Context API, PandaCSS
 

구현

먼저 큰 틀은 아래 그림과 같이 설정했다.
notion image
 
Stepper를 사용하는 쪽에서는 현재 어떤 라운드인지 알아야 한다. 즉 <Stepper activeStep={step} /> 처럼 사용해야 한다. 그리고 기본적으로 Stepper 내부에 있는 Step의 디자인은 고정으로 두고 안에 들어갈 내용(icon, text)은 사용하는 쪽에서 자유롭게 쓸 수 있게 했다.
 
activeStep에 따라 Step의 디자인이 달라져서 해당 state를 Step 컴포넌트에서도 알고 있어야 한다. 처음에는 우리 기술 스택인 jotai를 이용해 component/stepper/store에 해당 전역상태를 두고 관리하는 식으로 했었다. 하지만 이렇게 할 경우 같은 페이지에서 Stepper를 여러개 사용할때 문제가 발생한다. Stepper는 여러개인데 이 상태를 하나로만 관리하기 때문에 모든 Stepper가 동일한 액션을 취하게 된다.
이렇게 된다….
이렇게 된다….
 
그래서 Context API를 이용해 각 Stepper 컴포넌트 별로 상태를 관리하게 해주었다.
 
먼저 Stepper 컴포넌트이다.

/components/stepper/Stepper.tsx

import React, { Fragment } from 'react'; import StepperContext from '@/components/stepper/StepperContext'; import { css, cx } from '@/styled-system/css'; import { hstack } from '@/styled-system/patterns'; type StepperProps = { activeStep: number; children: React.ReactNode; className?: string; }; export const Stepper = ({ activeStep, children, className }: StepperProps) => { const childrenArray = React.Children.toArray(children).filter( Boolean, ) as React.ReactElement[]; const steps = childrenArray.map((step, index) => { return ( <Fragment key={`${step.key}`}> {React.cloneElement(step)} {index !== childrenArray.length - 1 && ( <span className={css({ width: '20px', height: '2px', backgroundColor: 'rgba(255, 255, 255, 0.19)', borderRadius: '5px', })} /> )} </Fragment> ); }); return ( <StepperContext.Provider value={{ activeStep }}> <div className={cx(hstack({ gap: '0' }), className)}>{steps}</div> </StepperContext.Provider> ); };
Context.Provider를 통해 activeStep을 내부 컴포넌트에서도 사용할 수 있게 해주었다. 또한 Stepper의 children을 통해 전체 length를 파악하고 각 Step 사이의 connector(가로 막대 같이 생긴거)를 만들어주었다.
 
다음은 Step 컴포넌트이다.

/components/stepper/Step.tsx

import React from 'react'; import { hstack } from '@/styled-system/patterns'; import { useStepperContext } from '@/components/stepper/StepperContext'; type StepProps = { id: number; children: React.ReactNode; }; export const Step = ({ id, children }: StepProps) => { const { activeStep } = useStepperContext(); return ( <span className={hstack({ color: activeStep === id ? 'gray.5' : 'rgba(255, 255, 255, 0.53)', rounded: '10px', p: '10px 20px', textStyle: 'h3', gap: '0', bg: activeStep === id ? 'purple.60' : 'transparent', fill: activeStep === id ? 'gray.5' : 'rgba(255, 255, 255, 0.53)', borderWidth: '1px', borderColor: activeStep === id ? 'purple.60' : 'rgba(255, 255, 255, 0.19)', transition: 'all 0.2s ease-in-out', })} > {children} </span> ); };
여기서는 activeStep인지 아닌지에 따라 ui를 다르게 보여준다.
 
최종적으로 아래와 같이 Stepper를 사용할 수 있다.
<Stepper activeStep={activeStep}> {ROUNDS.map(({ label, Icon }, index) => ( <Step key={label} id={index}> <Icon className={css({ marginRight: '10px' })} /> <span className={css({ textStyle: 'h3' })}>{label}</span> </Step> ))} </Stepper>
 

마무리

역시 공통 컴포넌트를 만드는 작업은 꽤나 재밌는것 같다. 어느정도까지의 자유도를 줄건지 등 어떻게 설계할지 고민을 하는 과정도 유익했던것 같다. 코드리뷰를 받으면서 미처 생각하지 못했던 부분들이 있었는데 역시나 아직은 많이 부족하다고 느낀다. mui 코드를 보면서 이런 거대한 시스템을 만든 분들이 참 대단하다고 느꼈다. 언젠가 이런 라이브러리를 만들어 보는것도 진짜 재밌을것 같았다. 어떤 프론트엔드 개발자가 되고 싶냐라는 질문의 답변중 하나가 될지도 모르겠다.ㅎㅎ