이번 미션에서 리액트와의 첫 만남을 가졌다. 첫 만남인 만큼 어색했고 어려웠으나 리액트의 편리함 또한 느낄 수 있었던 미션이었다.
이제 컴포넌트를 만들 수는 있다. 하지만 ‘컴포넌트를 잘! 만드는 것’을 고민해야할 때라고 느꼈다. 잘 만들어진 컴포넌트의 특징은 역시 높은 ‘재사용성’과 ‘예측 가능성’이라고 생각한다. 다른 개발자가 봐도 사용하기 쉽도록 말이다.
재사용성을 높이려면 적당히 잘! 쪼개진 컴포넌트를 만들어야하고 리액트에서 잘 쪼개진 컴포넌트를 만들기 위해서는.. 이번 2단계 미션에서의 요구사항이었던 'Custom Hook
만들기'에서, ‘어디서부터 어디까지를 담을지, 어떻게 하면 재사용성을 높일 수 있을지 더 많이 고민해봐야할 것 같다’고 생각했다. 아직도 커스텀 훅은 어떻게 구성해야할지 감이 잘 안오지만..ㅎㅎ;
컴포넌트와 도메인 데이터를 다루는 로직 간 관심사 분리를 위해 도메인 데이터와 관련한 로직을 담당하는 도메인 객체를 만들주었다.
LunchDataService
: 도메인 데이터를 localStorage(앱 외부 저장소)로부터 받아오고 가공해서 전해주는 등 도메인 데이터를 관리하는 도메인 객체
interface LunchDataServiceType {
restaurants: Restaurant[];
setInitialRestaurants(): void;
filterBy(category: Category): Restaurant[];
sortBy(criterion: Criterion, restaurants: Restaurant[]): Restaurant[];
filterAndSort(category: Category, criterion: Criterion): Restaurant[];
getRestaurant(id: string): Restaurant;
getRestaurants(category: Category, criterion: Criterion): Restaurant[];
getProcessedRestaurants(category: Category, criterion: Criterion): Restaurant[];
}
const LunchDataService: LunchDataServiceType = {
restaurants: [],
...
};
this
바인딩이 필요 없다.setState
로 state를 변경했고 함수컴포넌트의 경우 useState
로 state를 바꿔주는 함수를 반환받아 사용한다.클래스 컴포넌트에서 반복되는 컴포넌트를 재사용하기 위해 고민했다. 특히 상속과 조합 중 어떤 구조를 이용하여 컴포넌트를 재사용할 수 있게 만들지 고민을 많이 했다.
처음엔 상속 구조를 선택하였으나 조합방식으로 구조를 변경했는데 이유는 다음과 같다.
React는 강력한 합성 모델을 가지고 있으며, 상속 대신 조합을 사용하여 컴포넌트 간에 코드를 재사용하는 것이 좋습니다.
상속의 단점: 캡슐화를 깨뜨린다. → 하위 클래스가 상위 클래스에 강하게 결합, 의존하게 된다. → 변화에 유연한 대처가 어려워짐
캡슐화: 타인이 외부에서 조작하는 것에 대비해 외부에서 특정 속성이나 메서드를 사용할 수 없도록 숨겨놓는 것
// DetailModal.tsx (조합 방식으로 Modal컴포넌트를 재사용해서 만든 컴포넌트)
<Modal>
<div className="restaurant__info">
/* content of DetailModal */
</div>
</Modal>
음식점 카테고리를 정의하는 Category타입과 음식점 목록의 정렬기준을 정의하는 Criterion타입을 만들었다.
event.target.value는 string 타입이어서 Category, Criterion 타입에 할당할 수 없었다.
// guard.ts
export function isCategoryType(input: string): input is Category {
const categories = Object.values(CATEGORY);
if (categories.includes(input)) return true;
return false;
}
export function isCriterionType(input: string): input is Criterion {
const criterions = Object.values(CRITERION);
if (criterions.includes(input)) return true;
return false;
}
타입 가드를 정의하기 위해 반환 타입이 타입 서술어(input is Category
)인 함수를 정의했다. isCategoryType(input)
이 호출될 때 기존 타입과 호환된다면 TypeScript는 input
을 Category
타입으로 제한한다.
// CategoryFilter.tsx
interface CategoryProps {
setCategory: (newCategory: Category) => void;
}
function CategoryFilter({ setCategory }: CategoryProps) {
const handleCategoryChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
if (isCategoryType(event.target.value)) setCategory(event.target.value);
};
return <SelectBox filter={CATEGORY} handleOptionChange={handleCategoryChange} />;
}
Be the best version of you!
글 잘 읽었습니다😄
사용자 정의 타입이 정말 멋져요! 저도 미션 중에 필요하다면 사용해봐야겠습다:)
: input is Category 이러한 문법을 지칭하는 용어가 있나요?
안녕하세요 첵스~!
저도 셀렉트 할 때 event.currentTarget.value가 string 값이라며 오류를 뿜어내더라구요! 저도 겪었던 오류를 첵스의 글에서 만나서 반가웠습니다 😀
저는 타입 가드할 때 동일한 로직이 반복되는 것 같아 hook으로 분리해보았어요.
useSafeUnionTypeState.ts
https://github.com/Gilpop8663/react-lunch/blob/step2/src/hooks/useSafeUnionTypeState.ts
사용 예시 )
const [selectedCategory, setSelectedCategory] = useSafeUnionTypeState<FoodCategory>('전체', FOOD_CATEGORY);
const [selectedSortingMethod, setSelectedSortingMethod] = useSafeUnionTypeState<SortMethod>('이름순', SORT_METHOD);
const onChangeSelect = (value: string, kind: 'filter' | 'sort') => {
if (kind === 'filter') {
setSelectedCategory(value);
}
if (kind === 'sort') {
setSelectedSortingMethod(value);
}
};
글 잘 읽었습니당 👍👍👍
--우스--
멋있네요 첵스 역시 시리얼은 첵스초코~