React + TS(FC, useMemo, useReducer, Context API)

박정호·2022년 11월 1일
0

Game Project

목록 보기
13/13
post-thumbnail

🚀 Start

간단한 로또추첨기, 틱택토 게임을 구현해보면서 FC, useMemo, useReducer는 어떻게 타이핑되는지 알아보자.


⭐️ React.FC

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를 사용해서 얻을 수 있는 이점은 두가지가 있다.

  • props 에 기본적으로 children 이 들어가있다는 것
  • defaultProps, propTypes, contextTypes 를 설정 할 때 자동완성이 된다는 것

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하게 취급하지 않고 SFCFC가 되었다.

FC의 사용이 좋을 때가 있고 반대로 안좋을 경우가 있을 것이다.
그러나 단순히 인수 (props)를 타이핑 하는 것이 더 JS의 느낌이 나고, 다양한 경우의 수로 부터 조금더 안전해 질 수 있을 것이다.


⭐️ useMemo

useMemo는 함수 값을 메모이제이션한다. 동일한 인수가 주어지면 함수가 실행되지 않도록 함수의 반환값을 캐싱하는 것과 유사하다. 이를 통해 리랜더링을 피하고 성능을 최적화할 수 있다.

useMemo은 타입추론에 의해서 별도의 타입 정의 없이도 정상적으로 작동되는 것을 확인 가능하다.


⭐️ useReducer

  • useReducer는 useState처럼 state를 관리하고 업데이트 할 수 있는 Hook
  • 컴포넌트의 외부에 작성하는 것을 가능하게 하여, 코드를 최적화 시켜준다
  • 상태관리를 위한 reducer function에 접근할 수 있게 한다
  • useState의 대체 함수이다.

참고: React Hooks - useReducer

useReducer 사용시 타이핑해야하는 곳은 state(initialState), action, reducer, props 정도이다.

const reducer = (state: stateType, action: actionType): stateType => {...}

state(initialState) 타이핑

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 타이핑

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 타이핑

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;

props 타이핑

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

Context API는 앱 안에서 전역적으로 사용되는 데이터들을 props로 일일이 넘기지 않아도 여러 컴포넌트들 끼리 쉽게 공유 할 수 있는 방법을 제공한다.

context API를 사용하기 위해서는 Provider , useContext , createContext가 필요하고 각각 타이핑을 해주자.

  • createContext : context 객체를 생성
  • Provider : 생성한 context를 하위 컴포넌트에게 전달하는 역할
  • Consumer : context의 변화를 감시하는 컴포넌트

참고: React Hooks - useContext


CreateContext

다른 컴포넌트에 전달하고자하는 contextcreateContext를 통해서 생성.

이때 생성되는 값들에 대해서 타이핑을 해준다.

  • 특히 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를 필요로 하는 컴포넌트들에게 변화를 알리고, valuecontext를 담아서 전달한다.

 <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 + Context API & 상태관리

앞서 보았듯이 useReducer만 사용할 경우에는 dispatchreducer를 통해 변경할 데이터(action)을 props최종 변경할 컴포넌트까지 전달해주어야한다.

반면 Context API는 자식 컴포넌트 뿐 아니라 해당 데이터를 사용할 컴포넌트 모두에게 다이렉트로 전달할 수 있다.

위에서 본 것과 같이 value 값으로 dispatch를 넘겨준 것을 볼 수 있다. 다른 곳에서 dispatch를 사용할 수 있게 Context API를 통해 다리엑트로 전달하는 것이다.

🧐 Context APIuseReducer가 함께 사용된 경우인데... 어떤 상황인걸까?
빠른 이해를 위해서 상태관리에 대해서 알아보자.


Context API는 상태 관리 도구가 아니다.

본론부터 말하자면 context api는 상태관리 도구가 아니다.

상태(State)란 어플리케이션의 작동에 관여하는 모든 데이터를 말한다.
중요한 것은 데이터가 필요 시 저장(stored), 읽히고(read), 업데이트(updated), 사용(used)되어야 한다는 점이다.

리액트의 useStateuseReducer 훅은 상태 관리의 좋은 예시가 된다.
이 두가지 훅을 통해 훅을 호출하면서 초기 값 저장 훅을 호출하면서 현재 값 읽기 setStatedispatch 함수를 호출하여 값 업데이트 컴포넌트가 리렌더되면서 값이 업데이트 된다.

이 맥락을 보면 알 수 있듯이 Context는 는 전달되는 값을 결정하는 역할을 하는 것이지 상태를 관리하는 것이 아니다.


useReducer + Context API의 조합

따라서, 앞서 말했듯이 useReducer로는 상태관리를 하고 Context로는 값을 전달하는 것이다.


useReducer + Context API vs Redux

useReducer + Context APIRedux 와 다음과 같은 기능과 동작으로 비슷한 구조를 가진다.

  • 저장(보관)되는 값
  • 리듀서 함수
  • 액션 디스패칭
  • 중첩 컴포넌트 내에서 값을 전달하고 읽는 방법

useReducer + Context API 한계

  • Context + useRederContext 를 통해 현재 상태 값을 전달하는데 의존한다.

  • React-ReduxContext 를 통해 현재 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를 사용하자.


출처 및 참고하기 좋은 사이트

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글