Refactor: Product Feedback App

React 강의를 완강한 후 리팩토링을 하려고 했으나 어디부터 손 대야 할지 막막했다. 배운 건 너무 많았고 고쳐야 할 것도 너무 많아서 고민이 됐다. 그래서 먼저 버그를 줄이는 방향이 좋을 것 같아 TypeScript로 재작성하면서 테스트 코드를 작성하며 자잘한 레거시 코드를 고치는 것이 일단 목표이다. 그리고 기존 코드가 Context가 많고 자주 호출되는 코드임에도 Context로 작성한 경우가 있어 성능에 더 좋은 Redux로 대체하며 복잡한 action을 가진 State의 경우 Reducer로 대체할 생각이다. 그런 다음 NextJS를 이용해 SEO를 개선하고 Firebase의 API 대신 MongoDB에 연결할 예정이다.

기존 프로젝트에 TypeScript 추가하기

  • npm : npm install --save typescript @types/node @types/react @types/react-dom @types/jest
  • yarn : yarn add typescript @types/node @types/react @types/react-dom @types/jest

TS 파일로 변경하고 개발 서버를 재시작해야 한다. 이때, tsconfig.json 파일이 자동으로 설치되지 않으니 수동으로 만들어주면 된다. 내용은 하단의 코드를 복사해서 붙여넣기 해주면 된다.

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["src"]
}

문제와 해결

TS에서 CSS 모듈을 사용할 수 없는 문제

오류 메시지: Cannot find module './SugHeader.module.css' or its corresponding type declarations.

TS에서 CSS 모듈 또는 SCSS 사용하는 법

Type 지정이 안 되어 있기 때문에 발생하는 문제이다.

  1. plugin을 사용한다.
    • npm install -D typescript-plugin-css-modules 설치
    • tsconfig.json에서 plugins으로 추가
      {
        "compilerOptions": {
          "plugins": [{ "name": "typescript-plugin-css-modules" }]
        }
      }
      
  2. d.ts 파일을 수정한다.
    • src 폴더 하위에 declaration.d.ts 파일을 생성하고 declare module "*.module.css";를 작성하면 된다. (파일명은 자유이며 .d 확장자만 지키면 된다)

plugin을 설치하기 귀찮고 라이브러리 의존성이 증가하는 걸 원치 않아서 2번째 방법을 사용했다.

window.matchMedia()를 테스트시 타입 에러 뜨는 문제

오류 메시지: TypeError: window.matchMedia is not a function

mocking matchMedia

JEST 사이트에서 해결책을 알려줬지만 본인은 해결이 안됐다…

삽질하다가 다른 검색어로 쳐볼까 해서 쳤더니 나온 건데 이건 통했다! mock 함수를 만들어서 해결했다.

document의 element를 가져올 때 type 오류 문제

오류 메시지 : Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Element | DocumentFragment'. Type 'null' is not assignable to type 'Element | DocumentFragment'.ts(2345)

null이 아님을 확신하는 법

!연산자를 이용해 null이 아님을 알려주면 된다.

  • [Argument of type ‘HTMLElement null’ is not assignable…](https://stackoverflow.com/questions/63520680/argument-of-type-htmlelement-null-is-not-assignable-to-parameter-of-type-el)

Redux Toolkit과 TS 같이 사용하는 법

Dispatch 함수의 타입 지정을 대충 빈 함수로 했다가 인자 개수가 다르다고 오류가 떴다. 공식 문서에서 본 것 처럼 타입을 지정해주면 된다.

export type AppDispatch = typeof store.dispatch;

redux toolkit 직렬화 오류 문제

오류 메시지 : A non-serializable value was detected in an action

해결법

직렬화, 즉 object 값을 string 형태로 변환해야 하는데 불가능한 값이 전달되어서 생기는 문제다. 스택오버플로우에서는 직렬화 체크를 비활성화해서 해결했지만 단순한 해결방법이라 별로 끌리진 않았다.

본인의 경우 모델 클래스를 전달해줘서 문제가 발생했다. object 또는 array를 사용해야 한다.

type vs interface

type을 써야 하나 interface를 써야 하나 고민이 됐었는데 합성할 경우를 대비해 좋은 성능을 가진 interface를 사용하는 것이 좋음을 알았다.

children 타입 지정하는 법

PropsWithChildren은 옵셔널로 사용할 경우만 사용하는 것이 좋다. 그 외에는 ReactNode 타입으로 지정하거나 StrictPropsWithChildren 유틸 타입을 만들어 작업 줄이는 방법도 있다.

React.FC를 제거하는 게 좋을까?

몇 가지 찾아보다보면 React.FC를 사용하지 말라고 하는데 그 이유는 children 요소를 암시적으로 가지고 있기 때문이라고 했다. 그런데 내가 사용할 땐 자식 오류가 떴었던 것 같아 찾아보니 18로 업데이트 되면서 이 단점이 제거되었다고 한다.

  React.FC 일반 함수
반환 형식 명시적 암시적
defaultProps 같은 정적 속성 제공X type 검사, 자동 완성 기능 제공

참고로 보면서 React.FC 대안으로 React.VFC가 등장했지만 이는 더이상 사용하지 않으니 사용하면 안된다.

하단 링크의 reddit에서 논의를 했지만 결국 의견이 분분해서 하단의 마지막 링크에서 글을 읽어보다가 깔끔해서 제거하는 것이 좋다고 생각했다.

const C1: React.FC<CProps> = (props) => {};
const C2 = (props: CProps) => {};

Redux Toolkit과 함께 Thunk 사용하는 방법

비동기 처리를 Redux에서 해야 하는데 Toolkit이 Thunk를 내장하고 있고 Redux 문서에 의하면 간단한 경우 thunk를 사용하는 것이 좋다고 한다.

재선택할 때 유용하게 사용할 수 있다.

upvote 반응 느리던 문제 해결법

기존 코드에서는 upvote 버튼을 누르면 느리게 반영이 됐었다. 아마 api로 보내고 데이터를 받아오기 때문인 것 같다. 이렇게 하니 재호출해야하는 문제점도 생겨서 이번엔 api로 보내주고 로컬에도 상태를 수정해서 업데이트 하는 방식으로 했다. 그랬더니 속도가 확연히 좋아졌다!

Overlay Position 설정 문제

오버레이를 최상단으로 portal하다보니 기존에는 근처 관련 엘리먼트에 relative를 설정해 위치를 조정할 수 있었으나 portal 후에는 설정할 수 없어서 위치를 가져와 계산하는 것이 좋을 것 같다.

getBoundingClientRect()getClientRects()는 윈도우에서 상대적인 위치이기에 이보다는 절대 위치를 가져오는 것이 스크롤시 모달창이 안 뜨고 좋을 것 같다. 라고 멍청하게 생각했는데 상대적이어야 스크롤시 위치도 같이 사라질 수 있었다.

엘리먼트의 절대 위치는 offsetTop(), offsetLeft(), offsetWidth(), offsetHeight()를 이용하여 알아낼 수 있다. 참고로 스크롤이 얼마나 되어 있는지는 pageXOffset(), pageYOffset를 이용하면 된다. 이 때, scrollX 속성도 똑같지만 다른 브라우저의 호환성을 위해 전자를 권장한다고 한다.

컴포넌트 style 속성 타입 지정하는 법

React의 CSSProperties를 이용하여 지정해주면 된다.

thunk로 error 상태 가져오는 법

에러를 발생하게 하는건 쉽지만 에러 값을 가져오는 방법을 모르겠어서 삽질했다. 여러가지 찾아봤지만 아직도 잘 모르겠다. 일단 해결한 방법으로는 rejected 상태에 값을 thunkAPI.rejectWithValue()로 넘겨주면 action.payload에 값을 받을 수 있다.

이 때 TS 타입 문제가 생기는데 catch문의 error는 항상 unknown으로 명시해줘야 하는데 reducer에 전달 할 땐 타입을 지정해줘야 하는 문제가 생겼다. 임의로 error 타입을 만들어 as를 이용해 타입을 지정해야 한다. 받을 때도 unknown으로 떠서 타입을 지정해줘야 했다.

// thunk
const fetchData = createAsyncThunk("", async (data, thunkAPI) => {
  try {
    //...
  } catch (error) {
    const errorObj = error as { message: string; stack: string };
    return thunkAPI.rejectWithValue(errorObj);
  }
});

// reducer
.addCase(fetchData.rejected, (state, action) => {
  const error = action.payload as { message: string; stack: string };
})

객체 속성을 같은 타입으로 지정하는 법

const exampleObj: { [k: string]: string } = {
  first: "premier",
  second: "deuxieme",
  third: "troisieme",
};

createAsyncThunk 함수의 ThunkAPI를 사용할 때 TS 오류 해결하는 법

createAsyncThunk<return_type, arg_type, { state: RootState }>로 변경해주면 된다.

useState 타입 지정

interface Props {
  state = [string, Dispatch<SetStateAction<string>>]
}

Query Params 사용법

이전에 강의를 들을 때는 useNavigate를 사용했지만 useSearchParams를 사용하면 코드가 짧아져서 사용했다.

let [searchParams, setSearchParams] = useSearchParams();

function handler() {
  searchParams.set("query명", queryValue);
  setSearchParams(searchParams);
}

처음엔 위의 블로그를 보고 사용하려고 했으나 ts가 url 객체를 이터러블로 변환할 수 없다고 했다. 이유는 ts가 js 파일로 변경할 때 낮은 js 버전에서는 지원하지 않기 때문이라고 한다. 그래서 어떻게 하지 고민하다가 찾은 방법이 저거였다.

사용할 때는 아래의 방법을 사용하면 된다.

searchParams.get("query명");

React Testing Library API

컴포넌트를 위한 render, 훅을 위한 renderHook

render

function render(
  ui: React.ReactElement<any>,
  options?: {
    /* You won't often use this, expand below for docs on options */
  }
): RenderResult;

예시:

import { render } from "@testing-library/react";
import "@testing-library/jest-dom";

test("renders a message", () => {
  const { asFragment, getByText } = render(<Greeting />);
  expect(getByText("Hello, world!")).toBeInTheDocument();
  expect(asFragment()).toMatchInlineSnapshot(`
    <h1>Hello, World!</h1>
  `);
});

renderHook

function renderHook<Result, Props>(
  render: (props: Props) => Result,
  options?: RenderHookOptions<Props>
): RenderHookResult<Result, Props>;

예시:

import { renderHook } from "@testing-library/react";

test("returns logged in user", () => {
  const { result } = renderHook(() => useLoggedInUser());
  expect(result.current).toEqual({ name: "Alice" });
});

rerender

rerender를 사용하면 똑같은 컴포넌트나 훅에 다른 props로 re-render한다.

// render
rerender(<NumberDisplay number={2} />);

// renderHook
rerender({ name: "Kaye" });

카테고리:

업데이트:

댓글남기기