오늘은 React + TS로 간단한 useState 응용 프로젝트에서 받은 코드 리뷰 공유 및 리펙토링 과정을 설명하겠다!
주요 내용을 아래와 같다.
- Enum 대체와 성능 최적화
- Enum의 Tree Shaking 문제점
- 대안: const object 또는 union type 사용
- 제어문 선택과 가독성
- 객체/타입 기반 분기 처리시 switch문 활용
- 실제 코드 예시와 개선된 가독성 비교
- 타입 안정성 강화
- 함수 반환값 타입 처리 방식
- Generic 활용 vs 타입 단언
- 목적별 함수 분리 전략
프로젝트 소개
간략하게 프로젝트 소개를 먼저하고 가겠다.
프로젝트는 올림픽 메달 트랙커로 특정 국가에 대한 메달 기록을 추가/갱신할 수 있는 웹 어플리케이션이다.
위 링크를 통해 배포된 사이트로 접속할 수 있다.
국가들의 매달 기록을 저장하기 위해 LocalStorage를 사용하였고, 잘못된 입력에 대해서는 Toast를 통한 알림을 주고 있다.
국가 및 메달을 입력 받는 form인 MedalForm 컴포넌트와 입력된 국가들의 메달 기록을 보여주는 MedalTable 컴포넌트로 구성되어 있다.
1. Enum 대체와 성능 최적화
MedalForm 컴포넌트를 보면 입력에 대해, 추가하기와 갱신하기 2가지 타입으로 분리해서 입력을 받고있다.
이 타입에 대해서 하드 코딩하기 보다는 Enum 자료형으로 타입을 만들어서 사용하였다.
export enum MedalFormSubmitType {
ADD = "ADD",
UPDATE = "UPDATE",
}
Enum 자료형을 사용한 이유는 휴면 에러를 방지하여 코드 안정성을 높이기 위함이었다.
하지만 Enum은 Tree Shaking이 되지 않아,
특정 페이지 및 컴포넌트에서 해당 Enum 타입을 사용하지 않더라도 컴파일 된 JavaScript 번들에 포함되어 로드되는 성능 이슈가 있다.
Enum을 대체하는 방법은 Union Type을 이용하는 것이다.
Union Type 이란?
Union Type은 TypeScript에서 "여러 타입 중 하나"가 될 수 있는 값을 표현하는 방법이다.
type StringOrNumber = string | number;
type Status = 'LOADING' | 'SUCCESS' | 'ERROR';
사용하고 싶은 타입을 OR 연산자 (|) 사용하여 결합으로 타입을 정의한다.
Status로 타입이 지정된 변수의 값으로는 'LOADING' , 'SUCCESS', 'ERROR' 이 3가지의 값만 들어올 수 있다.
Enum과 같이 점 표기법으로 접근하고 싶다면 const assertion을 사용한 객체와 함께 정의할 수 있다.
export const MedalFormSubmitType = {
ADD: "ADD",
UPDATE: "UPDATE",
} as const;
export type MedalFormSubmitType = (typeof MedalFormSubmitType)[keyof typeof MedalFormSubmitType];
2. 제어문 선택과 가독성
MedalForm 컴포넌트에서 이벤트 처리를 각각의 추가하기/갱신하기 버튼에 대해 하나의 onSubmit 으로 처리하고 있었다.
따라서 어떤 액션인지 판별하고 그에 따른 다른 로직이 필요했다.
이때 액션을 판별하는 부분을 if문을 이용하여 제어하고 있었다.
function saveMedalList(formData: MedalDataDto, type: MedalFormSubmitType) {
if (type === MedalFormSubmitType.ADD)
setMedalList((prev) => [
...prev, formData,
]);
if (type === MedalFormSubmitType.UPDATE)
setMedalList((prev) => [
...prev.filter((item) => item.country !== formData.country),
formData,
]);
}
onSubmit을 관리하는 함수에서 전달된 정보를 메달 기록으로 저장하는 함수이다.
이를, if문이 아닌 switch 문으로 제어할 경우 얻는 이점은 아래와 같다.
- 타입 안정성
- TypeScript에서는 switch 문에서 모든 case가 처리되었는지 검사할 수 있다.
- 따라서 default 케이스에서 타입 체크로 모든 타입에 대한 케이스가 있는지 컴파일 단계에서 알 수 있다.
- 코드의 가독성 개선
- switch 문은 각 케이스를 명확하게 구분하고, break 나 return으로 종료시점을 명확히 할 수 있다.
- 또한 if문은 케이스가 아닌 조건에 대한 제어문을 생성할 수 있어서,
if문 보다 switch 문을 사용했을 때, 코드의 의모가 명확하게 드러난다.
이에 의해 개선된 코드는 아래와 같다.
function saveMedalList(formData: MedalDataDto, type: MedalFormSubmitType) {
switch (type) {
case MedalFormSubmitType.ADD:
setMedalList((prev) => [
...prev, formData,
]);
break;
case MedalFormSubmitType.UPDATE:
setMedalList((prev) => [
...prev.filter((item) => item.country !== formData.country),
formData,
]);
break;
default:
const exhaustiveCheck: never = type;
throw new Error(`Unhandled color case: ${exhaustiveCheck}`);
}
}
3. 타입 안정성 강화
MedalForm 컴포넌트에서 발생한 onSubmit의 액션 타입이 추가하기인지 갱신하기 인지 알기 위해 getFormActionValue 함수를 사용했고, 이후 assertion 문을 활용해 타입 단언을 해주었다.
// 타입 단언을 사용한 방식
const actionType = getFormActionValue(e) as MedalFormSubmitType;
해당 코드는 타입 단언을 함수 내부에서 처리하여, 함수만 남겨둔다면 코드 가독성을 좋게 할 수 있다.
따라서 Generic을 사용하여 함수를 정의하여 해당 타입의 반환값을 반환하도록 수정하였다.
const actionType = getFormActionValue<MedalFormSubmitType>(e);
느낀 점
가독성을 높이기 위해서는 최대한 로직을 숨겨야한다는 점이었다.
따라서 최상위로 갈수록 프로젝트의 구조를 쉽게 볼 수 있고, 내부적으로 들어갈 수록 상세한 내용이 나오게 된다.
또한 타입 단언을 여기저기서 남발했다면 앞으로는 좀더 Generic 문을 활용할 수 있도록 해봐야겠다!
사실 좀 더 다양한 코드 리뷰를 받았었다. 해당 내용들은 아래의 PR에서 자세하게 볼 수 있다!
https://github.com/llddang/React-Paris-Olympics/pull/10
https://github.com/llddang/React-Paris-Olympics/pull/12
참고
'💻 개발 > toy project' 카테고리의 다른 글
자연스러운 UX: 뒤로 가기에서 스크롤 위치 유지하기 (0) | 2025.02.04 |
---|---|
Vite + React + TS 프로젝트에서 절대 경로 설정하기 (0) | 2025.02.03 |
영원히 입력 안되는 폼이 있다? : 진실혹은거짓 (0) | 2025.01.23 |
바닐라 JS 코드 리팩터링 스토리: 컴포넌트 설계 변화 탐구 (0) | 2025.01.17 |
바닐라 JS로 토스트 구현: Class 컴포넌트 (0) | 2025.01.14 |