타입스크립트를 이용하여 기본적인 리액트 훅을 사용하는 방법 알아보자
타입을 정의하지 않고 사용하면 다음과 같은 에러가 난다.
⚠️ Type '사용한 타입(number, string...)' is not assignable to type 'never'.
당황하지 말고 useState 함수를 호출할 때 어떤 타입이 state로 들어갈지 지정해주면 된다.
export default function UseStateComponent() {
const [arr, setArr] = useState<number[]>([]);
// 타입 없이 useState([])를 쓰면 never[] 타입이 된다.
// Error ===> Type 'number' is not assignable to type 'never'.
// useState<number[]> 와 같이 타입을 지정해주기
return (
<div>
<div>
<button onClick={() => setArr([...arr, arr.length + 1])}>Add to array</button>
{JSON.stringify(arr)}
</div>
</div>
);
}
이 경우도 마찬가지다. null로 값을 초기화하여 타입스크립트는 name의 타입이 null일 것이라고 추론했고, SetStateAction이라는 내부 로직에서도 추론한 반환값인 null로 동작하려고 해 오류가 난다.
string 혹은 null 값이 name으로 올 수 있다고 정의해주자.
⚠️ TS2345: Argument of type '"Jim"' is not assignable to parameter of type 'SetStateAction'.
const [name, setName] = useState<string | null>(null);
return (
<div>
<button onClick={() => setName(**'Jim'**)}>Add to array</button>
{JSON.stringify(name)}
</div>
);
useEffect의 정의를 살펴보자.(VS코드에서 f12 혹은 우클→Go to definition)
useEffect는 첫 번째 인자로 EffectCallback 타입을 받고 있다. EffectCallback이란 함수이며, ‘void’ 혹은 ‘void나 undefined를 반환하는 함수’를 반환한다는 것을 알 수 있다.
💡 props 드릴링을 할 필요 없이 createContext로 생성한 context를 이용해 Provider를 만들 수 있다. Provider의 자식 위치에서 useContext(스토어)를 사용하면 Consumer 로서 스토어에 저장된 값들을 빼올 수 있다.
컨텍스트를 만들어주는 createContext는 어떻게 정의되어 있을까?
제네릭으로 타입을 받아 해당 타입을 다시 반환하는 것을 볼 수 있다. defaultValue로 추론하지만 개발할 때 컨텍스트의 타입을 명시해주는 것이 좋다.
💡 복잡한 상태관리가 필요할 때 사용한다. 현재 진행 중인 프로젝트에서는 리덕스 툴킷(RTK)를 사용할 것이지만 기본이 되는 리액트의 useReducer를 사용해 액션부터 차근차근 만들어보자.
import { useReducer } from 'react';
// reducer는 복잡한 상태 관리가 필요할 때 사용한다.
// 따라서 이런 간단한 상태는 예제만을 위한 것이라는 것을 염두에 두고 살펴보자.
const initialState = {
counter: 100,
};
// ACTION 타입을 설정한다. type은 반드시 존재해야 하고 payload는 선택이다.
type ACTIONTYPES = { type: 'increment'; payload: number } | { type: 'decrement'; payload: number };
// reducer 함수는 state의 값을 action에 따라 어떻게 처리할지 가이드해주는 순수함수다.
// default 값으로는 적절한 action type이 들어오지 않은 것이니 에러 처리를 해주자.
function counterReducer(state: typeof initialState, action: ACTIONTYPES) {
switch (action.type) {
case 'increment':
return {
...state,
counter: state.counter + action.payload,
};
case 'decrement':
return {
...state,
counter: state.counter - action.payload,
};
default:
throw new Error('Bad action');
}
}
// useReducer 훅을 이용해 위에서 만든 리듀서와 초기값을 전달한다.
// useReducer는 state와 dispatch를 반환하는데, state는 초기값과 같은 타입을 유지하며 상태관리되는 값이고
// dispatch는 Action(type,payload)을 받아 리듀서로 전달해주는 역할을 한다.
function UseReducerComponent() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<div>
<div>{state.counter}</div>
<button
onClick={() =>
dispatch({
type: 'increment',
payload: 10,
})
}
>
Increment
</button>
<button
onClick={() =>
dispatch({
type: 'decrement',
payload: 5,
})
}
>
Decrement
</button>
</div>
);
}
export default UseReducerComponent;
💡 실제 DOM 요소를 추적하고 싶을 때 사용한다. 타입은 <해당 돔요소 | null>
로 해주면 끝!
DOM 요소들의 타입에 대해 알아보려면 공식문서를 참고하자.
Documentation - DOM Manipulation
https://www.typescriptlang.org/docs/handbook/dom-manipulation.html#the-document-interface
import { useRef } from 'react';
// 실제 DOM요소를 추적하고 싶을 때 사용
function UseRefComponent() {
const inputRef = useRef<HTMLInputElement | null>(null);
return <input ref={inputRef} />;
}
export default UseRefComponent;
export interface CardProps {
url: string;
user?: {
image: string;
link: string;
};
}
6-1. 커스텀 훅 useFetchData 만들기 : 제네릭을 사용하지 않은 리팩토링이 필요한 코드다.
function useFetchData(url: string): { data: CardProps[] | null; done: boolean } {
const [data, dataSet] = useState<CardProps[] | null>(null);
const [done, doneSet] = useState(false);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then((d: CardProps[]) => {
dataSet(d);
doneSet(true);
});
}, [url]);
return { data, done };
}
function CustomHookComponent() {
const { data, done } = useFetchData('/card-mock.json');
return <div>{done && <img alt='' src={data?.[0].user?.image} />}</div>;
}
export default CustomHookComponent;
6-2. 커스텀 훅 useFetchData 만들기 : 제네릭을 사용해 재사용성이 높은 커스텀 훅으로 만들기
// 제네릭 함수로 변경해보자. 통상적으로 <T>를 사용한다.
// 함수 정의할 때 함수 이름 옆에 <T>
// 호출할 때 지정해 줄 data 타입이 필요한 곳에 모두 T로 지정한다.
function useFetchData<T>(url: string): { data: T | null; done: boolean } {
const [data, dataSet] = useState<T| null>(null);
const [done, doneSet] = useState(false);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then((d: Payload) => {
dataSet(d);
doneSet(true);
});
}, [url]);
return { data, done };
}
function CustomHookComponent() {
// 제네릭 함수를 호출할 때 타입을 지정된다.
const { data, done } = useFetchData**<CardProps[]>**('/card-mock.json');
return <div>{done && <img alt='' src={data?.[0].user?.image} />}</div>;
}