
요렇게 생긴 CalendarBadge 컴포넌트를 만들어보자.
사용기술
우선 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 },
타입을 정확하게 명시해준 후,
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 타입을 추가하고 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'}
/>