-
Notifications
You must be signed in to change notification settings - Fork 26
[황휘태] sprint5 #96
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
[황휘태] sprint5 #96
The head ref may contain hidden characters: "React-\uD669\uD718\uD0DC-sprint5"
Conversation
|
스프리트 미션 하시느라 수고 많으셨어요. |
| <Routes> | ||
| {/* 메인 화면 */} | ||
| <Route path="/" element={<MainLayout />}> | ||
| <Route index element={<Home />} /> | ||
| </Route> | ||
|
|
||
| {/* 로그인, 회원가입 */} | ||
| <Route element={<AuthLayout />}> | ||
| <Route path="login" element={<Login />} /> | ||
| <Route path="signup" element={<Signup />} /> | ||
| </Route> | ||
|
|
||
| {/* 중고 마켓 */} | ||
| <Route element={<ItemsLayout />}> | ||
| <Route path="items" element={<Items />} /> | ||
| </Route> | ||
| </Routes> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
굿굿 ! react-router를 적절히 사용하셨군요 !
훌륭합니다. element를 통해 레이아웃을 구성하신 것을 보니 라이브러리 사용 방법도 꼼꼼히 확인하고 적용하신 것 같네요 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 이미지 속의 아이콘을 벡터 이미지를 사용해볼 수 있겠네요 !
이하 MDN
SVG(Scalable Vector Graphics)는 2차원 벡터 그래픽을 서술하는XML기반의 마크업 언어입니다.SVG는 텍스트 기반의 열린 웹 표준 중 하나로, 모든 사이즈에서 깔끔하게 렌더링 되는 이미지를 서술하며 CSS, DOM, JavaScript, SMIL (en-US) 등 다른 웹 표준과도 잘 동작하도록 설계됐습니다. SVG는 달리 말하자면 HTML과 텍스트의 관계를 그래픽에 적용한 것입니다
![[Pasted image 20241212111054.png]]
이미지 파일은 최소 단위가 픽셀로 되어있으며 확대했을 때 이미지가 깨질 수 있어요 !
피그마에서 export하실 때에 svg로 export할 수 있습니다 😊
꺽쇠 부분의 아이콘을 svg로 저장하고 원 모양의 백그라운드는 border-radius, background-color 등을 통해서 만들어볼 수 있겠어요 😊
| import { memo, useEffect, useState } from 'react'; | ||
|
|
||
| function DropdownList({ changeOrder }) { | ||
| /** | ||
| * 드롭다운 리스트 메뉴 | ||
| */ | ||
| const orderList = [ | ||
| { name: '최신순', value: 'recent' }, | ||
| { name: '좋아요순', value: 'favorite' }, | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
orderList는 컴포넌트 내부의 상태나 props 등에 의존하지 않는군요 !
| import { memo, useEffect, useState } from 'react'; | |
| function DropdownList({ changeOrder }) { | |
| /** | |
| * 드롭다운 리스트 메뉴 | |
| */ | |
| const orderList = [ | |
| { name: '최신순', value: 'recent' }, | |
| { name: '좋아요순', value: 'favorite' }, | |
| ]; | |
| import { memo, useEffect, useState } from 'react'; | |
| /** | |
| * 드롭다운 리스트 메뉴 | |
| */ | |
| const orderList = [ | |
| { name: '최신순', value: 'recent' }, | |
| { name: '좋아요순', value: 'favorite' }, | |
| ]; | |
| function DropdownList({ changeOrder }) { | |
해당 값은 컴포넌트 내부의 상태나 props 등에 의존하지 않으므로 컴포넌트 바깥에 선언하시면 DropdownList가 리렌더링 될 때 불필요한 재선언을 방지할 수 있습니다 !
또한, 컴포넌트 내부에는 상태와 관련된 코드만 남게 되므로 코드 분리에도 용이하겠죠? 😉
| /** | ||
| * 드롭다운 리스트 열림/닫힘 이벤트 | ||
| */ | ||
| const onClickOpen = () => { | ||
| setIsOpen(!isOpen); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
다음과 같이 updater function을 사용해볼 수 있습니다 😉
| /** | |
| * 드롭다운 리스트 열림/닫힘 이벤트 | |
| */ | |
| const onClickOpen = () => { | |
| setIsOpen(!isOpen); | |
| }; | |
| /** | |
| * 드롭다운 리스트 열림/닫힘 이벤트 | |
| */ | |
| const onClickOpen = () => { | |
| setIsOpen(prev => !prev); | |
| }; |
이 전의 상태를 참고할 수 있습니다 !
| orderList.map((menu) => { | ||
| return ( | ||
| <li | ||
| key={menu.name} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
굿굿 ! 키를 꼼꼼히 작성하셨군요 !
| /** | ||
| * 화살표 모양으로 다음 페이지로 이동할 때 이벤트 | ||
| * @param {event} e | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jsdoc을 잘 사용하고 계시네요 😊
근데 너무 많이 사용하시면 휘태님도 힘드실 것 같아요 !
"자주 사용되는 공통 함수(컴포넌트)"에만 작성해도 충분하지 않을까 싶어요 !
물론 ! 주석을 꼼꼼히 작성하시는건 좋은 자세입니다 😊
| /** | ||
| * mediaQuery에 반응하여 데이터를 반환한다. | ||
| */ | ||
| export default function useMediaQuery(query) { | ||
| /** | ||
| * 반환할 데이터를 담아둘 state | ||
| */ | ||
| const [isMatch, setIsMatch] = useState(() => getMatches(query)); | ||
|
|
||
| useEffect(() => { | ||
| const media = window.matchMedia(query); | ||
|
|
||
| const handleChange = (e) => { | ||
| setIsMatch(e.matches); | ||
| }; | ||
|
|
||
| media.addEventListener('change', handleChange); | ||
|
|
||
| return () => { | ||
| media.removeEventListener('change', handleChange); | ||
| }; | ||
| }, [query]); | ||
|
|
||
| return isMatch; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
크으 ~ 깔끔하고 멋진 훅입니다 👍👍
| import ic_kakao from '../../assets/icons/ic_kakao.png'; | ||
| import ic_google from '../../assets/icons/ic_google.png'; | ||
| import { useEffect, useState } from 'react'; | ||
| import { useForm } from 'react-hook-form'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오잉? react-hook-form까지 사용하셨군요? 🫢
| const requestProductList = async ( | ||
| orderBy = 'recent', | ||
| page = 1, | ||
| pageSize = 10 | ||
| ) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(제안) 파라메터를 객체로 나타내면 순서에 상관 없이 사용할 수 있겠네요 !
이 방식도 문제는 없지만 일부 값만 전달할 때도 위치를 맞춰야 해서 사용성이 떨어질 수 있겠어요 😉
| const requestProductList = async ( | |
| orderBy = 'recent', | |
| page = 1, | |
| pageSize = 10 | |
| ) => { | |
| const requestProductList = async ({ | |
| orderBy = 'recent', | |
| page = 1, | |
| pageSize = 10, | |
| } = {}) => { |
혹은
| const requestProductList = async ( | |
| orderBy = 'recent', | |
| page = 1, | |
| pageSize = 10 | |
| ) => { | |
| /** | |
| * 상품 목록을 서버에서 요청합니다. | |
| * | |
| * @param {Object} query - 상품 요청에 필요한 쿼리 파라미터입니다. | |
| * @param {string} [query.orderBy='recent'] - 정렬 기준 (예: 'recent', 'popular' 등). | |
| * @param {number} [query.page=1] - 요청할 페이지 번호. | |
| * @param {number} [query.pageSize=10] - 페이지당 항목 수. | |
| * @returns {Promise<Object>} 서버로부터 받아온 상품 목록 데이터. | |
| */ | |
| const requestProductList = async (query) => { |
orderBy,page, pageSize는 쿼리에 속하는 개념이므로 객체로 표현해도 좋겠어요 😉
| const url = new URL('https://panda-market-api.vercel.app/products'); | ||
| url.searchParams.append('page', page); | ||
| url.searchParams.append('pageSize', pageSize); | ||
| url.searchParams.append('orderBy', orderBy); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
크으 ~ 훌륭합니다. searchParams를 사용하셨군요 👍👍
|
수고하셨습니다 휘태님 !! 미션 수행하시느라 수고 많으셨습니다 휘태님 ! |
| /** | ||
| * 베스트 상품 목록을 가져온다. | ||
| */ | ||
| const getBestProducts = async (count) => { | ||
| try { | ||
| const { list: bestList } = await requestProductList('favorite', 1, count); | ||
| if (!bestList) { | ||
| throw new Error('베스트 상품 목록 데이터를 불러오지 못했습니다.'); | ||
| } | ||
| setBestProducts(bestList); | ||
| } catch (e) { | ||
| console.error(e); | ||
| } finally { | ||
| setisLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * 전체 상품 목록을 가져온다. | ||
| */ | ||
| const getProducts = async (order, page, count) => { | ||
| try { | ||
| const productList = await requestProductList(order, page, count); | ||
|
|
||
| if (!productList) { | ||
| throw new Error('상품목록 데이터를 불러오지 못했습니다.'); | ||
| } | ||
| setProducts(productList); | ||
| } catch (e) { | ||
| console.error(e); | ||
| } finally { | ||
| setisLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| /** | ||
| * 현재 뷰포트에 맞추어 count를 담을 객체 | ||
| */ | ||
| let currentItemCounts; | ||
|
|
||
| if (isMobile) { | ||
| // 모바일 뷰 | ||
| currentItemCounts = { best: 1, all: 4 }; | ||
| } else if (isTablet) { | ||
| // 태블릿 뷰 | ||
| currentItemCounts = { best: 2, all: 6 }; | ||
| } else { | ||
| // 데스크탑 뷰 | ||
| currentItemCounts = { best: 4, all: 10 }; | ||
| } | ||
|
|
||
| setItemsCounts(currentItemCounts); | ||
| getProducts(order, pageNum, currentItemCounts.all); | ||
| getBestProducts(currentItemCounts.best); | ||
| }, [isTablet, isMobile, order, pageNum]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상품 목록을 불러오는 로직에서 useEffect()의 의존성 배열로 페이지의 번호를 넣어주고 있습니다.
따라서 페이지를 넘길 때 마다 데이터를 새로 요청하게 됩니다.
이런 방식은 큰 규모의 프로젝트 일 때는 성능에 대한 이슈가 있을거라 생각되는데, 현업에서는 어떤 방식으로 처리하는지 궁금합니다.
| import { useEffect, useState } from 'react'; | ||
|
|
||
| const getMatches = (query) => { | ||
| return window.matchMedia(query).matches; | ||
| }; | ||
|
|
||
| /** | ||
| * mediaQuery에 반응하여 데이터를 반환한다. | ||
| */ | ||
| export default function useMediaQuery(query) { | ||
| /** | ||
| * 반환할 데이터를 담아둘 state | ||
| */ | ||
| const [isMatch, setIsMatch] = useState(() => getMatches(query)); | ||
|
|
||
| useEffect(() => { | ||
| const media = window.matchMedia(query); | ||
|
|
||
| const handleChange = (e) => { | ||
| setIsMatch(e.matches); | ||
| }; | ||
|
|
||
| media.addEventListener('change', handleChange); | ||
|
|
||
| return () => { | ||
| media.removeEventListener('change', handleChange); | ||
| }; | ||
| }, [query]); | ||
|
|
||
| return isMatch; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반응형에 맞추어 보여지는 물품의 개수를 다르게 설정하기 위해 커스텀 훅을 만들어 사용했습니다.
실제 현업에서 이러한 상황은 어떤 방식으로 처리하는지 궁금합니다.
요구사항
기본
심화
주요 변경사항
스크린샷
멘토에게