블로그
전체 32025. 03. 23.
0
[인프런 워밍업 클럽 3기] 풀스택 과정 3주차 발자국 👣
어느새 3주차다!느낀점은, 혼자 강의를 신청했으면 절대 이만큼 못왔을 거란 거다. 스터디에 참여했기에 강제성이 있어 이만큼 올 수 있었다.일하면서 강의를 듣는다는게 시간도 체력도 많이 필요한 일임을 느꼈다.다행인 점은 과제가 그렇게 많이 어렵진 않아서 다행히도 주말만 투자해서도 해낼 수 있다는 것..!마지막 주는 좀 더 빡셀지 모르겠으나,, 이번 주 공부한 내용을 정리해본다. 깃허브 링크 결과물 이미지! 이슈 해결1. 초기 세팅 후 npm install, npm run dev 다시 하기tailwind 같은 라이브러리는 설치 후 서버 다시 런해야 적용될 때가 있으니 스타일 적용이 안된다 싶으면 npm run dev 다시 해주는 걸 잊지 말자.2. Recoil과 최신 React 버전 충돌 문제Error: Cannot destructure property 'ReactCurrentDispatcher' of 'react__WEBPACK_IMPORTED_MODULE_0___default(...).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED' as it is undefined.Recoil의 현재 버전은 0.7.7에 멈춰있음React 19, 18과 호환이 안되는 것으로 보임Zustand를 사용할까 했는데, Next.js의 속성과 충돌하는 면이 있다고 함아래의 인용 참고 (원문)in Next.jsNext.js 환경에서는 위 장점이 온전히 발휘되지 못한다. 공식적으로 Context 안에서 스토어를 초기화하고 저장하는 것을 권장한다. 때문에 기존 스토어 생성 -> 사용의 두 단계에서 Context 생성 -> 스토어 생성 -> 스토어 초기화 -> Provider 등록 -> 사용의 다섯 단계로 과정이 늘어난다.예시 코드는 공식 문서에서 볼 수 있다.이렇게 세팅의 차이가 나는 이유는 Zustand의 스토어가 전역 변수 기반이기 때문이다. Next.js는 서버와 클라이언트, 두 가지 환경에서 모두 실행되므로 전역 변수의 동작이 Zustand의 가정과 다르다.서버 컴포넌트, 클라이언트 컴포넌트 나뉘는 것이 원인인 듯함추가로, 가끔 발생하던 Hydration 에러를 좀 더 이해하게 됐는데, 서버와 클라이언트 컴포넌트 간에 공유되는 상태의 값이 서로 다를 때 발생하는 에러라고 함ssr과 hydration 관련 zustand 문서그래서, 궁금하기도 했던 Context API를 활용해보기로 함3. 무한 스크롤 - 새 데이터 불러오고 스크롤 위치 초기화되는 문제원인: 데이터 패치 중인 상태에 리턴할 컴포넌트를 먼저 배치하고, 그 아래 리스트 컴포넌트를 배치 → 새 데이터를 불러온 뒤 리스트 컴포넌트가 다시 렌더링되게 하는 듯if (isFetching || isFetchingNextPage) { return ( ); } return ( {/* 배열(전체 무비 데이터) 속 배열(페이지별 무비 데이터)이므로, 평탄화 */} {data?.pages?.map((page) => page.data ?.flat() ?.map((movie) => ) )} {/* IntersectionObserver - 무한스크롤 구현을 위해 */} ); 해결: 순서를 바꿈return ( {/* 로딩 인디케이터 */} {isFetching && !isFetchingNextPage && ( )} {data && ( {/* 배열(전체 무비 데이터) 속 배열(페이지별 무비 데이터)이므로, 평탄화 */} {data?.pages?.map((page, pageIndex) => page.data ?.flat() ?.map((movie) => ( )) )} {/* IntersectionObserver - 무한스크롤 구현을 위해 */} {isFetchingNextPage && ( )} )} ); 4. 검색 - 대소문자 구분 없이 검색 및 로딩 인디케이터 제거대소문자 구분 없이 검색 - ilike를 사용.ilike("title", `%${search.trim().replace(/\\s+/g, "%")}%`) // 대소문자 구분 X 로딩 인디케이터 관련: 검색 시 무한 스크롤 코드에서 계속 hasNextPage가 true로 되어 데이터를 무한으로 호출하는 버그였음hasNextPage 값을 처리하는 코드 개선 - 데이터 패치 필요없는 경우들에 명확히 undefined를 반환해 더이상 데이터 불러오지 않게끔 개선// useInfiniteQuery 사용하는 코드 getNextPageParam: (lastPage) => { // lastPage?.page ? lastPage.page + 1 : null, // 검색어 입력 시 무한으로 다시 호출하는 버그 // 데이터가 없으면 undefined를 반환하여 hasNextPage를 false로 설정 if (!lastPage.data || lastPage.data.length === 0) { return undefined; } // 마지막 페이지의 데이터 개수가 pageSize보다 작으면 undefined를 반환 if (lastPage.data.length 새로운 정보1. 무한스크롤 구현1) React-Intersection-Observer 라이브러리 (useInView 훅)화면에 요소가 보이게 됐는지 확인할 때 사용ref를 전달해 어떤 요소를 감지할지 명시,무한스크롤 구현에 사용보여줄 데이터 제일 밑에 보이지 않는 태그를 추가이 태그가 화면에 보이는 순간 다음 페이지 데이터를 가져오는 함수 호출‘이 태그가 보이는 순간’을 감지하는 데 useInView 사용순간을 감지하면 실행하라,는 코드는 useEffect 로 구현2) React-Query의 useInfiniteQueryuseQuery와 유사한데, fetchNextPage, hasNextPage, isFetchingNextPage라는 특수한 props 가짐2. Next.js - 페이지별 메타데이터 생성: generateMetadata영화 리스트 - 영화 상세 페이지로 이동했을 때 그 영화 관련 정보로 그 페이지의 메타데이터를 구성하고 싶을 경우 등에 사용ogImage: 해당 페이지 링크 공유 시 뜨는 이미지// movies/[id]/page.tsx 상단에 정의 // 각 영화 페이지에 맞는 동적인 메타데이터를 생성 export async function generateMetadata({params}: { params: { id: string }; }) { // 사용할 데이터 패치 const movie = await getMovieById({ movieId: Number(params.id), }); return { title: movie?.title || "", description: movie?.overview || "", // 메타데이터를 위한 이미지 URL - og이미지. 사이트 url 공유 시 보이는 이미지임 openGraph: { images: [movie?.image_url || ""], }, }; } export default function MovieDetail({ // 후략... 미션Notflix Clone 프로젝트에 "찜하기" 기능을 추가하세요. • 사용자가 특정 영화를 "찜"할 수 있도록 Supabase를 활용해 즐겨찾기 리스트 구현 찜한 영화를 영화 리스트 화면의 최상단으로 보여주도록 정렬찜하기 기능 추가 - movie 테이블에 favorited row 추가, boolean 타입으로최상단 보여주기 - 데이터 조회 코드를 수정찜한 데이터를 먼저 불러오고, 그외의 데이터를 불러오도록 수정?1. 찜하기 update 처리 후 movies 리스트 순서가 바뀌는 문제쿼리문의 order 기준을 정해 항상 같은 순서로 데이터를 불러오게 함release_date가 같은 경우 존재, 고유 값인 id를 보조 정렬 규칙으로 사용.order("release_date", { ascending: false }) .order("id", { ascending: true }) // 고유한 ID를 보조 정렬 키로 추가 2. 찜한 데이터 리스트 따로 조회 및 보여주기최근에 찜한 순서로 보여주는 게 일반적이라 판단, favorited → favorited_date (timestampz)로 변경조회하는 코드 분리 및 따로 리스트 생성: 찜한 컨텐츠는 Row, 가로 스크롤 배치
웹 개발
・
웹개발
・
Next.js
・
Supabase
2025. 03. 16.
1
[인프런 워밍업 클럽 3기] 풀스택 과정 2주차 발자국 👣
2주차에 배운 내용을 정리해본다.깃허브 링크 1. 배운 내용1. Next.js에서 메타데이터 정의하기 태그를 이용해 사이트 정보를 정의하려면,서버 컴포넌트 파일에서Metadata를 정의해줘야 함// page.tsx import { Metadata } from "next"; import Ui from "./Ui"; // 페이지의 메타데이터를 정의 // use client에서는 사용 불가 - 클라이언트 코드는 Ui.tsx에서 정의하는 이유 export const metadata: Metadata = { title: "Dropbox Clone", description: "A minimalist Dropbox Clone", }; export default function Home() { return ; } 2. 파일 드랍 존 만들기 태그를 이용, type="file"3. supabase1. storage bucket 만들기업로드 가능한 파일 종류 설정 가능만들 때 Allowed MIME types 옵션에서 image/* 등의 조건을 추가하면 됨2. policy 생성사이드바 policy 메뉴에서 생성 가능이름, 가능한 액션 종류 선택, 누구에게 가능하게 할지 선택 가능4. 파일 드래그앤드롭 - react-dropzone 라이브러리 사용사용법은 npm 공식 문서의 코드 조각을 확인https://www.npmjs.com/package/react-dropzone 2. 이슈 사항1. storage의 get url 형태 변경: getImageUrl 함수 커스텀 어려움이미지의 만료일을 지정할 수 있게 변경됨그러면서 token이라는 서치 파라미터가 필수값으로 추가된 듯토큰을 누락한 형태로 확인 시 에러가 발생하며 이미지 로드 실패{"statusCode":"400","error":"Error","message":"querystring must have required property 'token'"} bucket을 public으로 전환하고, supabase에서 제공하는 getPublicUrl 메서드를 사용storage에서 bucket 이름 옆 드롭다운 메뉴 → edit bucket → public으로 설정 getImageUrl 함수 내부를 아래와 같이 수정const { data } = supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET!) .getPublicUrl(path); /** * A simple convenience function to get the URL for an asset in a public bucket. If you do not want to use this function, you can construct the public URL by concatenating the bucket URL with the path to the asset. * This function does not verify if the bucket is public. If a public URL is created for a bucket which is not public, you will not be able to download the asset. * * @param path The path and name of the file to generate the public URL for. For example `folder/image.png`. * @param options.download Triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename. * @param options.transform Transform the asset before serving it to the client. */ getPublicUrl( path: string, options?: { download?: string | boolean; transform?: TransformOptions } ): { data: { publicUrl: string } } { const _path = this._getFinalPath(path) const _queryString = [] const downloadQueryParam = options?.download ? `download=${options.download === true ? '' : options.download}` : '' if (downloadQueryParam !== '') { _queryString.push(downloadQueryParam) } const wantsTransformation = typeof options?.transform !== 'undefined' const renderPath = wantsTransformation ? 'render/image' : 'object' const transformationQuery = this.transformOptsToQueryString(options?.transform || {}) if (transformationQuery !== '') { _queryString.push(transformationQuery) } let queryString = _queryString.join('&') if (queryString !== '') { queryString = `?${queryString}` } return { data: { publicUrl: encodeURI(`${this.url}/${renderPath}/public/${_path}${queryString}`) }, } } StorageFileApi.ts를 참고하면 다양한 메서드가 있음 - createSingedUrl를 이용하면 expiresIn을 직접 지정 가능. 이걸 이용하면 bucket이 public이 아니어도 가능할듯 /** * Creates a signed URL. Use a signed URL to share a file for a fixed amount of time. * * @param path The file path, including the current file name. For example `folder/image.png`. * @param expiresIn The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute. * @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename. * @param options.transform Transform the asset before serving it to the client. */ async createSignedUrl( path: string, expiresIn: number, options?: { download?: string | boolean; transform?: TransformOptions } ): Promise { try { let _path = this._getFinalPath(path) let data = await post( this.fetch, `${this.url}/object/sign/${_path}`, { expiresIn, ...(options?.transform ? { transform: options.transform } : {}) }, { headers: this.headers } ) const downloadQueryParam = options?.download ? `&download=${options.download === true ? '' : options.download}` : '' const signedUrl = encodeURI(`${this.url}${data.signedURL}${downloadQueryParam}`) data = { signedUrl } return { data, error: null } } catch (error) { if (isStorageError(error)) { return { data: null, error } } throw error } } /** * Creates multiple signed URLs. Use a signed URL to share a file for a fixed amount of time. * * @param paths The file paths to be downloaded, including the current file names. For example `['folder/image.png', 'folder2/image2.png']`. * @param expiresIn The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute. * @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename. */ async createSignedUrls( paths: string[], expiresIn: number, options?: { download: string | boolean } ): Promise { try { const data = await post( this.fetch, `${this.url}/object/sign/${this.bucketId}`, { expiresIn, paths }, { headers: this.headers } ) const downloadQueryParam = options?.download ? `&download=${options.download === true ? '' : options.download}` : '' return { data: data.map((datum: { signedURL: string }) => ({ ...datum, signedUrl: datum.signedURL ? encodeURI(`${this.url}${datum.signedURL}${downloadQueryParam}`) : null, })), error: null, } } catch (error) { if (isStorageError(error)) { return { data: null, error } } throw error } } 2. server action 파일에서 console.log는 개발자 도구가 아닌 터미널에 찍힌다는 사실..storageActions.ts에 정의한 uploadFile 함수를 수정하고 제대로 데이터가 들어가는지 확인하려고 console.log를 사용아무리 해도 useQuery까지는 잘 로그가 찍히는데, uploadFile에 작성한 로그가 브라우저 개발자도구에 전혀 찍히지 않아 뭐가 문제인지 한참 헤맴코드 에디터 터미널에 찍히고 있었음.. ㅠㅠ3. 파일명에 한글이 포함될 경우 업로드 안되는 오류디스코드에 다른 러너분들이 공유해준 정보에 따르면 supabase storage는 AWS의 S3 스토리지와 호횐되어, 파일명도 AWS S3의 Safe charaters - 영문, 숫자, 일부 특수기호 만 허용한다고 함어떻게 저장할까?base64 인코딩을 통해 S3-safe한 이름으로 변경해 저장하면 업로드 가능⇒ 저장 및 다운로드, 이름 표시하는 코드에서 인코딩/디코딩 함수를 사용하게 변경 완료어떻게 검색할까?인코딩/디코딩된 값으로 검색 호환이 안됨⇒ db에 저장해야 함db에 저장하도록 변경했는데, 한글 초성만 검색됨..todo-list 검색 때와의 차이가 뭐길래 안되는거지? 3. 미션 - 파일의 마지막 수정(업로드) 시간 표시하기storage를 확인하면, Added on, Last modified 정보가 있음file이 어떤 형태인지 로그 찍어 확인file.updated_at 키에 저장된 값임을 확인, 이 값을 사용추가로 file.metadata에 파일 타입과 사이즈 등의 정보도 있어 그 정보들도 적절히 표시하도록 함 (ui는 supabase storage를 참고함)
웹 개발
・
Next.js
・
Supabase
・
React.js
・
File
2025. 03. 09.
0
[인프런 워밍업 클럽 3기] 풀스택 과정 1주차 발자국 👣
1주차 주요 내용강의 및 스터디 소개SupabaseNext.jsReact QueryRecoilTailwind CSSTodo List 프로젝트 들어가며Firebase와 Flutter Web으로 개발한 서비스를 Supabase와 React.js로 (기획을 보강하여) 다시 개발하는 작업을 진행 중이다. 클라이언트 상태 관리로는 Zustand, 서버 상태 관리로는 React Query를 사용하고 CSS는 Mantine 키트를 사용하고 있다. 새로운 기술 스택이 많아 삽질도 많이 하며 작업 속도가 안 나고 있던 중에, Supabase, React Query 그리고 궁금했던 Next.js를 사용하는 강의+스터디가 있음을 알게 되어 좋은 기회라고 생각해 참여했다. 일과 병행하느라 강의를 들을 시간을 많이 확보하진 못했지만, 진득하게 앉아서 해보니 재미있게 진행했다. 강의를 통해 새롭게 알게 된 것들 위주로 정리하고, 미션 수행에서 겪었던 어려움과 회고를 정리해보려 한다. 새롭게 알게 된 것들Next.js기본 개념, 특징리액트 기반의 서버사이드 렌더링 프레임워크SSR의 이점 - 서버에서 이미 렌더링한 HTML을 검색 봇(브라우저)&사용자에게 전달하는 것이기 때문에SEO에 유리: 모든 페이지의 html 마크업이 서버에서 생성 → 검색 엔진이 페이지 인식&색인하기 유리CSR(Client Side Rendering)이 SEO에 불리한 이유는: JavaScript가 실행되기 전에는 빈 HTML 파일에 불과하기 때문페이지 로딩 속도 개선HTTP API 구현 가능 - route.ts 파일에!route.ts 파일의 코드는 웹에서 접근 불가: 서버에서 돌아가는 코드이기 때문 ⇒ DB 접속 등 보안 중요한 코드 여기서 넣으면 더 안전browser api (window, navigation, 리액트 코드 등) 사용 불가 리액트와의 차이 - route 폴더명이 route와 일치 dynamic route도 가능 (폴더명을 app/movies/[id]와 같이 [] 이용)params, searchParams를 컴포넌트의 props로 받아서 컴포넌트 내에서 사용 가능폴더 내 파일의 역할이 정해져 있음page.tsx: 각 폴더(루트)의 대표 파일layout.tsx: 화면 레이아웃 정의하는 파일route.ts: HTTP API 구현하는 파일Link 태그:a 태그 대신 Link 태그 사용할 것을 권함 Server Side Redirect 말고 Client Side Routing이 동작하게 됨 Next.js가 프로젝트 안에 어떤 링크들이 있는지 수집하는 것을 도움서버 컴포넌트, 클라이언트 컴포넌트가 구분되어 존재함React Query개요데이터 fetching, caching, db와의 동기화를 돕는 클라이언트 라이브러리cf) 캐싱 = 데이터의 복사본을 저장해 동일한 데이터의 재접근 속도를 높이는 것장점페이지 이동할 때마다 다시 불러올 필요가 없는 데이터를 캐싱하여 사용 → 불필요한 API 콜 줄임클라이언트 데이터 (zustand, recoil, useState 등으로 관리)와 서버 데이터 (react query 등으로 관리) state 관리 명확히 분리 가능React Query - 관련 글 아카이브카카오 페이 테크React Query로 비동기 데이터를 처리하는 세가지 이유불필요한 코드의 감소 ( Redux의 보일러플레이트 코드)업무와 협업의 효율성을 위한 규격화된 방식 제공: useQuery, useMutation 사용 규칙이 정해져 있음사용자 경험 향상을 위한 다양한 Built-in 기능: 로딩, 에러 처리 등이 간단리액트 쿼리 개념잡기데이터의 상태최신 데이터는 fresh, 기존 데이터는 stalestaleTime은 데이터가 fresh → stale 상태로 변경되는 데 걸리는 시간, 기본값 0cacheTime은 데이터가 inactive한 상태일 때 캐싱된 상태로 남아있는 시간특정 컴포넌트가 unmount(페이지 전환 등으로 화면에서 사라질 때) 되면 사용된 데이터는 inactive상태로 바뀌고, 이때 데이터는 cacheTime만큼 유지cacheTime 이후 데이터는 가비지 콜렉터로 수집되어 메모리에서 해제cacheTime이 지나지 않았는데 해당 데이터를 사용하는 컴포넌트가 다시 mount되면, 새로운 데이터를 fetch해오는 동안 캐싱된 데이터를 보여줌React-query는 React의 ContextAPI를 기반으로 동작useQueries - Promise.all() 처럼 여러 useQuery를 한번에 실행, 배열을 리턴select 키워드로 raw data로부터 원하는 데이터 추출해 리턴하는 사용도 가능 import { useQuery } from 'react-query' function User() { const { data } = useQuery({ queryKey: ["user"], queryFn: fetchUser, select: user => user.username, }) return Username: {data} }+궁금해진 것 - React Query의 데이터 리패치 트리거 종류에는 무엇이 있는지?Recoil개요전역 상태 관리 라이브러리 ⇒ 여러 컴포넌트에서 하나의 상태를 공유/관리하는 상황에서 유용오픈 소스주요 개념atom.ts - 모든 state를 모아두는 파일 (store.ts와 유사)atom: 초기 기본 상태를 정의하는 함수selector: 기존 상태에서 파생된 상태 - 클라이언트단에서 특정 컴포넌트에 필요한 상태만 뽑아서 사용할 때 유용useRecoilState: 리액트의 useState와 유사한 형태로 사용useRecoilValue: 값을 set 할 필요는 없고 value만 필요할 경우에 사용Supabase 연동하기DB 데이터의 타입 자동 생성하기package.json의 "scripts" 부분에, "generate-types"로 시작하는 명령어를 추가, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "generate-types": "npx supabase gen types typescript --project-id [YOUR_PROJECT_ID] --schema public > types_db.ts" },project-id 뒤 [YOUR_PROJECT_ID] 부분에 프로젝트 아이디를 추가 ([] 없이 문자열 바로)프로젝트 아이디는 api settings의 url 부분에 https:// 와 .supabase 사이에 있는 문자열실행하기 위해서 supabase 로그인하기npx supabase login브라우저 열리며 코드 입력 과정이 진행됨!(안될 경우, supabase 먼저 설치하기)실행하기 - npm run generate-typestypes_db.ts 파일이 생성된 걸 확인할 수 있음!ANON KEY란?브라우저에서 동작할 때 누구나 사용할 수 있는 API 키 값 => 이 키를 사용할 땐 누구나 사용 가능하게 설정되어 있는 API들에만 접근 가능cf) SERVICE_ROLE은 서버쪽에서 사용하는 값, secret으로 유지해야 하므로 환경변수명에 public 포함하지 않을 것 미들웨어란?새로고침이나 API 호출을 하는 등의 Request를 하고 Response를 받는 과정 중간에 추가 작업을 넣고 싶거나 쿠키를 세팅하는 등의 중간 단계를 거치고 싶을 때 인터셉트를 할 수 있게 돕는 것 미션 수행깃허브 레포지토리 링크도 함께 첨부한다!어려움미션을 수행함에 있어서 예상치 못한 난제가 있었는데, Material Tailwind 라이브러리 세팅 과정에서 버전 이슈로 충돌이 나는지 무한히 무언가 실행되려고 하는 듯했다.Error: Maximum call stack size exceeded Call Stack 55 Hide 45 ignore-listed frames Array.forEach (0:0) ...강의에서 알려주신 대로 Next.js 프로젝트를 만들었는데도 tailwind.config.ts 파일이 기본으로 없어서 직접 추가했고, tailwind css를 import 구문에서 계속 찾지 못한다고 해 uninstall과 install을 몇번을 했는데도 해결되지 않았다. 결국 tailwind 4에서 3으로 다운그레이드도 시도해보고, 다른 분들이 올린 소스코드를 참고해 package.json을 수정하는 등 여러 방법을 시도했으나, 속시원히 해결하지 못했다.시간이 많이 남진 않았어서 Tailwind CSS만 사용해 스타일링을 진행하기로 결정했다. Supabase, React Query, Next.js 사용법을 익히는 것이 지금 당장 더 중요했기 때문이다. 아 그리고 잊고 있었는데 하나 더 있었다. 기본으로 사용하는 크롬 브라우저에 익스텐션을 여러 개 깔아뒀는데, 지금 보고 있는 화면의 문서를 건드리는 것들도 있어서 그런지 로컬호스트로 실행했을 때 Hydration 관련 오류도 났었다. 조금 헤매다가 익스텐션 때문일 수 있단 답을 얻고 아무런 설정 없는 계정으로 브라우저를 열어 개발했다. 회고수행하며 느낀 점도 정리해보겠다. 우선 FontAwesome을 오랜만에 다시 썼는데 굉장히 편리했다. 쉽게 임포트할 수 있어서 간단한 프로젝트에선 이것을 사용하는 게 좋겠다. 플러터 백그라운드여서 타입을 명시해 개발하는 걸 선호하는데, 타입스크립트에서 타입 명시하는 방식도 감을 잡아가고 있다. 그래서 강의 내용에서 보충해 타입을 추가해서 작업을 해보았다. 또 React Query 사용하는 코드들은 UI, Todo 컴포넌트와 분리하여 services 내에 따로 파일을 추가했고 각 컴포넌트에서 임포트하여 사용했다.export function updateTodoMutation() { return useMutation({ mutationFn: ({ todo }: { todo: TodoRowUpdate }) => updateTodo({ todo }), onSuccess: () => { // 모든 todos 쿼리 무효화 queryClient.invalidateQueries({ queryKey: ["todos"], }); }, onError: (error) => { console.error("Error updating todo:", error); throw error; }, }); }import { deleteTodoMutation, updateTodoMutation } from "services/todo_queries"; const updateTodo = updateTodoMutation(); const deleteTodo = deleteTodoMutation(); const handleUpdate = ({ newTitle, newCompleted, }: { newTitle?: string; newCompleted?: boolean; }) => { updateTodo.mutate( { todo: { id, title: newTitle ?? title, completed: newCompleted ?? completed, completed_at: newCompleted ? new Date().toISOString() : null, }, }, { onSuccess: () => { // updateTodoMutation의 onSuccess 콜백과 함께 실행할 함수 // 컴포넌트 내부 상태관리! setIsEditing(false); }, } ); };completed 값은, 클릭 이벤트 시점의 값으로 setState를 해주고 state 값으로 업데이트하려 했을 때 정확한 상태값이 저장되지 않아서 처리 코드를 보완해줬다.onChange={(e: React.ChangeEvent) => { const newCompleted = e.target.checked; // 정확한 값 업데이트를 위해 setCompleted(newCompleted); // promise를 반환하지 않아 await해도 작업 완료까지 기다리지 않음 handleUpdate({ newCompleted }); }}일을 하며 알아낸 방법들과 강의에서 소개해준 방법들을 조합하여 간단한 프로젝트를 진행하는 거라 재미있었다. 초기 Material Tailwind 세팅에서 헤맸던 순간은 좀 고통스러웠지만.. 추가 미션인 completed_at을 저장하는 것은 위에 첨부한 코드 스니펫에서 보이는데, 서버에 저장할 completed의 값이 true이면 그 시간 정보를, false이면 null을 저장하도록 했다.투두 추가한 시간 보여주는 것도 간단하게 세로로 배치했다. {title} {todo.created_at && ( {new Date(todo.created_at).toLocaleString()} )} 사실 우리 서비스가 Todo를 다루는 거라 굉장히 익숙한 주제여서 더 재밌게 했던 것 같다. 다음 주부터는 모르는 개념이 더 많을텐데, 화이팅해서 완주하고 많이 배워가고 싶다. 화이팅.
풀스택
・
워밍업클럽
・
ReactQuery
・
Next.js
・
supabase