Skip to content

Conversation

@cskime
Copy link
Collaborator

@cskime cskime commented Aug 6, 2025

요구사항

기본

상품 상세

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

상품 문의 댓글

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

심화

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

주요 변경사항

  • 상품 목록에서 상품을 클릭하면 상세 페이지로 이동합니다.
  • 상세 페이지에서 상품 설명 등 상세 내용과 댓글(문의) 목록을 표시합니다.
  • 코드잇에서 제공하는 API로 가져온 상품 데이터에는 대부분 comments 데이터가 존재하지 않았습니다. 그래서, comments-mock.json mock 데이터를 사용해서 comment 목록을 보여주고 comment 등록, 수정, 삭제 등 기능은 comments를 가져오는 API를 제대로 활용할 수 있게 되면 추가로 구현하려고 합니다.

스크린샷

Desktop Tablet Mobile
desktop tablet mobile

멘토에게

  • 코드를 많이 정리하지 못해서 읽기 어려우실 수 있을 것 같습니다. 기초 프로젝트가 끝난 뒤에 다음 미션에서 코드를 더 정리해 보겠습니다 ㅠㅠ
  • Component 분리 질문
    • item-comment-list.jsx에는 ItemCommentListCommentListItem 두 개의 component가 구현되어 있습니다.
    • CommentListItem component는 ItemCommentList component 내부에서만 사용될 거라 생각해서 따로 파일로 분리하지 않았는데요. 재사용 되지 않더라도 코드 가독성과 유지보수를 위해 별도의 파일로 분리하는게 좋을까요?
  • JavaScript에서 enum 사용 방법 질문
    • Button component의 size, style, type 등 고정된 개수의 값을 묶음으로 처리할 때 다른 언어의 enum type을 사용하고 싶었습니다.
    • JavaScript는 enum은 지원하지 않는 것 같은데요. JavaScript를 사용할 때는 이런 상황에서 어떤 방식으로 구현하면 좋을까요?

cskime added 30 commits August 4, 2025 21:01
외부와 간접적으로 접점이 없는 styled-components로 만든 component들은 `Styled~` 이름 규칙을 따르지 않습니다.
테스트를 위해 comments API 연동 코드를 주석 처리하고 dummy `CommentListItem`을 작성
- 판다마켓 API에 등록된 상품들에 comment 데이터가 없어서 mock data를 사용
- Comment 수정 및 삭제 기능은 실제 comments API 연동 후 추가 예정
@cskime cskime requested a review from kiJu2 August 6, 2025 08:45
@cskime cskime self-assigned this Aug 6, 2025
@cskime cskime added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Aug 6, 2025
@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 7, 2025

스프리트 미션 하시느라 수고 많으셨어요.
참솔님 학습에 도움 되실 수 있게 꼼꼼히 리뷰 하도록 해보겠습니다. 😊

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 7, 2025

코드를 많이 정리하지 못해서 읽기 어려우실 수 있을 것 같습니다. 기초 프로젝트가 끝난 뒤에 다음 미션에서 코드를 더 정리해 보겠습니다 ㅠㅠ

ㅎㅎㅎ 괜찮습니다. 정리할 때 이렇게하면 좋겠다 ~ 싶은거 있으면 제안드려볼게요 !

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 7, 2025

Component 분리 질문
item-comment-list.jsx에는 ItemCommentList와 CommentListItem 두 개의 component가 구현되어 있습니다.
CommentListItem component는 ItemCommentList component 내부에서만 사용될 거라 생각해서 따로 파일로 분리하지 않았는데요. 재사용 되지 않더라도 코드 가독성과 유지보수를 위해 별도의 파일로 분리하는게 좋을까요?


확인해보겠습니다 !

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 7, 2025

JavaScript에서 enum 사용 방법 질문
Button component의 size, style, type 등 고정된 개수의 값을 묶음으로 처리할 때 다른 언어의 enum type을 사용하고 싶었습니다.
JavaScript는 enum은 지원하지 않는 것 같은데요. JavaScript를 사용할 때는 이런 상황에서 어떤 방식으로 구현하면 좋을까요?


넵 안타깝지만 현재는 지원하지 않습니다 🥲 나중에 배우실 타입스크립트에서는 사용하실 수 있어요.
현재로서는 객체나 배열로 활용해야할 것으로 보입니다:

const BUTTON_SIZE = Object.freeze({
  SMALL: 'small',
  MEDIUM: 'medium',
  LARGE: 'large',
});

Object.freeze ?

Comment on lines +9 to +22
export function createUrl(path, params) {
const trimmed = trimPath(path);
const url = new URL(`${import.meta.env.VITE_API_BASE_URL}/${trimmed}`);

if (!params) {
return url;
}

Object.keys(params).forEach((key) =>
url.searchParams.append(key, params[key])
);

return url;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

(심화/응용) 해당 함수는 Base URL을 만드는 유틸 함수로 보이는군요 !

다음과 같이 fetch를 간단히 사용할 수 있으면 얼마나 좋을까요?:

const response = codeitApiInstance.get("products") //  => fetch("https://bootcamp-api.codeit.kr/api/products")

그 외에도 여러 상황들을 고려해볼 수 있어요.
예를 들어서 method를 고려해야 하며, 그 중 post일 경우 body를 고려해볼 수 있고, get일 경우에는 쿼리(?query=1)를 고려하게 될겁니다 !

더 나아가면 공통적인 예외 처리, 응답 실패 시의 예외 처리, 특정 path에 대한 예외 처리 등등도 고려될 수 있겠네요.
이러한 모듈을 만들어 보는 것도 좋은 경험이 될 수 있을거예요.

다음은 GPT로 만들어낸 예시입니다:

class HttpClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }

  async request(endpoint, { method = 'GET', body, headers = {}, query } = {}) {
    const url = new URL(`${this.baseURL}/${endpoint}`);

    // GET 요청일 때 쿼리스트링 처리
    if (method === 'GET' && query) {
      Object.entries(query).forEach(([key, value]) => {
        url.searchParams.append(key, value);
      });
    }

    const options = {
      method,
      headers: {
        'Content-Type': 'application/json',
        ...headers,
      },
    };

    if (body && method !== 'GET') {
      options.body = JSON.stringify(body);
    }

    try {
      const response = await fetch(url.toString(), options);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return await response.json();
    } catch (error) {
      console.error('Fetch error:', error.message);
      throw error;
    }
  }

  get(endpoint, config = {}) {
    return this.request(endpoint, { ...config, method: 'GET' });
  }

  post(endpoint, body, config = {}) {
    return this.request(endpoint, { ...config, method: 'POST', body });
  }

  put(endpoint, body, config = {}) {
    return this.request(endpoint, { ...config, method: 'PUT', body });
  }

  delete(endpoint, config = {}) {
    return this.request(endpoint, { ...config, method: 'DELETE' });
  }
}

그리고 사용 예시:

const api = new HttpClient('https://bootcamp-api.codeit.kr/api');

// GET 요청
api.get('products', { query: { page: 1, pageSize: 10 } })
  .then((data) => console.log(data));

// POST 요청
api.post('login', {
  email: '[email protected]',
  password: '1234',
});

참솔님이라면 이러한 학습 욕심도 있으실 것 같아서 제안드려봅니다 ~!

Copy link
Collaborator

Choose a reason for hiding this comment

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

혹은 편하게 axios를 사용하는 방법도 있어요 !

fetch 모듈을 잘 만든다는 것은 어렵습니다. 다음 사항들을 고려해볼 수 있어요:

  1. 만약 get이 아닌 메써드(post, patch, delete 등)일 경우는 어떻게 처리할 수 있을까요?
  2. querybody가 필요할 때는 어떻게 처리 할 수 있을까요?
  3. 로그인 인가를 위한 토큰을 request 전에 자동으로 삽입할 수는 없을까요? (인증/인가를 자동으로 할 수 없을까요?)
  4. 처음 한 번에 Base URL을 지정할 수는 없을까요?
    1. Base URL을 사용하다가 타 Domain에 보내야 될 때는 어떻게 할 수 있을까요?
      이 모든 요구사항들을 '잘 만든다는 것'은 어려워요. 따라서 이 모든걸 만들어진 fetch 모듈을 사용해보고 후에 fetch모듈을 만들어 보는 것도 좋은 학습 방법이 될 수 있어요.

axios 시작하기

Copy link
Collaborator

Choose a reason for hiding this comment

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

어떻게 세팅하면 될까? 🤔

instance를 만들어서 export를 하고 사용해보는 것 정도로 시도해보면 좋을 것 같아요. axios-instance 파일을 만들어서 instance를 생성하고 export한 후 사용해보는건 어떨까요?
다음과 같이 만들어볼 수 있어요:

const baseURL = process.env.NEXT_PUBLIC_LINKBRARY_BaseURL;

const instance = axios.create({
  baseURL: baseURL,
  headers: {
    'Content-Type': 'application/json',
  },
});

export default instance

axios instance

인가에 필요한 accessTokenlocalStorage가 있다면 axios의 인터셉터를 활용할 수 있습니다 !

인터셉터는 혼자 해결해보시는 것을 권장드립니다. 혹시 모르시겠으면 다음 위클리 미션에 질문해주세요. 😊

사용 방법 🚀

사용 방법은 정말 간단해요. 다음과 같이 사용할 수 있습니다:

instance.get(`/user/${userId}`)

딱 보니. 마이그레이션도 정말 쉽게 할 수 있겠죠? 😊

axios API

}
`;

function ItemCommentList({ comments }) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

(선택) CommentList라는 이름도 괜찮겠네요 !

CommentListCommentItem으로 하셔도 될 것 같고
CommentsComment로 하셔도 될 것 같은데 전자가 더 읽기 편해보이네요 ~!

}
`;

function CommentListItem({ writer, updatedAt, content, onEdit, onDelete }) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

굿굿 ! 해당 컴포넌트는 프라이빗한 컴포넌트군요 !

파일 내부에서만 사용되므로 굳이 export 시키지 않고 있군요.
"내부 구성이 어떻게 되있는지 알 필요 없도록" 설계되어있는 좋은 패턴입니다 !

게다가 나중에 "댓글 중 가장 인기 많은 댓글을 상위에 하나 출력한다"라는 특수한 요구사항이 추가된다면,
해당 컴포넌트만 export 해주면 되겠어요. 낮은 결합도로 잘 구성하셨습니다 👍👍

또한, 별도의 파일로 분리하는게 가독성이 좋을까요?

이건 주관적인 영역이라 이견이 있을거로 보여요.
하지만 하나 확실한건 파일을 분리하게 되면 해당 컴포넌트를 결국 글로벌하게 export를 시켜줘야 할 것으로 보입니다.

또한, 코딩을 직접 해본 입장에서 두 파일을 교차하면서 보는게 더 편하신가요?
아니면 하나의 파일을 보면서 코딩하시는게 편하신가요?

이에 따라 또 다를 것 같습니다.
순전히 주관적인 입장에서. 위와 같은 이유로 저였다면 현재 상태 그대로 두는게 좋겠다고 사료되네요. 😉

Comment on lines +90 to +111
const handleMoreClick = () => {
setIsMenuOpen((prev) => !prev);
};

const handleEditClick = () => {
setIsMenuOpen(false);
setIsEditing(true);
};

const handleDeleteClick = () => {
setIsMenuOpen(false);
onDelete();
};

const handleCancelClick = () => {
setIsEditing(false);
};

const handleEditDoneClick = () => {
setIsEditing(false);
onEdit(inputRef.current.value);
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으 ~ 상황에 따른 핸들러들이 아주 깔끔하네요 👍

Comment on lines +86 to +88
const handleInputChange = () => {
// TODO: Comment 등록 구현
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

주석으로 메모를 남겨 놓는 것도 정말 좋은 메모습관입니다 !

필요한 메모를 필요한 곳 주변에 배치하는 것. 나중에 까먹지 않고 작업을 이어갈 수 있으므로 좋은 습관이죠 👍

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 7, 2025

수고하셨습니다 참솔님 !
기초 프로젝트나 끝나고 나서 fetch를 어떻게 더 유용하게 사용할 수 있을까에 대한 고민도 해보시면 좋을 것 같아요 !
지금도 훌륭히 잘 해주셨고, 잘해주셨기에 드릴 수 있는 피드백이라고 생각합니다. 😉

참솔님께서 기초 프로젝트에 어떤 기여를 하실지 정말 기대가 되는군요 ! 👍

@kiJu2 kiJu2 merged commit 32a4a76 into codeit-bootcamp-frontend:React-김참솔 Aug 7, 2025
@cskime cskime deleted the React-김참솔-sprint7 branch August 7, 2025 04:25
@cskime cskime mentioned this pull request Aug 7, 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