Skip to content

[이태경] Sprint7 #235

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

LeeTaegyung
Copy link
Collaborator

@LeeTaegyung LeeTaegyung commented Jul 17, 2025

요구사항

기본

상품 상세

  • [x]상품 상세 페이지 주소는 "/items/{productId}" 입니다.
  • response 로 받은 아래의 데이터로 화면을 구현합니다.
    • favoriteCount : 하트 개수
    • images : 상품 이미지
    • tags : 상품태그
    • name : 상품 이름
    • description : 상품 설명
  • 목록으로 돌아가기 버튼을 클릭하면 중고마켓 페이지 주소인 "/items" 으로 이동합니다

상품 문의 댓글

  • 문의하기에 내용을 입력하면 등록 버튼의 색상은 "3692FF"로 변합니다.
  • response 로 받은 아래의 데이터로 화면을 구현합니다
    • image : 작성자 이미지
    • nickname : 작성자 닉네임
    • content : 작성자가 남긴 문구
    • description : 상품 설명
    • updatedAt : 문의글 마지막 업데이트 시간

심화

  • 모든 버튼에 자유롭게 Hover효과를 적용하세요.

주요 변경사항

  • 특정 파일 타입스크립트 적용
    • 상품 상세 페이지 src/pages/ProductDetailPage/
    • 공통 컴포넌트
  • scss → emotion 마이그레이션중
    • import 관련 오류가 발생합니다.
      → 모든 페이지 마이그레이션 완료하면 해결 될 것으로 보입니다.
      @import rules can't be after other rules. Please put your @import rules before your other rules. Error Component Stack
  • 공통 컴포넌트 추가
    • Dropdown → 컴파운드 패턴 적용
    • Button
    • TagItem
    • TextArea

스크린샷

image image image

멘토에게

  • 이모션과 타입스크립트를 동시에 마이그레이션 하려고 하니 시간이 꽤 오래 걸리는데, 현업에서도 이렇게 종종 여러 기술들을 동시에 마이그레이션 하는 상황이 발생하나요?
  • Suspense를 한번 적용해보려고 했는데... 프로미스를 반환해야한다는 부분에서 이해가 되지 않아서 사용을 못했습니다. 구글링을 좀 더 해보니깐 라이브러리와 함께 사용하는게 좋다는? 글을 봐서, 단순히 fetch로는 사용이 힘들까요?
  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@LeeTaegyung LeeTaegyung added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Jul 17, 2025
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

구글링을 하다가 컴파운드 패턴을 발견해서 적용해봤습니다. 컴포넌트의 순서가 고정되지 않고 부모 컴포넌트에서 사용시 유동적으로 바꿀 수 있는 부분이 재사용 하기 좋다는 느낌을 받았습니다.
컴파운트 패턴 방식을 현업에서 많이 사용되나요?

Copy link
Collaborator

Choose a reason for hiding this comment

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

스스로 찾아보시고 적용까지 해보시다니 좋은 시도를 하셨네요! 굳굳 👍

네, 컴파운드 패턴은 재사용성을 극대화하기 위해 많이 사용하는편입니다.
특히 이럴때 사용하시면 좋아요:

  • 여러 하위 컴포넌트가 상호작용해야 할 때
    ex. Dropdown의 Trigger, List, Item처럼 여러 자식 컴포넌트가 상태를 공유해야 할 때

  • 컴포넌트의 구조(순서, 중첩 등)를 유연하게 바꿔야 할 때
    ex. 부모가 자식의 순서, 개수, 중첩을 자유롭게 조합할 수 있어야 할 때

  • 상태와 로직을 부모에서 관리하고, 자식은 UI만 담당하게 하고 싶을 때 (관심사 분리 의도)

하지만, 이 패턴을 사용할 경우 코드양이 증가하고 가독성이 떨어질수있습니다.

따라서 추후 변경될 여지가 있어보이는 코드에 사용할때는 적절하지만, 오히려 역할이 명확한 컴포넌트라면 재사용성과 변경 가능성을 위해 이런 패턴을 사용하는것이 좋지 않을수있으니 판단할 수 있는 기준을 참고해보고, 상황에 따라 적절히 활용해보시면 좋을것같아요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

단일 원칙 책임을 기준으로 컴포넌트를 구성하다보니 컴포넌트가 너무 많이 나뉘게 된 것 같습니다. 처음엔 /ProductDetailPage/components/InquiryUserProfile/InquiryUserProfile.tsx 와 같이 컴포넌트별로 폴더를 구성해주었는데, 나중엔 어디에 어떻게 들어가는 컴포넌트인지 구분이 되지 않아서 현재 아래 이미지와 같이 폴더 구조를 수정하였습니다.
이런식으로 내부적으로 계속 분리되는 컴포넌트의 경우 이런 폴더 구조로 관리 해도 될까요?

Copy link
Collaborator

Choose a reason for hiding this comment

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

네! 사용하는 입장에서 찾기 편하게끔 사용처에서 최대한 가깝게 구조 유지하시면 됩니다.

Copy link
Collaborator Author

@LeeTaegyung LeeTaegyung Jul 17, 2025

Choose a reason for hiding this comment

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

/ProductInfo 에서 사용하는 데이터와 /ProductInquiry 에서 사용하는 데이터가 컴포넌트를 쪼개고 props를 내리다보니 프롭스 드릴링이 발생했습니다.
zustand로 프롭스 드릴링을 해결할까 하다가, zustand는 중앙 상태 관리 도구라 전역에서 사용하는 상태를 관리할 때 사용하는게 맞는 것 같다는 생각이 들었습니다. 근데도 이런 경우에 사용을 해도 되는지 궁금합니다.
contextAPI도 생각을 해봤었는데, 코드가 더 복잡해지는 것 같아서 적용하지 않았습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

좋은 질문이네요! 설계 고민을 평소에 깊게 하시는군요 🤩

우선 zustand의 경우에는 하나의 페이지보다는 동시에 여러 페이지/컴포넌트에서 공유해 접근해야하는 상태를 관리할 때 적합합니다. 모든 상태를 전역으로 관리하기때문에, 불필요한 전역 상태가 많아지면 관리도 어렵고 구조 자체가 흔들릴 수 있겠죠?

Context API의 경우에는, Provider로 감싸면 하위 컴포넌트 어디서든 필요한 레벨에서 접근 가능하다는 장점이 있지만
상태가 바뀌면 Provider 하위 모든 컴포넌트가 리렌더링되기때문에, 성능 이슈가 발생할 수 있습니다.
따라서, 상태를 내려주는 깊이가 2~3단계 이내라면 그냥 두는 것도 괜찮습니다.
하지만 더 깊어지거나, 여러 곳에서 접근해야 한다면 개선이 필요하겠죠? :)

지금과 같이 페이지 단위나 특정 영역에서만 공유되는 상태라면 코드 베이스를 구성하는 측면에서 zustand보다는 Context API를 사용하시는게 적합합니다.

위에서 불필요한 리렌더링을 방지할수있는 방법에 대해 코멘트 드렸는데, 참고해보시고 리팩토링하시면 좋을것같네요!

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.

태경님, 타입스크립트 처음 쓰시는건데 너무 잘쓰시는데요? ㅎㅎ
props의 타입을 확장성을 위해 interface를 사용하는것만 고쳐주시면 좋을것같아요!

나머지는 설계 측면에서 궁금해하셨던 부분, 유지보수와 성능을 위해 개선해야할 부분 위주로 코멘트 드려봤습니다.

설계에 대한 고민을 깊게 시도하시는 점이 보여서 너무 좋습니다! 🥇

주요 리뷰 포인트

  • props와 객체의 타입은 type보다는 interface로 작성해보기
  • 공용 컴포넌트에서 스타일링 관련 props를 늘리기보다는 variant 기반으로 관리해보기
  • css vars 사용하지않고 변수 사용하기
  • 컴파운드 패턴 사용의 장단점 비교하고, 적절히 활용하기
  • Context 사용 시 불필요한 리렌더링 방지하는 방법 제안 (selector 패턴 등)
  • zustand vs. Context API (설계 관련 피드백)

Comment on lines +9 to +11
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
BaseButtonProps &
ButtonStyleProps;
Copy link
Collaborator

Choose a reason for hiding this comment

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

보통 props와 객체의 타입은 type보다는 interface로 작성하는것이 권장됩니다.

이유는, interface는 선언적 확장이 가능합니다.
동일한 이름의 interface를 여러 번 선언하면 자동으로 합쳐집니다(선언적 병합).

따라서 라이브러리나 대규모 프로젝트에서 props가 점진적으로 확장될 가능성이 있거나, 외부에서 커스텀 props를 추가할 때 types를 사용하는것보다 확장성 측면에서 더 유리해집니다.

객체의 구조를 type보다 더 명확히 표현할 수 있는 장점또한 존재합니다.

아래와 같이 바꿔볼까요?

export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>,ButtonStyleProps {
  children: React.ReactNode;
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Button의 경우에도 ButtonStyleProps에 스타일 관련 props를 늘리는 방법을 선택하기보다는,
variant 기반으로 바꾸면 스타일 규칙이 제한된 범위 내에서 사용되니까 일관성 측면에서도 좋고, 관리에도 용이할것같아요.
만약 공용 버튼 컴포넌트를 사용해야하는데 커스텀으로 추가되어야하는 스타일이 생기면, variantStyles에 추가하는 방식으로 확장하면되겠죠? :)

export interface ButtonStyleProps {
  variant?: "primary" | "secondary" | "danger";
}

const variantStyles = {
  primary: css`
    ...
  `,
  secondary: css`
    ...
  `,
  danger:  css`
    ...
  `,
};

export const ButtonStyle = styled.button<ButtonStyleProps>`
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  ${({ variant = "primary" }) => variantStyles[variant]}
`;

Comment on lines +15 to +32
background-color: var(--btn-primary);

&:hover {
background-color: var(--btn-primary-hover);
}

&:active {
background-color: var(--btn-primary-click);
}

&:disabled {
background-color: var(--btn-disabled);
}
`,
white: css`
border: 1px solid var(--primary-color);
color: var(--primary-color);
background-color: var(--white);
Copy link
Collaborator

Choose a reason for hiding this comment

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

타입스크립트 환경에서 css-in-js 도구를 쓰면서 css vars를 쓰는건 이런 차이가 납니다:

// CSS 변수 사용 시
const styles = css`
  color: var(--primary-color); // 런타임에만 오류 발견
`;

// JavaScript 변수 사용 시
const styles = css`
  color: ${COLORS.primary.blue}; // 컴파일 타임에 오류 발견
`;

타입스크립트는 정적 테스트 도구이자 강력한 생산성 도구입니다.

타입스크립트 환경에서는 두번째 방식을 사용하게되면 컴파일 타임에 타입 오류를 잡아줄수도있고, IDE에서 자동완성과 타입 체크를 제공받고, 시스템상에서 존재하지 않는 변수를 사용하려하면 즉시 오류를 발견할수도 있죠? :)

또한 자바스크립트 변수는 사용하지 않는 변수가 있다면 트리 셰이킹을 통해 제거되어 번들 크기를 최적화하고 런타임 성능을 향상시키는데 도움이 되는데, CSS 변수는 런타임에 해석되다보니 성능에 도움이 되지 못합니다.

theme을 사용하거나, css vars가 아닌 자바스크립트 변수를 사용하는 방향으로 리팩토링해볼까요?

Copy link
Collaborator

Choose a reason for hiding this comment

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

스스로 찾아보시고 적용까지 해보시다니 좋은 시도를 하셨네요! 굳굳 👍

네, 컴파운드 패턴은 재사용성을 극대화하기 위해 많이 사용하는편입니다.
특히 이럴때 사용하시면 좋아요:

  • 여러 하위 컴포넌트가 상호작용해야 할 때
    ex. Dropdown의 Trigger, List, Item처럼 여러 자식 컴포넌트가 상태를 공유해야 할 때

  • 컴포넌트의 구조(순서, 중첩 등)를 유연하게 바꿔야 할 때
    ex. 부모가 자식의 순서, 개수, 중첩을 자유롭게 조합할 수 있어야 할 때

  • 상태와 로직을 부모에서 관리하고, 자식은 UI만 담당하게 하고 싶을 때 (관심사 분리 의도)

하지만, 이 패턴을 사용할 경우 코드양이 증가하고 가독성이 떨어질수있습니다.

따라서 추후 변경될 여지가 있어보이는 코드에 사용할때는 적절하지만, 오히려 역할이 명확한 컴포넌트라면 재사용성과 변경 가능성을 위해 이런 패턴을 사용하는것이 좋지 않을수있으니 판단할 수 있는 기준을 참고해보고, 상황에 따라 적절히 활용해보시면 좋을것같아요!

Copy link
Collaborator

Choose a reason for hiding this comment

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

NIT: Context를 사용하면 props drilling을 방지할수있는 장점은 생기지만,
동시에 하위 컴포넌트가 많아질수록 불필요한 리렌더링이 증가한다는 단점이 생길 수 있어요.
(DropdownProvider의 value가 바뀌면 해당 Context를 소비하는 모든 하위 컴포넌트가 리렌더링되기 때문에)

지금은 value가 몇개 없어 괜찮지만, 나중에 value에 점점 많은 값이 추가된다면
상태가 자주 바뀌는 값과 그렇지 않은 값에 대해 Context를 분리해서 관리한다거나,
하위 컴포넌트에서 React.memo, useMemo, useCallback 등을 활용해 리렌더링을 최적화하는 방법 등이 있을 수 있으니 추후 코드가 변화하는 양상을 지켜보다가 리팩토링하면 좋겠죠? :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

혹은 필요한 상태만 선택적으로 구독할수있게끔 hook 내부에서 selector 패턴을 쓰는 방법도 있답니다!

만약 selector 패턴을 더 최적화한 형태로 사용하고싶다면, use-context-selector 라이브러리 참고해보세요!

참고)

import { createContext, ReactNode, useContext } from "react";

interface DropdownContextType {
  isOpen: boolean;
  setIsOpen: (value: boolean) => void;
}

const initialValue: DropdownContextType = {
  isOpen: false,
  setIsOpen: () => {},
};

const DropdownContext = createContext<DropdownContextType>(initialValue);

interface DropdownProviderProps extends DropdownContextType {
  children: ReactNode;
}

const DropdownProvider = ({
  isOpen,
  setIsOpen,
  children,
}: DropdownProviderProps) => {
  return (
    <DropdownContext.Provider value={{ isOpen, setIsOpen }}>
      {children}
    </DropdownContext.Provider>
  );
};

// selector 패턴 적용
export function useDropdownSelector<T>(
  selector: (ctx: DropdownContextType) => T
): T {
  const context = useContext(DropdownContext);
  return selector(context);
}

export default DropdownProvider;
  • 실제 사용
const DropdownTrigger = ({ children, ...props }: DropdownTriggerProps) => {
  const isOpen = useDropdownSelector((ctx) => ctx.isOpen);
  const setIsOpen = useDropdownSelector((ctx) => ctx.setIsOpen);
  const handleClick = () => setIsOpen(!isOpen);

  return (
    <button onClick={handleClick} {...props}>
      {children}
    </button>
  );
};

Comment on lines +11 to +21
type TextProps = HTMLAttributes<HTMLSpanElement> &
TagItemBase & {
type: "text";
};
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
TagItemBase & {
type: "button";
onClick: () => void;
};

type TagItemProps = TextProps | ButtonProps;
Copy link
Collaborator

Choose a reason for hiding this comment

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

위에서 언급했던것처럼, 객체 형태를 가지고있거나 props의 타입을 정의할때는 type보다는 interface를 사용해보세요! :)

Comment on lines +5 to +19
export const getData = async ({
page = 1,
pageSize = 10,
orderBy = "recent",
}) => {
const query = `page=${page}&pageSize=${pageSize}&orderBy=${orderBy}`;
const res = await fetch(`${BASE_URL}/products?${query}`);

if (!res.ok) {
throw new Error("상품 리스트를 불러오는데 실패했습니다.");
}

const data = await res.json();
return data;
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기도 getProductInfo처럼 Promise를 반환한다는걸 명시해주면 예기치 못한 상황이나 실수를 방지하고자 타입 체커의 도움을 받을 수 있겠죠? :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

네! 사용하는 입장에서 찾기 편하게끔 사용처에서 최대한 가깝게 구조 유지하시면 됩니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

좋은 질문이네요! 설계 고민을 평소에 깊게 하시는군요 🤩

우선 zustand의 경우에는 하나의 페이지보다는 동시에 여러 페이지/컴포넌트에서 공유해 접근해야하는 상태를 관리할 때 적합합니다. 모든 상태를 전역으로 관리하기때문에, 불필요한 전역 상태가 많아지면 관리도 어렵고 구조 자체가 흔들릴 수 있겠죠?

Context API의 경우에는, Provider로 감싸면 하위 컴포넌트 어디서든 필요한 레벨에서 접근 가능하다는 장점이 있지만
상태가 바뀌면 Provider 하위 모든 컴포넌트가 리렌더링되기때문에, 성능 이슈가 발생할 수 있습니다.
따라서, 상태를 내려주는 깊이가 2~3단계 이내라면 그냥 두는 것도 괜찮습니다.
하지만 더 깊어지거나, 여러 곳에서 접근해야 한다면 개선이 필요하겠죠? :)

지금과 같이 페이지 단위나 특정 영역에서만 공유되는 상태라면 코드 베이스를 구성하는 측면에서 zustand보다는 Context API를 사용하시는게 적합합니다.

위에서 불필요한 리렌더링을 방지할수있는 방법에 대해 코멘트 드렸는데, 참고해보시고 리팩토링하시면 좋을것같네요!

@addiescode-sj
Copy link
Collaborator

addiescode-sj commented Jul 18, 2025

질문에 대한 답변

멘토에게

  • 이모션과 타입스크립트를 동시에 마이그레이션 하려고 하니 시간이 꽤 오래 걸리는데, 현업에서도 이렇게 종종 여러 기술들을 동시에 마이그레이션 하는 상황이 발생하나요?

실무하실땐 좀 더 보수적으로 접근하시는게 좋아요. 단계적이고 점진적으로 그때 필요한 업데이트를 지속해나가는게 일반적입니다 :)

  • Suspense를 한번 적용해보려고 했는데... 프로미스를 반환해야한다는 부분에서 이해가 되지 않아서 사용을 못했습니다. 구글링을 좀 더 해보니깐 라이브러리와 함께 사용하는게 좋다는? 글을 봐서, 단순히 fetch로는 사용이 힘들까요?

Suspense는 렌더링 중에 Promise를 throw하면, 그 프로미스가 resolve될 때까지 로딩 UI를 보여줍니다.
하지만 지금과 같은 상황에서는 일반적으로 컴포넌트에서 fetch를 호출하면, 이미 렌더링이 끝난 뒤에 응답 받습니다.
즉, Suspense를 사용하려면 렌더링 중에 프로미스를 throw하는 패턴을 직접 구현해야 합니다.
참고할만한 예시 코드를 작성해드릴게요!

// fetch를 Suspense에 맞게 래핑
function wrapPromise(promise) {
  let status = "pending";
  let result;
  let suspender = promise.then(
    r => {
      status = "success";
      result = r;
    },
    e => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw suspender; // Suspense가 잡음
      } else if (status === "error") {
        throw result; // ErrorBoundary가 잡음
      } else if (status === "success") {
        return result;
      }
    }
  };
}

// 사용 예시
const userResource = wrapPromise(fetch("/api/user").then(res => res.json()));
  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@addiescode-sj addiescode-sj merged commit 5b6b6b6 into codeit-bootcamp-frontend:React-이태경 Aug 4, 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