🚩 글의 주제 : 타입스크립트를 이용하여 기본적인 리액트 훅을 사용하는 방법 알아보기
❓ 리액트도 아직 제대로 다뤄본 적이 없는데 생전 초면인 TS 까지 하려니 프로젝트 진행시 막히는 부분이 많은 것 같았다. 잠시 프로젝트를 내려놓고 TS를 이용해 Hooks를 하나하나 사용해보려고 한다.
npx create-react-app my-app --template typescript
Hooks를 import한 후 사용하면 라이브러리에서 정의한 해당 Hooks(함수)의 정의를 볼 수 있다. (f12 혹은 우클→ Go to definition)
훅스의 사용법과 타입스크립트의 정의에 집중하기 위해 타입과 컴퍼넌트를 구분하지 않았습니다.
타입을 정의하지 않고 사용하면 다음과 같은 에러가 난다.
⚠️ 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를 반환하는 함수’를 반환한다는 것을 알 수 있다.
컨텍스트를 만들어주는 createContext는 어떻게 정의되어 있을까?
제네릭으로 타입을 받아 해당 타입을 다시 반환하는 것을 볼 수 있다. defaultValue로 추론하지만 개발할 때 컨텍스트의 타입을 명시해주는 것이 좋다.
이제 정의를 살펴봤으니 컨텍스트를 만들어보자
createContext
를 이용해 초기 값을 설정해준다.
이 때 컨텍스트 타입을 명시해주는 방법은 다양하다.
typeof
로 뽑기import { createContext } from 'react';
// 초기값 설정
const initialState = {
first: 'Jim',
last: 'Julian',
};
// 초기값에 대한 타입 뽑기
export type UserState = typeof initialState;
// 타입을 사용해 컨텍스트 만들기
const context = createContext<UserState>(initialState);
export default context;
interface
로 타입 설정 후 사용하기import { createContext } from 'react';
export interface UserStateInterface {
first: string;
last: string;
}
const initialState: UserStateInterface = {
first: 'Jim',
last: 'Julian',
};
const context = createContext<UserStateInterface>(initialState);
export default context;
Provider 혹은 Consumer로 사용하기
<UserContext.Provider value={user}>
useContext
훅을 이용해 Consumer를 만들어 컨텍스트의 값을 받아올 수 있다.UserState
혹은 UserStateInterface
)으로 지정해주면 된다.import React, { useState, useContext } from 'react';
import UserContext, { UserStateInterface, initialState } from './store';
// 위에서 생성한 context는 default 방식으로 내보냈기 때문에
// UserContext와 같이 이름을 유동적으로 지을 수 있다.
function ConsumerComponent() {
const user = useContext<UserStateInterface>(UserContext);
return (
<div>
<div>First: {user.first}</div>
<div>Last: {user.last}</div>
</div>
);
}
function UseContextComponent() {
const [user, setUser] = useState<UserStateInterface>(initialState);
return (
<UserContext.Provider value={user}>
<ConsumerComponent />
<button onClick={() => setUser({ first: 'new Name', last: 'new Last Name' })}>
Change context
</button>
</UserContext.Provider>
);
}
export default UseContextComponent;
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 요소들의 타입에 대해 알아보려면 공식문서를 참고하자.
Documentation - DOM Manipulation
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;
};
}
커스텀 훅 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;
커스텀 훅 useFetchData
만들기 : 제네릭을 사용해 재사용성이 높은 커스텀 훅으로 만들기
1번에서 만든 커스텀 훅은 무조건 CardProps[] 타입을 반환하는 경우에만 사용할 수 있다. useFetchData가 요청에 따라 다양한 형태의 data를 받아오려면 어떻게 할까? ⇒ Generic 사용 !
// 제네릭 함수로 변경해보자. 통상적으로 <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>;
}
💡 알게된 점
📌 참고 링크