Skip to content

Conversation

@hyeonjiroh
Copy link
Collaborator

요구사항

기본

중고마켓

  • 중고마켓 페이지 주소는 “/items” 입니다.
  • 페이지 주소가 “/items” 일때 상단네비게이션바의 '중고마켓' 버튼의 색상은 “3692FF”입니다.
  • 상단 네비게이션 바는 이전 미션에서 구현한 랜딩 페이지와 동일한 스타일로 만들어 주세요.
  • 상품 데이터 정보는 https://panda-market-api.vercel.app/docs/#/ 에 명세된 GET 메소드 “/products” 를 사용해주세요.
  • '상품 등록하기' 버튼을 누르면 “/additem” 로 이동합니다. ( 빈 페이지 )
  • 전체 상품에서 드롭 다운으로 “최신 순” 또는 “좋아요 순”을 선택해서 정렬을 할 수 있습니다.

중고마켓 반응형

  • 베스트 상품
    • Desktop : 4개 보이기
    • Tablet : 2개 보이기
    • Mobile : 1개 보이기
  • 전체 상품
    • Desktop : 12개 보이기
    • Tablet : 6개 보이기
    • Mobile : 4개 보이기

심화

  • 페이지 네이션 기능을 구현합니다.

주요 변경사항

  • 코드 리뷰 피드백 반영
    • const 키워드로 email_regex 변수 선언
    • 입력 값을 확인하는 함수들을 간결하게 변경
    • togglePassword 함수를 가독성 좋게 변경

멘토에게

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

hanseulhee and others added 30 commits October 10, 2023 14:15
[Fix] delete merged branch github action
@hyeonjiroh hyeonjiroh requested a review from kiJu2 January 16, 2025 14:45
@hyeonjiroh hyeonjiroh added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Jan 16, 2025
@kiJu2
Copy link
Collaborator

kiJu2 commented Jan 20, 2025

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

@@ -0,0 +1,11 @@
const BASE_URL = "https://panda-market-api.vercel.app";
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은 환경 변수에 저장하시는게 좋습니다!

환경 변수(Environment Variable): process.env에 내장되며 앱이 실행될 때 적용할 수 있는 값입니다!

다음과 같이 적용할 수 있습니다:

// .env.development
REACT_APP_BASE_URL="http://localhost:3000"

// .env.production
REACT_APP_BASE_URL="http://myapi.com"

// 사용시
<a href={`${process.env.REACT_APP_BASE_URL}/myroute`}>URL</a>

왜 환경 변수에 저장해야 하나요?

개발(dev), 테스트(test), 실제 사용(prod) 등 다양한 환경에서 앱을 운영하게 되는 경우, 각 환경에 따라 다른 base URL을 사용해야 할 수 있습니다. 만약 코드 내에 하드코딩되어 있다면, 각 환경에 맞춰 앱을 배포할 때마다 코드를 변경해야 하며, 이는 매우 번거로운 작업이 됩니다. 하지만, 환경 변수를 .env.production, .env.development, .env.test와 같이 설정해두었다면, 코드에서는 단지 다음과 같이 적용하기만 하면 됩니다.

const apiUrl = `${process.env.REACT_APP_BASE_URL}/api`;

이러한 방식으로 환경 변수를 사용하면, 배포 환경에 따라 쉽게 URL을 변경할 수 있으며, 코드의 가독성과 유지보수성도 개선됩니다.

실제 코드 응용과 관련해서는 다음 한글 아티클을 참고해보세요! => 보러가기

Comment on lines +4 to +8
const query = `page=${page}&pageSize=${pageSize}&orderBy=${order}`;
const response = await fetch(`${BASE_URL}/products?${query}`);
if (!response.ok) {
throw new Error("데이터를 불러오는데 실패했습니다");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

쿼리는 URLSearchParams로 손쉽게 사용할 수 있어요 !

    const params = new URLSearchParams({ limit: '100' });
    const { data } = await instance.get(`/articles/${articleId}/comments`, { params });

axios를 사용하실 경우 URLSearchParams와 함께 객체로 손쉽게 핸들링할 수 있습니다 !
객체로 구성할 수 있어 가독성이 좋고, URL 인코딩을 자동으로 처리하여 특수 문자나 공백이 포함된 값에서도 안전하게 동작합니다 !

URLSearchParams: URLSearchParams 인터페이스는 URL의 쿼리 문자열을 대상으로 작업할 수 있는 유틸리티 메서드를 정의합니다.

쿼리를 생성하실 때에 참고해서 사용해보세요 😊


export async function getItems({ page = "", pageSize = "", order = "" }) {
const query = `page=${page}&pageSize=${pageSize}&orderBy=${order}`;
const response = await fetch(`${BASE_URL}/products?${query}`);
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 시작하기

어떻게 세팅하면 될까? 🤔

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

Comment on lines +77 to +81
<input
name="search"
className="searchBar"
placeholder="검색할 상품을 입력해주세요"
/>
Copy link
Collaborator

Choose a reason for hiding this comment

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

input의 타입 중 search를 적용해볼 수 있을 것 같아요 😊

Suggested change
<input
name="search"
className="searchBar"
placeholder="검색할 상품을 입력해주세요"
/>
<input
name="search"
type="search"
className="searchBar"
placeholder="검색할 상품을 입력해주세요"
/>

MDN - input search

Comment on lines +6 to +8
if (!response.ok) {
throw new Error("데이터를 불러오는데 실패했습니다");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

API 통신부에서 에러가 났을 때 어떻게 대처하면 될까요?

현재 error를 모두 "데이터를 불러오는데 실패했습니다"에러를 throw 해주고 있기에 해당 함수를 사용하는 컴포넌트에서는 분기처리하기 힘들거예요 !

그렇다면 어떻게 할까요?

방법은 다양합니다만, 지금 바로 해볼 수 있는 방법은 throw를 해보는거예요:

Suggested change
if (!response.ok) {
throw new Error("데이터를 불러오는데 실패했습니다");
}
try {
} catch (error) {
console.error(`Failed to fetch data: ${error}`); // 통신부에서 처리할 로직 및 로깅
throw(error);
}

위처럼 throw를 해준다면 서버에서 반환되는 에러 메시지를 사용자에게 toast든, 모달이든, 알러트든 보여줄 수 있겠죠?

다음과 같이요 !!:

  // Component
  useEffect(() => {
    try {
      getItemsComments();
  // 이어서..
    } catch (err) {
      alert(err.message) // 만약 서버에서 적절한 `err.message`를 반환하지 않는다면 현지님께서 정의해둔 "데이터를 불러오는데 실패했습니다"를 출력해도 무방함.
    }
  }, [])

Comment on lines +64 to +68
function getPageSize(width) {
if (width > 1200) return 10; // PC
else if (width > 768) return 6; // Tablet
else return 4; // Mobile
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

해당 함수는 컴포넌트 외부에 선언해볼 수 있습니다 !

해당 함수는 컴포넌트의 자원(상태, props)을 사용하고 있지 않으므로 컴포넌트 바깥에 선언해볼 수 있어요. 이는, 리렌더링에 의한 불필요한 함수 재선언을 방지해주며, 컴포넌트와 무관한 함수를 분리함으로서 컴포넌트 내부에는 컴포넌트의 자원을 사용하는 코드만 남게 되므로 가독성이 향상될 수 있어요 😊

const [pageSize, setPageSize] = useState(10);
const [pageBound, setPageBound] = useState(0);
const [items, setItems] = useState([]);
const pageArr = [1, 2, 3, 4, 5];
Copy link
Collaborator

Choose a reason for hiding this comment

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

임의로 페이지 배열을 만들어두신 듯 하군요 !

다음 미션에서는 totalCountoffset을 계산하여 만들어봅시다 😊

Comment on lines +21 to +30
useEffect(() => {
function handleResize() {
const newPageSize = getPageSize(window.innerWidth);
if (newPageSize !== pageSize) {
setPageSize(newPageSize);
}
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [pageSize]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

(제안/선택)디바운싱/쓰로틀링을 통하여 성능 최적화를 시켜볼까요?

해당 이벤트에 console.log를 호출해보면 리사이징이 될 때마다 정말정말 많은 호출하는 것을 볼 수 있을거예요 !
그만큼 성능에 좋지 못하다는 이야기겠지요?
따라서, 프론트엔드 개발자들은 이렇게 잦은 이벤트가 발생할 때(리사이징, 스크롤, 타이핑 등) 디바운싱/쓰로틀링을 통하여 최적화를 시키곤 합니다.

쓰로틀링(Throttling): 일정 시간 동안 하나의 함수만 호출되도록 하는 기법입니다. 예를 들어, 사용자가 스크롤을 할 때, 매번 이벤트를 처리하지 않고 일정 간격으로 한 번만 처리하게 합니다. 이를 통해 성능을 향상시킬 수 있습니다.

디바운싱(Debouncing): 여러 번 발생하는 이벤트 중 마지막 이벤트가 발생한 후 일정 시간이 지난 다음에 한 번만 실행되도록 하는 기법입니다. 예를 들어, 사용자가 검색어를 입력할 때, 입력이 끝난 후 일정 시간 동안 추가 입력이 없으면 검색 요청을 보냅니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

다음과 같은 코드가 있습니다:

function Container() {
    useEffect(() => {
      window.addEventListener('resize', handleResize);
      return () => {
        window.removeEventListener('resize', handleResize);
      }, []);
// ... Some code
}

디바운싱으로 다음과 같이 최적화를 시킬 수 있습니다 !:

function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

function Container() {
  const handleResize = () => {
    console.log('Window resized');
  };

  useEffect(() => {
    const debouncedHandleResize = debounce(handleResize, 300);
    window.addEventListener('resize', debouncedHandleResize);
    return () => {
      window.removeEventListener('resize', debouncedHandleResize);
    };
  }, []);

  // ... Some code
}

이렇게 하면 연속된 이벤트가 끝난 후 0.3초마다 호출하게 되어 기존보다 훨씬 최적화된 기능이 될 수 있습니다 !

Copy link
Collaborator

Choose a reason for hiding this comment

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

매번 작성하기 번거로운가요?

그렇죠... 번거롭죠.. (사실 저도 GPT한테 맡긴 코드입니다 하하하핳ㅎ 그래서 될지는 장담못함..)
디바운싱, 쓰로틀링은 정말 흔한 기법이어서 참조할 수 있는 문서가 많습니다 !

useHooks

해당 라이브러리를 사용한 코드로 볼까요?

import { useRef, useState } from 'react';
import { useDebounceCallback } from 'usehooks-ts';

function useDebounceValue(initialValue, delay, options) {
  const eq = options?.equalityFn ?? ((left, right) => left === right);
  const unwrappedInitialValue = typeof initialValue === 'function' ? initialValue() : initialValue;
  const [debouncedValue, setDebouncedValue] = useState(unwrappedInitialValue);
  const previousValueRef = useRef(unwrappedInitialValue);

  const updateDebouncedValue = useDebounceCallback(setDebouncedValue, delay, options);

  // Update the debounced value if the initial value changes
  if (!eq(previousValueRef.current, unwrappedInitialValue)) {
    updateDebouncedValue(unwrappedInitialValue);
    previousValueRef.current = unwrappedInitialValue;
  }

  return [debouncedValue, updateDebouncedValue];
}

export default useDebounceValue;

그리고 사용 방법:

import React, { useState } from 'react';
import useDebounceValue from './useDebounceValue';

const ExampleComponent = () => {
  const [inputValue, setInputValue] = useState('');
  const [debouncedValue] = useDebounceValue(inputValue, 500);

  const handleChange = (event) => {
    setInputValue(event.target.value);
  };

  return (
    <div>
      <input type="text" value={inputValue} onChange={handleChange} placeholder="Type something..." />
      <p>Debounced Value: {debouncedValue}</p>
    </div>
  );
};

export default ExampleComponent;

@kiJu2
Copy link
Collaborator

kiJu2 commented Jan 20, 2025

현지님 정말정말 수고하셨습니다 !
과제를 충실히 잘 수행하셨어요 ! 너무 잘 따라와주고 계시기에 리뷰도 욕심내서 작성하게 되네요 ㅎㅎㅎ
추 후 페이지네이션은 동적으로 계산 되어 페이지를 결정지을 수 있게 만들어봅시다 !

과제하시느라 수고 많으셨습니다 현지님 ! 😊😊

@kiJu2 kiJu2 merged commit 08657e0 into codeit-bootcamp-frontend:React-노현지 Jan 20, 2025
@hyeonjiroh hyeonjiroh self-assigned this Feb 20, 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.

4 participants