[우테코]레벨4 - 6주차 회고

Sally·2022년 10월 9일
1
post-thumbnail

Card 컴포넌트로 추상화 하기 🧚

5 주차 포스팅에 적었던 것처럼 이번주에는 ArticleItem을 추상화하는 시간을 가졌다.
코드를 찬찬히 리팩토링 하다보니 문제점을 발견할 수 있었다.

기존에 PageLayout이라는 컴포넌트를 컴포넌트에 스타일을 주기 위해서 계속해서 재사용 하고 있었다. 그런데 컴포넌트 마다 width, height등의 값이 다르기 때문에 조금씩 props의 값을 다르게 해서 넘겨주어야 했다. 그러다 보니 이런식의 코드가 완성되었다

<PageLayout
		width="80%"
		maxWidth="25rem"
		height="14rem"
		flexDirection="column"
		justifyContent="space-around"
		padding="1rem"
>{children}</PageLayout

지금 와서 보니 두 가지 ✌️ 문제점을 발견할 수 있었다.

첫 번째로, width, height등의 css 속성들을 모두 props로 일일이 넘겨주고 있었다.
이로 인해서 코드의 길이가 길어지고 읽기 어려워진다.
만약 스타일링 해야하는 속성이 더 많이진다면 이 문제가 더 크게 다가올 것이다.

두 번째로, 값을 string으로 넘겨주고 있다는 점이다.
넘겨주는 값이 모두 string으로 지정되어 있기 때문에 실수를 잡기 힘들었다.
작성 중에 철자가 틀려도 어떠한 타입 에러를 발생시키지 않기 때문에 스타일링이 적용이 되지 않은 상황들이 왔을 때에 디버깅하기 힘들어진다. 또한 theme으로 미리 약속 해 놓은 스타일링 값들을 재사용하기 어렵다는 점도 있다.

과거에 해당 컴포넌트를 제작할 때에는 해당 문제점들을 발견하지 못했었다. 하지만 이제는 코드의 양이 길어지면서 짧고 간략한 것을 선호하게 되었고 타입스크립트를 배우면서 타입 체킹의 중요성을 알아버렸기 때문에 이 문제점들을 무시하고 넘어갈 수 없었다.

그래서 Card라는 컴포넌트를 만들면서 기존의 PageLayout컴포넌트를 폐기하고 새롭게 단장하기로 하였다.
여러가지 방법 들을 고민하였는데 emotion의 장점을 살리면서 최대한 깔끔하게 가도록 가기 위해 노력했다.

Card 컴포넌트에서 달라진 점들

const Card = ({
	cssObject,
	media,
	hasActiveAnimation,
	onClick,
	children,
}: PropsWithStrictChildren<CardProps>) => (
	<S.Container
		css={css`
			width: ${cssObject.width};
			height: ${cssObject.height};
			padding: ${cssObject.padding ? cssObject.padding : 0};
			max-width: ${cssObject.maxWidth ? cssObject.maxWidth : cssObject.width};
			justify-content: ${cssObject.justifyContent ? cssObject.justifyContent : 'normal'};
			align-items: ${cssObject.alignItems ? cssObject.alignItems : 'normal'};
			gap: ${cssObject.gap ? cssObject.gap : 0};
			flex-direction: ${cssObject.flexDirection ? cssObject.flexDirection : 'column'};
			flex-wrap: ${cssObject.flexWrap ? cssObject.flexWrap : 'nowrap'};
		`}
		media={media ? media : ''}
		hasActiveAnimation={hasActiveAnimation}
		onClick={onClick}
	>
		{children}
	</S.Container>
);

이렇게 emotion의 css를 활용해서, props로 넘겨져온 값들을 바로 스타일 값을 지정해주었다.

export const Container = styled.section<{
	media: { minWidth: string; width?: string; height?: string } | '';
	hasActiveAnimation: boolean;
}>`
	display: flex;
	flex-direction: column;

	${({ theme }) => css`
		border-radius: ${theme.size.SIZE_010};
		box-shadow: 0 ${theme.size.SIZE_008} ${theme.size.SIZE_024} ${theme.boxShadows.secondary};
	`}

	${({ hasActiveAnimation }) =>
		hasActiveAnimation &&
		css`
			&:hover,
			&:active {
				animation: ${scaleAnimation} 0.3s ease-in;
				animation-fill-mode: forwards;
				cursor: pointer;
			}
		`}
	${({ media }) =>
		media !== '' &&
		css`
			@media (min-width: ${media.minWidth}) {
				width: ${media.width};
				height: ${media.height};
			}
		`}
`;

모든 Card에 공통적으로 들어갈 스타일의 경우 <S.Container> 이라는 스타일드 컴포넌트에 지정해주었다.
animation과 반응형의 경우 Card에 따라 들어 갈 수도 있고 안 들어 갈 수도 있어서 Container에 props으로 넘겨주어서 있을 경우에만 스타일이 지정되도록 해주었다.

export interface CardProps {
	cssObject: {
		width: string;
		maxWidth?: string;
		height: string;
		justifyContent?: string;
		alignItems?: string;
		gap?: string;
		padding?: string;
		flexWrap?: string;
		flexDirection?: string;
	};
	media?: {
		minWidth: string;
		width?: string;
		height?: string;
	};
	hasActiveAnimation: true | false;
	onClick?: () => void;
}

Card props들에는 타입들을 지정하여서 지정해 놓지 않은 css 속성이 넘어오거나 스펠링이 틀릴 경우에 대비할 수 있도록 만들어 주었다.

export const ArticleItemCardStyle: CardProps = {
	cssObject: {
		width: '80%',
		height: theme.size.SIZE_160,
		padding: theme.size.SIZE_016,
	},
	media: {
		minWidth: theme.breakpoints.DESKTOP_LARGE,
		width: theme.size.SIZE_300,
		height: theme.size.SIZE_220,
	},
	hasActiveAnimation: true,
};

그리고 각각의 Card스타일들을 적용할 컴포넌트들에 대해서 따로 styles폴더의 cardStyle이라는 파일 밑에서 선언해주었다.
해당 곳에서는 theme을 사용하여 각각의 value를 지정해주어서 스펠링이 틀리거나 하는 휴먼에러를 최대한 줄이고자 하였다.

<Card {...ArticleItemCardStyle} onClick={onClick}>

객체로 이미 스타일들이 모두 선언되어 있기 때문에,
바로 props로 넘겨 줄 수 있다. 이 덕분에 PageLayout에서 여러 줄을 통해서 넘겨주어야 했던 속성들이 컴포넌트 단에서는 한 줄로 처리할 수 있었다.

조금 더 개선할 수 있는 것 🤔

  • css 속성에 들어가는 값이 여전히 string인 요소들이 존재한다.
    그래서 여전히 실수를 통해서 스타일이 잘못 선언되거나 적용되지 않을 가능성이 존재한다.
    또한 props에서 타입을 지정해 줄 때에도 string으로 타입을 선언했기 때문에 theme을 최대한 사용한다고 하여도 실수가 나올 가능성이 있다. 이런 점들에 대해서는 미리 상수로 선언하거나 타입들을 지정해 놓으면 (예를 들어, justifyContent에 space-around, space-between등의 값들만 들어 올 수 있도록 타입 가드를 진행한다던지) 개선해 나갈 수 있는 부분인 것 같다.

무한 스크롤 컴포넌트 추상화하기🧚

ArticleItem에 이어서 무한 스크롤과 반응형컴포넌트를 합친 ResponsiveInfiniteCardList를 만들게 되었다. 이름이 길긴 하지만 이름만 알아도 컴포넌트의 역할을 알 수 있도록 하게 하다 보니 이것이 최선이였다

기존의 경우 Card들의 목록들을 보여주는 곳에서 공통적으로 반응형과 무한스크롤이 적용되어 있었기 때문에 아래와 같은 코드들이 곳곳에서 반복되고 있었다.

<InfiniteScrollObserver
		hasNext={data?.pages[data.pages.length - 1].hasNext}
		fetchNextPage={fetchNextPage}>
			<S.ArticleItemList>
                   ...card 리스트들
            </S.ArticleItemList>
 </InfiniteScrollObserver>

-> InfiniteScrollObserver에는 스크롤을 감지하여 다음 페이지에 대한 요청을 보내는 로직이 들어있다.
-> S.ArticleItemList에는 자신의 children으로 들어온 Card 리스트들에 대해서 반응형에 대응해주는 로직이 들어가있다.

그래서 이 둘을 포함하는 하나의 컴포넌트를 제작하기로 하였다.
사실, 이 두 가지를 합치는 데에는 어려운 점인 없었다. 다만 서버에서 오는 데이터가 Article이 아닌 다른 값이 여도 다시 재사용할 수 있도록 하는 점을 신경썼다.

export interface ResponsiveInfiniteCardListProps<DataType> {
	hasNext: boolean;
	fetchNextPage: () => Promise<InfiniteQueryObserverResult<DataType, Error>>;
}

const ResponsiveInfiniteCardList = <DataType extends ObserverResponseType = ObserverResponseType>({
	hasNext,
	fetchNextPage,
	children,
}: PropsWithStrictChildren<ResponsiveInfiniteCardListProps<DataType>>) => (
	<InfiniteScrollObserver hasNext={hasNext} fetchNextPage={fetchNextPage}>
		<S.Container>{children}</S.Container>
	</InfiniteScrollObserver>
);

그래서 props에 대한 타입을 지정할 때에 제너릭을 활용하여서 다른 데이터 타입을 응답값으로 가진다고 하여도 재사용할 수 있도록 조치하였다.

0개의 댓글