0. 이번 주는 공통 컴포넌트를 만들기로 했다.
현재 프론트 2명, 백엔드 1명, AI 1명으로 구성된 프로젝트에 참가 중이다. 나랑 같이 웹 프론트엔드로 참가하는 친구는 교내 개발동아리와 연합 개발동아리에서 프로젝트 개발을 하며 경험치를 많이 쌓아서 개발에 능통하다. 이 친구와 이번 주에는 프로젝트의 공통 컴포넌트를 제작하기로 했다. 친구는 컴포넌트를 어떻게 만들까? 친구의 코드를 살펴보기로 했다.
1. 폴더(파일) 구조부터 확인하자
src 안에 components 폴더를 만드는 것은 똑같은데, common components들을 저장할 common 폴더를 따로 만들고, 도메인별로 폴더를 만들어서 컴포넌트 파일들을 분리한다.
input 컴포넌트를 만든다고 가정하자. 친구는 components/common 안에 input 폴더를 만들고 그 안에 3개의 파일을 생성한다.
- index.ts
- Input.tsx
- Input.types.ts
index.ts에는 다음과 같은 코드가 작성된다.
export { default } from './Input';
export * from './Input.types';
위 코드를 보면 알다시피 Input 컴포넌트와 관련된 타입들을 export 하고 있다. 따로 이렇게 분리를 해놓은 이유가 궁금해서 AI에게 물어봤고 AI는 다음과 같이 답했다.
index.ts 파일의 역할은 주로 모듈의 진입점(entry point)으로 사용되며, 여기서는 Input 컴포넌트와 관련된 타입들을 외부로 노출시키는 역할을 합니다. 이러한 방식의 코드 구조는 몇 가지 이점이 있습니다:
- 간편한 임포트: 다른 파일에서 Input 컴포넌트를 사용할 때, 디렉토리 경로만으로 임포트할 수 있게 해줍니다. 예를 들어서 이렇게 하면 Input.tsx 파일을 직접 지정하지 않아도 됩니다. ->import Input from './components/Input';
- 타입 노출: Input.types.ts에서 정의된 모든 타입을 재익스포트함으로써, 이 모듈을 사용하는 다른 파일에서 타입을 쉽게 임포트할 수 있게 합니다.
- 캡슐화: 모듈의 내부 구조를 숨기고, 외부에서 필요한 부분만 노출시킵니다. 이는 모듈의 사용을 단순화하고 내부 구현의 변경을 더 쉽게 만듭니다.
- 유지보수성: 만약 나중에 Input 컴포넌트의 파일 이름이나 구조를 변경하더라도, 이 index.ts 파일만 수정하면 되므로 다른 곳에서의 임포트 문을 변경할 필요가 없습니다.
이러한 방식은 React 프로젝트에서 흔히 사용되는 패턴으로, 코드의 구조를 깔끔하게 유지하고 모듈의 사용을 단순화하는 데 도움을 줍니다.
2. Named Export와 Default Export
위와 같은 장점들 때문에 index.ts를 따로 생성했던 것이다. 여기서 생기는 궁금증, ‘./Input’은 default export를 활용했고(처음에는 default가 중괄호로 둘러쌓여 있어서 named export인가 싶었는데 default export가 맞는 것 같다.), ‘/Input.types’는 named export를 사용했다. 왜 이렇게 따로 사용하는 것일까?
일단 default export와 named export가 뭔지 알아보자.
- Named Export 특징
- 한 파일 내에서 여러개의 변수/클래스/함수를 Export 할 수 있다.
- Import할 때 as 키워드를 사용해서 다른 이름을 지정할 수 있다.
- Default Export 특징
- 한 파일 내에서 단 한개의 변수/클래스/함수만을 Export 할 수 있다.
- from 뒤에 명시한 파일에서 단 한개의 모듈만 가져오기 때문에 as 키워드 없이 원하는대로 이름을 바꿀 수 있다.
출처: https://velog.io/@oneook/ES6-Modules-Named-Export-vs-Default-Export
그렇다면 이제 알 수 있다. ‘./Input’에서는 Input 컴포넌트만 export하면 되기 때문에 default export를 활용하고, ‘./Input.types’에서는 여러 개의 타입을 export 해야 되기 때문에 named export를 활용하는 것이었다.
컴포넌트 폴더 안에 index.ts를 따로 생성해서 컴포넌트와 type들을 export해서 다른 곳에서 import를 편하게 하는 방법은 이번에 처음 알게 되었다. 유용하게 써야겠다.
3. as const, keyof, typeof, Record
그러면 이제 Input.types.ts를 가보자
export type InputProps = {
size: InputSize;
className?: string;
defaultValue?: string | number;
placeholder: string;
maxLength?: number;
icon?: React.ReactNode;
type?: string; // password input에 사용
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
};
export const inputSize = {
sm: 'sm',
md: 'md',
lg: 'lg',
xl: 'xl',
} as const;
export type InputSize = (typeof inputSize)[keyof typeof inputSize];
export const inputSize = {
sm: 'sm',
md: 'md',
lg: 'lg',
xl: 'xl',
} as const;
export type InputSize = (typeof inputSize)[keyof typeof inputSize];
조금 복잡한, 이 부분을 분석해보자.
- sm: ‘sm’으로 키와 값을 동일하게 한 이유는?
- 키 값의 정확성을 보장하기 위해서. 즉, inputsize.sm은 항상 ‘sm’ 문자열 값을 가지게 된다.
- 키와 값이 동일하면 나중에 코드를 유지보수하기 쉽다. 만약 새로운 크기를 추가해야 한다면, 단순히 객체에 새로운 키-값 쌍을 추가하면 된다. 예시: xxl: ‘xxl’
- as const는 TypeScript의 const assertion으로, 이 객체를 읽기 전용으로 만들고 각 프로퍼티를 리터럴 타입으로 추론하게 한다. → { readonly sm: "sm"; readonly md: "md"; readonly lg: "lg"; readonly xl: "xl"; }로 타입을 추론
- export type InputSize = (typeof inputSize)[keyof typeof inputSize];
- typeof inputSize: inputSize 객체의 타입을 가져온다.
- keyof typeof inputSize: inputSize 객체의 모든 키를 유니온 타입으로 가져온다.(keyof 연산자는 타입의 키들을 추출) → "sm" | "md" | "lg" | "xl"
- (typeof inputSize)[keyof typeof inputSize]: typeof inputSize 타입에서 키가 ‘sm’,’md’,’lg’,’xl’ 중 하나인 값의 타입을 나타냄(inputSize 객체의 모든 값들의 유니온 타입을 생성) → "sm" | "md" | "lg" | "xl"
그리고 Input.tsx에는 아래와 같은 style 객체가 선언되어 있다.
const style: {
base: string;
sizes: Record<InputSize, string>;
} = {
base: 'flex w-full h-10 rounded-[10px] px-[15px] border border-bk-50 focus:outline-sub-300 bg-wh text-body5 text-bk-90 placeholder-bk-50',
sizes: {
sm: 'max-w-60',
md: 'max-w-[280px]',
lg: 'max-w-[300px] !text-body4',
xl: 'h-[45px] focus:outline-sub-100', // 채팅 입력창
},
};
sizes에서 Record를 쓰는데,
Record는 TypeScript에서 제공하는 유틸리티 타입으로, 객체 타입을 정의할 때 사용된다. Record<K,T>의 형태로 사용되며, 여기서 K는 키의 타입, T는 값의 타입을 나타낸다.
→ Record를 활용해 특정 키 타입과 값 타입을 가진 객체를 정의할 수 있다.
Record<InputSize, string>의 뜻은 다음과 같다.
{
sm: string;
md: string;
lg: string;
xl: string;
}
그리고 style 객체에서는 sm, md, lg, xl에서 사이즈별 스타일을 지정해준다.
4. clsx를 통해 조건부 클래스 이름을 생성하자
그리고 이제 return에서 clsx를 활용한다.
return (
<div className={clsx(style.sizes[size], 'relative')}>
<div
className={clsx(
'absolute top-2 left-[15px]',
isFocused ? 'text-sub-300' : 'text-bk-50',
)}
>
{icon}
clsx는 조건부 클래스 이름을 생성하는데 사용되는 유틸리티 라이브러리이다.
-> 여러 클래스 이름을 동적으로 결합할 수 있게 해주고 조건부로 클래스를 추가하거나 제거할 수 있다.
사용법 예시
clsx('foo', 'bar'); // => 'foo bar'
clsx('foo', { bar: true }); // => 'foo bar'
clsx({ 'foo-bar': true }); // => 'foo-bar'
clsx({ 'foo-bar': false }); // => ''
clsx({ foo: true }, { bar: true }); // => 'foo bar'
clsx({ foo: true, bar: false }); // => 'foo'
<div className={clsx(style.sizes[size], 'relative')}>
위 코드는 이제 size가 sm이냐, md냐에 따라 아까 지정해놨던 사이즈별 스타일이 적용되고 ‘relative’는 항상 적용된다.
<div className={clsx(
'absolute top-2 left-[15px]',
isFocused ? 'text-sub-300' : 'text-bk-50',
)}>
위 코드의 경우는 ‘absolute top-2 left-[15px]는 항상 적용되고, isFocused의 여부에 따라 'text-sub-300' 또는 'text-bk-50' 클래스가 동적으로 적용된다.
위 방법들을 적용해서 나도 컴포넌트들을 만들고 있다. 친구 코드를 보면서 그동안 소홀히 했던 개념들에 대해 다시 복습하고, 또 모르는 내용들에 대해서 배울 수 있었다. 이제 열심히 개발해보자!!
'Frontend > React' 카테고리의 다른 글
'react-datepicker'에서 날짜 상태를 관리할 때 겪었던 시행착오 (0) | 2024.08.03 |
---|---|
[React, TS] 프로젝트를 진행하며 상태 관리에 대해 알게 된 것들 (1) | 2024.01.13 |
[React] useEffect()에 대해서 (0) | 2023.08.17 |
[React] Props에 대해서 (0) | 2023.08.17 |
[React] useState()에 대해서 (0) | 2023.08.17 |