Skip to content

관심사 분리와 컨벤션 준수로 가독성 향상

이선근 edited this page Oct 10, 2023 · 1 revision

아래 코드는 리팩토링 이전 프로필 페이지입니다.

export default function Profile() {
  const [view, setView] = useState('list');
  const { accountname } = useParams();
  const currentUserData = useRecoilValue(recoilData);
  const myAccountName = currentUserData.accountname;
  const { products } = useGetProducts(accountname);
  const { profile } = useGetProfile(accountname);
  const { posts, fetchNextPosts, hasNextPosts } = useGetInfinitePosts(
    accountname,
    'userpost',
  );
  const { toggleFollowMutate } = useToggleFollow();
  const { deletePostMutate } = useDeletePost(accountname);
  const { deleteProductMutate } = useDeleteProduct(accountname);

  // 무한 스크롤
  const isBottom = useScrollBottom();
  useEffect(() => {
    if (isBottom && hasNextPosts) {
      console.log(hasNextPosts);
      fetchNextPosts();
    }
  }, [isBottom]);

  ///공유 버튼 클릭시 링크 클립보드에 복사
  const handleCopyClipBoard = async (text) => {
    ...
  };

  return (
    <>
      <TopNav>
        <TopNav.BackButton />
        <TopNav.OptionButton />
      </TopNav>
      <S.Container>
        <S.ProfileSection>
          <S.ProfileWrap>
           ...
          </S.ProfileWrap>
          <S.UserName>{profile.username}</S.UserName>
          <S.UserId>@ {profile.accountname}</S.UserId>
          <S.UserIntro>{profile.intro}</S.UserIntro>
          {myAccountName === accountname ? (
            <S.UserBtnWrap>
              ...
            </S.UserBtnWrap>
          ) : (
            <S.UserBtnWrap>
              ...
            </S.UserBtnWrap>
          )}
        </S.ProfileSection>
        {products.length > 0 && (
          <S.ProductSection>
            <S.StyledH2>판매 중인 상품</S.StyledH2>
            <S.ProductList>
              ...
            </S.ProductList>
          </S.ProductSection>
        )}
        {posts?.pages?.length > 0 && (
          <S.PostSection>
            <S.PostTop>
              <S.viewButton
                view={view}
                type="button"
                onClick={() => setView('list')}
              ></S.viewButton>
              <S.viewButton
                view={view}
                type="button"
                onClick={() => setView('album')}
              ></S.viewButton>
            </S.PostTop>
            <S.PostList view={view}>
              ...
            </S.PostList>
          </S.PostSection>
        )}
      </S.Container>
      <TabMenu />
    </>
  );
}

리팩토링을 한 가장 큰 이유 중 하나입니다. 관심사 분리가 되지 않았고 각 컴포넌트의 추상화 수준이 차이나서 가독성이 크게 떨어지는 문제가 있습니다.

프로필의 페이지는 현재 많은 역할을 하고있습니다. 각 섹션에 필요한 데이터 페칭과 렌더링, 무한스크롤 등 비즈니스 로직 뿐만 아니라 UI관련 로직도 함꼐 다루고있습니다.

페이지에서는 모듈을 조합해 보여주도록하고, 각 컴포넌트에서 비즈니스 로직 그리고 더 세분화해서 UI 로직을 분리하도록 설계했습니다. 이를 통해 각 컴포넌트가 작고 명확한 역할을 담당하면서 가독성과 유지보수성을 향상시켰습니다.

image-6

src\features\profiles\routes\ProfileDetailPage.jsx
export default function ProfileDetailPage() {
  return (
    <>
      <HeaderBar>
        <HeaderBar.BackButton />
        <HeaderBar.OptionButton />
      </HeaderBar>
      <S.PageWrapper>
        <S.Section>
          <Profile />
        </S.Section>
        <S.Section>
          <ProductList />
        </S.Section>
        <S.Section>
          <PostList postType="user" hasViewController />
        </S.Section>
      </S.PageWrapper>
    </>
  );
}
// src\features\products\components\ProductList.jsx
export default function ProductList() {
  const { accountname } = useParams();

  const { data: productList, isLoading } = useGetProductList(accountname);

  if (isLoading) return <ProductListSkeleton />;

  return (
    <S.ProductListWrapper>
      <S.ProductHeading>판매 중인 상품</S.ProductHeading>
      {productList.length === 0 ? (
        <p>현재 판매 중인 상품이 없습니다.</p>
      ) : (
        <S.ProductList>
          {productList.map((product) => (
            <li key={product.id}>
              <Product product={product} />
            </li>
          ))}
        </S.ProductList>
      )}
    </S.ProductListWrapper>
  );
}

컨벤션

네이밍

네이밍 컨벤션을 카멜케이스를 사용한다. 정로도 정했더니 변수명이 난잡했습니다.

그래서 리팩토링을 하는 과정에서 변수명도 아래와 같은 규칙을 가지고 수정했습니다.

  • 변수명: 명사
  • 함수명: 동사로 시작
  • 이벤트 핸들러: handle prefix
  • 상수: 대문자와 언더스코어

import

import 순서를 정하지 않아 알아보기 어려운 문제가 있었습니다.

eslint-plugin-import 패키지로 import를 형식별로 그룹화해 정리했습니다.

"rules": {
  "import/order": [
    "warn",
    {
      "groups": [ // import 문을 그룹화하는 규칙 정의
        "builtin", // 내장 모듈 (예: fs, path)
        "external", // 외부 패키지 (npm 등으로 설치한 모듈)
        "internal", // 내부 프로젝트 모듈 (프로젝트 내부에서 사용하는 모듈)
        ["parent", "sibling"], // 상위 디렉터리 및 동일한 디렉터리에 있는 모듈
        "type", // .d.ts (타입 정의) 모듈
        "unknown", // 알 수 없는 모듈 유형
        "object" // 객체나 클래스 같은 객체 기반 모듈
      ],
      "pathGroups": [ // 특정 패턴에 따라 경로를 그룹화하는 규칙을 정의
        {
          "pattern": "{.,..}/**/*.styled", // .styled 확장자를 가진 파일의 경로를 그룹화
          "group": "object" // 객체 기반 모듈로 분류
        }
      ],
      "newlines-between": "always", // import 그룹 사이에 항상 빈 줄을 넣어주도록 설정
      "alphabetize": { // import 문을 알파벳 순서로 정렬하는 규칙을 정의
        "order": "asc", // 오름차순으로 정렬
        "caseInsensitive": true // 대소문자를 구분 X
      }
    }
  ]
}

또한 craco 패키지로 alias를 사용했습니다. 기존에는 상대경로로 다른 모듈을 가져올때 ../../../~와 같이 depth가 깊어져 가독성이 떨어지던 문제를 해결했습니다.

module.exports = {
  webpack: {
    alias: {
      '@': require('path').resolve(__dirname, 'src'),
    },
  },
};

eslint-plugin-import와 craco로 정리된 import 예시

import React from 'react';
import { useParams } from 'react-router-dom';

import FollowButton from './FollowButton';
import ProfileSkeleton from './ProfileSkeleton';
import { useGetProfile } from '../api/getProfile';

import Button from '@/components/Elements/Button/Button';
import CircleImage from '@/components/Elements/CircleImage/CircleImage';
import useAuth from '@/hooks/useAuth';
import shareURL from '@/util/shareURL';

import * as S from './Profile.styled';


export default function Profile() {
  ...