타입스크립트로 리액트 컴포넌트 만들기
1. 컴포넌트 속성 타입 명시하기
일반적인 JSX로 작성된 컴포넌트를 봤을 때, 각 속성에 어떤 타입의 값을 전달해야 할 지 명확하기 알기 힘들다.
이를 JSX로 작성된 Select 컴포넌트를 통해서 개선해보자
const Select = ({ onChange, options, selectedOption }) => {
const handleChange = (e) => {
const selected = Object.entries(options).find(
([_, value]) => value === e.target.value
)?.[0];
onChange?.(selected);
};
return (
<select
onChange={handleChange}
value={selectedOption && options[selectedOption]}
>
{Object.entries(options).map(([key, value]) => (
<option key={key} value={value}>
{value}
</option>
))}
</select>
);
};
1-1) JSDocs로 해결하기
컴포넌트의 속성 타입을 명시하기 위해서 JSDocs를 사용할 수 있다. 이를 통해 컴포넌트에 대한 설명과 각 속성이 어떤 역할을 하는지 간단하게 알려줄 수 있다.
/**
*Select 컴포넌트
*@param {Object}props - Select 컴포넌트가 넘겨주는 속성
*@param {Object}props.options - { [key:string]:string} 형식으로 이루어진 객체
*@param {string | undefined} props.selectedOption - 현재 선택된 option의 key 값 (optional)
*** 등등
*/
1-2) props 인터페이스 적용하기
JSDocs를 사용해도, options가 어떤 형식의 객체를 나타내는지나, onChange의 매개변수 및 반환 값에 대한 구체적인 정보를 알기는 쉽지 않다. 하지만 타입스크립트를 사용하면 좀 더 정교하고 구체적인 타입을 지정할 수 있다.
type Option = Record<string, string>;
interface SelectProps {
options: Option;
selectedOption?: string;
onChange?: (selected?: string) => void;
}
const Select = ({
onChange,
options,
selectedOption,
}: SelectProps): JSX.Element => {
const handleChange = (e) => {
const selected = Object.entries(options).find(
([_, value]) => value === e.target.value
)?.[0];
onChange?.(selected);
};
return (
<select
onChange={handleChange}
value={selectedOption && options[selectedOption]}
>
{Object.entries(options).map(([key, value]) => (
<option key={key} value={value}>
{value}
</option>
))}
</select>
);
};
2. 리액트 이벤트
리액트에서는 가상 DOM을 다루면서 이벤트도 별도로 관리한다. 따라서 리액트 이벤트는 브라우저의 고유한 이벤트와 완전히 동일하게 동작하지는 않는다. (ex : 리액트 이벤트 핸들러는 이벤트 버블링 단계에서 호출됨)
또한 리액트는 브라우저 이벤트를 함성한 합성 이벤트(SyntheticEvent)를 제공한다.
앞선 코드에서 handleChange 함수의 타입이 명시되지 않았다. 이를 다뤄보려고 한다.
React.ChangeEventHandler<HTMLSelectElement> 타입을 적용하였다.
const Select = ({
onChange,
options,
selectedOption,
}: SelectProps): JSX.Element => {
const handleChange: React.ChangeEventHandler<HTMLSelectElement> = (e) => {
const selected = Object.entries(options).find(
([_, value]) => value === e.target.value
)?.[0];
onChange?.(selected);
};
return (
<select
onChange={handleChange}
value={selectedOption && options[selectedOption]}
>
{Object.entries(options).map(([key, value]) => (
<option key={key} value={value}>
{value}
</option>
))}
</select>
);
};
3. 훅에 타입 추가하기
훅에서는 제네릭을 사용하여 타입을 추가할 수 있다.
useState를 예시로 들면 아래 state는 string과 undefined(초기 값이 없는 경우)가 가능하다.
만약 타입을 지정하지 않는다면 undefined만 오게 된다. 이때 훅에서 타입을 세세하게 잡아서 사이드 이펙트를 방지할 수 있다.
const [state, setState] = useState<string | undefined>();
4. 제네릭 컴포넌트 만들기
select의 옵션의 경우 일반적인 Record<string,string> 으로 타입을 지정할 경우, 올바르지 않은 옵션을 받아도 에러가 발생하지 않는다. 하지만 사용하는 입장에서 불편하게 되는데, 이럴 때 제네릭을 사용한 컴포넌트로 제한된 키와 벨류만 받을 수 있도록 할 수 있다.
interface GenericSelectProps<OptionType extends Record<string, string>> {
options: OptionType;
selectedOption?: keyof OptionType;
onChange?: (selected?: keyof OptionType) => void;
}
const GenericSelect = <OptionType extends Record<string, string>>({
options,
selectedOption,
onChange,
}: GenericSelectProps<OptionType>) => {
//
};
5. HTMLAttribuites, ReactProps 적용하기
className, id 와 같은 리액트 컴포넌트의 기본 props를 리액트에서 제공하는 타입을 사용하면 더 정확한 타입을 설정할 수 있다.
6. styled-component에 타입 적용하기
컴포넌트에 CSS파일 대신 자바스크립트 안에 직접 스타일을 정의하는 방식을 css-in-js라고 한다.
그 중 대표 라이브러리인 styled-component에서 typescript를 적용하면 아래와 같다.
type Theme = typeof theme;
type FontSize = keyof Theme['fontSize'];
type Color = keyof Theme['color'];
interface SelectStyleProps {
color: Color;
fontSize: FontSize;
}
const StyledSelect = styled.select<SelectStyleProps>`
color: ${({ color }) => theme.color[color]};
`;
7. 공변성과 반공변성
일반적인 타입은 공변성을 가지고 있어서 좁은 타입에서 넓은 타입으로 할당이 가능하다.
interface User {
id: string;
}
interface Member extends User {
nickname: string;
}
let users: Array<User> = [];
let members: Array<Member> = [];
users = members;
members = users//User[]' 형식은 'Member[]' 형식에 할당할 수 없습니다. 'nickname' 속성이 'User' 형식에 없지만 'Member' 형식에서 필수입니다.
하지만 제네릭 타입은 반공변성을 지닌다. 즉 T<B>가 T<A>의 서브타입이 되어 좁은 타입T<A>의 함수를 넓은 타입T<B>의 함수에 적용할 수 없다.
type PrintUserInfo<U extends User> = (user: U) => void;
let printUser: PrintUserInfo<User> = (user) => console.log(user.id);
let printMember: PrintUserInfo<Member> = (user) =>
console.log(user.id, user.nickname);
printMember = printUser;
printUser = printMember; //intUserInfo<Member>' 형식은 'PrintUserInfo<User>' 형식에 할당할 수 없습니다. 'nickname' 속성이 'User' 형식에 없지만 'Member' 형식에서 필수입니다.
마찬가지로
interface Props<T extends string> {
onChangeA?: (selected: T) => void;
onChangeB?(selected: T): void;
}
A와 같이 함수 타입을 화살표 표기법으로 작성한다면 반공변성을 띠게 된다.
B와 같이 함수 타입을 지정하면 공변성과 반공변성을 가지는 이변성을 띠게 된다.
안전한 타입 가드를 위해서는 특수한 경우를 제외하고는 일반적으로 반공변적인 함수 타입을 설정하는 것이 권장된다.
📘 타입스크립트 공변성 & 반공변성 완벽 이해
타입의 공변성과 반공변성 타입스크립트는 자바스크립트에 타입을 추가해준 라이브러리 이지만, 타입을 다루는 언어이기도 하다. 그래서 어느새 타입 자체를 코딩하고 있는 자신을 발견하기도
inpa.tistory.com
를 참고해도 좋을 거 같다.
'독서' 카테고리의 다른 글
우아한 타입스크립트 with React -10장 상태 관리 (0) | 2024.03.17 |
---|---|
우아한 타입스크립트 with React - 9장 훅 (0) | 2024.03.14 |
우아한 타입스크립트 with React -8장 JSX에서 TSX로 - 1 (0) | 2024.03.06 |
우아한 타입스크립트 with React -7장 비동기 호출 -2 (0) | 2024.02.26 |
우아한 타입스크립트 with React -7장 비동기 호출 -1 (0) | 2024.02.25 |