[React] CalendarBadge 컴포넌트 만들기

복숭아는딱복·2024년 8월 1일

토이프로젝트 2

목록 보기
2/7
post-thumbnail

요렇게 생긴 CalendarBadge 컴포넌트를 만들어보자.

사용기술

  • React
  • lucide-react
  • emotion/styled
  • typescript

우선 CalendarBadge.tsx 파일을 만들고 해당코드를 작성한다. 백에서 workType data를 영어형식(workType: 'open' | 'middle' | 'close';)으로 보내주기 때문에 workTypeLabels를 통해 한글으로 파싱해준다.

import { Clock4 } from 'lucide-react';
import { ICalendarBadgeProps } from '@/interfaces/calendar';
import { colors } from '@/constants/colors';
import styled from '@emotion/styled';

const CalendarBadge = ({ workType }: ICalendarBadgeProps) => {
	const Badge = BadgeContainer[workType]; // [오픈,미들,마감]
    
    	const workTypeLabels = {
		open: '오픈',
		middle: '미들',
		close: '마감',
	};

	return (
		<Badge>
			<Clock4 size={10} />
			{workType}
		</Badge>
	);
};

export default CalendarBadge;

그리고 emotion/styled를 사용하여 스타일을 추가해준다. 우선 중복코드를 줄이기 위해 BaseBadge라는 공통 스타일을 선언해준다. 그리고 BadgeContainer 객체를 만들어 workType에 따라 스타일이 동적으로 변경될 수 있게 해주고 각 BaseBadge을 적용한다.

const BaseBadge = styled.li`
	display: flex;
	align-items: center;
	border-radius: 4px;
	gap: 2px;
`;

const BadgeContainer = {
	open: styled(BaseBadge)`
		background-color: ${colors.primaryYellow};
		color: ${colors.primaryYellow};
	`,
	middle: styled(BaseBadge)`
		background-color: ${colors.afternoonPink};
		color: ${colors.afternoonPink};
	`,
	close: styled(BaseBadge)`
		background-color: ${colors.nightGreen};
		color: ${colors.nightGreen};
	`,
};

🚨에러발생

첫번째 에러

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ 오픈: StyledComponent<{ theme?: Theme | undefined; as?: ElementType<any, keyof IntrinsicElements> | undefined; } & ClassAttributes<HTMLLIElement> & LiHTMLAttributes<...> & { ...; }, {}, {}>; 미들: StyledComponent<...>; 마감: StyledComponent<...>; }'.
  No index signature with a parameter of type 'string' was found on type '{ 오픈: StyledComponent<{ theme?: Theme | undefined; as?: ElementType<any, keyof IntrinsicElements> | undefined; } & ClassAttributes<HTMLLIElement> & LiHTMLAttributes<...> & { ...; }, {}, {}>; 미들: StyledComponent<...>; 마감: StyledComponent<...>; }'.

TypeScript가 workType을 string 타입으로 인식하고 있어서 발생한다고 한다. 내가 지정해놓은 interface는 string인데,

export interface ICalendarBadgeProps {
	workType: string;
}

BadgeContainer의 키는 특정 문자열 리터럴 타입이기 때문에, 일반적인 string으로는 접근할 수 없다. 이 문제를 해결하기 위해 타입 단언(type assertion)을 사용하거나 타입 가드를 추가해야 한다.

const Badge = BadgeContainer[workType];

const BadgeContainer = {
	open: styled(BaseBadge)`
		background-color: ${colors.primaryYellow};
		color: ${colors.primaryYellow};
	`,
	middle: styled(BaseBadge)`
		background-color: ${colors.afternoonPink};
		color: ${colors.afternoonPink};
	`,
	close: styled(BaseBadge)`
		background-color: ${colors.nightGreen};
		color: ${colors.nightGreen};
	`,
};

타입 단언(Type Assertion): TypeScript에서 변수나 값의 타입을 강제로 지정하는 방법
타입 가드(Type Guard): TypeScript에서 변수의 타입을 확인하고, 조건부로 타입을 좁히는 기술. 이를 통해 TypeScript는 조건문 내에서 변수가 어떤 특정 타입임을 더 정확히 알 수 있다

두번째 에러

BadgeContainer[workType]이 undefined를 반환할 때 발생한다. 이는 주로 workType의 값이 BadgeContainer 객체에 정의되지 않은 키일 때 발생하는데 내가 목데이터에 '대타'라는 알 수 없는 키를 넣어둬서 발생했다.

	{ userId: '11', workDate: '2024-08-13', workType: '대타', isOfficial: true },

✅해결방법

첫번째 에러: 타입 단언(Type Assertion)을 사용하여 해결

타입을 정확하게 명시해준 후,

export interface ICalendarBadgeProps {
	workType: 'open' | 'middle' | 'close';
}
<CalendarBadge
	key={data.userId}
	workType={data.workType as 'open' | 'middle' | 'close'}
/>

as keyof typeof BadgeContainer로 타입 단언한다.

const Badge = BadgeContainer[workType as keyof typeof BadgeContainer];

두번째 에러: default 타입을 추가

아무타입도 해당되지 않을때 반환될 default 타입을 추가하고 workType이 BadgeContainer에 있으면 workType을, 없으면 default를 반환하게 한다.

const validWorkType = workType in BadgeContainer ? workType : 'default';
	const Badge = BadgeContainer[validWorkType as keyof typeof BadgeContainer];
    
const BadgeContainer = {
	open: styled(BaseBadge)`
		background-color: ${colors.primaryYellow};
		color: ${colors.primaryYellow};
	`,
	middle: styled(BaseBadge)`
		background-color: ${colors.afternoonPink};
		color: ${colors.afternoonPink};
	`,
	close: styled(BaseBadge)`
		background-color: ${colors.nightGreen};
		color: ${colors.nightGreen};
	`,
	default: styled(BaseBadge)`
		background-color: ${colors.gray};
		color: ${colors.gray};
	`,
};

최종 코드

//CalendarBadge.tsx

import { Clock4 } from 'lucide-react';
import { ICalendarBadgeProps } from '@/interfaces/calendar';
import { colors } from '@/constants/colors';
import { badgeColors } from '@/constants/badgeColors';
import { fontSize } from '@/constants/font';
import styled from '@emotion/styled';

const CalendarBadge = ({ workType }: ICalendarBadgeProps) => {
	const Badge = BadgeContainer[workType];

	const workTypeLabels = {
		open: '오픈',
		middle: '미들',
		close: '마감',
	};

	return (
		<Badge>
			<Clock4 size={10} />
			{workTypeLabels[workType]}
		</Badge>
	);
};

export default CalendarBadge;

const BaseBadge = styled.li`
	display: flex;
	align-items: center;
	border-radius: 4px;
	gap: 2px;
	padding: 2px 3px;
	font-size: ${fontSize.xxs};
`;
const BadgeContainer = {
	open: styled(BaseBadge)`
		background-color: ${badgeColors.primaryYellow};
		color: ${colors.black};
		svg {
			color: ${colors.primaryYellow};
		}
	`,
	middle: styled(BaseBadge)`
		background-color: ${badgeColors.afternoonPink};
		color: ${colors.black};
		svg {
			color: ${colors.afternoonPink};
		}
	`,
	close: styled(BaseBadge)`
		background-color: ${badgeColors.nightGreen};
		color: ${colors.black};
		svg {
			color: ${colors.nightGreen};
		}
	`,
	default: styled(BaseBadge)`
		background-color: ${colors.veryLightGray};
		color: ${colors.black};
		svg {
			color: ${colors.black};
		}
	`,
};
//calendar.d.ts
export interface ICalendarBadgeProps {
	workType: 'open' | 'middle' | 'close';
}
//다른페이지에서 가져다 쓸 때

import CalendarBadge from './CalendarBadge';

<CalendarBadge
	key={data.userId}
	workType={data.workType as 'open' | 'middle' | 'close'}
/>

0개의 댓글