이 글의 디자인 패턴 부분은 TypeScript | Organizing and Storing Types and Interfaces (by Andres Reales)를 참고하여 작성하였습니다.
기본적으로는 컴포넌트 내부에 정의하되, 컴포넌트 간 의존성을 경계하기
만약 type이나 interface가 특정 컴포넌트에서만 필요한 것이라면 그냥 그 컴포넌트 내부에 정의하면 된다.
보통 이런 local적인 데이터에는 props가 있다.
// StudentList 컴포넌트
interface Student {
id: number;
name: string;
age: number;
}
interface StudentListProps {
classNum: number;
students: Student[];
}
function StudentList({className, students}: StudentListProps) {
// ...
}
그런데 만약 위의 예시에 나온 Student 인터페이스를 다른 컴포넌트에서 사용하려고 import 해온다면 어떨까?
StudentList와 관련성이 없는 쌩뚱맞은 컴포넌트에서 Student 타입이 필요하다는 이유로 가져온다면 말이다.
그리고 그렇게 Student를 import해오는 컴포넌트가 여러개로 늘어난다면...
다른 컴포넌트와 의존성 높게 연결된 것은 별로 좋지 않아보인다.
그리고 이렇게 다른 컴포넌트에 있는 interface/type을 서로 참조하다보면 컴포넌트끼리의 결합이 더 끈끈해지고, 나아가 순환참조가 일어날 수 있다.
그러므로 별도의 파일에 타입을 정의해주는 게 필요하다.
디자인 패턴
어떤 구조로 타입을 저장할지에는 하나로 정해진 정답이 없다. 현재 하고 있는 프로젝트의 특성에 맞게 구성하면 된다.
1. global-based (하나의 글로벌 파일로 관리)
type, interface, enums, class에 대한 정의를 모두 types.ts라는 한 파일에 정의하는 것이다.
이렇게 타입을 한 개의 파일에 몰아서 관리하면
- ✅ 팀원들은 타입을 찾을 때 types.ts라는 파일만 찾으면 된다.
- ❌여러 팀원들이 작업한다면 types.ts 파일 하나에서 타입이 자주 변경되기 때문에, 타입을 나눠서 정의할 때보다 merge conflict가 잦아지게 된다.
- ❌한 파일 내에서 어떤 타입과 비슷하지만 필드가 약간 다른 새로운 타입을 만드려고 할 때, 이름의 충돌을 피하고자 Car, CarTypes, ... 처럼 이름을 정의할 가능성이 높아진다. 이름이 비슷하면서 무슨 용도인지 짐작이 힘든 타입이 늘어난다.
- ❌ 타입 정의가 몇 천 줄을 넘어갈 수 있다.
2. component-based (컴포넌트 기반)
style, jsx, test 코드, type 파일을 모두 관련이 있는 컴포넌트 폴더 안에 모아놓는 구조이다.
- ✅ 컴포넌트 별로 타입을 갖고 있기 때문에 이름이 같은 타입이여도 (ex- Car) import 할 때 경로가 달라 중복과 혼동을 방지할 수 있다.
- ❌ 조금만 다른 필드를 가진 타입이 필요할 때 그 컴포넌트를 위한 타입을 새로 정의해야 해서, 인터페이스가 중복될 수 있다.
3. Model-based, Object-based (모델 기반)
내가 가장 좋아하는 유형이다! 이렇게 하면 프론트엔드와 백엔드가 통신할 때 데이터 타입을 검증하기 좋았던 경험이 있다.
- ✅ 컴포넌트에 상관없이 참조가 자유롭다.
만약 필드가 조금 변경된 타입을 사용하고 싶다면 해당 컴포넌트에서 import 해온 후, 유틸리티 타입의 Pick, Omit 등을 사용해 일부를 제거하거나 pick해서 새로운 타입을 정의하면 될 것이다. 그러면 타입을 중복선언하는 것을 막을 수 있다.
개인적으로는 REST API로 통신할 때의 리소스를 기준으로 models/User.ts 처럼 파일을 만들고,
그 안에 인터페이스와 뿐만 아니라 class를 정의하는 편이다.
User class에는 인스턴스 생성할 때 필드에 값을 저장하는 코드 뿐만 아니라, 서버에서 요구하는 양식대로 필드를 가공하고 JSON 객체로 만들어주는 메소드도 정의해 놓는다. 이러면 타입 검증과 데이터 json 가공까지 한 파일로 할 수 있어서 좋다.
const user1 = new User(id, firstName, lastName, ...);
axios.post(url, user1.generateJSONData())
.then(res => { ... })
.catch(err => { ... });
(근데 이렇게 클래스와 json화 해주는 메소드까지 작성하는 것은 모델과 서비스가 결합된 것 같기도 하고 잘 모르겠다.
아시는 분은 댓글로 알려주세요)
4. Type-based (타입 기반)
enum인지, interface 혹은 type인지 유형별로 분류하고, 그 안에서 각 타입별로 파일을 생성하는 방식.
- ✅ 필요한 타입만 import 해오면 되니까 깔끔하다.
- ❌ 타입 정의가 많아질수록 양이 많아져 찾기 곤란할 듯
5. Hybrid (장점만 골라 섞어서 사용하기)
글로벌, 컴포넌트, 모델, 타입 기반을 적절히 섞어 사용하기
개인적으로는 데이터 리소스의 타입은 모델 기반으로, 특정 컴포넌트에서만 사용하는 것은 컴포넌트 내부에, 여러 곳에서 참조하는 타입은 맥락이 비슷한 것끼리 파일별로 묶어서 정의하는 편이다.
결론: 많은 실험을 통해 이 중에서 자신에게 알맞는 것들을 골라 최적의 구조를 찾아가자
이렇게 되지 않도록 노력해보자...
Reference
- TypeScript | Organizing and Storing Types and Interfaces (by Andres Reales)
- How to organize types definitions in a React Project w/ Typescript (Stackoverflow)
'Front-end > Typescript' 카테고리의 다른 글
[Typescript] 이미 존재하는 CRA에 Typescript 추가하기 (0) | 2023.06.02 |
---|---|
[Typescript] interface vs type (0) | 2023.05.29 |