Skip to content
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 엔젤브릿지 권혜인 & 이가빈 미션 제출합니다. #11

Open
wants to merge 69 commits into
base: main
Choose a base branch
from

Conversation

billy0904
Copy link

@billy0904 billy0904 commented Nov 16, 2024

🍀배포 링크

무한 스크롤과 Intersection Observer API의 특징에 대해 알아봅시다.

무한 스크롤 (Infinite Scroll)

스크롤을 무한으로 할 수 있는 기능

  • 페이지의 최하단에 도달했을 때 신규 콘텐츠를 로드하는 식으로 동작

장점

  • 간단한 콘텐츠 탐색 - 별도의 추가 동작이 필요하지 않다.
  • 모바일 환경 또는 세로로 긴 화면의 디바이스에서 강점을 가진다.

단점

  • 한 페이지 내에 많은 콘텐츠가 로드 되기때문에 페이지 성능이 느려진다.
  • 많은 컨텐츠가 로드되고 나면 눈여겨봤던 콘텐츠로 다시 돌아가기 어렵다.
  • 사이트 하단(Footer)을 찾기 어렵다.

Intersection Observer API (교차 관찰자 API)

관찰 중인 요소가 뷰포트와 교차하고 있는지를 감지하는 API

  • 즉, 관찰 중인 요소가 사용자가 보는 화면 영역 내에 들어왔는지를 알려주는 API
  • 사용자 화면에 해당 요소가 지금 보이는지 아닌지를 구별

image (3)

  • 비동기적으로 실행되기 때문에 메인 스레드에 영향을 주지 않으면서 요소들의 변경사항을 관찰할 수 있다.
    • addEventListener()scroll 과 같은 이벤트 기반의 요소 관찰에서 발생하는 렌더링 성능이나 이벤트 연속 호출같은 문제 없이 사용할 수 있다.

🥲기존 scroll 의 문제점

  • scroll 이벤트의 경우 단시간에 수백번 호출이 되며 동기적으로 실행된다.
  • 각 요소마다 이벤트가 등록되어있는 경우 사용자가 스크롤할 때마다 이벤트가 끊임없이 호출되기 떄문에 몇 배로 성능 문제가 발생한다.
  • getBoundingClientRect() 역시 계산을 할 때마다 리플로우 현상이 일어난다는 단점이 있다.
💡

리플로우 (reflow)

문서 내 요소의 위치와 도형을 다시 계산하기 위한 웹브라우저 프로세스

  • 문서의 일부 또는 전체를 다시 렌더링하는데 사용

🌟사용 방법

  • Intersection Observer API는 다음과 같은 상황에 콜백 함수를 호출한다.
    • 타겟 요소가 기기 뷰포트나 특정 요소와 교차할 때
    • 관찰자가 최초로 타겟을 관측하도록 요청받을 때

✅ 기본 문법

  • new IntersectionObserver() 를 통해 생성한 인스턴스 io 로 관찰자를 초기화하고 관찰할 요소를 지정한다.
  • 생성자는 인수로 callbackoptions 두 개를 갖는다.
const io = new IntersectionObserver(callback, options) // 관찰자 초기화
io.observe(element) // 관찰할 요소 등록

✅ Callback

  • 관찰할 대상이 등록되거나 가시성에 변화가 생기면 실행된다.
  • 콜백은 인수로 entriesobserver 두 개를 갖는다.
const io = new IntersectionObserver((entries, observer) => {}, options)
io.observe(element)

✅ entries

IntersectionObserverEntry 의 배열

const io = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    console.log(entry) // entry is 'IntersectionObserverEntry'
  })
}, options)

io.observe(element1) // 관찰 대상 1
io.observe(element2) // 관찰 대상 2
// ...

✅ observer

콜백 함수가 호출되는 IntersectionObserver

const io = new IntersectionObserver((entries, observer) => {
  console.log(observer)
}, options)

io.observe(element)

✅ options

  • 관찰이 시작되는 상황에 대한 옵션을 설정할 수 있다.
  • 기본값들이 정해져 있으므로 필수는 아니다.
  • root , rootMargin , threshold 세 가지 값을 옵션 값으로 설정할 수 있다.
let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

let observer = new IntersectionObserver(callback, options);

tanstack query의 사용 이유(기존의 상태 관리 라이브러리와는 어떻게 다른지)와 사용 방법(reactJS와 nextJS에서)을 알아봅시다.

Tanstack Query

웹 어플리케이션에서 서버 상태를 가져오고, 캐싱하고, 동기화하고, 업데이트하는 것을 쉽게 만들어주는 상태 관리 라이브러리

  • A.K.A React Query
  • 즉, React에서 API 요청과 상태 관리를 쉽게 해주는 도구
  • API 통신 및 비동기 데이터 관리에 특화된 라이브러리
  • useQuery 훅을 사용하여 서버에 필요한 데이터를 비동기적으로 요청한다.
import { useQuery } from '@tanstack/react-query'

function App() {
  const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}

// 1. queryKey에 콜백 함수의 결과값으로 반환되는 프로미스 값을 가리킬 고유한 키 값 지정
// 2. queryFn에 프로미스 값을 반환하는 함수 작성
// 3. useQuery 훅의 {data, error, isLoading, isError, ...} 등 프로퍼티 값이 담긴 객체 반환

✅ 상태 관리 라이브러리 vs. tanstck query

📍상태 관리 라이브러리

어플리케이션의 상태를 관리하고 여러 컴포넌트 간 상태를 공유하기 위한 라이브러리

  • 주로 어플리케이션의 전역 상태를 관리하고 여러 컴포넌트에서 접근할 수 있어야 할 때 사용한다.
  • 로그인 상태, 테마, 언어 설정 등 전역적인 상태를 관리할 때 유용하다.
  • Redux, Recoil, zustand, MobX, Context API 등

📍tanstck query

데이터 요청 및 캐싱을 관리하기 위한 라이브러리

  • 주로 데이터 요청과 관련된 작업을 다룰 때 사용한다.
  • API 호출, 데이터 캐싱, 새로고침 관리 등을 통해 원격 데이터를 가져와 어플리케이션에서 사용한다.

✅ 기술적 장점

1️⃣ 데이터 캐싱 및 재사용

  • 사용자가 여러 컴포넌트에서 동일한 데이터를 사용하는 경우 캐싱된 데이터 재사용
  • useQuery 훅을 사용하여 데이터를 호출했을 때 동일한 queryKey로 호출한 기록이 존재한다면 캐시되어있던 해당 데이터를 재사용하고, 그렇지 않을 경우 queryFn을 호출하여 새롭게 데이터를 받아온다.

2️⃣ 간편한 상태 관리

  • useQuery 훅을 통한 비동기 요청의 로딩(isLoading, isPending), 성공(data), 실패(isError, error) 등의 상태를 제공한다.
  • 개발자가 별도로 상태 관리 로직을 작성할 필요 없이 반환되는 객체에서 원하는 프로퍼티 값을 사용하면 된다.
import { useQuery } from '@tanstack/react-query';

function Todos() {
  const { isPending, isError, data, error } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodoList,
  });

  if (isPending) return <span>Loading...</span>
  if (isError) return <span>Error: {error.message}</span>

  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

3️⃣ 서버 사이드 렌더링 (SSR) 지원

  • 렌더링 전에 서버에서 데이터를 미리 받아와 클라이언트에 초기 데이터로 전달이 가능하다.
  • 처음 화면이 로딩되는 시간을 단축하고 웹 사이트의 SEO를 향상시킬 수 있다.

4️⃣ 낙관적 업데이트 (optimistic update)

  • 새로운 업데이트 시 서버의 응답을 기다리지 않고 UI를 먼저 추가한다.
  • 이후 서버에게 보낸 요청이 성공한 경우 먼저 추가한 데이터 상태를 유지하고 실패한 경우 이전 상태로 되돌린다.

🌟사용 방법

✅ React.js

📍기본 설치

npm i @tanstack/react-query
npm i @tanstack/react-query-devtools
  • ReactQueryProvider 를 컴포넌트 트리의 루트에 배치
 // utils/ReactQueryProvider.tsx
import React, { PropsWithChildren } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

function ReactQueryProvider({ children }: PropsWithChildren) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { staleTime: 5000 } },
  });

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools />
    </QueryClientProvider>
  );
}

export default ReactQueryProvider;
  • 최상위 컴포넌트(App 컴포넌트)에서 ReactQueryProvider 적용
// App.tsx
import React from 'react';
import ReactQueryProvider from './utils/ReactQueryProvider';

function App() {
  return (
    <ReactQueryProvider>
      <div>
        {/* 사용할 컴포넌트들 */}
      </div>
    </ReactQueryProvider>
  );
}

export default App;

📍useQuery

  • queryKey 기반 값 캐싱
import { useQuery } from '@tanstack/react-query';

const MyComponent = () => {
  const { data: selectableTagList, isLoading, isError } = useQuery({
    queryKey: ['rootTag', typeID],
    queryFn: () => getRootTagList(typeID),
  });

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error occurred</div>;

  return (
    <div>
      {selectableTagList?.map((tag) => (
        <div key={tag.id}>{tag.name}</div>
      ))}
    </div>
  );
};

📍useSuspenseQuery

  • Suspense 사용 방식 그대로 적용 가능
import React, { Suspense } from 'react';
import { useSuspenseQuery } from '@tanstack/react-query';

const Statistic = () => {
  const { data: statisticList } = useSuspenseQuery({
    queryKey: ['statisticList', filter.archiveTypeID, filter.start, filter.end],
    queryFn: () => getRootTagStatisticListByArchiveType(filter),
  });

  return (
    <ul>
      {statisticList?.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
};

const StatisticPage = () => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Statistic />
    </Suspense>
  );
};

export default StatisticPage;

📍useMutation

  • mutate 동작을 원하는 이벤트 핸들러에 넣어준다.
import { useMutation } from '@tanstack/react-query';

const LedgerForm = () => {
  const { mutate } = useMutation({
    mutationFn: (data: LedgerCreateParams) => postLedger(data),
    onSuccess: () => {
      alert('내역 저장 완료');
      setForm(defaultForm);
    },
    onError: () => {
      alert('내역 저장 실패');
    },
  });

  const handleSubmit = (data: LedgerCreateParams) => {
    mutate(data);
  };

  return (
    <form onSubmit={(e) => e.preventDefault() || handleSubmit(form)}>
      {/* form fields */}
      <button type="submit">Save</button>
    </form>
  );
};

📍useQueries

  • 병렬적으로 쿼리를 수행하고 싶을 때 사용한다.
import { useQueries } from '@tanstack/react-query';

const useChildTagList = (tagList: number[]) => {
  const queryResult = useQueries({
    queries: tagList.map((tag) => ({
      queryKey: ['childTag', tag],
      queryFn: () => getChildTagList(tag),
    })),
    combine: (results) => ({
      data: results.map((result) => result.data),
      pending: results.some((result) => result.isPending),
    }),
  });

  return queryResult;
};

const ChildTags = ({ tagList }) => {
  const { data, pending } = useChildTagList(tagList);

  if (pending) return <div>Loading...</div>;

  return (
    <ul>
      {data?.map((tags) =>
        tags.map((tag) => <li key={tag.id}>{tag.name}</li>),
      )}
    </ul>
  );
};

✅ Next.js

📍기본 설치

npm i @tanstack/react-query
npm i @tanstack/react-query-devtools
// utils/ReactQueryProvider.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental';
import React from 'react';

function ReactQueryProvider({ children }: React.PropsWithChildren) {
  const [queryClient] = React.useState(
    () =>
      new QueryClient({
        defaultOptions: { queries: { staleTime: 5000 } },
      }),
  );

  return (
    <QueryClientProvider client={queryClient}>
      <ReactQueryStreamedHydration>{children}</ReactQueryStreamedHydration>
      <ReactQueryDevtools />
    </QueryClientProvider>
  );
}
export default ReactQueryProvider;
// app/layout.tsx
export default function RootLayout({ children }: IRootLayoutProps) {
  return (
    <html lang="kr">
      <body>
        <div id="portal" />
            <ReactQueryProvider>{children}</ReactQueryProvider>
      </body>
    </html>
  );
}

📍useQuery

  • react hook 사용 방식과 같이 사용 가능
  • queryKey 기반 값 캐싱
const { data: selectableTagList,isLoading,isError } = useQuery({
    queryKey: ['rootTag', typeID],
    queryFn: () => getRootTagList(typeID),
  });

📍useSuspenseQuery

  • 이전 버전에서는 useQuery의 suspense:true 옵션으로 사용했는데 v5 부터는 useSuspenseQuery를 통해 suspense와 사용할 수 있다.
const { data: statisticList } = useSuspenseQuery({
    queryKey: ['statisticList', filter.archiveTypeID, filter.start, filter.end],
    queryFn: () => getRootTagStatisticListByArchiveType(filter),
  });
`function StatisticPage() {
  return (
    <Layout>
      <Suspense fallback={<Loading />}>
        <Statistic />
      </Suspense>
    </Layout>
  );
}

📍useMutation

  • mutate 동작을 원하는 이벤트 핸들러에 넣어준다.
const { mutate } = useMutation({
    mutationFn: (data: LedgerCreateParams) => postLedger(data),
    onSuccess: () => {
      alert('내역 저장 완료');
      setForm(defaultForm);
    },
    onError: () => {
      alert('내역 저장 실패');
    },
  });

📍useQueries

  • 병렬적으로 쿼리를 수행하고 싶을 때 사용한다.
  • combine 함수에서 data를 반환하도록 추가할 수 있다.
export const useChildTagList = (tagList: number[]) => {
  const [childTagList, setChildTagList] = useState<TagType[]>([]);

  const queryResult = useQueries({
    queries: tagList.map((tag) => {
      return { queryKey: ['childTag', tag], queryFn: () => getChildTagList(tag) };
    }),
    combine: (results) => {
      return {
        data: results.map((result) => {
          return result.data;
        }),
        pending: results.some((result) => result.isPending),
      };
    },
  });

기본적인 git add, commit, push, pull, merge, rebase 등의 명령어에 대해 알아봅시다(+ git branch 전략이나 다른 git 명령어도 좋습니다!)

✅ git add

작업 디렉토리의 변경 내용을 스테이징 영역에 추가

git add <파일명>         # 특정 파일 추가
git add .                # 현재 디렉토리의 모든 변경 사항 추가

# ex
git add index.html       # index.html 파일을 스테이징 영역에 추가

✅ git commit

스테이징 영역에 있는 변경사항을 로컬 저장소에 저장

git commit -m "커밋 메시지"  # 메시지를 포함한 커밋
git commit                # 메시지를 입력할 에디터가 열림

# ex
git commit -m "Fix: Corrected header style"

✅ git push

로컬 저장소의 변경 사항을 원격 저장소에 업로드

git push <원격저장소> <브랜치명>
git push origin main      # 'origin' 저장소의 'main' 브랜치에 푸시

# ex
git push origin feat/#42

✅ git fetch

원격 저장소의 변경 사항을 로컬 저장소로 가져오지만 병합은 하지 않음

git fetch <원격저장소>

#ex
git fetch origin

✅ git pull

원격 저장소의 변경 사항을 가져와 현재 브랜치에 병합

git pull <원격저장소> <브랜치명>
git pull origin main

#ex
git pull origin develop

✅ git merge

두 브랜치의 변경 사항을 병합

git merge <병합할 브랜치명>

#ex
git checkout main         # 병합 대상 브랜치로 이동
git merge feat/#42  # 'feat/#42'의 변경 사항 병합

✅ git rebase

현재 브랜치의 변경 사항을 다른 브랜치의 최신 상태로 재배치

git rebase <대상 브랜치명>

#ex
git checkout feat/#42
git rebase main           # 'main' 브랜치의 최신 변경 사항을 재배치

(+) ETC…

  • git status: 현재 상태를 확인(스테이징된 파일, 변경된 파일, 트랙되지 않은 파일 등)
  • git log: 커밋 기록 확인
  • git branch: 브랜치 목록 확인 및 생성/삭제
  • git reset: 이전 상태로 되돌림(스테이징 또는 커밋 제거)

@jsomnium
Copy link

jsomnium commented Nov 17, 2024

이번 코드 리뷰 맡은 박지수입니다!!
전 주차 과제였던 랜딩 페이지 애니메이션 어떻게 이렇게 부드럽게 잘 구현하셨는지 신기합니다.. ㅎㅎ
그리고 검색 페이지에서 검색 시에 버튼 클릭 하기 전에도 검색 목록이 반영되도록 돼있는 점 좋았습니다.
고생 많으셨고 다음 과제도 파이팅이에요!! (o´ω`o)ノ

Copy link

@hiwon-lee hiwon-lee left a comment

Choose a reason for hiding this comment

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

이번주도 멋진 과제 하느라 수고하셨습니다
역시 key question도 너무나 정리를 잘 해주신것 같아서 보면서 많이 배워갈 수 있었습니다ㅎㅎ
이제 커리상 프론트 단독으로하는 과제는 다음주가 마지막이더라구요 마지막까지 파이팅~~

Comment on lines +3 to +7
interface Movie {
id: number;
title: string;
poster_path: string;
}

Choose a reason for hiding this comment

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

따로 movieInterface.ts를 만든 것을 쓰면 더 좋을것같아요~~

import { fetchMovies } from '@/api/MovieList';
import * as styles from '@/styles/MovieList.css';

// 와 진짜 레전드 노가다

Choose a reason for hiding this comment

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

ㅋㅋㅋㅋㅋ

import { useSearchStore } from "@/utils/search/useStore";
import { fetchSearch } from "@/api/searchApi";

export const ContentList: React.FC = () => {
Copy link

@hiwon-lee hiwon-lee Nov 18, 2024

Choose a reason for hiding this comment

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

여기서만 갑자기 왜 React.FC를 사용하시 건가요??

Copy link
Author

Choose a reason for hiding this comment

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

그러게요 저도 궁금합니다 @혜인언니

Choose a reason for hiding this comment

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

vanilla-extract쓰는거 처음봤어요!!
저번에 혜인언니랑 집갈때 제가 관련라이브러리 정리한거 있다고 한 거 찾았어요
허접하긴한데,, 그냥 정리겸 ㅎㅎ

  • tailwind
  • styled component or emotion -> Server Component SSR 문제가 되고 있다.
  • sass
  • css module -> 간단하게 가자
  • vanilla extact -> windows와 문제가 생길 수 있음 (요즘 핫함)

Copy link
Author

Choose a reason for hiding this comment

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

윈도우에서 css 적용 안되는 문제는 해결되었다고 듣긴 했는데 문제가 또 생긴 건가요🥹🥹 우리 둘다 윈도우 쓰는데... 찾아봐야겠다....

Copy link
Author

@billy0904 billy0904 left a comment

Choose a reason for hiding this comment

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

바쁜 와중에도 빠른 속도로 뚝딱뚝딱 너무 잘 구현해줘서 고마워용💚🌟 내 파트너가 짱임... 엔브도 같이 즐겁게 달려봅시다 ㅎㅎ

이번 주차 과제도 수고하셨습니다!🍀

Copy link
Author

Choose a reason for hiding this comment

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

아니 이거 안지웠네..ㅋㅋ

Comment on lines +1 to +13
import { Movie } from "@/types/movieInterface";
import { instance } from "./instance";

interface FetchMoviesResponse {
results: Movie[];
}

export const fetchSearch = async (page: number = 1): Promise<Movie[]> => {
const response: FetchMoviesResponse = await instance.get(
`/movie/popular?api_key=${process.env.NEXT_PUBLIC_API_KEY}&language=en-US&page=${page}`,
);
return response.results;
};
Copy link
Author

Choose a reason for hiding this comment

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

API 관련 파일은 이 파일처럼 어쩌구Api로 이름을 통일시키는게 좋을 것 같네요! 아무 생각 없었는데 언니 파일명 보고 생각남..ㅎ

import Image from "next/image";
import { fetchSearch } from "@/api/searchApi";

const IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w500/";
Copy link
Author

Choose a reason for hiding this comment

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

상수 처리로 깔끔한 코드 너무 좋습니다! 👍👍

import { useSearchStore } from "@/utils/search/useStore";
import { fetchSearch } from "@/api/searchApi";

export const ContentList: React.FC = () => {
Copy link
Author

Choose a reason for hiding this comment

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

그러게요 저도 궁금합니다 @혜인언니

Comment on lines +14 to +33
const fetchMovies = useCallback(() => {
const fetchData = async () => {
try {
const data = await fetchSearch(1);
const filteredData = searchText
? data.filter((movie) =>
movie.title.toLowerCase().includes(searchText.toLowerCase()),
)
: data;
setMovies(filteredData);
} catch (error) {
console.error("검색리스트 실패", error);
}
};
fetchData();
}, [searchText]);

useEffect(() => {
fetchMovies();
}, [searchText]);
Copy link
Author

Choose a reason for hiding this comment

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

실시간 검색은 이렇게 구현하는거군요...!! 코드 보면서 많이 배워갑니다~!👍💚

Comment on lines +33 to +39
<Image
style={{ cursor: "pointer" }}
className={style.icon}
src={cancelIcon}
alt="검색취소"
onClick={cancelSearch}
/>
Copy link
Author

Choose a reason for hiding this comment

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

이런 꼼꼼한 alt 설정 너무 좋습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants