리액트 함수 컴포넌트에서 상태를 관리하기 위해 useState 훅을 활용할 수 있다.
function useState<S>(
initalState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>];
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);
튜플의 첫 번째 요소는 제네릭으로 지정한 S 타입이며, 두 번쨰 요소는 상태를 업데이트할 수 잇는 Dispatch 타입의 함수이다. Dispatch 함수의 제네릭으로 지정한 SetStateAction에는 useState로 관리할 상태 타입인 S 또는 이전 상태 값을 받아 새로운 상태를 반환하는 함수인 (prevState:S)⇒S가 들어갈 수 있다.
import React from "react";
const MemberList = () => {
const [memberList, setMemberList] = useState([
{
name: "kim",
age: 10,
},
{
name: "park",
age: 20,
},
]);
};
// addMember 함수를 호출하면 sumAge는 NaN이 된다.
const sumAge = memberList.reduce((sum, member) => sum + member.age, 0);
const addMember = () => {
setMemberList([
...memberList,
{
name: "lee",
age: 11,
},
]);
};
이 예시의 memberList에 새로운 맴버 객체를 추가할 때 문제가 발생한다. 기존 memberList 배열 요소에는 없는 agee라는 잘못된 속성이 포함된 객체가 추가되었다. sumAge 변수가 NaN이 되는 예상치 못한 사이드 이펙트가 발생
import React from "react";
interface Member {
name: string;
age: number;
}
const MemberList = () => {
const [memberList, setMemberList] = useState<Member[]>([]);
};
const sumAge = memberList.reduce((sum, member) => sum + member.age, 0);
const addMember = () => {
//Error: Type Member | {name:string; agee:number;}
//is not assignable to type 'Member'
setMemberList([
...memberList,
{
name: "lee",
agee: 11,
},
]);
};
useEffect와 useLayoutEffect
렌더링 이후 리액트 함수 컴포넌트에 어떤 일을 수행해야 하는지 알려주기 위해 useEffect 훅을 활용할 수 있다.
function useEffect(effect:EffectCallback, deps?:DependencyList):void;
type DependencyList = ReadonlyArray<any>;
type EffectCallback = () => void | Destructor;
useEffect의 첫 번째 인자이자 effect의 타입인 EffectCallback은 Destructor를 반환하거나 아무것도 반환하지 않는 함수이다. Promise 타입은 반환하지 않으므로 useEffect의 콜백 함수에는 비동기 함수가 들어갈 수 없다. useEffect에서 비동기 함수를 호출할 수 있다면 경쟁 상태를 불러일으킬 수 있기 때문.
💡 경쟁상태 멀티스레당 환경에서 동시에 여러 프로세스나 스레드가 공유된 자원에 접근하려고 할 때 발생할 수 있는 문제. 이러한 실행 순서나 타이밍을 예측할 수 없게 되어 프로그램 동작이 원하지 않는 방향으로 흐를 수 있다.두 번째 인자인 deps는 옵셔널하게 제공되며 effect가 수행되기 위한 조건을 나열한다 .예를 들어 deps 배열의 원소가 변경되면 실행한다는 식으로 사용. 다만 deps의 원소로 숫자나 문자열 같은 타입스크립트 기본 자료형이 아닌 객체나 배열을 넣을 때는 주의
type SomeObject = {
name:string;
id:string;
}
interface LabelProps{
value:SomeObject;
}
const Label:Rect.FC<LabelProps> = ({value})=>{
useEffect(()=>{
//value.name과 value.id를 사용해서 작업.
},[value])
//
}
useEffect는 deps가 변경되었는지를 얕은 비교로만 판단하기 때문에, 실제 객체값이 바뀌지 않았더라도 객체의 참조 값이 변경되면 콜백 함수가 실행. 앞의 예시처럼 부모에서 받은 인자를 직접 deps로 작성한 경우, 원치 않는 렌더링이 반복될 수 있다. 이를 방지하기 위해서든 다음과 같이 실제로 사용하는 값을 useEffect의 deps에 사용해야 함.
💡 얕은 비교 객체나 배열과 같은 복합 데이터 타입의 값을 비교할 때 내부의 각 요소나 속성을 재귀적으로 비교하지 않고, 해당 값들의 참조나 기본 타입값만을 간단하게 비교하는 것을 말함. 💡 클린업 함수 useEffect나 useLayoutEffect와 같은 리액트 훅에서 사용되며, 컴포넌트가 해제 되기전에 정리작업을 수행하기 위한 함수를 말한다.type DependencyList = ReadonlyArray<any>;
function useLayoutEffect(efffect:EffectCallback,deps?:DependencyList):void;
@types/react 패키지에 정의 된 타입을 살펴보자.
declare namespace React{
//ReactElement
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = | string | JSXElementConstructor<any>>{
type:T;
props:P;
key: Key | null;
}
//ReactNode
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
type ReactFragment = {} | Iterable<ReactNode>;
type ReactNode =
| ReactChild
| ReactFragment
| ReactPortal
| boolean
| null
| undefined
type ComponentType<P ={}> = ComponentClass<P> | functionComponent<P>;
}
//JSX.Element
namespace JSX{
interface Element extends React.ReactElement<any,any>{
}
}
리액트 엘리먼트를 생성하는 createElement 메서드에 대해 들어본 적이 있을 것이다. 리액트를 사용하면 JSX라는 자바스크립트를 확장한 문법을 자주 접했을 탠데 JSX가 createElement 메서드를 호출하기 위한 문법
const element = React.createElement(
"h1",
{ className: "greeting" },
"Hello,world"
);
//다음 구조는 단순화 되었따.
const element = {
type: "h1",
props: {
className: "greeting",
children: "hello,world!",
},
};
declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> {}
}
}
ReactNode 타입에 대해 알아보기 전엔 먼저 ReatChildren 타입을 살펴보자.
type ReactText = string | number;
type ReactChild = ReactElement | ReactText
ReactChild 타입은 ReactElement | string | number로 정의되어 ReactElement 보다는 좀 더 넓은 범위를 갖고 있다.
type ReactFragement = {} | Iterable<ReactNode>;
type ReactNode = ReactChild |
ReactFragement
| ReactPortal
| boolean
| null
| undefined
ReactNode는 앞에서 설명한 ReactChild 외에도 boolean, null, undefined 등 훨씬 넓은 범주의 타입을 포함. 즉, ReactNode는 리액트의 render 함수가 반환할 수 있는 모든 형태를 담고 있다고 볼 수 있음.
declare global{
namespace JSX{
interface Element extends React.ReactElement<any,any>{
}
}
}
jsx.Element는 ReactElement의 제네릭으로 props와 타입 필드에 대해 any 타입을 가지도록 확장하고 있다. 즉 JSX.Element는 ReactElement의 특정 타입으로 props와 타입 필드를 any로 가지는 타입이라는 것을 알 수 있다.
ReactNode
리액트의 render 함수가 반환할 수 있는 모든 형태를 담고 있기 때문에 리액트 컴포넌트가 가질 수 있는 모든 타입을 의미한다.
interface MyComponentProps{
children?:React.ReactNode;
}
JSX 형태의 문법을 때로는 string, number, null, undefined같이 어떤 타입이든 children prop으로 지정할 수 있게 하고 싶다면 ReactNode 타입으로 children을 선언하면 된다.
type PropsWithChildren<P = unknown> = P & {
children?:ReactNode | undefined;
}
interface MyProps{
}
type MyComponentProps = PropsWithChildren<MyProps>
JSX.Element
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} />} />;
};
ReactElement
JSX.Element 예시를 확장하여 추론 관점에서 더 유용하게 활용할 수 있는 방법은 JSX.Element 대신에 ReactElemeent을 사용하는 것. 이때 원하는 컴포넌트의 props을 ReactElement의 제네릭으로 지정해줄 수 있다. 만약 JSX.element가 ReactElement의 props 타입으로 any가 지정되었다면, ReactElement 타입을 활용하여 제네릭에 직접 해당 컴포넌트의 props 타입을 명시해준다.
interface IconProps {
size: number;
}
interface Props {
//Reactelement의 props 타입으로 IconProps 타입 지정
icon: React.ReactElement<IconProps>;
}
const Item = ({ icon }: Props) => {
//icon prop으로 받은 컴포넌트의 props에 접근하면 props의 목록이 추론된다.
const iconSize = icon.props.size;
return <li>{icon}</li>
};
const Squrebutton = () => <button>정사각형 버튼</button>
기존 HTML 태그의 속성 타입을 활용하여 타입을 지정하는 방법 예)onClick
먼저 React.DetailedHTmlProps를 활용하는 경우에는 아래와 같이 쉽게 HTML 태그 속성과 호환되는 타입을 선언할 수 있다.
type NativeButtonProps = React.DetailedHTMLProps<RecordingState.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement>;
type ButtonProps={
onClick?:NativeButtonProps["onClick"]
}
React.ComponentWithoutRef 타입은 아래와 같이 활용
type NativeButtonType = React.ComponentPropsWithoutRef<"button">;
type ButtonProps {
onClick?:NativeButtonType["onClick"]
}
HTML.button 태그와 동일한 역할을 하지만 커스텀한 UI를 적용하여 재사용성을 높이기 위한 Button 컴포넌트를 만든다고 가정해보자.
const Button = ()=>{
return <button>버튼</button>;
}
먼저 HTLM.Button 태그를 대체하는 역할이므로 아래와 같이 기존 button 태그의 HTML 속성을 props로 받을 수 있게 지원해야 할 것.
HTMLButtonElement의 속성을 모두 props로 받아 button 태그에 전달했으므로 문제없어 보임. 그러나 ref를 props로 받을 경우 고려해야할 사항이 있다.
아래는 JSX로 구현된 Select 컴포넌트이다. 이 컴포넌트는 각 option의 키 값 쌍을 객체를 받고 있으며, 선택된 값이 변경될 때 호출되는 onChange 이벤트 핸들러를 받도록 구현되어 있다.
const Select = ({ onChange, options, selectedOption }) => {
const handleChange = (e) => {
const selected = Object.entries(options).find(
([, value]) => value === e.taget.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>
);
};
컴포넌트의 속성 타입을 명시하기 위해 JSDocs를 사용할 수 있다. JSDoc를 활용하면 컴포넌트에 대한 설명과 각 속성이 어떤 역할을 하는지 간단하게 알려줄 수 있다.
/**
* Select 컴포넌트
* @param {Object} props - Select 컴포넌트로 넘겨주는 속성
* @param {Object} props.options {[key:string]:string} 형식으로 이루어진 option 객체
* @param {string | undefined} props.selectedOption - 현재 선택된 option의 key값 {optional}
* @param {function } props.onChange - select 값이 변경되었을 때 불리는 callback 함수
* @returns {JSX.Element}
*/
options가 어떤 형식의 객체를 나타내는지나 onChange의 매개변수 및 반환 값에 대한 구체적인 정보를 알기 쉽지 않아서 잘못된 타입이 전달될 수 있는 위험이 존재한다. 타입스크립트를 사용하여 이러한 문제를 해결하기 위해 좀 더 정교하고 구체적인 타입을 지정할 수 있다. JSX 파일의 확장자를 TSX로 변경한 후에 TSX로 변경한 후에 Select 컴포넌트의 props에 대한 인터페이스를 작성해보자.
type Option = Record<string, string>; //
interface SelectProps{
option:Option;
selectedOption?:string;
onChange?:(selected?:string) =>void;
}
const Select = ({options,selectedOption,onChange}:SeleectProps):JSX.Element=>
예시에서는 먼저 Option이라는 타입을 정의하고, SelectProps에서 이 타입을 재사용하고 있다. Record는 키와 값의 타입이 모두 string인 객체 타입을 생성하는 유틸리티 타입으로 사용된다. options의 타입을 정의해줌으로써 string이 아닌 배열이나 다른 유형의 value를 가진 객체는 전달할 수 없게 되었다. onChange는 선택된 string 값을 매개변수로 받고 어떤 값도 반환하지 않는 함수임을 명확하게 표현하고 있다. 또한 onChange는 옵셔널 프로퍼티이기 때문에
리액트는 가상 DOM을 다루면서 이벤트도 별도로 관리한다. onClick, onChange같이 DOM 엘리먼트에 등록되어 처리하는 이벤트 리스너와 달리, 리액트 컴포넌트에 등록되는 이벤트 리스너는 onClick, onChange처럼 카멜 케이스로 표기.
또한 리액트는 브라우저 이벤트를 합성한 합성 이벤트를 제공.
type EventHandler<Event extends React.SyntheticEvent> = (
e: Event
) => void | null;
type ChangeEventHandler = EventHandler<ChangeEvent<HTMLSelectElement>>;
const eventHandler1:GlobalEventHandlers["onChange"] = (e) =>{
e.target ; // 일반 Event는 target이 없음.
}
const eventHandler2:ChangeEventHandler = (e)=>{
e.target
}
리액트에서 제공하는 기본 컴포넌트도 SelectProps처럼 각각 props에 대한 타입을 명시해두고 있으므로 리액트 컴포넌트에 연결할 이벤트 핸들러도 해당 타입을 일치 시켜줘야 한다.
useState 같은 함수 역시 타입 매개변수를 지정해줌으로써 반환되는 state 타입을 지정해 줄 수 있다. 만약 제네릭 타입을 명시하지 않으면 타입스크립트 컴파일러는 초기값의 타입을 기반으로 state 타입을 추론.
const fruits = {
apple: "사과",
banana: "바나나",
blueberry: "블루베리",
};
const FruitSelect: VFC = () => {
const [fruit, changeFruit] = useState<string | undefined>();
return(
<Select onChange={changeFruit} options={fruit} selectedOption={fruit}/>
)
};
const FruitSelect = () => {
const [fruit, changeFruit] = useState<string | undefined>();
return(
<Select onChange={changeFruit} options={fruit} selectedOption="oragne"/>
)
};
interface SelectProps<OptionType extends Record<string,string>>{
options:OptionType;
selectedOption?:keyof OptionType;
onChange?:(selected?:keyof OptionType)=>void;
}
const Select = <OptionType extends Record<string,string>>({
options,
selectedOption,
onChange,
}:SelectProps<OptionType>)=>{
}
Select 컴포넌트에 전달되는 props의 타입 기반으로 타입이 추론되어 <Select<추론된_타입>> 형태의 컴포넌트가 생성된다. 이제 FruitSelect에서 잘못된 selectedOption을 전달하면 타입 에러가 발생
className,id와 같은 리액트 컴포넌트의 기본 props를 추가하려면 SelectProps에 직접 className?: string; id?:string;을 넣어도 되지만 리액트에서 제공하는 타입을 사용하면 더 정확한 타입을 설정할 수 있다.
type ReactSelectProps = React.ComponentPropsWithoutRef<'select'>
const Select = <OptionType extends Record<string,string>>({
id?:ReactSelectProps["id"],
className?:ReactSelectProps["className"]
}:SelectProps<OptionType>)=>{
}
css 파일 대신 자바스크립트 안에 직접 스타일을 정의한느 css-in-js 기법을 사용
const theme = {
fontSize: {
default: "16px",
small: "14px",
large: "18px",
},
color: {
white: "#fffff",
black: "#000000",
},
};
const Theme = {
fontSize: {
default: "16px",
small: "14px",
large: "18px",
},
color: {
white: "#fffff",
black: "#000000",
},
};
type FontSize = keyof Theme["fontSize"];
type Color = keyof Theme["color"]
StyeldSelect를 작성
interface SelectStyleProps {
color: Color;
fontSize: FontSize;
}
const StyledSelect = styled.select<SelectStyleProps>`
color:${({color}=>theme.color[color])}
font-size:${({fontSize})=>theme.fontSize[fontSize]}
`;