-
Notifications
You must be signed in to change notification settings - Fork 13
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
[6주차] Team 커피딜 송유선 & 최지원 미션 제출합니다. #14
base: main
Are you sure you want to change the base?
Conversation
[Feat] Install svgr webpack & Icon setup, store color
[Chore] Change folder structure, Setup eslint, prettier
[Feat] Footer ui, Landing logo
Feat: font, path
[Chore] move public folder and Setup absolute path
[Feat] Header, API
[Fix] render main page data using useQuery, [Feat] add metadata
[Feat] axios -> fetch (use revalidate), Apply SkeletonItem and lazy loading to SearchClient, Replace img tag with Iamge, Replace { params: any } with { params: Promise<Params> } in Detail page, ETC
[Docs] ReadME.md
const { ref, inView } = useInView(); // 스크롤 영역 판단 | ||
const [isLoading, setIsLoading] = useState(true); | ||
|
||
// 입력값 디바운싱 |
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.
헉 입력 값에 디바운싱 적용할 생각은 못했는데 너무 좋은 것 같습니다... 배워 갑니다아... 😍
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.
디바운싱을 적용해서 입력할 때마다 api 요청이 발생하는 걸 방지한 건 진짜 짱,,
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.
어라 유선 님 코드가... 아니군요 ㅜㅜㅜ!!!! 하지만 진짜 짱...
|
||
// components | ||
import SearchInput from '@components/search/SearchInput'; | ||
const Item = dynamic(() => import('@components/search/Item')); // 레이지 로딩 |
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.
헉 레이지 로딩!!!! 이번 과제에서는 최적화에 중점을 두셨다고 하셨는데 정말 제대로 하셨네요... 사용자의 뷰포트에만 들어온 아이템만 렌더링 하니 훨씬 로딩 속도가 빠를 것 같습니다 🥳🥳
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.
안녕하세요, 최고의 파트너 지원님! 🪽
사실 이번엔 저번에 비해서 추가된 것도 많이 없고 ㅋ큐ㅠㅠㅋ 이미 이야기 나눴던 것들이 있어서 정말 어떤 리뷰를 달아야 할 지 모르겠는 거에요,,, 그래서 사실 일부는 이미 이야기했던 것들 정리해서 다시 쓴 것도 있습니다 (...) 이번에 필수 기능들 말고 선택 기능들도 다 구현할 수 있도록 옆에서 함께 노력해줘서 넘 감사했습니당...🥹 다음 과제도, 커피딜 프로젝트도 함께 파이팅해요! 🔥🍀
export const metadata = { | ||
title: 'Netflix-Onedwo', | ||
description: 'Netflix Clone Coding using Next.js by Onedwo-Punch', | ||
keywords: 'Next.js, web development, SEO, Netflix clone', | ||
robots: 'index, follow', | ||
author: 'Onedwo-Punch', | ||
openGraph: { | ||
title: 'Netflix-Onedwo', | ||
description: 'Netflix Clone Coding using Next.js by Onedwo-Punch', | ||
url: 'https://next-netflix-20th-onedwo.vercel.app', | ||
type: 'website', | ||
site_name: 'Netflix Clone', | ||
}, | ||
}; |
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 Footer from '@components/common/Footer'; | ||
|
||
export default async function Main() { | ||
try { |
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.
사실 원래 지원 님께서 도와주시려고 query를 사용해서 메인에서 매번 데이터 패칭을 하지 않도록 함수를 조금 수정해주셨었는데, 제가 서버 클라이언트로 남겨두고 싶어서 (ㅋ큐ㅠㅋ) 다시 바꿨어요,,, api 키 노출되지 않는 것도 고려해야 해서요,, getContents 함수를 fetch로 바꾸고 next의 revalidate을 사용했습니당..!
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.
저도 useQuery 적용만 생각하다가 키 노출되는걸 깜빡했네요ㅠㅋㅋ큐
덕분에 코드리뷰하면서 revalidate에 대해서도 알아갑니다!!
function Footer({ tab }: FooterProp) { | ||
const [isActive, setIsActive] = useState(tab); // 클릭하는 메뉴 id | ||
|
||
const menus = [ |
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.
지난 번에는 저희가 따로 설정해 놓지 않아서 comming이나 download, more를 클릭하면 그냥 랜딩으로 이동되었었는데, 이번에 미리 꼼꼼하게 각각의 페이지들을 만들어서 연결시켜주셨더라구요..! 👍🏻
const { ref, inView } = useInView(); // 스크롤 영역 판단 | ||
const [isLoading, setIsLoading] = useState(true); | ||
|
||
// 입력값 디바운싱 |
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.
디바운싱을 적용해서 입력할 때마다 api 요청이 발생하는 걸 방지한 건 진짜 짱,,
const [query, setQuery] = useState(''); // 입력한 검색어 | ||
const [debouncedQuery, setDebouncedQuery] = useState(''); // 디바운싱된 검색어 | ||
const { ref, inView } = useInView(); // 스크롤 영역 판단 | ||
const [isLoading, setIsLoading] = useState(true); |
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.
현재 isLoading과 queryLoading을 별도로 관리하고 있어, 상태 관리가 약간 중복될 가능성이 있는 것 같아요..!! queryLoading만을 이용하여 로딩 상태를 관리하는 건 어떨까요?
const isLoading = queryLoading || isFetchingNextPage;
이런 식으로 작성하는 것도 괜찮아보입니다..!
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.
어머 저랑 같은 부분에서 코드리뷰를,, ㅋㅋㅋㅋㅋㅋ
살짝 해봤는데 추가해주신 queryLoading이 있어서 제가 처음에 정의했던 isLoading 아예 없애도 되겠더라구요!!
// 새 데이터 불러올 때 검색 결과 업데이트 | ||
useEffect(() => { | ||
if (data) { | ||
const allItems = data?.pages.flatMap((page) => page.results) || []; |
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.
contents 상태를 직접 업데이트하는 대신애, 인피니트쿼리가 제공하는 데이터를 useMemo로 메모이제이션하도록 해서 flatMap 결과를 캐시하면 불필요한 상태 업데이트를 방지할 수 있을 테니까 더 좋을 것 같습니당!
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 res = await fetch(url, { | ||
next: { revalidate: 300 }, // 5분 동안 데이터 캐싱하도록 함(매번 새로 불러오지 않도록) | ||
}); |
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.
revalidate
란 속성 알아갑니다!! 캐싱할 시간도 지정해줄 수 있어 좋은 것 같아요!
다만 해당 getContents는 메인에서 불러오는 함수로, 데이터가 5분보다는 훨씬 드문 주기로 바뀌겠단 생각이 들어서.. 시간을 더 길게 지정하거나, 아니면 이전에 캐싱된 데이터에서 변경사항이 있을 때만 불러오도록 하면 더 효율적일 것 같아요! 지금으로선 후자의 방법에는 useQuery밖에 떠오르지 않아서 저도 더 고민해봐야할 것 같습니다 😂😂
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.
깔끔한 애니메이션 활용 넘 좋아요!~!
{ type: 'SquareList', contents: contents[7], title: 'US TV Shows' }, | ||
]; | ||
|
||
const renderSquareList = () => { |
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.
지난주 코드리뷰 반영 짱짱 👍🫶
interface Params { | ||
media_type: string; | ||
id: string; | ||
} | ||
|
||
export default async function Detail({ params }: { params: Promise<Params> }) { |
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.
비동기 작업을 통해 가져오는 데이터는 Promise로 감싸주어야 했군요,,, 제 코드 쓸 때는 신경쓰지 않았던 부분인 것 같은데 쓰고 안 쓰고의 차이에 집중해 저도 다시 한 번 생각해보겠습니당
// 로딩 상태 관리 | ||
useEffect(() => { | ||
if (queryLoading) { | ||
setIsLoading(true); | ||
} else { | ||
setIsLoading(false); | ||
} | ||
}, [queryLoading]); |
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.
위에서 isLoading : queryLoading 를 써주어서 queryLoading이 이미 최신인 로딩 상태를 나타내기 때문에 isLoading 상태를 삭제하고 이 부분도 없애도 될 것 같네용!!
5 ~ 6주차 미션: Next-Netflix
🪄 결과물
🩵 구현 기능
피그마 화면 구현
SSR을 적용해서 구현
Open api를 사용해서 데이터 패칭을 진행
동적 라우팅으로 상세 페이지 구현
실시간 키워드 검색으로 검색 페이지 구현
검색 페이지 무한스크롤 구현
레이지 로딩 및 스켈레톤 UI 적용
데이터 캐싱 및 컴포넌트 최적화
🩵 느낀 점
[송유선] 지난주에 필수 기능들을 모두 구현했던 덕분에 이번에는 코드를 어떻게 최적화할지 고민하는 데 집중할 수 있었다. 우선 main 페이지에 갈 때마다 서버 컴포넌트가 데이터를 다시 불러오는 것을 막기 위해 { next: { revalidate: 300 } } 옵션을 추가해 5분 동안 데이터 캐싱을 적용했다. 이를 위해 기존 axios instance를 삭제하고 getContents 함수를 fetch로 리팩토링했다. 지난 스터디 피드백에서 들은 리액트 쿼리를 사용하지 않은 이유는, 리액트 쿼리를 쓰려면 use client를 붙여 클라이언트 컴포넌트로 만들어야 했기 때문이다. (최대한 서버 단에서 해결하고 싶었음...) 또, 검색 페이지에 레이지 로딩과 스켈레톤 UI를 적용했다. 원래는 메인에도 적용하려고 했는데 메인은 서버 컴포넌트라 레이지 로딩의 이점이 크지 않을 것 같았다. (+ 어느 정도 최적화된 상태라 레이지 로딩이며 스켈레톤이며 이것저것 쓰는 게 코드만 복잡해지고 더 별로일 것 같았음.) 근데!! 배포하고 보니 로컬에서와 달리 처음 로딩할 때 약간의 대기 시간이 있어서... 다음에 비슷한 플젝을 하게 된다면 next.js의 loading.tsx 를 활용해 스켈레톤 main 페이지를 따로 만든 다음에 적용시킬 것 같다. 이번에 못해서 아쉽... 그리고 검색은 이미 쿼리를 쓰고 있기도 하고 무한 스크롤이 적용되어 있으니까, 스켈레톤 UI로 사용자 경험을 향상시키고 레이지 로딩을 통해 불필요한 리소스 로드를 줄이고자 했다. 원래는 suspense로 처리했었는데, 스켈레톤이 1개만 뜨고 이후 10개가 다시 렌더링되는 문제가 있어 isLoading으로 따로 관리했다. 마지막으로 빌드 오류났던 디테일 페이지의 타입 정의 부분.. 저번 주는 급하니까 일단 params: any로 하고 냈었는데 이번에 다시 천천히 살펴보면서 인터페이스를 정의해 params의 타입을 명확히 하고 Promise로 비동기 처리를 해서 오류를 해결했다. (지난번에도 분명 비슷한 거 시도했던 거 같은데 그땐 왜 오류가 났었는지 의문...ㅋ큐ㅠㅠㅋ) 그 외에도 img 태그를 Image로 리팩토링하거나, 중복되는 부분을 함수로 묶어 깔끔하게 정리하는 등 최적화를 위해 자잘한 코드들을 수정했다. 지난주에 비해 들인 시간은 적지만, 그래도 지난주 못지않게 많은 걸 공부할 수 있었던 것 같다.
(그리고 최고의 파트너 지원s..🤍 협업하면서 한 번도 힘든 적 없었고 오히려 늘 고마운... 내가 바쁜 시기일 때 많이 배려해 준 천사...🪽)
[최지원] 이번 주차는 검색페이지에 추가적으로 무한 스크롤을 구현하는데 집중했습니다. useInfinteQuery의 구조를 먼저 이해하고 코드를 써봤지만 각 리턴값이 필수인지 아닌지, 필요하다면 어느 용도로 쓰이는지 등을 세세히 알아봐야했던 점에서 확신하고 코드를 쓰기까지 시간이 많이 걸렸습니다. 또 오픈 api를 요청하는 url에서 쿼리에 쓰이는 &와 ?의 의미를 오해하고 있어서 401, 404 등의 에러를 한참 마주하며 헤매었습니다.. ?는 존재할 수도, 안할수도 있음의 의미가 아닌 쿼리 스트링의 시작에 붙는 기호라는 점.. 그리고 쿼리들 간의 순서는 상관이 없다는 점..
useInfinteQuery를 이용한 데이터를 성공적으로 불러온 이후에는 페이지 하단에 도달했을 때 다음 페이지를 fetch 하도록 하는 과정에서 처음엔 react-infinite-scroller을 사용하려 했습니다. 하지만 loadMore에 넘겨주는 prop 부분에서 함수에 자꾸 page인자를 넘겨주어야 한다고 뜨는데, 그걸 쓸 부분이 없어서 에러를 해결하지 못했고, 관련하여 찾아본 공식 문서나 레퍼런스들이 조금 이전 자료인 점을 고려해 결국 useInview 훅을 활용하기로 했습니다. 이를 통해 isFetchingNextPage가 아니면서 hasNextPage가 존재할 때 로드를 트리거할 div 태그에 ref를 달아두고, 스크롤을 감지하는 inView에 따라 다음 페이지를 불러오는 방식으로 생각보다 간단하게 구현할 수 있었습니다. 저번 주차에는 초기 데이터를 불러오는 useEffect부분과 검색어 입력값이 발생했을 때 데이터를 불러오는 함수 구현 부분이 분리되어 있어서 무언가 비효율적이라는 생각이 들었는데, 이번 무한스크롤을 적용하면서 tanstack query의 비동기 함수 호출로써 한 번에 처리할 수 있게 되었다는 점에서 앞으로도 tanstack query를 더 알아보고 적극 이용해야겠다는 생각이 들었습니다. 또 key question을 작성하면서 알게 된 Intersection Observer API의 observer을 활용하는 방식도 어떻게 다를지 직접 활용해서 구현해보고 싶습니다.
💡Key Questions
1️⃣ 무한 스크롤과 Intersection Observer API의 특징에 대해 알아봅시다.
무한 스크롤: 사용자가 페이지 하단에 도달했을 때 콘텐츠가 계속 로드되는 사용자 경험 방식, 한 페이지 아래로 스크롤 하면 끝없이 새로운 화면을 보여줌
Intersection Observer API: 브라우저 뷰포트(Viewport)와 원하는 요소(Element)의 교차점을 관찰하며, 요소가 뷰포트에 포함되는지 아닌지 (사용자 화면에 지금 보이는 요소인지 아닌지) 구별하는 기능을 제공함.
비동기적으로 실행되어 요소 관찰에서 발생하는 렌더링 성능이나 이벤트 연속 호출 문제가 생기지 않는다.
IntersectionObserverEntry
의 속성을 활용하여 요소들의 위치를 파악하므로 리플로우 현상을 방지함와 같은 정보를 요청할 때 (주로 스타일 정보)
사용법
2️⃣ tanstack query의 사용 이유(기존의 상태 관리 라이브러리와는 어떻게 다른지)와 사용 방법(reactJS와 nextJS에서)을 알아봅시다.
Tanstack Query는 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 매우 쉽게 만듦.
전역 상태 관리 도구는 애플리케이션의 상태를 관리하고 여러 컴포넌트 간 상태를 공유하기 위한 라이브러리로 Redux, Context API 등이 있다면, React Query는 데이터 요청 및 캐싱을 하기 위한 라이브러리이다. API 호출, 데이터 캐싱, 상태 관리 등의 작업을 담당함.
Tanstack Query의 장점
사용법
useQuery
(GET)useMutation
(POST, UPDATE, DELETE)캐싱하는 방법
3️⃣ 기본적인 git add, commit, push, pull, merge, rebase 등의 명령어에 대해 알아봅시다(+ git branch 전략이나 다른 git 명령어도 좋습니다!)
git add [디렉토리] / . : working directory에서 staging area로 올리기 (다음 변경(commit)을 기록할 때까지 변경분을 모아놓는 작업)
git commit -m “commit message” : staging area에 저장된 변경 사항들을 로컬 저장소로 올리기
git push : commit된 파일들을 원격 저장소로 업로드하기
git merge : 브랜치를 병합하는 커밋 로그가 master(main)에 Head로 새로 추가되어 합치기
git rebase : 브랜치를 베이스로 커밋을 재정렬하여 합치기
Git Branch 전략
master
: 제품 출시 버전을 관리하는 메인 브랜치develop
: 다음 출시 버전을 위해 개발하는 브랜치feature
: 새로운 기능을 개발하는 브랜치release
: 다음 출시 버전을 준비하는 브랜치hotfix
: 출시된 제품의 버그를 고치기 위한 브랜치