Skip to content

Conversation

@Chiman2937
Copy link
Collaborator

@Chiman2937 Chiman2937 commented May 19, 2025

요구사항

기본

상품 등록

  • 상품 등록 페이지 주소는 “/additem” 입니다.
  • 페이지 주소가 “/additem” 일때 상단네비게이션바의 '중고마켓' 버튼의 색상은 “3692FF”입니다.
  • 상품 이미지는 최대 한개 업로드가 가능합니다.
  • 각 input의 placeholder 값을 정확히 입력해주세요.
  • 이미지를 제외하고 input 에 모든 값을 입력하면 ‘등록' 버튼이 활성화 됩니다.
  • API를 통한 상품 등록은 추후 미션에서 적용합니다.

심화

상품 등록

  • 이미지 안의 X 버튼을 누르면 이미지가 삭제됩니다.
  • 추가된 태그 안의 X 버튼을 누르면 해당 태그는 삭제됩니다.

주요 변경사항

  • (전체)페이지 일부 구역 컴포넌트화
  • 로그인 상태가 아니면 items 페이지의 상품 등록하기 버튼이 비활성화 되도록 수정
  • items 페이지에서 상품 검색 시 쿼리스트링 변경/ 주소창 검색 기능 추가
  • items 페이지에 skeleton UI 적용
  • items 페이지에서 이전/다음 버튼 클릭 로직 변경 (ex. 1페이지에서 다음 버튼 클릭 시 6페이지로 이동)
  • 미션 5 피드백 내용 반영
    • Items 페이지의 pagination 컴포넌트 분리, 커스텀 훅으로 기능 분리
    • validators 함수를 useFormField에서 통합 관리
    • 불필요한 함수를 객체로 변경
    • useReducer는 조금 더 공부해보고 적용해보고 싶어서 아직 시도하지 않았습니다.
    • render props를 이용해 로그인, 회원가입 Form을 구성해 봤는데 저에겐 아직 코드가 잘 읽히지 않는 것 같아서 다시 원래 방식으로 적용하였습니다.

배포

https://sprint-mission-kcy.netlify.app/

스크린샷

미션5 변경사항 - skeleton UI

image

미션5 변경사항 - 쿼리스트링

image

반응형 디자인(PC)

image

반응형 디자인(TABLET, MOBILE)

image

이미지 업로드

image

이미지 업로드(이미지가 있는 상태에서 이미지 등록 버튼을 한번 더 누를 경우)

image

이미지 삭제

image

가격 입력(자동 콤마 적용)

image

태그 입력(텍스트 입력 후 Enter)

image

태그 삭제(x 버튼)

image

등록 버튼 활성화

  • 태그 input에 값이 있을 때가 아니라 태그가 1개 이상 있을 때 등록 버튼이 활성화 되어야 한다고 생각해서, input에 값이 없더라도 태그가 1개 이상이면 등록버튼이 활성화 되도록 작업했습니다.
    image

멘토에게

  • Loading Spinner와 Skeleton UI 중 어떤 상황에서 어떤 것이 더 적합한지, 어떤 것을 더 많이 사용하는 추세인지 등을 알고 싶습니다.

  • pagination 기능을 커스텀 훅으로 분리하면서 아래와 같이 기능 분리를 정해 놓고 작업했는데, 더 좋은 방식이 있는지 의견을 여쭤보고 싶습니다.

    • Items 컴포넌트 : Request 데이터에 대한 정보 관리(총 데이터 개수, offset 등)
    • usePagination 커스텀 훅 : 현재 선택된 페이지, 보여줄 페이지 번호, 총 데이터 개수 관리
    • Pagination 컴포넌트 : Pagination UI 관리, 페이지 번호, 이전, 다음 버튼 로직 관리
  • 미션6에서 Field를 컴포넌트로 만들어서 넣고 싶었는데, input의 형태가 다르다보니 어떻게 하는게 좋을 지 고민이 돼서 일단 Flat한 형태로 마무리 했습니다. 모양이 다른 Field들 마다 컴포넌트를 별도로 만드는게 좋을까요?

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

Chiman2937 added 16 commits May 15, 2025 17:30
@Chiman2937 Chiman2937 requested a review from addiescode-sj May 19, 2025 07:52
@Chiman2937 Chiman2937 added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label May 19, 2025
@Chiman2937 Chiman2937 changed the base branch from Basic-김치영 to React-김치영 May 21, 2025 00:22
Copy link
Collaborator

@addiescode-sj addiescode-sj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다!
이번에도 한번에 읽어야하는 코드양이 많아서 리뷰가 조금 힘들었네요 ㅎㅎ

열심히하셔서 좋지만, 나중에 팀 프로젝트로 협업하실땐 리뷰어 입장에서 퀄리티있는 리뷰가 가능하도록 PR을 쪼개서 올려보는 연습을 해봅시다! ㅎㅎ

주요 리뷰 포인트

  • 코드 스타일
  • 가독성, 관리 포인트 개선
  • 스켈레톤 (합성 컴포넌트 패턴) 개선 제안
  • 공용으로 취급되는 폴더 구분 제안


const CurrentItemsSection = ({ pageSize }) => {
//prettier-ignore
const isLogin = useIsLogin();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoginStateContext 내부를 들여다보니 이렇게 반환값마다 따로 훅을 만드셨던데,
반환값마다 훅을 만들어 사용하게되면, 오히려 Context를 사용해 관련있는 값끼리 응집도를 가지고 모여져있어 유지보수에 용이해지는 장점도 사라지고, 개발자가 어떤 훅을 사용해야 어떤 반환값을 사용할수있다는 사실을 인지해야하므로 불필요한 관리가 추가되게됩니다.

LoginContext를 만들때, 로그인 여부와 간단한 유저정보 등 로그인에 관련된 모든 정보를 한꺼번에 관리하는 형태로 개선해주세요! :)

  • BEFORE
export const useIsLogin = () => {
  const context = useContext(LoginStateContext);
  if (!context) {
    throw new Error('반드시 LoginStateProvider 안에서 사용해야 합니다');
  }
  return context.isLogin;
};

export const useSetIsLogin = () => {
  const context = useContext(LoginStateContext);
  if (!context) {
    throw new Error('반드시 LoginStateProvider 안에서 사용해야 합니다');
  }
  return context.setIsLogin;
};
  • AS-IS
export const LoginProvider = ({ children }) => {
 // 로그인 식별 정보뿐만 아니라, 로그인에 관련된 다른 정보 (유저 닉네임, 연락 처 등등)도 같이 한 Context로 관리 
  const [isLogin, setIsLogin] = useState(false);
  return (
    <LoginContext.Provider value={{ isLogin, setIsLogin }}>
      {children}
    </LoginContext.Provider>
  );
};

export const useLoginContext = () => {
  const context = useContext(LoginContext);
  if (!context) {
    throw new Error('반드시 LoginProvider 안에서 사용해야 합니다');
  }
  return context;
};

<div className={`${styles["items-container"]} ${styles[listName]}`}>
{itemList.length === 0
? Array.from({ length: pageSize }, (_, i) => {
return <ItemCardSkeleton key={i} />;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스켈레톤 적용하셨군요! 굳굳 👍

다만, 해당 스켈레톤은 여러 버전의 UI를 가질 수 있지만 재사용 가능한 부분 (예를 들면 background color나 사이즈에 따라 스켈레톤 UI가 생성되는 등의 반복되는 로직들)을 분리해 합성 컴포넌트 패턴으로 Skeleton 컴포넌트로 만들어보는건 어떨까해요.

조금 생소하겠지만 이 아티클 참고해보시고, 제가 예시로 작성한 코드를 같이 보면서 읽어만보셔도됩니다! :)

  • 기본 스켈레톤 컴포넌트 만들기
import styles from "./Skeleton.module.css";

const Skeleton = ({ children }) => {
  return children;
};

// 기본 스켈레톤 컴포넌트
Skeleton.Base = ({
  width = "100%",
  height = "100%",
  borderRadius = "4px",
  className,
  ...props
}) => {
  return (
    <div
      className={`${styles["skeleton-base"]} ${className || ""}`}
      style={{
        width,
        height,
        borderRadius,
      }}
      {...props}
    />
  );
};
  • 기본 스켈레톤을 합성한 상품 목록 스켈레톤 만들기
// 상품 목록 스켈레톤
Skeleton.ProductList = ({ count, className }) => {
  return (
    <div className={`${styles["product-list-skeleton"]} ${className || ""}`}>
      {Array.from({ length: count }, (_, i) => (
        <Skeleton.Base
          key={i}
          width="280px"
          height="360px"
          className={styles["product-card-skeleton"]}
        />
      ))}
    </div>
  );
};
  • 사용 예시
// 상품 목록 스켈레톤
<Skeleton.ProductList count={5} />

// 커스텀 크기의 카드 스켈레톤
<Skeleton.Card width="320px" height="240px" />

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이처럼, 리액트는 상속보다는 합성에 특화되어있습니다.
예전 버전이긴하지만, 공식문서에도 나와있는 내용입니다.

@addiescode-sj
Copy link
Collaborator

addiescode-sj commented May 21, 2025

질문에 대한 답변

멘토에게

  • Loading Spinner와 Skeleton UI 중 어떤 상황에서 어떤 것이 더 적합한지, 어떤 것을 더 많이 사용하는 추세인지 등을 알고 싶습니다.

컨텐츠의 특성에 따라 다를것같네요. 스켈레톤의 경우 어느정도의 레이아웃이 픽스된 상태에서 CLS를 방지하기 유리하고, 로딩 스피너는 레이아웃이 픽스되어있지않다거나, 한꺼번에 처리하는 데이터의 양이 매우 많고, 과정이 복잡할 경우 사용하기 적합합니다.

  • pagination 기능을 커스텀 훅으로 분리하면서 아래와 같이 기능 분리를 정해 놓고 작업했는데, 더 좋은 방식이 있는지 의견을 여쭤보고 싶습니다.

    • Items 컴포넌트 : Request 데이터에 대한 정보 관리(총 데이터 개수, offset 등)
    • usePagination 커스텀 훅 : 현재 선택된 페이지, 보여줄 페이지 번호, 총 데이터 개수 관리
    • Pagination 컴포넌트 : Pagination UI 관리, 페이지 번호, 이전, 다음 버튼 로직 관리

Items 컴포넌트는 usePagination, Pagination을 사용하는 컴포넌트인거죠?
구조적으로는 좋습니다. 관심사의 분리가 적절하게 이루어져있네요!

내부 로직을 작성할때 이런 점들을 좀 더 신경써주시면 좋을것같아요.

  • 상태 관리 명확성: 각 훅이 관리하는 상태가 명확하게 구분되는지 체크
    예를 들면, 데이터 fetching 상태와 페이지네이션 상태가 분리되어 있고, 디버깅이 용이한지 확인

  • 유지보수성 향상: 각 기능이 독립적으로 분리되어 있어 코드 수정이 필요할 때 끼치는 영향 범위가 제한적이고, 새로운 기능 추가가 용이한지 체크

  • 미션6에서 Field를 컴포넌트로 만들어서 넣고 싶었는데, input의 형태가 다르다보니 어떻게 하는게 좋을 지 고민이 돼서 일단 Flat한 형태로 마무리 했습니다. 모양이 다른 Field들 마다 컴포넌트를 별도로 만드는게 좋을까요?

스켈레톤을 예시로 합성 컴포넌트 패턴을 알려드렸는데, 참고해보시면 도움이 되실것같네요 :)
혹은 Radix UI가 채택한 Render delgation 기법을 사용하셔도 좋을것같습니다.
참고

@addiescode-sj addiescode-sj merged commit 8d81b58 into codeit-bootcamp-frontend:React-김치영 May 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants