Hi, I’m Kang Byeong-hyeon.

..

[카카오 테크 캠퍼스 2기] 1차 코드리뷰 반영 및 리팩토링 진행기




미션을 진행하면서 받은 코드리뷰를 바탕으로 개인적으로 리팩토링 해보는 시간을 가질 수 있었다.

이번 코드 리뷰를 반영해보면서 폴더 구조 개선코드 리팩토링, 그리고 스타일 관리 방법에 대해 고민하고 개선한 내용들을 기록해 보았다.




1. 폴더구조 개선

초기에는 각 페이지의 컴포넌트들을 components 폴더 하위에 페이지별로 디렉토리를 나눠서 관리했었다.

그러나, 페이지가 많으면 많아질수록 디렉토리 구조가 복잡해지고 파일을 찾는 데 많은 시간이 걸릴 수 있다는 것을 느꼈다.

때문에, 미션 코드에서 제안한 features 디렉토리에 대한 의미를 살펴보고 적용해보면서, 기능 단위로 컴포넌트들을 그룹화하고 각 페이지별 디렉토리 구조를 간소화하여 각 기능에 해당하는 파일을 찾는데 소요되는 시간을 줄일 수 있었다.





2. Layout 컴포넌트 설계

기존에는 HeaderFooter 컴포넌트를 각 페이지마다 개별적으로 렌더링했기 때문에, 동일한 코드를 반복 작성하는 비효율이 발생하였다.

이를 개선하기 위해 HeaderFooter 같은 공통 레이아웃 컴포넌트를 Layout이라는 컴포넌트로 추상화하여 설계하게 되었다.

이렇게 함으로써 보일러플레이트 코드를 줄이고, 모든 페이지에서 공통 레이아웃을 손쉽게 재사용할 수 있게 되었다.


import React, { ReactNode } from "react";
import { Header, Footer } from "@components/common";

export interface PageWrapperProps {
  children: ReactNode;
}

export default function Layout({ children }: PageWrapperProps) {
  return (
    <>
      <Header />
      <main>{children}</main>
      <Footer />
    </>
  );
}




3. Colocation 원칙 준수

PrivateRoute

import React from "react";
import { useAuth } from "@context/auth/useAuth";
import { ROUTE_PATH } from "@routes/path";
import { Navigate, Outlet } from "react-router-dom";

export default function PrivateRoute() {
  const { isAuthenticated } = useAuth();

  return isAuthenticated ? <Outlet /> : <Navigate to={ROUTE_PATH.LOGIN} />;
}


PrivateRoute는 라우팅과 관련된 컴포넌트로, 라우팅 기능과 밀접한 관계가 있다.

기존에 이 컴포넌트를 components/common에서 관리하고 있었지만, Colocation 원칙을 준수하여 routes 폴더로 이동시켰다.

Colocation이란 관련된 코드와 파일을 서로 가까이 배치하여 관리하는 원칙을 의미한다.

연관된 기능을 한 곳에서 쉽게 찾을 수 있어 코드 가독성과 유지보수성이 크게 향상되는 장점이 있다..

PrivateRoute 컴포넌트는 라우팅과 관련된 컴포넌트이므로, routes 폴더에 배치함으로써 라우팅 관련 기능이 집중적으로 관리될 수 있도록 구조를 개선해보았다.

이로 인해 코드 구조가 더욱 명확해지고, 관련 코드를 찾는 데 걸리는 시간을 대폭 줄일 수 있다.


스크린샷 2024-07-09 오후 2.41.00.png 스크린샷 2024-07-09 오후 2.42.40.png


useLoginForm

스크린샷 2024-07-09 오후 3.26.11.png


useLoginFormLoginForm은 밀접하게 결합된 로직이므로, root 경로에 있는 hooks 에서 관리하는 것이 아닌 LoginForm과 함께 관리하도록 구조를 개선해보았다.

해당 방식이 컴포넌트의 책임로직의 관계를 관련 폴더에 관리함으로서 명확히 구분할 수 있다고 판단되어 Colocation을 준수하여 개선해보았다.





4. 폴더 및 파일 네이밍

path

기존에 routes/constants.ts로 네이밍했던 파일을 path.ts로 변경하여, 해당 파일이 라우팅 경로(path)만을 관리하는 파일임을 명확히하였다.

추후에 관리해야 할 페이지가 많아지면, 관리해야할 경로(path)또한 많아지게 된다.

따라서, 모든 상수를 정의하는 constants라는 네이밍은 포괄적인 의미를 가진다고 생각하였고, 라우팅 경로만을 관리하는 역할과는 부합하지 않다고 판단하였다.

이러한 이유로 파일명을 path.ts로 변경함으로써 해당 파일이 경로(path) 상수만을 관리하는 역할을 명확하게 하였고, 가독성과 관리 효율성을 높임과 동시에 파일의 역할과 목적을 더 쉽게 이해할 수 있도록 개선할 수 있었다.



mocks

기존의 data 디렉토리는 실제 데이터가짜 데이터를 모두 관리하는 의미로 보일 수 있다고 판단하였다.

때문에 이를 mocks 라는 디렉토리로 네이밍하여 가짜 데이터테스트용 응답만을 관리하는 폴더임을 명확하게 알 수 있도록 개선해보았다.





6. 타입 제약 조건 활용한 maxWidth 관리 방식

스크린샷 2024-07-09 오후 3.43.00.png


스크린샷 2024-07-09 오후 3.43.31.png 스크린샷 2024-07-09 오후 3.43.46.png

이전에는 maxWidth에 대한 값들을 assets/variants라는 파일에서 관리하고 있었다.


import React, { ReactNode } from "react";
import styled from "@emotion/styled";

export interface InnerProps {
  maxWidth: number;
  children: ReactNode;
}

export default function Inner({ maxWidth, children }: InnerProps) {
  return <Container maxWidth={maxWidth}>{children}</Container>;
}

const Container = styled.div<{ maxWidth: number }>`
  max-width: ${({ maxWidth }) => maxWidth}px;
  margin: 0 auto;
`;


기존에 Inner 컴포넌트에서 maxWidth라는 값을 number로 받고 있었는데, 해당 maxWidth값들을 varaints에서 관리하고 있다면, maxWidth값에 제약 조건을 걸어 유연하게 활용할 수 있도록 개선해볼 수 있었다.


import React, { ReactNode } from "react";
import styled from "@emotion/styled";
import { BREAK_POINTS } from "@assets/styles/variants";

type MaxWidth = "xs" | "sm" | "md" | "lg";

export interface InnerProps {
  maxWidth: MaxWidth;
  children: ReactNode;
}

export default function Inner({ maxWidth, children }: InnerProps) {
  return <Container maxWidth={maxWidth}>{children}</Container>;
}

const Container = styled.div<{ maxWidth: MaxWidth }>`
  max-width: ${({ maxWidth }) => BREAK_POINT[maxWidth]}px;
  margin: 0 auto;
`;


따라서 위와 같이 MaxWidth 라는 타입 제약조건을 걸어서 ‘initial’, ‘xs’ , ‘sm’ , ‘md’ , ‘lg’로 구분해서 값을 받도록 개선하였다.

또한 Inner라는 컴포넌트 명칭이 정확한 책임이 무엇인지 파악하기 어렵기 때문에 CenteredContainer으로 변경하여 요소들을 중앙으로 배치하는 Container라는 역할을 명확하게 알 수 있게 하기위해 다시 네이밍하였다.


import React, { ReactNode } from "react";
import styled from "@emotion/styled";
import { BREAK_POINTS } from "@assets/styles/variants";

type MaxWidth = "xs" | "sm" | "md" | "lg";

export interface CenteredContainerProps {
  maxWidth: MaxWidth;
  children: ReactNode;
}

export default function CenteredContainer({
  maxWidth,
  children,
}: CenteredContainerProps) {
  return <Container maxWidth={maxWidth}>{children}</Container>;
}

const Container = styled.div<{ maxWidth: MaxWidth }>`
  max-width: ${({ maxWidth }) => BREAK_POINTS[maxWidth]}px;
  margin: 0 auto;
`;




7. spread operator 대신 명시적으로 Props 전달

필자는 spread operator를 사용하면 더 간결하게 코드를 작성할 수 있다고 생각하여 작성하고 있었다.

export default function GoodsItemList() {
  return (
    <GoodsItemListContainer>
      <CenteredContainer maxWidth="md">
        <Grid gap={GRID_GAP} columns={GRID_COLUMNS}>
          {goodsItemList.map(
            ({ id, imageSrc, subtitle, title, amount, target, wish }) => (
              <GoodsItem
                key={id}
                {...{ imageSrc, subtitle, title, amount, target, wish }}
              />
            )
          )}
        </Grid>
      </CenteredContainer>
    </GoodsItemListContainer>
  );
}


하지만 spread operator를 사용하면 존재하지 않는 props를 TypeScript가 인지하지 못하는 문제가 발생할 수 있다.

때문에 spread operator를 사용하는 대신, key-value 형태로 props를 명시적으로 전달하도록 변경해야한다.


export default function GoodsItemList() {
  return (
    <GoodsItemListContainer>
      <CenteredContainer maxWidth="md">
        <Grid gap={GRID_GAP} columns={GRID_COLUMNS}>
          {goodsItemList.map(({ id, imageSrc, subtitle, title, amount }) => (
            <GoodsItem key={id} **imageSrc={imageSrc} subtitle={subtitle} title={title} amount={amount}** />
          ))}
        </Grid>
      </CenteredContainer>
    </GoodsItemListContainer>
  );
}




8. sr-only 클래스 관리 방식

스크린샷 2024-07-09 오후 4.39.15.png


어떻게 보면 sr-only클래스는 reset 스타일링과는 다른 성격을 가진다.

모든 컴포넌트 스타일링에서 공통으로 사용할 수 있는 클래스이기 때문에 필자는 commonStyles.ts 파일로 분리하기로 관리할 수 있도록 결정하였다.

이후 resetStyles와 commonStyles는 전역에서 사용되는 스타일이기 때문에 global 디렉토리로 관리하였다.


import { css } from "@emotion/react";

const commonStyles = css`
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    border: 0;
    clip: rect(0 0 0 0);
  }
`;

export default commonStyles;


이후 resetStyles와 commonStyles를 모두 Global Provider에 styles에 전달해주어야 하기 때문에

styles디렉토리에 index.tsx로 컴포넌트 파일을 만들어 모든 style의 파일들을 하나로 모으고 Global Provider도 이안에서 관리하도록 분리해보았다.


import React from "react";
import { Global, css } from "@emotion/react";
import commonStyles from "@assets/styles/global/commonStyles";
import resetStyles from "@assets/styles/global/resetStyles";

const globalStyles = css`
  ${resetStyles}
  ${commonStyles}
`;

export default function GlobalStyles() {
  return <Global styles={globalStyles} />;
}


이후 GlobalStyles 컴포넌트만 프로젝트 루트에 사용하기만 하면 된다.


import React from 'react';
import { Outlet } from 'react-router-dom';
import GlobalStyles from '@assets/styles/GlobalStyles';

function App() {
  return (
    <>
      <GlobalStyles />
      <Outlet />
    <>
  );
}

export default App;





마치면서

이번 코드리뷰를 통해 폴더 구조, 컴포넌트 역할 명확화, 그리고 스타일 관리 방식을 개선할 수 있었다.

특히, 코드의 유지보수성과 재사용성을 높이기 위한 고민을 많이 하게 되었으며, 이 과정에서 Colocation과 같은 원칙을 적용해 구조를 단순화하고 명확하게 할 수 있다는 것 또한 알게 되었다.

리팩토링은 단순히 코드의 가독성을 높이는 것 이상의 효과를 가져다 주는 것 같다.

앞으로도 지속적으로 개선점을 찾고, 더 나은 코드와 구조를 만드는 데 노력하면서 그동안 숙지하지 못했던 내용들을 학습해 나갈 예정이다.