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 지정이 안 되어 있기 때문에 발생하는 문제이다.
- plugin을 사용한다.
npm install -D typescript-plugin-css-modules
설치tsconfig.json
에서 plugins으로 추가{ "compilerOptions": { "plugins": [{ "name": "typescript-plugin-css-modules" }] } }
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) => {};
- Is React.FC not recommended? what are other alternative and recommended way?
- Function Components - 밑에 Why… 참고
- Remove React.FC from Typescript template - 2019년 글
Redux Toolkit과 함께 Thunk 사용하는 방법
비동기 처리를 Redux에서 해야 하는데 Toolkit이 Thunk를 내장하고 있고 Redux 문서에 의하면 간단한 경우 thunk를 사용하는 것이 좋다고 한다.
- Modern Redux with Redux Toolkit
- What async middleware should I use? How do you decide between thunks, sagas, observables, or something else?
재선택할 때 유용하게 사용할 수 있다.
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 }>
로 변경해주면 된다.
-
arg type: payload를 뜻한다.
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" });
댓글남기기