공부해봅시당
[Typescript] JSX에서 TSX로 본문
들어가기 전
1. Component
컴포넌트는 리액트 애플리케이션의 가장 기본적인 빌딩 블록이다.
각각의 컴포넌트는 독립적이고 재사용 가능한 UI 조각을 정의하며, HTML, CSS, JavaScript 코드가 결합된 하나의 UI 단위를 표현한다.
클래스형 컴포넌트와 함수형 컴포넌트가 있다.
(1) 클래스형 컴포넌트(Class Component)
클래스 문법으로 정의된 컴포넌트이다.
리액트 생명주기 메서드를 사용할 수 있으며, state를 통해 상태를 관리한다.
class MyComponent extends React.Component {
render() {
return <div>Hello, {this.props.name}!</div>;
}
}
(2) 함수형 컴포넌트(Functional Component)
함수로 정의된 컴포넌트이다. 상태 관리는 주로 훅(Hooks)을 통해 이루어지며, 간결한 문법이 특징이다.
따라서 최근 몇 년 동안 리액트 개발 커뮤니티에서는 함수형 컴포넌트가 클래스형 컴포넌트보다 선호되는 추세이다.
특히 리액트 16.8 버전에서 **훅(Hooks)**이 도입된 이후 더욱 두드러졌다.
function MyComponent(props) {
return <div>Hello, {props.name}!</div>;
}
Hooks
Hook을 이용하여 기존 Class 바탕의 코드를 작성할 필요 없이 상태 값과 여러 React의 기능을 사용할 수 있다.
클래스 없이도 상태를 관리하거나, 리액트의 생명주기 메서드를 사용하는 것과 같은 효과를 낼 수 있다.
은 기존의 클래스형 컴포넌트의 복잡성을 줄이고, 함수형 컴포넌트의 단순함을 유지하면서도 다양한 기능을 활용할 수 있게 해준다.
useState
useState 훅은 함수형 컴포넌트에서 상태를 관리할 수 있게 해준다.
호출 시 상태 변수와 상태를 갱신할 수 있는 함수를 반환한다.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
위 코드에서 useState(0)은 count라는 상태 변수와 setCount라는 상태를 업데이트하는 함수를 반환한다.
useEffect
useEffect 훅은 함수형 컴포넌트에서 부수 효과(side effects)를 처리할 수 있게 해준다. 이는 componentDidMount, componentDidUpdate, componentWillUnmount 같은 생명주기 메서드를 대체한다.
import React, { useEffect } from 'react';
function Example() {
useEffect(() => {
console.log('Component mounted or updated');
return () => {
console.log('Component will unmount');
};
}, []);
return <div>Example Component</div>;
}
- 첫 번째 인자로 전달된 함수는 컴포넌트가 렌더링되거나 업데이트될 때 실행된다.
- 두 번째 인자로 전달된 빈 배열 []은 이 효과가 컴포넌트가 처음 마운트될 때만 실행되고, 언마운트될 때만 클린업이 실행되도록 만든다.
useContext
useContext 훅은 리액트의 컨텍스트 API를 사용하여, 전역 상태를 쉽게 사용할 수 있게 해준다. useContext를 통해 컨텍스트에서 제공하는 값을 읽어올 수 있다.
import React, { useContext } from 'react';
const MyContext = React.createContext();
function Display() {
const value = useContext(MyContext);
return <div>{value}</div>;
}
function App() {
return (
<MyContext.Provider value="Hello from context">
<Display />
</MyContext.Provider>
);
}
2. Props
props(properties의 줄임말)는 컴포넌트 간 데이터를 전달하는 방법이다.
부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달할 때 사용된다.
props는 읽기 전용으로, 자식 컴포넌트에서 변경할 수 없다.
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
function App() {
return <Greeting name="React" />;
}
위 예시에서 App 컴포넌트는 Greeting 컴포넌트에 name이라는 props를 전달하며, Greeting은 이를 받아서 화면에 출력한다.
3. State
state는 컴포넌트 내부에서 관리되는 동적인 데이터를 나타낸다.
state는 컴포넌트 내에서 값이 변경될 수 있으며, 값이 변경되면 해당 컴포넌트가 다시 렌더링된다.
state는 주로 클래스형 컴포넌트와 함수형 컴포넌트의 훅(Hooks)에서 사용된다.
클래스형 컴포넌트에서의 state:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
함수형 컴포넌트에서의 state (React Hooks 사용):
function Counter() {
const [count, setCount] = React.useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
특징 | props | state |
데이터 소유권 | 부모 컴포넌트가 소유 | 컴포넌트 자체가 소유 |
변경 가능 여부 | 변경 불가 (읽기 전용) | 변경 가능 (컴포넌트 내부에서만) |
사용 목적 | 컴포넌트 간 데이터 전달 | 컴포넌트의 동적 데이터 관리 |
초기화 및 설정 | 부모 컴포넌트에서 설정 | 컴포넌트 내부에서 초기화 (constructor 또는 useState) |
데이터 전달 방식 | 부모에서 자식 컴포넌트로 전달 | 컴포넌트 내부에서 관리 |
리렌더링에 대한 영향 | props가 변경되면 자식 컴포넌트가 리렌더링됨 | state가 변경되면 해당 컴포넌트가 리렌더링됨 |
주요 사용 사례 | 컴포넌트 간 데이터 전달, 컴포넌트 재사용성 향상 | 사용자 입력, 네트워크 응답 등 동적 데이터 관리 |
리액트 컴포넌트의 타입
1. 클래스 컴포넌트 타입
아래의 예시를 통해 클래스 컴포넌트 타입에서 타입을 쓰는 것을 확인할 수 있다.
import { ComponentLifecycle } from 'react';
interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> {}
class Component<P, S> {}
class PureComponent<P = {}, S = {}, SS = any> extends Component<P, S, SS> {}
interface WelcomProps {
name: string;
}
class Welcome extends React.Component<WelcomProps> {
// 생략
}
P와 S는 각각 props와 state이다.
SS는 getSnapshotBeforeUpdate 생명주기 메서드에서 반환된는 값을 나타낸다.
getSnapshotBeforeUpdate 메서드는 리액트의 생명주기 메서드 중 하나로, 컴포넌트가 DOM에 반영되기 직전에 호출된다.
이 메서드는 컴포넌트가 업데이트되기 직전의 상태에 기반하여 스냅샷 데이터를 생성하고 반환한다.
SS는 이 메서드에서 반환된 스냅샷 데이터를 나타내며, 이 데이터는 이후의 componentDidUpdate 메서드에서 세 번째 인자로 전달된다.
2. 함수 컴포넌트 타입
//함수 선언을 사용
function Welcome(props: WelcomeProps): JSX.Element {
return <div></div>;
}
// 함수 표현식을 사용한 방식 3가지
const Welcome2: React.FC<WelcomeProps> = ({ name }) => {
return <div></div>;
};
const Welcome3: React.VFC<WelcomeProps> = ({ name }) => {
return <div></div>;
};
const Welcome4 = ({ name }: WelcomeProps): JSX.Element => {
return <div></div>;
};
FC는 FunctionComponent 의 약자이고, VFC는 children이 없는 FC이다. 18v에서는 VFC가 사라지고, FC의 기본 옵션이 children 있음에서 없음으로 변경되었다.
3. Children props 타입 지정
가장 보편적인 children의 타입은 ReactNode | undefined가 된다.
ReactNode 타입은 ReactElement 외에도 boolean, number 등 여러 타입을 포함하고 있는 타입이다.
세분화하고 싶으면 아래와 같이 사용가능하다.
type SpecificProps = {
children: '천생연분' | '더 귀한 분' | '귀한 분' | '고마운 분';
};
type StringProps = {
children: string;
};
type ReactElementProps = {
children: ReactElement;
};
4. render 메서드와 함수 컴포넌트의 반환 타입-React.ReactElement, JSX.Element, React.ReactNode
React.ReactElement 와 JSX.Element , React.ReactNode 타입은 헷갈리기 쉽다.
3가지 모두 리액트의 요소를 나타내는 타입인데, 차이점이 존재한다.
JSX.Element < React.ReactElement < React.ReactNode ( 포함관계)
JSX.Element
declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> {}
}
}
JSX.Element 타입은 위의 코드를 보면 알 수 있듯이 ReactElement를 확장하고 있는 타입이며, 글로벌 네임스페이스에 정의되어 있어 외부 라이브러리에서 컴포넌트 타입을 재정의 할 수 있는 유연성을 제공한다.
JSX.Element는 ReactElement의 특정 타입으로 props와 타입 필드를 any 로 가지는 타입이다.
리액트 엘리먼트를 prop으로 전달받아 render props 패턴으로 컴포넌트를 구현할 때 유용하다.
interface Props {
icon: JSX.Element;
}
const Item = ({ icon }: Props) => {
const iconSize = icon.props.size;
return <li>{icon}</li>;
};
const App = () => {
return <Item icon={<Icon size={14} />} />;
};
React.ReactElement
React. ReactElement 는 리액트 컴포넌트를 객체 형태로 저장하기 위한 포맷이다.
interface ReactElement<
P = any,
T extends string | JSXElementConstructor<any> =
| string
| JSXElementConstructor<any>
> {
type: T;
props: P;
key: Key | null;
}
ReactElement 타입은 JSX의 createElement 메서드 호출로 생성된 리액트 엘리먼트를 나타내는 타입이다.
ReactElement의 제네릭으로 컴포넌트의 props를 지정해 줄 수 있다.
React.ReactNode
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
type ReactFragment = {} | Iterable<ReactNode>;
type ReactNode =
| ReactChild
| ReactFragment
| ReactPortal
| boolean
| null
| undefined;
ReactNode는 리액트의 render함수가 반환할 수 있는 모든 형태를 담고 있다.
따라서 prop으로 리액트 컴포넌트가 다양한 형태를 가질 수 있게 하고 싶을 때 유용하게 사용된다.
5. 리액트에서 기본 HTML 요소 타입 활용하기
HTML 태그의 속성 타입을 활용하는 대표적인 2가지 방법은 DetailedHTMLProps 와 ComponentWithoutRef가 있다.
차이점은 ref의 포함 유무이다.
DetailedHTMLProps
type NativeButtonProps = React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>;
type ButtonProps = {
onClick?: NativeButtonProps['onClick'];
};
ComponentWithoutRef
type NativeButtonType2 = React.ComponentPropsWithoutRef<'button'>;
type ButtonProps2 = {
onClick?: NativeButtonType2['onClick'];
};
최근에는 함수 컴포넌트를 많이 쓴다. 이때 함수 컴포넌트의 props로 DetailedHTMLProps와 같이 ref를 포함하는 타입을 사용하게 되면, 실제로는 동작하지 않는 ref를 받도록 타입이 지정되어 예기치 않은 에러가 발생할 수 있다
따라서 HTML 속성을 확장하는 props를 설계할 때는 ComponentPRopsWithoutRef 타입을 사용하여 ref가 실제로 forwardRef와 함께 사용할 때만 props로 전달되도록 타입을 정의하는 것이 안전하다.
타입스크립트로 리액트 컴포넌트 만들기
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와 같이 함수 타입을 지정하면 공변성과 반공변성을 가지는 이변성을 띠게 된다.
안전한 타입 가드를 위해서는 특수한 경우를 제외하고는 일반적으로 반공변적인 함수 타입을 설정하는 것이 권장된다.
출처
https://ungumungum.tistory.com/91
우아한 타입스크립트 with React -8장 JSX에서 TSX로 - 1
리액트 컴포넌트의 타입 리액트로 타입스크립트로 작성할 때 @types/react 패키지에 정의된 내장 타입을 사용한다. 이중에서 헤깔릴 수 있는 타입도 존재하고 그에 대한 유의점을 알려준다. 1. 클래
ungumungum.tistory.com
📘 타입스크립트 공변성 & 반공변성 완벽 이해
타입의 공변성과 반공변성 타입스크립트는 자바스크립트에 타입을 추가해준 라이브러리 이지만, 타입을 다루는 언어이기도 하다. 그래서 어느새 타입 자체를 코딩하고 있는 자신을 발견하기도
inpa.tistory.com
'STUDY > Typescript' 카테고리의 다른 글
[Typescript] 상태관리 (1) | 2024.09.03 |
---|---|
[Typescript] Hook (0) | 2024.09.03 |
[Javascript] JSX(JavaScript XML) (0) | 2024.09.03 |
[Typescript] 비동기 호출 (0) | 2024.08.20 |
[Typescript] 타입스크립트 컴파일 (0) | 2024.08.20 |