오늘 한 일
- GDSC Gachon 1기 core member 면접
- gloddy 개발
- 모임 개설 페이지 리팩토링
- 캘린더 구현
gloddy
render props 패턴 활용하기 (ItemList)
GroupingCard, ArticleItem 등 이런 것들을 리스트로 보여줄 때마다 이렇게 작성하고 있었다.
<Flex direction="column">
{groupMembers.map((member, index) => (
<Fragment key={member.userId}>
<MemberItem member={member} />
{groupMembers.length - 1 !== index && <Divider />}
</Fragment>
))}
</Flex>
groupMember가 아니더라도 Article, Grouping 등 다른 데이터가 들어오는 경우가 많았고, groupMembers.length - 1 !== index 이것도 불편해서 ItemList 컴포넌트로 추상화해서 사용하도록 했다.
ItemList 컴포넌트 구현
컴포넌트 명은 최대한 도메인에 종속되어있지 않은 이름으로 지었다. 어떤 데이터든지 리스트로 보여줄 수 있기 때문이다.
// ItemList.tsx
interface ItemListProps<T> extends Omit<ComponentProps<typeof Flex>, 'children'> {
data: T[]
renderItem: (data: T) => JSX.Element
direction?: 'row' | 'column'
hasDivider?: boolean
}
export default function ItemList<T>({
data,
renderItem,
direction = 'column',
hasDivider = true,
...props
}: ItemListProps<T>) {
return (
<Flex direction={direction} {...props}>
{data.map((item, index) => {
return (
<Fragment key={index}>
{renderItem(item)}
{hasDivider && index !== data.length - 1 && <Divider />}
</Fragment>
)
})}
</Flex>
)
}
사용 예시
이제 ItemList 컴포넌트를 사용하면 이렇게 작성할 수 있다.
<ItemList data={groupMembers} renderItem={(member) => <MemberItem member={member} />} />
한줄 컷!
사실 지금 우리의 상황에서는 Divider UI가 고정이라 ItemList 컴포넌트에 직접 넣어주었기 때문에 완전한 Headless 컴포넌트는 아니라고 생각한다.
더 확장성을 높이고 싶다면 Divider 컴포넌트 또한 render props 패턴을 활용해서 구현하면 될 것 같다!
모임 개설 페이지 리팩토링
모임 개설 페이지에서 디자인이 변경되어 수정했다.
원래는 모임 일시를 클릭하면 바텀시트가 올라와서 입력할 수 있는 디자인이었는데, 바텀시트 대신 페이지로 바뀌었다.
이렇게 바뀌면서 로직도 많이 변경하게 되었다. 이전에 만들어두었던 useFunnel 훅을 사용해서 구현하면 form state들도 사라지지 않으면서 깔끔하게 구현할 수 있었다.
1. 페이지 컴포넌트
// page.tsx
export default function CreateGroupPage() {
return (
<CreateGroupContextProvider>
<ModalProvider>
<CreateFunnel />
</ModalProvider>
</CreateGroupContextProvider>
)
}
2. Funnel 컴포넌트
헤더는 currentStep에 따라 다르게 보여주도록 구현했다.
그리고 useFunnel 훅을 사용하여 각 스텝에 해당하는 컴포넌트를 보여주도록 구현했다.
// CreateFunnel.tsx
export default function CreateFunnel() {
const { Funnel, currentStep, setStep, prevStep } = useFunnel(['main', 'meetDate'])
const handleSubmit: SubmitHandler<CreateGroupContextValue> = (data) => {
// ...
}
return (
<Funnel>
<CreateHeader currentStep={currentStep} />
<Funnel.Step name="main">
<MainStep onSelectMeetDate={() => setStep('meetDate')} onSubmit={handleSubmit} />
</Funnel.Step>
<Funnel.Step name="meetDate">
<MeetDateStep onDone={prevStep} />
</Funnel.Step>
</Funnel>
)
}
3. 스텝 컴포넌트
각 스텝들은 섹션 별로 컴포넌트를 분리해서 추상화 수준을 맞추었다.
// MainStep.tsx
interface MainStepProps {
onSelectMeetDate: () => void
onSubmit: SubmitHandler<CreateGroupContextValue>
}
export default function MainStep({ onSelectMeetDate, onSubmit }: MainStepProps) {
const { handleSubmit } = useCreateGroupContext()
return (
<>
<UploadSection />
<InputSection />
<Divider thickness="thick" />
<SettingSection onSelectMeetDate={onSelectMeetDate} />
<Spacing size={60} />
<ButtonGroup>
<Button onClick={handleSubmit(onSubmit)}>완료</Button>
</ButtonGroup>
</>
)
}
// MeetDateStep.tsx
interface MeetDateStepProps {
onDone: () => void
}
export default function MeetDateStep({ onDone }: MeetDateStepProps) {
return (
<>
<CalendarSection />
<Divider thickness="thick" />
<TimeSection type="from" />
<TimeSection type="to" />
<Spacing size={60} />
<ButtonGroup>
<Button onClick={onDone}>완료</Button>
</ButtonGroup>
</>
)
}
캘린더 구현
react-datepicker 라이브러리를 사용해서 캘린더를 구현했다.
다행히 기존에 캘린더를 구현한 코드가 있어서 스타일만 변경했다. 스타일 주는 게 어려울 줄 알았는데 생각보다 금방 구현했다 ㅎㅎ
// Calendar.tsx
interface CalendarProps {
dateValue: Date | null
setDateValue: (date: Date) => void
}
export default function Calendar({ dateValue, setDateValue }: CalendarProps) {
return (
<DatePicker
locale={ko}
dateFormat="yyyy-MM-dd"
formatWeekDay={(nameOfDay) => nameOfDay.substring(0, 1)}
minDate={new Date()}
selected={dateValue}
onChange={(date: Date) => setDateValue(date)}
inline
renderCustomHeader={({ date, decreaseMonth, increaseMonth }) => (
<Flex align="center">
<p className="text-subtitle-1">
{getYear(date)}년 {getMonth(date)}월
</p>
<Spacing size={0} className="grow" />
<IconButton size="medium" onClick={decreaseMonth}>
<Image
src="/icons/24/navigate_before.svg"
alt="navigate_before"
width={24}
height={24}
/>
</IconButton>
<IconButton size="medium" onClick={increaseMonth}>
<Image src="/icons/24/navigate_next.svg" alt="navigate_next" width={24} height={24} />
</IconButton>
</Flex>
)}
/>
)
}
react-datepicker 스타일 넣기
.react-datepicker {
@apply w-full !border-none;
}
.react-datepicker__month-container {
@apply w-full;
}
.react-datepicker .react-datepicker__header {
@apply flex flex-col gap-16 border-none bg-inherit pb-28 pt-0;
}
.react-datepicker .react-datepicker__month {
@apply m-0 flex flex-col gap-20;
}
.react-datepicker .react-datepicker__day--outside-month {
@apply invisible cursor-default;
}
.react-datepicker .react-datepicker__header .react-datepicker__day-names {
@apply flex justify-between;
}
.react-datepicker .react-datepicker__day-name {
@apply m-0 flex h-32 w-32 items-center justify-center text-subtitle-3 text-sign-tertiary;
}
.react-datepicker .react-datepicker__month .react-datepicker__week {
@apply flex justify-between;
}
.react-datepicker .react-datepicker__day {
@apply flex h-32 w-32 items-center justify-center text-paragraph-1 text-sign-secondary;
}
.react-datepicker .react-datepicker__day:hover {
@apply flex h-32 w-32 items-center justify-center rounded-full bg-primary-light text-white;
}
.react-datepicker .react-datepicker__day--selected,
.react-datepicker .react-datepicker__day--keyboard-selected {
@apply flex h-32 w-32 items-center justify-center rounded-full bg-primary text-white;
}
.react-datepicker .react-datepicker__day--selected:hover {
@apply flex h-32 w-32 items-center justify-center rounded-full bg-primary-light text-white;
}
.react-datepicker .react-datepicker__triangle {
@apply hidden;
}
내일 할 일
- gloddy 개발