Skip to content

Commit

Permalink
feat: "[카카오 테크 캠퍼스 2기] 2차 코드리뷰 반영 및 리팩토링 진행기" 포스팅
Browse files Browse the repository at this point in the history
  • Loading branch information
kang-kibong committed Sep 4, 2024
1 parent 328df5c commit e9aef2b
Show file tree
Hide file tree
Showing 7 changed files with 1,192 additions and 447 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,366 @@
---
layout: post
title: "[카카오 테크 캠퍼스 2기] 2차 코드리뷰 반영 및 리팩토링 진행기"
category: kakao2
---

<br />
<br />
<br />

이전 코드리뷰에 이어서 **불필요한 코드 정리**, **성능 최적화**, 그리고 **타입 충돌 해결** 등의 내용에 집중해 리팩토링을 진행해보았다.

이번 포스팅에서는, 자주 사용했던 **rest operator****barrel file pattern**, 그리고 **절대 경로** 문제를 해결하는 과정을 다루고, 이를 바탕으로 리팩토링한 주요 항목들을 정리해보았다.

<br />
<br />
<br />

# 1. .env 파일이 깃허브에 올라갔을 때 해결 방법

프로젝트의 **.env** 파일이 깃허브에 실수로 올라간 경우, 해당 파일을 원격 저장소에서 삭제하면서도 로컬에서는 유지하는 방법을 사용해야한다.

이는 다음 명령어를 통해 해결할 수 있었다.

<br />

```bash
git rm --cached .env
```

<br />

이후 다시 커밋하고 푸시하면 **.env** 파일이 원격 저장소에서 삭제되지만, 로컬 개발 환경에서는 계속 사용할 수 있었다.

<br />
<br />
<br />

# 2. 불필요한 rest operator 제거

**rest operator**를 습관적으로 사용하던 문제가 있었다.

특히, 컴포넌트를 구현할 때 필수적으로 사용해야 한다고 오해한 부분이 있었지만, **제한된 props**만을 받는 컴포넌트에서는 불필요한 경우가 많다.

예를 들어, 아래의 `Button` 컴포넌트 같은 경우, rest operator를 사용하여 Button의 다양한 속성들을 props로 받을 수 있게 설계할 수 있다.

<br />

```tsx
type ButtonTheme = "kakao" | "primary" | "darkGray";
type ButtonSize = "small" | "large" | "responsive";

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
theme?: ButtonTheme;
size?: ButtonSize;
children: ReactNode;
}

export default function Button({
theme = "kakao",
size = "large",
children,
...rest
}: ButtonProps) {
return (
<StyledButton theme={theme} size={size} {...rest}>
{children}
</StyledButton>
);
}
```

<br />

하지만 특정 props만을 받는 **GoodsItem** 컴포넌트를 예시로 들면, rest operator를 사용하여 추가적인 props를 받을 필요가 없다.

<br />

```tsx
interface GoodsItemProps {
imageSrc: string;
subtitle: string;
title: string;
amount: number;
rankingIndex?: number;
}

export default function GoodsItem({
imageSrc,
subtitle,
title,
amount,
rankingIndex,
...rest
}: GoodsItemProps) {
const imageSize = rankingIndex ? IMAGE_SIZE_RANKING : IMAGE_SIZE_GOODS_ITEM;

const renderRanking = () =>
rankingIndex && <Ranking rankingIndex={rankingIndex} />;

return (
<StyledGoodsItem rankingIndex={rankingIndex} {...rest}>
...
</StyledGoodsItem>
);
}
```

<br />

때문에 아래와 같이 제한된 props를 받는 컴포넌트에서 불필요하게 rest operator를 사용하는 것은 피하고, 필요한 props만을 명시적으로 전달하는 방식으로 개선할 수 있었다.

<br />

```tsx
interface GoodsItemProps {
imageSrc: string;
subtitle: string;
title: string;
amount: number;
rankingIndex?: number;
}

export default function GoodsItem({
imageSrc,
subtitle,
title,
amount,
rankingIndex,
}: GoodsItemProps) {
const imageSize = rankingIndex ? IMAGE_SIZE_RANKING : IMAGE_SIZE_GOODS_ITEM;

const renderRanking = () =>
rankingIndex && <Ranking rankingIndex={rankingIndex} />;

return <StyledGoodsItem rankingIndex={rankingIndex}>...</StyledGoodsItem>;
}
```

<br />
<br />
<br />

# 3. 불필요한 Barrel File Pattern 지양

```tsx
export { default as Button } from "./Button";
export { default as Footer } from "../features/Layout/Footer";
export { default as InputField } from "./Form/InputField";
export { default as Image } from "./Image";
export { default as GoodsItem } from "./GoodsItem";
export { default as Ranking } from "./GoodsItem/Ranking";
export { default as Header } from "../features/Layout/Header";
export { default as Container } from "./Layout/Container";
export { default as Grid } from "./Layout/Grid";
export { default as CenteredContainer } from "./Layout/CenteredContainer";
export { default as StatusHandler } from "./StatusHandler";
```

<br />

초기에 **barrel file pattern**을 사용하여 `index.ts` 파일에서 여러 컴포넌트를 한 번에 export하는 방식을 사용했었다.

믈론, import 구문을 간결하게 만들 수 있는 장점은 있지만, 프로젝트 규모가 커지게 되면 모든 모듈들을 한번에 로드하는 데 시간이 오래 걸릴 수 있고, 성능에 부정적인 영향을 미칠 수 있다.

<br />

- **모든 모듈 로드 문제**: 배럴 파일은 폴더 내의 모든 모듈을 한 번에 로드하기 때문에, 필요하지 않은 모듈까지 로드되어 성능 저하가 발생할 수 있다.
- **불필요한 사용**: 소규모 컴포넌트나 단일 용도의 컴포넌트에까지 배럴 파일을 사용하면 오히려 가독성을 떨어뜨리고, 성능에 부정적인 영향을 미칠 수 있다.

<br />

때문에, **`components/common` 경로**와 같이 여러 공통 컴포넌트들을 관리하는 디렉토리에서는 배럴 파일의 사용을 유지하였고, 소수의 컴포넌트만 있는 디렉토리에서는 **불필요한 배럴 파일을 제거**하여 불필요한 모듈 로드와 성능 저하를 예방하도록 개선하게 되었다.

<br />

배럴 파일 사용 시의 성능 문제에 대한 자세한 정보는 해당 [문서](https://github.com/yeonjuan/dev-blog/blob/master/JavaScript/speeding-up-the-javascript-ecosystem-the-barrel-file-debacle.md)를 참고하였다.

<br />
<br />
<br />

# 4. useInfiniteScroll 훅 개선

```tsx
import { useEffect } from "react";
import { useInView } from "react-intersection-observer";

interface useInfiniteScrollProps {
fetchNextPage: () => void;
hasNextPage?: boolean;
isFetchingNextPage?: boolean;
}

export default function useInfiniteScroll({
fetchNextPage,
hasNextPage,
isFetchingNextPage,
}: useInfiniteScrollProps) {
const { ref, inView } = useInView();

useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) fetchNextPage();
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);

return ref;
}
```

<br />

기존 **useInfiniteScroll** 훅은 react-query와 결합되어 있다.

하지만 해당 훅의 핵심 역할은 **스크롤이 끝에 도달하면 fetchNextPage를 호출**하는 것이므로, 데이터 의존성을 낮추고 훅의 역할을 더 명확히 하기 위해서는 **조건문**을 받아 처리하는 방식으로 리팩토링하는 것이 낫은 방식이라고 판단하였다.

<br />

```tsx
import { useEffect } from "react";
import { useInView } from "react-intersection-observer";

interface useInfiniteScrollProps {
condition: boolean;
fetchNextPage: () => void;
}

export default function useInfiniteScroll({
condition,
fetchNextPage,
}: useInfiniteScrollProps) {
const { ref, inView } = useInView();

useEffect(() => {
if (inView && condition) fetchNextPage();
}, [inView, condition, fetchNextPage]);

return ref;
}
```

<br />

이렇게 되면 해당 훅은 조건문을 받아서 fetchNextPage를 호출하는 역할만 수행하게 되므로 react-query와의 결합은 줄이고 유연성은 높일 수 있다.

<br />
<br />
<br />

# 5. @types 절대경로 충돌 해결

기존에 `@types`로 aliasing하니 module을 찾을 수 없다는 에러가 발생하였다.

원인은 `node_modules`에 있는 `@types`와 충돌되었기 때문이었다.

해결 방법은 두 가지가 있다.

<br />

첫번째는 `"@types/custom/*": ["src/types/custom/*"]`처럼 `@types` 하위에 별도 중첩 네임스페이스를 사용해서 피하는 방식과 다른 이름을 쓰는 방식이있다.

하지만, npm 패키지에서 설치한 타입인지 내부 프로젝트에서 직접 관리하는 코드인지 쉽게 알기 힘들기에, 두번째 방법으로 그냥 다른 이름으로 aliasing하는 것이 더 좋은 방법이 될 수 있다고 판단하였다.

따라서 프로젝트 내부에서 사용되는 타입이라는 의미로 `@internalTypes`로 aliasing 하기로 결정하였다.

<br />

이후 `tsconfig`, `craco.config`, `eslintrc` 파일의 절대경로 alias를 다시 작성하여 해결하였다.

<br />

```tsx
// tsconfig.path.json

{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@assets/*": ["./src/assets/*"],
"@hooks/*": ["./src/hooks/*"],
"@pages/*": ["./src/pages/*"],
"@routes/*": ["./src/routes/*"],
"@utils/*": ["./src/utils/*"],
"@context/*": ["./src/context/*"],
"@internalTypes/*": ["./src/types/*"],
"@apis/*": ["./src/apis/*"]
}
}
}
```

<br />

```tsx
// craco.config.js

const path = require("path");

module.exports = {
webpack: {
alias: {
"@": path.resolve(__dirname, "src/"),
"@components": path.resolve(__dirname, "src/components"),
"@assets": path.resolve(__dirname, "src/assets"),
"@hooks": path.resolve(__dirname, "src/hooks"),
"@pages": path.resolve(__dirname, "src/pages"),
"@routes": path.resolve(__dirname, "src/routes"),
"@utils": path.resolve(__dirname, "src/utils"),
"@context": path.resolve(__dirname, "src/context"),
"@internalTypes": path.resolve(__dirname, "src/types"),
"@apis": path.resolve(__dirname, "src/apis"),
},
},
};
```

<br />

```tsx
// .eslintrc

{
"settings": {
"import/resolver": {
"alias": {
"map": [
["@", "./src"],
["@components", "./src/components"],
["@assets", "./src/assets"],
["@hooks", "./src/hooks"],
["@pages", "./src/pages"],
["@routes", "./src/routes"],
["@utils", "./src/utils"],
["@context", "./src/context"],
["@internalTypes", "./src/types"],
["@apis", "./src/apis"]
],
"extensions": [".js", ".jsx", ".ts", ".tsx"]
}
}
}
}
```

<br />
<br />
<br />

# 마치면서

이번 리팩토링 과정에서, **rest operator**를 습관적으로 남발했던 점을 되돌아볼 수 있었다.

특히, **제한된 props만을 받는 컴포넌트**에서는 rest operator가 오히려 **불필요한 코드**가 될 수 있다는 것을 깨달았다.

<br />

다음으로, **barrel file pattern**을 사용해 import 구문을 간결하게 만드는 데 집중했으나, 프로젝트가 커질수록 **불필요한 모듈 로드**로 인해 **성능 저하**가 발생할 수 있다는 문제를 알게 되었다.

모든 컴포넌트를 배럴 파일로 묶는 것이 항상 최선의 방법은 아니었고, **컴포넌트 수가 많은 디렉토리에서는 배럴 파일을 유지하되**, 그렇지 않은 경우에는 배럴 파일을 제거하여 **불필요한 의존성 로드**를 줄이는 것이 성능 최적화에 도움이 된다는 것을 알 수 있었다.

<br />

마지막으로, **절대 경로 설정**이 편리하지만, `@types`와 같은 네임스페이스와 **충돌이 발생할 수 있다는 문제**도 알게 되었다.

프로젝트 내부에서 사용하는 타입과 외부 라이브러리의 타입이 충돌할 수 있기 때문에, 이를 해결하기 위한 **aliasing 전략**이 중요하다는 것을 알게 되었다.
5 changes: 5 additions & 0 deletions _site/archive.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ <h1 class="title">Hi, I’m <strong>Kang Byeong-hyeon</strong>.</h1>
<a href="/%EC%9A%B0%EC%84%A0-%EC%9A%B0%EC%84%A0%EC%9D%84-%EC%A4%91%EB%8B%A8%ED%95%98%EB%A9%B0.html" class="post-item"
>[우선] 우선을 중단하며</a
>
</li><li>
<span>2024-07-22</span>
<a href="/%EC%B9%B4%EC%B9%B4%EC%98%A4-%ED%85%8C%ED%81%AC-%EC%BA%A0%ED%8D%BC%EC%8A%A4-2%EA%B8%B0-2%EC%B0%A8-%EC%BD%94%EB%93%9C%EB%A6%AC%EB%B7%B0-%EB%B0%98%EC%98%81-%EB%B0%8F-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%A7%84%ED%96%89%EA%B8%B0.html" class="post-item"
>[카카오 테크 캠퍼스 2기] 2차 코드리뷰 반영 및 리팩토링 진행기</a
>
</li><li>
<span>2024-07-21</span>
<a href="/husky%EC%99%80-lint-staged%EB%A1%9C-git-hooks-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%98%EA%B8%B0.html" class="post-item"
Expand Down
Loading

0 comments on commit e9aef2b

Please sign in to comment.