간단한 로또추첨기, 틱택토 게임을 구현해보면서 FC
, useMemo
, useReducer
는 어떻게 타이핑되는지 알아보자.
props를 받아올 때 다음과 같이 일반적으로 받아오고 타이핑을 해주었었다.
하지만, 다음과 같이 FC와 제네릭 사용에 대해서도 알게 되었다.
✅ 그렇다면 React.FC
란 무엇일까?
리액트는 TS로 작성되지 않기 때문에, @types/react
패키지를 제공받아 리액트에 타이핑을 지원한다.
이때 제공되는 것 중에 FC(FunctionComponnet)
라는 제네릭 타입이 있는데, 이는 함수형 컴포넌트를 다음과 같이 타이핑할 수 있게 도와준다.
import { FC } from 'react'
type GreetingProps = {
name: string
}
const Greeting: FC<GreetingProps> = ({ name }) => {
return <h1>Hello {name}</h1>
}
✅ 함수를 타이핑 하지만, 인수를 타이핑 하지는 않는다.
React.FC
는 함수를 타이핑해준다. 함수 타이핑은 일반적인 기명 함수에 적용하기 매우 어렵다.
아래와 같은 코드에 함수 타이핑을 적용해보자.
type GreetingsProps = {
name: string;
};
function Greeting({ name }) {
return <h1>Hello {name}</h1>
}
// 함수타이핑
const Greeting: FC<{ name: string }> = ({ name }) => {
return <h1>Hello {name}</h1>
}
만약 함수타아핑을 사용하지 않으면 인수를 타이핑하여 다음과 같이 작성한다.
function Greeting({ name }: GreetingProps) {
return <h1>Hello {name}</h1>
}
✅ 이처럼 React.FC
를 사용 할 때는 props
의 타입을 Generics
로 넣어서 사용한다. 이렇게 React.FC
를 사용해서 얻을 수 있는 이점은 두가지가 있다.
✅ children을 암시적으로 가지고 있다.
FC를 이용하면 컴포넌트 props는 type이 ReactNode인 children을 암시적으로 가지게 된다. 이는 꼭 타입스크립트에 한정하지 않더라도 안티패턴이다.
아래의 경우 <App/> 컴포넌트
에서 children
을 다루고 있지 않음에도 Example
에서 children
을 넘겨주고 있으며, 어떤 런타임 에러도 발생하지 않는다.
const App: React.FC = () => {
return <div>hi</div>;
};
const Example = () => {
<App>
<div>Unwanted children</div>
</App>;
};
이런 실수는 FC
를 사용하지 않는다면 잡아낼 수 있다.
물론 FC
를 사용한다면 자주 사용하는 children
의 타입을 하나하나 작성해주지 않아도 된다는 편리함이 있다.
🧐 그럼 FC를 사용하지 않고 어떻게 children을 구현하면 될까?
암묵적으로 선언되었던 children을 명시적으로 컴포넌트에 맞게 선언하는 것이다.
type Props = {
children: React.ReactNode;
};
const Example: React.FC<Props> = ({ children }) => <div>{children}</div>;
✅ 제네릭을 지원하지 않는다
다음과 같은 제네릭 컴포넌트는 FC
에서 허용되지 않는다.
type GenericComponentProps<T> = {
prop: T;
callback: (t: T) => void;
};
const GenericComponent = <T>(props: GenericComponentProps<T>) => {
/*...*/
};
왜냐하면 T
를 넘겨줄 방법이 없기 때문이다.
const GenericComponent: React.FC</* ??? */> =
<T>(props: GenericComponentProps<T>) => {/*...*/}
✅ 네임 스페이스 패턴을 이용할 때 더 불편하다
// FC를 사용할 때
const Select: React.FC<SelectProps> & { Item: React.FC<ItemProps> } = (
props
) => {
/* ... */
};
Select.Item = (props) => {
/*...*/
};
// FC를 사용하지 않을 때
const Select = (props: SelectProps) => {
/* ... */
};
Select.Item = (props: ItemProps) => {
/*...*/
};
✅ FC를 이용하면 코드가 더 길다
const C1: React.FC<CProps> = (props) => {};
const C2 = (props: CProps) => {};
😄 이전에는는 함수형 컴포넌트를
SPC(stateless function component)
(무상태 함수형 컴포넌트)라고 불렀었다.훅이 소개된 이후로, 함수형 컴포넌트에는 많은 상태가 들어오기 시작했고 이제는 더이상 stateless하게 취급하지 않고
SFC
는FC
가 되었다.
FC
의 사용이 좋을 때가 있고 반대로 안좋을 경우가 있을 것이다.
그러나 단순히인수 (props)
를 타이핑 하는 것이 더 JS의 느낌이 나고, 다양한 경우의 수로 부터 조금더 안전해 질 수 있을 것이다.
useMemo
는 함수 값을 메모이제이션한다. 동일한 인수가 주어지면 함수가 실행되지 않도록 함수의 반환값을 캐싱하는 것과 유사하다. 이를 통해 리랜더링을 피하고 성능을 최적화할 수 있다.
useMemo
은 타입추론에 의해서 별도의 타입 정의 없이도 정상적으로 작동되는 것을 확인 가능하다.
✅ useReducer
사용시 타이핑해야하는 곳은 state(initialState)
, action
, reducer
, props
정도이다.
const reducer = (state: stateType, action: actionType): stateType => {...}
useState
에서도 초기 상태값을 지정하듯이 useReducer
에서도 initialState
값을 지정한다.
해당 틱텍토 게임 프로젝트에서 필요한 값은 winner
, turn
, tableData
, recentCell
이다.
✅ 따라서, initialState
값에 대해서 타이핑을 해주자.
interface ReducerState {
winner: "O" | "X" | "무승부" | "";
turn: "O" | "X";
tableData: string[][];
recentCell: [number, number];
}
const initialState: ReducerState = {
winner: "",
turn: "O",
tableData: [
["", "", ""],
["", "", ""],
["", "", ""],
],
recentCell: [-1, -1],
};
action
은 업데이트해야할 정보를 가지고 있는 것으로, dispatch
의 인자에 담긴다.
쉽게 말해 reducer
가 무엇을 해야할지 담겨있는 명령어들이다.
따라서, 다음과 같이 dispatch
를 통해서 action
과 값을 넘길 수 있다.
<button onClick={() => dispatch({ type: "SET_WINNER", "O"})}>
그리고 reducer
에서 해당 action
에 대해서 업데이트를 실행한다.
const reducer = (state, action) => {
switch (action.type) {
case SET_WINNER:
case CLICK_CELL:
case CHANGE_TURN:
case RESET_GAME:
}
default:
}
};
✅ 코드를 간략화하기 위하여 액션을 상수화 시켜줄 수 있다.
export const SET_WINNER = "SET_WINNER";
export const CLICK_CELL = "CLICK_CELL" as const;
export const CHANGE_TURN = "CHANGE_TURN" as const;
export const RESET_GAME = "RESET_GAME" as const;
✅ 함수를 생성하여 dispatch
의 요청에 대한 값을 반환시켜준다.
이때, 파라미터에 대한 타입과 반환 값의 타입을 정의해준다. interface
로 타입을 정의해주며 typeof
를 통해서 이미 정의된 action
에 대한 타입으로 설정한다.
interface SetWinnerAction {
type: typeof SET_WINNER;
winner: "O" | "X" | "무승부";
}
const setWinner = (winner: "O" | "X" | "무승부"): SetWinnerAction => {
return { type: SET_WINNER, winner };
};
------------------------------------------------------------------
interface ClickCellAction {
type: typeof CLICK_CELL;
row: number;
cell: number;
}
export const clickCell = (row: number, cell: number): ClickCellAction => {
return { type: CLICK_CELL, row, cell };
};
------------------------------------------------------------------
interface ChangeTurnAction {
type: typeof CHANGE_TURN;
}
------------------------------------------------------------------
interface ResetGameAction {
type: typeof RESET_GAME;
}
reducer는 외부에서 state
을 업데이터히는 로직을 담당하는 함수이다. action
의 값에 따라서 기존의 state
값을 새롭게 return한다.
✅ state
의 값과 반환값
은 initialState
을 타이핑한 interface인 ReducerState
로 타입을 정의한다.
const reducer =(state: ReducerState, action: ReducerActions): ReducerState => {..}
✅ action
에 대한 interface
는 총 네개였으므로, type
을 이용하여 인터페이스를 묶는다
type ReducerActions =
| SetWinnerAction
| ClickCellAction
| ChangeTurnAction
| ResetGameAction;
useReducer
의 장점은 컴포넌트의 외부에서 작성이 가능하다는 것이고, 그 뜻은 다른컴포넌트에서 dispatch
를 통해서 reducer
가 있는 컴포넌트에 업데이트 요청을 할 수 있다는 것이다.
따라서, dispatch
를 포함하여 다른 컴포넌트에게 전달해야할 props
값들이 존재할 것이고 props
에 대해서도 타이핑을 해주어야 한다.
✅ Table 컴포넌트에 props
로 다음과 같이 전달하려고 한다.
<Table onClick={onClickTable} tableData={tableData} dispatch={dispatch} />
✅ Table 컴포넌트에서는 전달받은 props
에 대해서 타이핑을 해준다.
interface Props {
tableData: string[][];
dispatch: Dispatch<any>;
onClick: () => void;
}
const Table = ({ tableData, dispatch }: Props) => {...}
💡잠깐) useReducer 타이핑
useReducer
의 경우에는 다음과 같이 타입추론이 된다.
⭐️ 만약 정확한 명시를 하기 위해서는 다음과 같이 작성.
Context API는 앱 안에서 전역적으로 사용되는 데이터들을 props로 일일이 넘기지 않아도 여러 컴포넌트들 끼리 쉽게 공유 할 수 있는 방법을 제공한다.
✅ context API
를 사용하기 위해서는 Provider
, useContext
, createContext
가 필요하고 각각 타이핑을 해주자.
createContext
: context 객체를 생성Provider
: 생성한 context를 하위 컴포넌트에게 전달하는 역할Consumer
: context의 변화를 감시하는 컴포넌트다른 컴포넌트에 전달하고자하는 context
를 createContext
를 통해서 생성.
✅ 이때 생성되는 값들에 대해서 타이핑을 해준다.
tableData
는 빈배열로 never타입
을 가지기 때문에 꼭 타입을 명시해주자.interface Context {
tableData: Codes[][];
halted: boolean;
dispatch: Dispatch<ReducerActions>;
}
export const TableContext = createContext<Context>({
tableData: [],
halted: true,
dispatch: () => {},
});
✅ 생성해준 context
들을 value
에 담아준다.
const value = useMemo(
() => ({ tableData, halted, dispatch }),
[tableData, halted]
);
✅ Provider
를 통해서context
를 필요로 하는 컴포넌트들에게 변화를 알리고, value
에 context
를 담아서 전달한다.
<TableContext.Provider value={value}>
<Form />
<div>{timer}</div>
<Table />
<div>{result}</div>
</TableContext.Provider>
✅ useContext
를 통해서 context로 공유한 데이터를 받아온다.
<Form.tsx>
const { dispatch } = useContext(TableContext);
<Table.tsx>
const { tableData } = useContext(TableContext);
🧐
Context API
과정에서는createContext
를 통한context
생성시 타입을 지정하는 것 이외에는 별다른 타이핑은 없는 것 같다.
앞서 보았듯이 useReducer
만 사용할 경우에는 dispatch
와 reducer
를 통해 변경할 데이터(action)을 props
로 최종 변경할 컴포넌트까지 전달해주어야한다.
반면 Context API
는 자식 컴포넌트 뿐 아니라 해당 데이터를 사용할 컴포넌트 모두에게 다이렉트로 전달할 수 있다.
위에서 본 것과 같이 value 값으로 dispatch
를 넘겨준 것을 볼 수 있다. 다른 곳에서 dispatch
를 사용할 수 있게 Context API
를 통해 다리엑트로 전달하는 것이다.
🧐
Context API
와useReducer
가 함께 사용된 경우인데... 어떤 상황인걸까?
빠른 이해를 위해서 상태관리에 대해서 알아보자.
✅ Context API는 상태 관리 도구가 아니다.
본론부터 말하자면 context api는 상태관리 도구가 아니다.
상태(State)
란 어플리케이션의 작동에 관여하는 모든 데이터를 말한다.
중요한 것은 데이터가 필요 시 저장(stored)
, 읽히고(read)
, 업데이트(updated)
, 사용(used)
되어야 한다는 점이다.
리액트의 useState
와 useReducer
훅은 상태 관리의 좋은 예시가 된다.
이 두가지 훅을 통해 훅을 호출하면서 초기 값 저장 훅을 호출하면서 현재 값 읽기 setState
나 dispatch
함수를 호출하여 값 업데이트 컴포넌트가 리렌더되면서 값이 업데이트 된다.
이 맥락을 보면 알 수 있듯이 Context
는 는 전달되는 값을 결정하는 역할을 하는 것이지 상태를 관리하는 것이 아니다.
✅ useReducer + Context API의 조합
따라서, 앞서 말했듯이 useReducer로는 상태관리를 하고 Context로는 값을 전달하는 것이다.
✅ useReducer + Context API vs Redux
useReducer + Context API
는 Redux
와 다음과 같은 기능과 동작으로 비슷한 구조를 가진다.
✅ useReducer + Context API 한계
Context + useReder
는 Context
를 통해 현재 상태 값을 전달하는데 의존한다.
React-Redux
는 Context
를 통해 현재 Redux 스토어 인스턴스를 전달한다.
useReducer
의 경우 새로운 상태 값을 생성 할 때 해당 Context
내부에 포함된 컴포넌트들이 상태값의 일부에만 관심이 있더라도 강제로 re-render 되기 때문에 성능 문제가 발생 할 수 있다.
React-Redux
를 사용하면 저장소 상태의 특정 부분만 사용하고 해당 값이 변경 될 때만 re-render
할 수 있다.
Context + useReducer
는 React 의 기능이기 때문에 React 외부에서는 사용이 불가하다.
Redux
는 UI 독립적이기 때문에 React 와 별도로 사용이 가능하다.
useReducer
는 미들웨어가 없다.
React DevTools
를 사용하면 현재의 상태 값은 볼 수 있지만 전달된 action
, 과 payload
, 처리 된 후의 상태등 시간에 따른 변화를 볼 수 없다. Redux Devtools
을 이용하면 시간에 따른 상태 차이를 볼 수 있다.
✅ 어떤 상황에 어떻게 사용할까?
단순히 prop-drilling을 피하고 싶다면 Context를 사용하자.
컴포넌트 복잡도가 보통 수준이거나 외부 라이브러리를 사용하고 싶지 않다면 Context + useReducer 조합을 사용자.
상태 변화 추적이 필요할 때, 상태 변화 시 특정 컴포넌트만 리렌더하고 싶을 때, 사이드 이펙트를 강력하게 컨트롤하고 싶을 때는 Redux + React-Redux를 사용하자.
출처 및 참고하기 좋은 사이트