블로그

leeebug

워밍업 클럽 스터디 3기 FS - 2주 차 발자국

인프런 워밍업 클럽 스터디에 참여하고 벌써 2주 차도 마무리에 접어들고 있다. 4주간의 스터디이기 때문에 생각보다 일정이 타이트하여 시간관리가 무엇보다도 중요한 시기라고 생각한다.이번 주에는 파일 업로드 기능을 구현해야하기에 1주 차 과제를 급하게 마무리하고 지난주 토요일부터 서둘어서 미리 학습을 시작했다.깃 레포지토리의 경우에는 지난주에 사용했던 템플릿을 거의 수정없이 그대로 사용해서 개발환경 구축은 크게 어렵지 않았다.이번 주 강의에서는 Supabase Storage를 사용했는데 API가 잘 준비되어있어서 사용 방법을 익히는데도 크게 어렵지는 않았다. 다만 아래에서도 언급하겠지만 Supabase Storage는 AWS S3 기반으로 구현되어 강력한 네이밍 규칙이 적용되어 개인적으로는 Supabase Storage와 Supabase DB를 함께 사용했다. 📝 1주 차 학습Supabase Storage클라우드 기반 객체 저장소로, AWS S3와 유사한 방식으로 파일을 저장하고 관리하는 서비스파일 및 이미지 업로드 및 관리 기능 제공PostgreSQL과 연동 가능권한 관리(RLS) 및 퍼블릭/프라이빗 파일 설정 가능Supabase SDK 또는 Restful API로 사용 가능  ✔ 파일명 규칙 (Supabase & AWS 공통) ASCII 문자, 숫자, 일부 특수 문자 허용 (- _ . /) 파일명을 /로 구분하여 폴더처럼 사용 가능 (folder/image.png) 공백 포함 가능하지만, URL Encoding이 필요할 수 있음 파일명에 한글, 이모지, 특수문자가 포함될 경우 정상적으로 업로드되지 않을 가능성이 있음 → URL-safe 변환 권장 React DropzoneReact에서 간편하게 파일 Drag & Drop 기능을 구현할 수 있는 라이브러리HTML5 File API를 활용하여 파일 업로드를 쉽게 구현할 수 있는 기능을 제공파일 타입, 크기, 개수 등 다양한 제약 조건 설정 가능비동기로 파일을 처리할 수 있는 onDrop 이벤트 제공 📋 1주 차 미션💬 GitHub 저장소🚀 데모 영상 보러가기미션 해결 과정 요약2주 차 미션은 Next.js, React Query, TailwindCSS를 사용하여 이미지 업로드 앱을 구현하기였다. 필수 구현 기능으로는 이미지 업로드 기능(클릭 업로드 방식과 Drag & Drop 방식, 다중 업로드)과 이미지 삭제와 이미지 검색 기능 구현하기였다. 추가 기능은 파일의 마지막 수정 시간을 화면에 출력하는 UI 구현하기였다. 여기에 과제의 완성도를 높이기 위해서 개인적인 챌린지로 파일명에 한글 또는 특수문자 포함된 파일 업로드 기능, 1MB 미만으로 이미지를 압축하는 기능, 다운로드 기능을 추가로 구현하였다. 과제 추가 구현 기능✅ 마지막 수정 시간 표시const { error: insertError } = await supabase.from(DB_TABLE_NAME).insert({ name: file.name, originalName: originalFileName, imageId: uploadedFile.id, imageUrl: publicUrl, createdAt: new Date(file.lastModified).toISOString(), })생성: DB에 파일 데이터 업로드 시 createdAt에 file.lastModified를 ISOString 형식으로 저장 if (dbData) { const { error: updateError } = await supabase .from(DB_TABLE_NAME) .update({ name: file.name, originalName: originalFileName, imageUrl: publicUrl, updatedAt: new Date().toISOString(), }) .eq('imageId', uploadedFile.id)수정: DB에 해당 ID가 존재할 경우 updatedAt(string | null)에 현재 시간을 ISOString 형식으로 저장// DropImageManager 컴포넌트에서 생성 시간, 수정 시간을 포멧팅하여 DropImage 컴포넌트에 프롭스로 전달 const localCreatedAt = getLocalTime(image.createdAt) const localUpdatedAt = image.updatedAt ? getLocalTime(image.updatedAt) : null <!-- JSX 정렬이 잘 안되서 렌더링 형태만 봐주세요! --> <div className="w-5/6 truncate"> <span className={`text-[0.7rem] font-semibold ${localUpdatedAt ? 'text-mint-800' : 'text-gray-500'}`} > {localUpdatedAt ? localUpdatedAt : localCreatedAt} </span> {localUpdatedAt && ( <span className="text-[0.7rem] font-semibold text-mint-800"> (수정)</span> )} </div>출력: updatedAt이 존재할 경우 updatedAt과 (수정) 을 함께 출력, updatedAt: null이라면 createdAt를 출력 개인 챌린지 기능✅ 파일명 자동 변환 후 이미지 업로드하는 기능을 구현 (UX 개선)파일명 검증: 정규식을 활용하여 한글 및 특수 문자 포함 여부를 확인자동 변환: 검증 후 8자리 랜덤 문자열로 안전한 파일명 생성업로드 처리: 변환된 파일명으로 File 객체 생성 후 formData.append로 원본 파일명 함께 전송서버 액션: Supabase Storage에 저장 후, 완료 시 DB에 메타데이터 저장하여 연동결론: 파일명 변환을 자동화하여 업로드 오류를 방지하고, 원본 파일명도 유지하여 검색 및 관리 UX 개선✅ 파일 용량이 1MB 초과 시 자동 압축 후 업로드하는 기능을 구현 (UX 개선)browser-image-compression 라이브러리를 사용하여 파일의 용량 검증 후 1MB 초과 시 이미지 압축 후 업로드결론: 이미지 최적화로 업로드 속도 향상, 스토리지 비용 절감 효과✅ Blob URL을 활용한 다운로드 기능 추가 (UX 개선)Blob URL 생성: 업로드된 이미지를 fetch()로 가져와 Blob으로 변환다운로드 기능 구현: window.URL.createObjectURL(blob)으로 브라우저에서 직접 다운로드 가능하도록 처리결론: Blob URL 다운로드 방식을 적용하여 최적화된 이미지를 빠르게 다운로드 받을 수 있도록 개선🚧 기능 구현 시 어려웠던 부분Supabase Storage에 전달하는 File 객체 커스텀 불가원본 파일명을 추가하려 했으나, File 객체 자체를 수정하는 것이 제한적이다.파일 객체를 복사하여 원본 파일명을 추가하는 방법 시도전개 연산자를 사용하여 객체 복사 후 원본 파일명을 추가하려 시도하였으나 file 객체는 일반적인 방법으로는 복사할 수 없는 특별한 객체이다.ExtendedFile 확장 클래스로 인스턴스를 생성했으나 서버에 전달되지 않는 문제 발생확장된 ExtendedFile 객체를 formData에 담아 서버로 전송했지만, 서버에 정상적으로 전달되지 않았다.최종 해결 방법formData.append("file", file) formData.append("originalFileName, file.name)file 객체와 원본 파일명을 함께 서버로 전송 후 가공하여 Supabase Storage의 파일명에는 안전한 파일명만 저장하고 DB에 스토리지Id, 원본 파일명, 안전한 파일명, 이미지URL 등 정보를 저장했다. 🧾 ERD 다이어그램👀 1주 차 회고아직 갈 길이 멀지만, 리팩토링을 통해 Next.js의 장점을 살릴 수 있는 구조로 점점 개선되어가는 과정을 경험하면서 이번 주 역시 알차게 보냈다고 생각한다.이번 주는 특히 MVP 패턴과 비슷한 형태로 컴포넌트 구조를 잡는 것에 익숙해지는 것을 개인적인 목표로 삼았다. 처음부터 MVP 패턴을 염두해 두고 설계한 것은 아니었지만, 진행하다보니 자연스럽게 MVP와 유사한 패턴으로 정리되어 가는 것을 느꼈다.화면 렌더링 시 상호작용이 필요하지 않은 정적인 요소들까지 클라이언트 컴포넌트로 관리하면 불필요한 하이드레이션 부담이 증가할 수 있다는 점을 다시 한번 체감했다. 클라이언트 컴포넌트 내에서도 역할을 나눠 서비스 레이어나 상태 관리만 담당하는 매니져 컴포넌트와 프롭스로 상태를 전달받아 단순히 화면을 렌더링을 담당하는 UI 컴포넌트로 분리하는 연습을 진행했다.이러한 구조로 개선하면서 클라이언트 컴포넌트의 부담을 줄이고, 유지보수성을 높이는 방향으로 점차 최적화되고 있다는 점이 느껴졌다. 아직 개선해야 할 부분이 많지만 점진적으로 개선하여 더 나은 아키텍쳐를 만들어가는 과정이 의미 있었다고 생각한다.

풀스택워밍업클럽3기발자국회고과제미션

치현

[인프런 워밍업 스터디 클럽 3기 풀스택] 2주차 발자국

학습 내용인프런 워밍업 클럽 스터디 2주차로, 이번 주는 드롭 박스 프로젝트와 함께 Supabase의 Storage를 다뤄볼 수 있는 시간이었다. Supbase Storage1. 기본 구성 요소Files: 모든 종류의 미디어 파일 저장 가능 (이미지, GIF, 비디오 등)Folders: 파일을 체계적으로 구성하기 위한 디렉토리 구조Buckets: 파일과 폴더를 담는 최상위 컨테이너 (접근 규칙별로 구분)2. 접근 제어 모델Private Buckets (기본값) RLS(Row Level Security) 정책을 통한 접근 제어JWT 인증 필요Signed URL을 통한 임시 접근 가능Public Buckets파일 조회 시 접근 제어 없음URL만 있으면 누구나 접근 가능업로드/삭제 등 다른 작업은 여전히 접근 제어 적용3. 보안 기능RLS 정책 설정 가능SELECT (다운로드)INSERT (업로드)UPDATE (수정)DELETE (삭제)소유권 관리owner_id 필드로 리소스 소유자 추적JWT의 sub claim 기반 소유권 할당4. 이미지 변환 기능 (Pro Plan 이상)실시간 이미지 최적화크기 조정품질 조정 (20-100)WebP 자동 최적화변환 옵션resize 모드: cover, contain, fillwidth/height 지정 (1-2500px)최대 파일 크기: 25MB최대 해상도: 50MP5. 인증 방식S3 액세스 키서버 사이드 전용모든 버킷에 대한 완전한 접근 권한세션 토큰클라이언트 사이드 사용 가능RLS 정책 기반 제한된 접근6. 통합 기능Next.js 이미지 로더 지원AWS S3 호환성PostgreSQL DB와 연동7. 제한사항파일명은 AWS S3 명명 규칙 준수 필요HTML 파일은 보안상 plain text로 반환이미지 변환 기능은 Pro Plan 이상에서만 사용 가능미션 2 구현 내용과제 구현 저장소Dropbox 중파일의 마지막 수정(업로드) 시간을 표시하는 기능 추가 하기 export interface FileObject { name: string bucket_id: string owner: string id: string updated_at: string created_at: string last_accessed_at: string metadata: Record<string, any> buckets: Bucket }=> DropboxImage 컴포넌트가 prop으로 받는 image의 타입은 FileObject로 그 중 업로드시간은 created_at을 의미하기에 이를 이미지에 추가하였다.(사진 참고) 포인트 1: 한글 파일명 es-hangul 사용// 안전한 파일명 생성을 위한 유틸리티 export class FileNameConverter { // 안전한 문자 패턴 정의 private static readonly SAFE_CHARACTERS = /^[a-zA-Z0-9!\-_.*'()]+$/; private static generateRandomString(length: number = 8): string { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; return Array.from({ length }, () => chars.charAt(Math.floor(Math.random() * chars.length)) ).join(""); } // 파일명이 안전한 문자들로만 구성되었는지 확인 private static isSafeFileName(name: string): boolean { return this.SAFE_CHARACTERS.test(name); } // 안전하지 않은 문자를 포함한 파일명을 안전한 형식으로 변환 private static convertToSafeFileName(name: string): string { try { // 파일명 정규화 const normalized = name.trim().normalize(); // 한글이나 특수문자가 있는지 확인 const hasKorean = /[ㄱ-ㅎㅏ-ㅣ가-힣]/.test(normalized); const hasSpecialChars = /[^A-Za-z0-9]/.test(normalized); if (!hasKorean && !hasSpecialChars) { return normalized; } // 한글이 있는 경우 로마자로 변환 시도 if (hasKorean) { const romanized = romanize(normalized); if (romanized && romanized !== normalized) { // 로마자 변환 결과에서 안전하지 않은 문자 제거 return romanized.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase(); } } // 변환 실패 시 랜덤 문자열 생성 return this.generateRandomString(); } catch (error) { console.error("Conversion error:", error); return this.generateRandomString(); } } // 원본 파일명을 안전한 형식으로 변환 static encode(fileName: string): string { console.log("Original filename:", fileName); const extension = fileName.split(".").pop() || ""; const nameWithoutExt = fileName.slice(0, fileName.lastIndexOf(".")); const safeName = this.isSafeFileName(nameWithoutExt) ? nameWithoutExt : this.convertToSafeFileName(nameWithoutExt); console.log("Safe filename:", safeName); return `${safeName}_${Date.now()}.${extension}`; } // 파일명에서 타임스탬프 제거하여 원본 이름 추출 static decode(fileName: string): string { const [name] = fileName.split("_"); return name || fileName; } }포인트 2 : 업로드 날짜 표시export function formatDate(timestamp: string): string { const date = new Date(timestamp); const now = new Date(); const diff = now.getTime() - date.getTime(); // 1일 이내 if (diff < 24 * 60 * 60 * 1000) { const hours = Math.floor(diff / (60 * 60 * 1000)); if (hours < 1) { const minutes = Math.floor(diff / (60 * 1000)); return `${minutes}분 전`; } return `${hours}시간 전`; } // 30일 이내 if (diff < 30 * 24 * 60 * 60 * 1000) { const days = Math.floor(diff / (24 * 60 * 60 * 1000)); return `${days}일 전`; } // 그 외 return date.toLocaleDateString("ko-KR", { year: "numeric", month: "long", day: "numeric", }); } 회고파일명 변환하는데 생각보다 시간이 많이 소요됐다.여찌저찌 구현은 헀지만, 이미지가 어떻게 encoding되고 decoding되는지 일련의 과정에 대한 공부가 필요함을 느끼는 이번주 였다.  

풀스택풀스택인프런워밍업스터디클럽Next3기SupabaseReact프론트엔드2주차발자국

디렉투스바스키

얼어붙은 채용시장에 대한 우리의 파훼법

얼어붙은 채용시장에 대한 우리의 파훼법파멸적인 개발자 채용 시장의 현재를 걱정하고 우려하시는 분들이 많네요.그런 와중에, 그럼에도, 아직 꾸준히 성장중인 시장은 분명히 존재합니다.(Calyptus 채용시장 보고서 요약 바로가기) 블록체인-웹3 개발자, 당신의 가치는 계속 상승 중! 🚀💰블록체인-웹3 시장은 간간히 주춤하다가도, 분명히 지속적으로 성장하고 있습니다.반면에 블록체인 관련 역량을 갖춘 개발자는 여전히 부족합니다.전세계의 블록체인 재단, 기업은 끝임없이 개발자 부족을 말하고 있죠.따라서 블록체인 개발자의 가치는 그만큼 상승하고 있습니다.글로벌 스마트 컨트랙트 개발자의 평균 연봉은 $150,269 입니다.통계상 국내 일반 개발자 평균의 약 2배 입니다.물론 통계의 헛점이 있을 수 있지만 평균적으로 높은 건 사실이죠. 블록체인-웹3 기업, 전 세계에서 경계 없는 협업! 🌍💡디지털 노마드 성향이 짖은 블록체인-웹3 업계는 주로 원격(재택)근무를 선호합니다.아무래도 부족한 개발자 인력을 채우기 위해 글로벌한 협업이 지향되는 편이고프로젝트 위주의 조직화가 빈번하다보니 그런면도 있죠.그래서 대부분의 블록체인-웹3 기업(82.5%)이 원격 근무를 선호합니다.글로벌 시장에 진입하면서도 굳이 거주지의 제약을 받지 않는다는 장점이 있습니다. 어떤 방향이든 세계는 새로운 시대로 나아가고 있습니다.그리고 분명하게 전방위적으로 디지털화가 진행되고있죠.블록체인-웹3 분야에 관심을 가져보시는 것도 나쁘지 않은 선택일 것 같습니다.(Calyptus 채용시장 보고서 요약 바로가기)

풀스택블록체인웹3

hee j

[인프런 워밍업 클럽 3기 풀스택 ] 1주차 발자국

목차Next.js, TailwindCss, Recoil, supabase 특징Todo List 미션  Next.jsReact 기반의 풀스택 프레임워크SSR(서버사이드 렌더링) 지원서버에서 미리 HTML을 렌더링하여 SEO와 초기 로딩 속도 개선fetch 등의 API 요청을 서버에서 처리해 클라이언트의 부담 감소SSG(정적 사이트 생성) 지원빌드 시 HTML을 미리 생성하여 빠르게 페이지 로딩파일 기반 라우팅pages/ 폴더 내 파일이 자동으로 라우트 됨app/ 디렉토리에서는 레이아웃 공유 & 동적 라우팅 가능서버 컴포넌트 & 클라이언트 컴포넌트 지원 API Routes 제공백엔드 서버 없이 NextJS 내에서 API 구축 가능이미지 최적화<Image /> 컴포넌트를 사용하면 자동으로 이미지 크기 조절 & 포맷 변환웹페이지 속도 향상SEO 최적화Middleware 지원요청이 처리되기 전에 인증, 리디렉션, 캐싱 등 제어 가능   TailwindCss유틸리티 퍼스트 방식의 CSS 프레임워크미리 정의된 클래스를 조합하여 빠르게 스타일 적용하기 때문에 CSS를 직접 작성할 필요 없음 => 개발 속도 향상 RecoilFacebook에서 개발한 React 전역 상태 관리 라이브러리React의 Context API보다 강력하고, Redux보다 간단하게 사용할 수 있음간단한 전역 상태 관리useState 처럼 쉽게 사용할 수 있음Atom을 이용해 상태를 관리하고 여러 컴포넌트에서 공유 가능 비동기 상태 관리 지원Selector를 이용하면 useEffect 없이도 비동기 데이터 관리 가능Redux보다 가볍고 React의 상태 관리 방식과 유사해 학습 부담이 적음   supabase오픈 소스 백엔스 서비스로 Firebase의 대체제로 사용됨PostgreSQL 기반의 데이터베이스RDBMS 기능 제공JSONB 데이터 타입 지원인증 권한 관리이메일, OAuth(Google, GitHub 등), Magic Link 로그인 지원 Row-Level Security(RLS) 를 통해 사용자별 데이터 접근 제한 가능스토리지 제공이미지, 파일 업로드 가능접근 권한을 설정해 보안 유지서버리스 함수(Edge Functions) 지원서버리스 API 생성 가능TypeScript와 호환Firebase 보다 쉬운 SQL 기반 데이터 관리  1주차 미션 - TODO List 제작 미션Next.js + Supabase 기반의 TODO List 제작생성 날짜와 수정 날짜 표시하기 TODO list 만들기생성 날짜와 수정 날짜 표시하기날짜를 "yyyy-MM-dd HH:mm:ss" 형태로 보여주기 위해 date-fns 설치 npm install date-fnscreated_at과 updated_at 값을 각각 상태로 관리하고,updated_at 값이 있다면 "수정됨:"이라는 텍스트와 함께 표시하고, 없다면 created_at 날짜만 표시// todo.tsx export default function Todo({ todo }) { const [created_at] = useState(todo.created_at); const [updated_at] = useState(todo.updated_at); const formatDate = (dateString: string) => { return format(new Date(dateString), "yyyy-MM-dd HH:mm:ss"); }; return <span className="text-sm text-gray-500"> {updated_at ? "수정됨: " + formatDate(updated_at) : formatDate(created_at)} </span>   

풀스택풀스택next.jsreactsupabase워밍업발자국

강신욱

스터디 1주차

강의 주요 내용  서버 액션: 비동기 함수서버에서 실행되는 함수Next.js는 서버에서 실행되는 서버 컴포넌트와 브라우저에서 실행되는 클라이언트 컴포넌트를 구분하여 동작한다.파일 최상단에 'use server'가 선언되어 있으면 서버 컴포넌트이다.'use client'가 선언되어 있으면 클라이언트 컴포넌트이다.서버 컴포넌트 내에서 정의한 함수를 클라이언트에서 직접 호출할 수 있다. 즉, 별도의 API를 구현하지 않아도 클라이언트에서 서버 함수를 바로 사용할 수 있어 개발이 더욱 간편해진다.Recoil: 전역 상태 관리 라이브러리Recoil이란?Recoil은 클라이언트 상태 관리 라이브러리로, 여러 컴포넌트에서 동일한 상태를 공유할 수 있도록 도와준다. 예를 들어:뷰어 상태 관리헤더 검색창 상태 관리이처럼 다양한 상태를 효율적으로 관리할 수 있으며, Redux나 MobX보다 간편하게 사용할 수 있다.Recoil의 핵심 개념Recoil은 Atom과 Selector라는 두 가지 핵심 개념으로 구성된다.1. Atom전역 상태를 정의하는 가장 기본적인 단위여러 컴포넌트에서 공유 가능2. SelectorAtom 상태에서 파생된 값을 생성할 때 사용특정한 값만 변형하여 제공 가능예: 텍스트 상태에서 문자열 길이만 가져오는 Selector 정의Selector를 활용하면 불필요한 연산을 줄이고 최적화된 방식으로 상태를 관리할 수 있다.React Query: 클라이언트 상태 관리 및 데이터 동기화React Query란?React Query는 TanStack에서 개발한 서버 상태 관리 라이브러리로,서버에서 데이터를 가져오거나데이터를 변경하는 요청을 보낼 때 사용된다.React Query의 역할클라이언트에서 데이터 가져오기(Fetching)가져온 데이터 캐싱(Caching)서버 데이터 변경 시 동기화(Syncing)즉, 서버와 클라이언트 간의 데이터 흐름을 효율적으로 관리할 수 있도록 도와준다.React Query의 주요 장점✅ 자동 캐싱 (Auto Caching)✅ 서버 데이터와 클라이언트 데이터 분리 (Separation of Server & Client State)Supabase 주요 기능1. Table EditorTable Editor는 데이터베이스에서 직접 테이블을 생성 및 수정할 수 있는 기능이다.2. SQL EditorSQL Editor를 통해SELECT, UPDATE, DELETE 등의 SQL 쿼리를 실행할 수 있다.Assistant 기능 지원: AI가 DB와 연동되어 SQL을 자동 생성해준다.예: "이 테이블에서 특정 컬럼을 가져오고 싶어!"라고 입력하면, 적절한 SQL 쿼리를 자동 생성해준다.SQL이 익숙하지 않은 사용자에게 유용한 무료 기능이다.3. 데이터베이스 관리생성된 테이블 조회데이터베이스 함수 및 트리거 설정예: 특정 row가 생성, 수정, 삭제될 때 실행되는 트리거 설정 가능액세스 컨트롤(Role-based Access Control)특정 유저가 특정 테이블에 접근할 수 있도록 권한을 설정할 수 있다.(※ 액세스 컨트롤 관련 내용은 본 강좌 범위에서 제외)미션 수행이번 미션에서는 실습에서 구현한 TODO 리스트에 생성 날짜를 표시하는 기능을 추가하는 것이 기본 목표였다.처음 도전해보는 분야였던 만큼 시행착오가 많았고, 라이브러리 버전 호환 문제나 기타 충돌로 인해 많은 구글링이 필요했다.관련해서 강사님이 추천해주신 강의를 다시 한 번 정독한 뒤, 선택 과제도 수행해볼 예정이다.이번 실습을 통해 Next.js, Recoil, React Query, 그리고 Supabase를 더욱 깊이 있게 이해할 수 있었으며, 앞으로의 프로젝트에서도 적극적으로 활용해볼 계획이다.

풀스택

희주

[인프런 워밍업 클럽 3기 풀스택] 1주차 발자국

학습 내용1주차에는 주로 사용할 기술들에 대한 소개와 기본 문법을 배우고 이를 이용해 TODO List를 만들었다.Firebase 이후 등장한 Supabase에 대해서 배웠다.오픈소스 프로젝트여서 자체 서버구축이 가능하고 특히 개인/소규모 풀스택 개발에 필요한 것이 대부분 갖춰져 있다는 장점이 있어 앞으로 진행할 프로젝트에 적절한 서비스임을 느꼈다. 부족한 문서 한글화가 단점이라고 하셨는데 강의에서 설정 방법을 자세히 알려주셔서 초보인 나도 쉽게 적용시킬 수 있었다.Next.js는 풀스택 개발에 최적화된 프레임워크로, SSR을 지원하고 별도 서버 구축 없이도 API 구축이 가능하다.이외에도 TailwindCSS(+MaterialUI), Recoil, React Query(TanStack Query)에 대해 배우고 사용해보았다.기존에 GraphQL을 써봤는데, 이번 기회에 궁금했던 React Query의 장점과 기본 문법을 알게 되어서 좋았다. 사용법 자체는 비슷하기도 해서 금방 적응할 수 있을 것 같았다.나는 프론트엔드만 더듬더듬 배운 적이 있고 제공되는 백엔드 API를 사용하기만 했었는데(만드는 걸 아주 간단하게 배운 적 있는데 엄청 어려웠다) Server Action 함수 만들고 호출하기만 해도 백엔드 없이 직접 DB를 조작할 수 있다는 게 신기했다.  미션생성된 TODO의 생성 시간을 저장하고 이를 표시하는 기능을 추가하세요.TODO 항목 옆에 생성 시간을 표시하기날짜 포맷 함수 만들기// utils/formatDate.ts export const formatDate = (dateString?: string): string => { if (!dateString) return ""; const date = new Date(dateString); const yyyy = date.getFullYear(); const mm = String(date.getMonth() + 1).padStart(2, "0"); const dd = String(date.getDate()).padStart(2, "0"); const hh = String(date.getHours()).padStart(2, "0"); const min = String(date.getMinutes()).padStart(2, "0"); const ss = String(date.getSeconds()).padStart(2, "0"); return `${yyyy}-${mm}-${dd} ${hh}:${min}:${ss}`; };생성 시각은 이미 supabase에 있으므로 created_at 필드 값을 가져와 사용나중에 완료 시각이 추가되어도 생성 시간은 위치가 바뀌지 않게 위쪽에 배치<> <p className={`flex-1 ${completed && "line-through"}`}>{title}</p> <div className="grid grid-rows-2 items-end pr-1"> <p className="text-xs text-right text-gray-500"> <i className="fas fa-pen pr-1" /> {formatDate(todo.created_at)} </p> <p className="text-xs text-right text-gray-500"></p> </div> </>completed_at 필드를 추가하여 완료한 시간도 함께 저장하기created_at과 같이 supabase에 completed_at 필드부터 추가(Allow Nullable 체크)이후 npm run generate-types 실행해 타입 파일도 수정todo가 체크되어 completed가 true일 때는, completed_at에 완료 시각을 저장하도록 updateTodo Server Action을 수정export async function updateTodo(todo: TodoRowUpdate) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("todo") .update({ ...todo, updated_at: new Date().toISOString(), completed_at: todo.completed ? new Date().toISOString() : null, }) .eq("id", todo.id); if (error) { handleError(error); } return data; }마지막으로 completed_at도 포맷팅하여 완료 시각을 표시하고, 조건부 렌더링을 적용해 완료 시각이 없을 때는 아이콘과 시각 모두 표시하지 않도록 작성<> <p className={`flex-1 ${completed && "line-through"}`}>{title}</p> <div className="grid grid-rows-2 items-end pr-1"> <p className="text-xs text-right text-gray-500"> <i className="fas fa-pen pr-1" /> {formatDate(todo.created_at)} </p> <p className="text-xs text-right text-gray-500"> {todo.completed_at ? ( <> <i className="fas fa-check mr-2" /> {formatDate(todo.completed_at)} </> ) : null} </p> </div> </>마무리이번 주 학습에서는 스스로 부족한 점을 많이 느꼈던 것 같다... 처음 배운 것, 지금까지 잘 몰랐던 것도 많았고 버전 문제 등 사소한 이슈를 많이 겪어서 시간이 꽤 들었다.그래도 생각보다 간단하게 Todo List가 만들어지는 걸 보면서, 완강할 때쯤 되면 나만의 프로젝트도 만들 수 있겠다는 자신감이 생기고 있다🙂 아직 너무나 익숙하지 않지만 클론하다 보면 조금씩 감이 잡힐 것 같다.미션을 진행하면서 발전시킬 수 있는 부분도 많을 것 같은데 이번 주에는 시간을 거의 내지 못해 아쉬웠다. 다음 주에는 더 열심히 따라가고 싶다!

풀스택풀스택웹개발Next.jsSupabase

Yang HyeonBin

[인프런 워밍업 클럽 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 <div>Username: {data}</div> }+궁금해진 것 - 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 <anonymous> (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<HTMLInputElement>) => { const newCompleted = e.target.checked; // 정확한 값 업데이트를 위해 setCompleted(newCompleted); // promise를 반환하지 않아 await해도 작업 완료까지 기다리지 않음 handleUpdate({ newCompleted }); }}일을 하며 알아낸 방법들과 강의에서 소개해준 방법들을 조합하여 간단한 프로젝트를 진행하는 거라 재미있었다. 초기 Material Tailwind 세팅에서 헤맸던 순간은 좀 고통스러웠지만.. 추가 미션인 completed_at을 저장하는 것은 위에 첨부한 코드 스니펫에서 보이는데, 서버에 저장할 completed의 값이 true이면 그 시간 정보를, false이면 null을 저장하도록 했다.투두 추가한 시간 보여주는 것도 간단하게 세로로 배치했다.<div className="flex-1"> <p className={`text-gray-800 ${ completed ? "line-through" : "" }`}> {title} </p> {todo.created_at && ( <span className="text-xs text-gray-400"> <i className="fas fa-clock mr-1" /> {new Date(todo.created_at).toLocaleString()} </span> )} </div>사실 우리 서비스가 Todo를 다루는 거라 굉장히 익숙한 주제여서 더 재밌게 했던 것 같다. 다음 주부터는 모르는 개념이 더 많을텐데, 화이팅해서 완주하고 많이 배워가고 싶다. 화이팅.

풀스택워밍업클럽ReactQueryNext.jssupabase

이혜린

[워밍업 클럽 3기] 풀스택 과정 1주차 미션 회고 기록 - TODO LIST 구현

학습목표NextJs 와 Supabase를 이용한 풀스택 개발CRUD 구현 📝 Next.js서버 사이드 랜더링 지원리엑트와 비슷한 문법을 사용서버 사이드 랜더링이기 떄문에 SEO(검색엔진) 최적화HTTP API 구축 가능 → 간단한 개인 프로젝트는 서버를 구축할 필요가 없음폴더명이 곧 Route(URL)이 됨. → 즉 정확한 폴더명을 정의해줄 필요가 있음 📝 Supabasefirebase 에 대응해 나온 백엔드 서비스 플랫폼장점오픈소스 프로젝트 ( 자체 서버 구축 가능)PostgreSQL 기반 ( 관계형 DB장점을 살릴 수 있다)파이어베이스 대비해 저렴다양한 연동방식을 지원( GraphQL, API, SDK, DB Connection )단점아직 성숙하지 않은 커뮤니티 기반비교적 적은 기능들, 적은 서비스 연동 지원부족한 문서화, 한글 문서 부족파이어베이스 보다 높은 러닝커브 📝TailwindCSSutility-First 컨셉을 가진 CSS 프레임 워크부트스트랩,Vuetify 등 과 같은 미리 세팅된 유틸리티 클래스를 활용하는 방식빠르게 원하는 디자인을 개발 가능하지만 코드의 복잡성으로 인해 유지보수가 어려워지는 단점 📝Recoil react 를 위한 상태 관리를 하는 라이브러리 📝Atomredux에 store와 같은 개념데이터 상태의 단위 📝Seletoratom을 기반으로 파생된 데이터를 만들어냄1주차 미션 - TODO List추가 사항업데이트 시 변경 내용이 기존 내용과 같을 경우, 즉 변경 사항이 없을 경우 업데이트가 일어나지 않게 코드를 변경하였습니다.첫번째 회고예전 프로젝트에서 React (Recoil, React Query) 와 Firebase 를 사용한 프로젝트를 개발해 본 경험이 있지만, 당시 무작정 만들기만 했고 왜 이렇게 사용해야 되는지에 대해, 또는 어떤 역할을 하는 기능인지에 대해 생각하지 않았습니다.이 강의와 스터디를 통해 React, Recoil, React Query 에 대해 조금 더 자세히 알 수 있었습니다.또한 NextJs와 Supabase, TailwindCss 에 대해 처음 배웠습니다.써보고 싶었던 기술을 배우고 사용해 본 경험이 생겨서 좋은 공부가 되었습니다.

인프런풀스택인프런워밍업클럽발자국

찬우 이

인프런 워밍업 클럽 3기 풀스택 - 1주차 발자국

1주차 학습 내용Firebase vs SupabaseFirebase 특징BaaS: 서버 없이도 빠르게 앱 출시 가능커뮤니티, 문서화 잘되어 있음, 단순한 NoSQL 기반으로 돌아감Supabse보다 비쌈, 오픈소스 아님NoSQL기반이라 복잡한 쿼리 불가하고, 웹 개발엔 그닥,, Supabase 특징오픈소스 구성, PostgreSQL 기반다양한 연동 방식을 지원함커뮤니티, 문서화 부족비교적 적은 기능 Server Action서버에서 실행되는 비동기 함수API 호출 없이 서버에서 바로 데이터 변환 메타데이터SEO를 정의하는 방식Static과 Dynamic 방식이 있는데 추 후 더 배울 예정Tailwindcss<div class="bg-blue-500 text-white p-4 rounded-lg shadow-lg"> 테일윈드 CSS 예제 </div>유틸리티 퍼스트(Utility-First) 방식의 CSS 프레임워크로, 미리 정의된 클래스를 조합하여 빠르게 스타일을 적용할 수 있도록 도와준다.CSS를 직접 작성할 필요 없이 클래스만 조합하여 스타일링 가능 RecoilReact에서 상태 관리를 쉽게 할 수 있도록 도와주는 상태 관리 라이브러리Redux 같은 라이브러리보다 가볍고 사용법이 간단함전역 상태뿐만 아니라 컴포넌트 간의 상태 공유를 효율적으로 관리할 수 있음비동기 상태 관리도 지원하여 서버 데이터를 다룰 때도 유용함  React Query React Query는 비동기 데이터(fetching, caching, synchronization)를 효율적으로 관리하는 라이브러리다.서버 상태 관리에 특화되어 있으며, API 호출 후 데이터를 캐싱하고 자동으로 최신 상태를 유지할 수 있도록 도와준다.React에서 API 요청을 효율적으로 관리하려면 필수적인 라이브러리다.1주차 미션1주차 미션에서는 강의에서 배운 TODO 앱을 기반으로,새로 생성한 TODO는 생성된 시간(created_at)을 표시하고,수정한 TODO는 수정된 시간(updated_at)을 즉시 UI에 반영하도록 구현하는 것이 목표였다. // ui.tsx const createTodoMutation = useMutation({ mutationFn: () => createTodo({ title: "New TODO", completed: false, created_at: new Date().toISOString(), }), onSuccess: () => { todosQuery.refetch(); }, });✅ useMutation을 통해 새로운 TODO를 생성할 때, created_at을 추가하여 서버에 요청하도록 구현했다.✅ 이를 통해 TODO가 생성된 시간을 함께 저장할 수 있도록 설정했다. // todo.tsx const [updatedTime, setUpdatedTime] = useState(todo.updated_at); const updateTodoMutation = useMutation({ mutationFn: () => updateTodo({ id: todo.id, title, completed, updated_at: updatedTime, }), onSuccess: () => { setIsEditing(false); queryClient.invalidateQueries({ queryKey: ["todos"], }); }, }); ---------------------- <> <p className={`flex-1 ${completed && "line-through"}`}>{title}</p> <p> {new Date(todo.updated_at ?? todo.created_at).toLocaleString( "ko-KR", { year: "numeric", month: "2-digit", day: "2-digit", hour: "numeric", minute: "2-digit", hour12: true, } )} </p> </>✅ 수정할 때마다 updated_at을 추가하여 최신화된 시간을 서버로 전송하도록 설정했다.✅ 수정된 TODO(updated_at)가 있다면 수정 시간을, 그렇지 않다면 생성 시간(created_at)을 표시하도록 구현했다.✅ 기본적으로 ?? 연산자를 사용해 updated_at이 존재하는 경우 이를 우선적으로 표시하도록 처리했다.✅ 시간 포맷이 "2025.03.09 오후 7:34" 형식으로 나타나도록 toLocaleString()을 활용해 변환했다.  1주차 회고 내가 이 강의를 수강한 이유는 배우고 싶었던 Recoil, React Query, Supabase를 익히기 위해서였다.하지만 생각보다 많이 어려웠다. 역시 한 번 본다고 해서 쉽게 익힐 수 있는 건 아니라고 느꼈다.섹션 2까지는 본격적인 실습을 하기 전에 준비 단계라고 생각했다.그리고 섹션 3부터는 본격적으로 TODO 프로젝트를 만들면서 학습을 진행했다.Supabase를 사용하면서 백엔드의 기본적인 동작 방식을 조금이나마 이해할 수 있었다.신기하기도 했지만, 한편으로는 어렵기도 했다.미션 자체는 그렇게 어렵다고 느껴지지는 않았다.결국 등록 시간과 수정 시간을 표현하는 것이 핵심이었기 때문이다.하지만 강의에서 React Query나 Supabase를 다루는 부분은 이해가 잘되지 않아 여러 번 반복해서 학습해야 할 것 같다.어려워서 살짝 우울하기도 하지만...결국 어려운 만큼 성장할 수 있는 부분이 많다는 뜻이니까! 😂🔥

풀스택풀스택미션인프런워밍업클럽supabasenext.js

thagyun

[인프런 워밍업 클럽 3기 풀스택] 1주차 발자국

 [풀스택 완성] Supabase로 웹사이트 3개 클론하기 (Next.js 14) - 로펀 1주차는 Supabase, Next.js, Tailwindcss, Recoil, React Query 소개와 기본 문법, Server Action CRUD 사용법을 배웠다.Supabase 특징오픈 소스 프로젝트 (자체 서버구축 가능)PostgresSQL 기반 (복잡한 요구사항 개발 가능)Firebase 대비 저렴다양한 연동방식 지원 (+SDK, DB Connection, GraphQL, API) Next.jsNext.js의 폴더명이 곧 Route와 같다.page.tsx는 각 폴더의 대표 파일과 같다.app/layout.tsx는 화면 레이아웃을 잡아주는 Next.js 서버 컴포넌트이므로, 클라이언트에서만 동작하는 상태 관리 라이브러리(Zustand, Recoil, React Query 등)를 직접 사용할 수 없다.따라서 React Query의 Client Provider를 별도의 클라이언트 컴포넌트로 만들어 layout.tsx에 주입해야 한다.(서버 컴포넌트는 클라이언트 상태나 브라우저 API에 접근할 수 없음)서버 액션은 API 라우트 없이 서버에서 직접 실행 가능하다. React Query서버에서 가져온 데이터를 캐싱, 동기화, 상태 관리까지 자동으로 해주는 라이브러리useQuery(): 데이터 조회 (GET 요청)const todosQuery = useQuery({ queryKey: ["todos"], // 캐싱 키 queryFn: fetchTodos, // API 요청 함수 }); const { data, isLoading, error } = todosQuery; queryKey: 데이터 캐싱 키queryFn: 데이터 호출 함수data: fetchTodos의 반환 값isLoading: 데이터 로딩 여부 (true면 로딩 중)error: 요청 실패 시 에러 객체 useMutation(): 데이터 변경 (POST, PUT, DELETE)const mutation = useMutation({ mutationFn: addTodo, onSuccess: () => { console.log("성공!"); }, onError: (error) => { console.error("에러 발생:", error); }, }); mutationFn: API 호출 함수onSuccess: 성공 시 실행할 함수onError: 실패 시 실행할 함수 TODO LIST 만들기Supabase todo 테이블 정의Supabase Next.js 연결.env 환경변수 설정Supabase Project API Keys를 사용하여 환경변수 추가package.json 수정scripts에서 generate-types 옵션 수정Supabase 로그인 후 타입 생성generate-types 실행하여 데이터베이스 테이블의 타입 정의 가져오기 Next.js에서 Supabase 클라이언트 생성브라우저/서버 환경에서 사용할 Supabase 클라이언트 생성Next.js의 모든 request에서 Supabase 인증 토큰을 갱신하도록 middleware 설정 TODO 조회 - server actionexport async function getTodos({ searchInput = "" }): Promise<TodoRow[]> { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("todo") .select("*") .like("title", `%${searchInput}%`) .order("created_at", { ascending: true }); if (error) { handleError(error); } return data; }getTodos 함수는 searchInput을 기본값 ""로 받아 Promise<TodoRow[]> 타입을 반환한다.이 함수는 서버 환경에서 실행되며, Supabase 클라이언트를 사용하여 todo 테이블에서 데이터를 조회한다.title 컬럼에서 searchInput을 포함한 값을 검색하며created_at을 기준으로 오름차순으로 정렬하여 반환한다.검색된 데이터가 없거나 오류가 발생하면 handleError 함수가 호출된다. TODO 조회 - React Query useQuery()const todosQuery = useQuery({ queryKey: ["todos", searchInput], queryFn: () => getTodos({ searchInput }), }); return ( // ... {todosQuery.data && todosQuery.data.map((todo) => <Todo key={todo.id} todo={todo} />)} // ... ) useQuery는 todosQuery라는 데이터를 서버에서 가져오는 비동기 작업을 관리한다.queryKey는 ["todos", searchInput]으로 설정되어, searchInput에 따라 조회된 결과가 달라지도록 한다.queryFn은 서버 액션 getTodos를 호출해, searchInput에 맞는 TODO 데이터를 반환한다.반환된 데이터(todosQuery.data)가 있을 경우, map()을 사용하여 각 TODO 항목을 렌더링한다. 1주차 미션github: https://github.com/thayoon/nextjs-supabase-todolist생성된 TODO의 생성 시간을 저장하고 이를 표시하는 기능을 추가하세요.TODO 항목 옆에 생성 시간을 표시하기component/todo.tsxexport default function Todo({ todo }) { // 한국 시간, YYYY/MM/DD HH:MM 포맷 함수 const getTime = (time: string): string => new Date(time) .toLocaleString("en-US", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "Asia/Seoul", }) .replace(",", ""); const createdTime = getTime(todo.created_at); return( // ... {isEditing ? ( <input className="flex-1 border-b-black border-b pb-1" value={title} onChange={(e) => setTitle(e.target.value)} /> ) : ( <div className="flex-1"> <p className={`${completed && "line-through"}`}>{title}</p> <span className="text-gray-400 text-xs">⏱️{createdTime}</span> </div> )} // ... ); } getTime 함수는 created_at 값을 한국 시간에 맞게 YYYY/MM/DD HH:MM 포맷으로 변환한다.isEditing이 false일 때, 생성된 TODO 항목의 생성 시간을 createdTime을 통해 표시한다.todo 매개변수는 useQuery로 가져온 데이터 객체이며, created_at 값을 이용해 생성 시간을 표시한다. (선택 사항) completed_at 필드를 추가하여 완료한 시간도 함께 저장Supabase에서 completed_at 필드를 추가하여, TODO 완료 시각을 저장한다.completed_at은 NULL을 허용하는 timestamptz 타입 칼럼이다.actions/todo-actions.ts (server action)updateTodo 함수는 completed 상태에 따라 completed_at 값을 업데이트한다.export async function updateTodo(todo: TodoRowUpdate) { const supabase = await createServerSupabaseClient(); console.log(todo); const { data, error } = await supabase .from("todo") .update({ ...todo, updated_at: new Date().toISOString(), completed_at: todo.completed ? new Date().toISOString() : null, }) .eq("id", todo.id); if (error) { handleError(error); } return data; } completed가 true일 경우 completed_at에 현재 시각을 저장하고, false일 경우 null을 저장한다.3. components/todo.tsx (React Query useMutation())completed_at 값을 확인하여 완료 시각을 표시한다.export default function Todo({ todo }) { // 한국 시간, YYYY/MM/DD HH:MM 포맷 함수 const getTime = (time: string): string => new Date(time) .toLocaleString("en-US", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "Asia/Seoul", }) .replace(",", ""); const completedTime = todo.completed_at ? getTime(todo.completed_at) : null; const updateTodoMutation = useMutation({ mutationFn: () => updateTodo({ id: todo.id, title, completed, }), onSuccess: () => { setIsEditing(false); queryClient.invalidateQueries({ queryKey: ["todos"], }); }, }); return( <div className="w-full flex items-center gap-1"> <Checkbox checked={completed} onChange={async (e) => { await setCompleted(e.target.checked); await updateTodoMutation.mutate(); }} /> {isEditing ? ( <input className="flex-1 border-b-black border-b pb-1" value={title} onChange={(e) => setTitle(e.target.value)} /> ) : ( <div className="flex-1"> <p className={`${completed && "line-through"}`}>{title}</p> <span className="text-gray-400 text-xs"> ⏱️{createdTime}{" "}{completedTime && `~ ${completedTime}`} </span> </div> )} // ... </div> ); } completed 상태에 따라 체크박스를 변경하고, 완료된 경우 completed_at 시각을 표시한다.setCompleted로 상태를 갱신하고, updateTodoMutation을 호출하여 서버에서 completed_at 값을 업데이트한다.완료된 TODO 항목에 대해 completed_at 값이 존재하면 완료 시각을 getTime() 함수로 포맷하여 표시한다.정리:TODO 완료 상태 변경:사용자가 체크박스를 클릭하면 setCompleted로 completed 상태를 갱신한다.갱신된 상태는 updateTodoMutation.mutate()를 통해 서버로 전송되어 completed_at 값이 업데이트된다.완료 시각 컴포넌트 업데이트:서버에서 completed_at 값이 갱신되면, 해당 TODO의 완료 시각이 한국 시간으로 포맷되어 표시된다.완료된 경우에는 completed_at을 getTime() 함수를 통해 표시하고, 완료되지 않은 경우에는 null이므로 표시되지 않는다.쿼리 데이터 갱신:updateTodoMutation.onSuccess에서 queryClient.invalidateQueries({ queryKey: ["todos"] })를 호출하여 todos 쿼리의 데이터를 무효화한다.쿼리의 데이터가 무효화되면 React Query는 자동으로 서버에서 최신 데이터를 다시 불러와 화면에 반영한다. 1주차 회고☀사실 Next.js를 아주 짧게 배운 상태로 강의를 듣게 됐는데, 섹션1에서 중요한 부분을 잘 설명해주셔서 큰 어려움없이 수강할 수 있었다. 그리고 실습을 따라하면서 어느정도 흐름은 알게 된 것 같다. 그렇지만 다른 Next.js 강의를 수강하며 구체적인 동작과정을 더 배워야 할 것 같다. 다음 실습도 기대된다. 다음주도 화이팅!

풀스택

김진현

[인프런 워밍업 클럽 Full Stack 3기] 1주차 TODO

Next.js 기본 개념 정리 (Part 1)Next.js란?“개인이 풀스택 개발을 하기에 최적화된 웹 프레임워크”서버사이드 렌더링(SSR) 지원 → SEO(검색 최적화) 강점별도 서버 구축 없이 API까지 개발 가능Next.js 프로젝트 생성npx create-next-app@latest [프로젝트명] 폴더 구조 및 주요 파일폴더 구조 = Route(URL) 구조와 동일app/movies → /moviesapp/movies/[id] → /movies/1 (Dynamic Route)주요 파일page.tsx(js): 해당 경로의 페이지 컴포넌트layout.tsx(js): 페이지 레이아웃 관리route.ts(js): API 서버 역할 (GET, POST, PUT, DELETE 등 구현 가능)Link 사용a 태그 대신 Link 컴포넌트 사용 권장 (클라이언트 사이드 라우팅 최적화)import Link from 'next/link'; export default function Page() { return <Link href="/dashboard">Dashboard</Link>; } Next.js Part 2 - Metadata & Server ActionServer Action이란?Next.js에서 백엔드 API 없이 서버에서 직접 데이터 처리 가능기존 fetch + REST API 방식보다 간단하고 빠름Server Action 사용 예시유저 검색 기능을 구현할 때 두 가지 방식 비교기존 방식 → fetch + REST API개선 방식 → Server Action 활용Metadata (SEO 및 공유성 강화)Next.js는 SEO 및 링크 미리보기를 위해 메타데이터 API 제공Static Metadata (정적 메타데이터 설정)import type { Metadata } from 'next' export const metadata: Metadata = { title: '페이지 제목', description: '페이지 설명', } export default function Page() {} Dynamic Metadata (동적 메타데이터 설정)import type { Metadata, ResolvingMetadata } from 'next' type Props = { params: { id: string } searchParams: { [key: string]: string | string[] | undefined } } export async function generateMetadata( { params }: Props, parent: ResolvingMetadata ): Promise<Metadata> { const id = params.id const product = await fetch(`https://.../${id}`).then((res) => res.json()) const previousImages = (await parent).openGraph?.images || [] return { title: product.title, openGraph: { images: ['/some-specific-page-image.jpg', ...previousImages], }, } } export default function Page({ params, searchParams }: Props) {} Next.js Part 3 - Supabase 프로젝트 생성1. Supabase 가입 및 프로젝트 설정Supabase 회원가입 후 대시보드 접속새 프로젝트 생성 → 프로젝트명: inflearn-supabase-projects테이블 생성 (todo 테이블)title (text)completed (boolean)created_at (timestampz)updated_at (timestampz)2. .env 파일 설정NEXT_PUBLIC_SUPABASE_URL=https://[project_id].supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=[anon_key] NEXT_SUPABASE_SERVICE_ROLE=[service_role_key] NEXT_SUPABASE_DB_PASSWORD=[db_password] 3. package.json 수정 (타입 자동 생성 스크립트 추가)"scripts": { "generate-types": "npx supabase gen types typescript --project-id [project_id] --schema public > types_db.ts" } 4. Supabase CLI 설치 및 로그인npx supabase loginNext.js Part 4 - 필수 라이브러리 설정 (React Query, Supabase)1. 필수 라이브러리 설치npm i @supabase/ssr @tanstack/react-query 2. React Query 설정config/ReactQueryClientProvider.tsx"use client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; export const queryClient = new QueryClient({}); export default function ReactQueryClientProvider({ children, }: React.PropsWithChildren) { return ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ); } app/layout.tsximport ReactQueryClientProvider from "config/ReactQueryClientProvider"; export default function RootLayout({ children }) { return ( <ReactQueryClientProvider> ... </ReactQueryClientProvider> ); } 3. Supabase 설정환경 변수 설정 (.env)NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_ANON_KEY= NEXT_SUPABASE_SERVICE_ROLE= NEXT_SUPABASE_DB_PASSWORD= utils/supabase/client.ts (브라우저용 Supabase 클라이언트)"use client"; import { createBrowserClient } from "@supabase/ssr"; export const createBrowserSupabaseClient = () => createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ); utils/supabase/server.ts (서버용 Supabase 클라이언트)"use server"; import { createServerClient, type CookieOptions } from "@supabase/ssr"; import { cookies } from "next/headers"; import { Database } from "types_db"; export const createServerSupabaseClient = async ( cookieStore: ReturnType<typeof cookies> = cookies(), admin: boolean = false ) => { return createServerClient<Database>( process.env.NEXT_PUBLIC_SUPABASE_URL!, admin ? process.env.NEXT_SUPABASE_SERVICE_ROLE! : process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { get(name: string) { return cookieStore.get(name)?.value; }, set(name: string, value: string, options: CookieOptions) { try { cookieStore.set({ name, value, ...options }); } catch (error) { // 서버 컴포넌트에서 set 호출 시 무시 } }, remove(name: string, options: CookieOptions) { try { cookieStore.set({ name, value: "", ...options }); } catch (error) { // 서버 컴포넌트에서 delete 호출 시 무시 } }, }, } ); }; export const createServerSupabaseAdminClient = async ( cookieStore: ReturnType<typeof cookies> = cookies() ) => { return createServerSupabaseClient(cookieStore, true); }; 4. Middleware 설정app/middleware.tsimport { createServerClient, type CookieOptions } from "@supabase/ssr"; import { type NextRequest, NextResponse } from "next/server"; export const applyMiddlewareSupabaseClient = async (request: NextRequest) => { let response = NextResponse.next({ request: { headers: request.headers, }, }); const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { get(name: string) { return request.cookies.get(name)?.value; }, set(name: string, value: string, options: CookieOptions) { request.cookies.set({ name, value, ...options }); response = NextResponse.next({ request: { headers: request.headers } }); response.cookies.set({ name, value, ...options }); }, remove(name: string, options: CookieOptions) { request.cookies.set({ name, value: "", ...options }); response = NextResponse.next({ request: { headers: request.headers } }); response.cookies.set({ name, value: "", ...options }); }, }, } ); await supabase.auth.getUser(); return response; }; export async function middleware(request: NextRequest) { return await applyMiddlewareSupabaseClient(request); } export const config = { matcher: [ "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", ], };Part 5. 할일 CRUD 기능 구현 (feat. Server Action)1. CRUD Server Action 만들기server/actions/todo.ts"use server"; import { Database } from "types_db"; import { createServerSupabaseClient } from "utils/supabase/server"; export type TodoRow = Database["public"]["Tables"]["todo"]["Row"]; export type TodoRowInsert = Database["public"]["Tables"]["todo"]["Insert"]; export type TodoRowUpdate = Database["public"]["Tables"]["todo"]["Update"]; function handleError(error: any) { console.error(error); throw new Error(error.message); } export async function getTodos({ searchInput = "" }): Promise<TodoRow[]> { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("todo") .select("*") .like("title", `%${searchInput}%`) .order("created_at", { ascending: true }); if (error) { handleError(error); } return data; } export async function createTodo(todo: TodoRowInsert) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.from("todo").insert({ ...todo, created_at: new Date().toISOString(), }); if (error) { handleError(error); } return data; } export async function updateTodo(todo: TodoRowUpdate) { const supabase = await createServerSupabaseClient(); console.log(todo); const { data, error } = await supabase .from("todo") .update({ ...todo, updated_at: new Date().toISOString(), }) .eq("id", todo.id); if (error) { handleError(error); } return data; } export async function deleteTodo(id: number) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.from("todo").delete().eq("id", id); if (error) { handleError(error); } return data; }Part 6. 추가 과제 - 할 일의 생성 및 업데이트 시간 표시기능 추가: 할 일 목록에 created_at(생성 시간)과 updated_at(업데이트 시간) 표시{setCreated_at ? ( <p className="flex-1">created_at: {formatDate(created_at)}</p> ) : ( <p className="flex-1">created_at: {formatDate(created_at)}</p> )} <p className="flex-1">updated_at: {formatDate(todo.updated_at)}</p> 업데이트 내용created_at: 할 일이 처음 생성된 시간 표시updated_at: 할 일이 수정된 최신 시간 표시

풀스택Next.jsReactQueryRecoilTailwindCSSSupabase

LC-02s

[인프런 워밍업 클럽 3기] 풀스택 스터디 1주차 미션 회고 발자국

학습 내용 요약인프런 워밍업 클럽 3기 풀스택 스터디 1주차에는 해당 강의에서 사용될 라이브러리 및 프레임워크에 대한 소개와 사용법 위주로 다루었습니다. 인프런에서 발자국을 작성할 때 강의를 보지 않고도 강의 내용을 파악할 수 있을 만큼 자세한 내용을 작성하는 건 지양해 달라고 가이드한 만큼 강의에 대한 필기는 최소화 하고 회고 위주로 적어보겠습니다. 강의의 목적인 Next.js와 Supabase를 사용해서 서비스를 만들어보는 것에 집중하여 스타일링이나 기타 상태관리 라이브러리에 대한 내용은 작성하지 않았습니다. Next.js 간단 정리Next.js란?개인이 풀스택 개발을 하기에 최적화 된 웹 프레임워크서버사이드 렌더링이 큰 특징이며, React와 비슷한 문법으로 더 현대적인 웹 서비스를 구현할 수 있음서버에서부터 HTML을 최적화해서 웹으로 내려주기 때문에 SEO에 도움이 됨자체적으로 API 구축이 가능하기 때문에 사이즈가 크지않은 개인 프로젝트 정도의 규모라면 별도로 서버구축을 할 필요 없이 Next.js만으로 서버 구축까지 전부 가능함Server Actions서버에서 실행되는 비동기 함수 생성 기능"use server" 지시어를 파일 최상단에 선언하면 해당 파일 내 모든 함수가 Server Action으로 간주됨서버 및 클라이언트 컴포넌트에서 호출 가능주로 폼 제출 및 데이터 변경(뮤테이션)에 사용내부적으로 POST 요청을 사용하며, 인자 및 반환값은 React에서 직렬화 가능해야 함 Supabase 간단 정리Supabase 란?PostgreSQL 기반의 오픈소스 백엔드 서비스로, 개발자들이 손쉽게 데이터베이스, 인증, 스토리지, 서버리스 함수 등을 활용할 수 있도록 지원하는 플랫폼상용 서비스인 Firebase의 대안으로 자주 언급됨Supabase 장점오픈소스 프로젝트 (자체 서버 구축 가능)PostgreSQL 기반 (관계형 DB 장점을 살릴 수 있다)Firebase 대비 저렴다양한 연동 방식 지원 (+ GraphQL, API, SDK, DB Connection)Supabase 단점아직 성숙하지 않은 커뮤니티 기반비교적 적은 기능들, 적은 서비스 연동 지원부족한 문서화, 한글 문서 부족Firebase보다 높은 러닝커브  Todo List 미션 회고풀스택 스터디 1주차 미션은 강의에서 진행하는 Next.js와 Supabase를 활용한 Todo List 앱에 기능을 더 추가해보는 것이었습니다. 진행했던 미션은 해당 링크에서 보실 수 있습니다. 미션 수행 내용아래는 제가 수행한 미션에 대한 내용과 함께 고민했던 점과 아쉬웠던 점을 정리해보았습니다. 기능 명세할 일 목록 조회 기능키워드 검색 기능할 일 수정 기능내용 변경 기능완료 여부 변경 기능할 일 삭제 기능정리해보면 위의 기능을 Next.js와 Supabase를 활용하여 구현하는 것이었습니다. 강의에서는 Material Tailwind 라이브러리를 사용했지만 저는 기존에 즐겨썼었던 TailWindCSS와 최근 관심가지게된 Mantine 라이브러리를 사용하여 UI를 구축하였고, 4주차 모두 동일한 환경으로 진행되는 것 같아 Turbo Repo와 pnpm workspace를 활용한 모노레포 환경을 구축해두고 프리셋을 관리하였습니다. 상세한 내용은 아래에 정리해두었습니다. 사용 기술프레임워크: Next.js v15, React v19데이터베이스: Supabase서버 상태관리: Tanstack Query v5클라이언트 상태관리: Zustand v5스타일 프레임워크: TailWindCSS v3, Mantine v7모노레포: Turbo Repo패키지 매니저: pnpm 강의에서는 TypeScript를 사용했지만 별도의 인터페이스를 작성하지는 않았는데, 저의 최근 관심사가 환경(라이브러리) 변화에 유연한 도메인 로직 작성하기여서 저는 아래와 같이 별도로 인터페이스를 작성 후 서버 액션을 사용하는 로직을 인터페이스에 맞추어 작성해 보았습니다. export interface Todo { id: number title: string createdAt: string updatedAt: string completed: boolean completedAt: string | null }export interface GetTodoListParams { query?: string } export interface GetTodoList { (params: GetTodoListParams): Promise<Todo[]> }export type CreateTodoParams = Pick<Todo, 'title'> export interface CreateTodo { (params: CreateTodoParams): Promise<void> }export type UpdateTodoParams = Pick<Todo, 'id' | 'title' | 'completed'> export interface UpdateTodo { (params: UpdateTodoParams): Promise<void> }export type DeleteTodoParams = Pick<Todo, 'id'> export interface DeleteTodo { (params: DeleteTodoParams): Promise<void> } 기존에는 서버 액션에 대한 이해가 부족했어서 항상 Next.js를 사용하며 Route Handler로 별도의 API를 만들어서 작업했었는데 별도의 API 엔드포인트를 관리하지 않고 서버 액션으로 모두 구축해볼 수 있었습니다. 이번에는 강의에서 진행한 것과 같이 GET 요청에도 서버 액션을 사용했었는데 개인적으로 관련 내용에 대한 고민을 조금 했었습니다. Next.js의 공식 문서에서는 일반적으로 서버 액션을 데이터 변경(뮤테이션) 로직에만 사용하는 것으로 소개되었기도 했고, 기본적으로 POST 메서드를 사용한다고 했었는데, 이렇게 GET, PUT, DELETE 메서드를 사용해야하는 로직도 모두 POST로 통합해도 되는가라는 의문이 하나 있었고, GET 요청에도 서버 액션을 사용하니 생긴 문제인데 Next.js에서의 캐싱 전략을 어떻게 사용할 수 있을까에 대한 의문이 있었습니다. 강사님께서 제시하는 생산성 높은 풀스택 개발 방법론은 Supabase로 백엔드 필요 기능을 쉽고 빠르게 구축하고 Next.js의 서버 액션을 활용하여 별도의 API를 관리하는 절차를 건너뛰는 것이라고 이해했습니다. 저도 1인 풀스택 개발에 뜻이 있는 입장에서 되게 괜찮은 방법론이라 생각했는데, 해당 의문은 이로인해 근본적으로 서버 액션이라는 기술의 활용 범위를 넓힘으로 인해 발생했다고 생각합니다. 첫 번째 의문은 Next.js의 특수성으로 미루어 보아 문제가 없다고 결론지었습니다. 서버 액션이라는 개념 자체가 일반적으로 REST API를 설계하는데 사용되는 개념이 아니기도 하고, 애초에 엔드포인트 자체도 루트 경로로 받기 때문에 의미가 없는 것이죠. 프레임워크가 관리해주기 때문에 메서드와 엔드포인트로 작업 단위를 표현하지 않아도 괜찮다고 생각했습니다. 두 번째는 의문이라기 보다는 문제였죠. 저는 해당 문제를 해결하기 위해 Tanstack Query를 사용했습니다만 근본적인 해결은 아니었습니다. HydrationBoundary 와 prefetchQuery를 활용하여 SSR과 서버 컴포넌트 환경을 통합하였고, useQuery 훅과 Query Key Factor 방식을 사용하여 캐싱 전략 관리를 시도했는데, 다 만들고 보니 이건 클라이언트에 해당하는 캐싱 전략이었어요. 서버 쪽에서는 적절한 캐싱 전략이 없었습니다. 해당 문제는 공식 문서를 찾아보니 라우트 세그먼트의 설정을 상속받아 maxDuration 같은 설정을 따른다고 안내되고 있었습니다. 적용해보지는 못했지만 page.tsx에서 결정할 수 있는 것 같아 아마 다음 미션에서 기회가 된다면 사용해볼 예정입니다. 벌써 다음 미션이 기대되네요. 긴글 읽어주셔서 감사합니다. ☺

프론트엔드워밍업클럽3기풀스택Next.jsSupabase

이수진

[인프런 워밍업 클럽 Full-Stack 3기] 1주차 발자국 - TODO List 구현

이제 막 3개월차에 들어가는 새내기 개발자. 일을 다니면서 퇴근 후 개인공부 또는 개발을 하며 멋있는 삶을 보내는 나날을 상상했지만 현실은 퇴근하면 누워있기 바쁜 그런 회사원A. 그러던 차에 인프런에서 스터디를 한다는 소식을 들었다.이런 프로젝트가 있다는 것은 이미 알고 있었지만, 이전에는 이미 들은 강의이거나 기술이 맞지 않아 참여를 하지 않았었는데 마침 관심있던 Supabase와 요즘 실무에서도 사용중인 Next.js를 결합한 프로젝트 강의라니! 심지어 할인된 가격에! 이건 안할 수 없겠다 싶어서 바로 신청했다. 솔직히 사놓고 안듣고있는 강의가 넘치는데 이런 프로그램에라도 참여해야 강의를 완강할 수 있겠다 싶은 생각도 있긴했다. ㅠㅠ 수강 내용Section 1, 2 : OT 및 기술 소개Section 1과 2에선 전체 프로젝트에 대한 소개와 사용 기술에 대한 설명이 주를 이뤘다.이번 강의로 다음과 같은 기술 스택들을 배울 수 있다고 했다.Supabase에서는 Storage, Database, Auth, Realtime 등 Next.js 14, Typescript, Tailwind CSS, Meterial UI Tailwind, Recoil, Tanstack QueryAWS, Vercel, GoDaddy(Domain) 확실히 기존에 개인 프로젝트를 할 때 Firebase를 사용했었는데 Firebase같은 경우는 나온지 오래되서 그런지 레퍼런스들이 굉장히 많았다면 Supabase는 그런 점이 좀 부족하다 생각했었는데 강의로 그런 부분들을 채울 수 있어서 좋았다. 실제로 혼자 한번 Supabase를 설정하려고 했었는데 안되서 한참 찾아봤던 문제를 강의에서 짚어주신 부분도 있었다.강의에서도 좀 중요하게 강조했던 부분이 있다면 Tanstack Query가 아닐 까 싶다. 사실 좋다 좋다 해서 사용하지 왜 좋은지 왜 사용하는지 잘 알지 못하고 썼었는데 이번 기회에 왜 사용하는지, 어떻게 사용하는지 좀 더 잘 알게 된 것 같아서 나중에 효율적으로 사용할 수 있을 것 같다는 생각이 들었다.Tanstack Query (React Query)서버와 통신할 때 관리해야 하는 상태들이 많이 다양한데, 이런 상태 관리를 클라이언트에서 사용하는 데 도움을 주는 라이브러리.fetching, caching, 서버 데이터와의 동기화를 지원해주는 라이브러리캐싱 : 캐싱을 통해 동일한 데이터에 대한 반복적인 비동기 데이터 호출을 방지하고, 불필요한 API 콜을 줄여 서버에 대한 부하를 줄이는 결과를 갖는다.Client 데이터와 Server 데이터 간의 분리 : 프로젝트 규모가 커지고 관리해야 할 데이터가 넘치다보면 클라이언트에서 관리하는 데이터와 서버에서 관리하는 데이터가 분리될 필요성을 느낀다.서버에서 가져오는 데이터 : react-query 클라이언트에서 관리하는 데이터 : recoil, zustand ...useQuery : 데이터를 가져오는 작업에 사용useMutation : 데이터를 변경하는 작업(PUT, POST, DELETE)에 사용queryClient.invalidateQueries : queryClient를 사용해 쿼리 요청을 다시 진행할 수 있다. (다른 곳에서 refetch를 진행하기 위함Section 3. TODO LIST 만들기(UI 구현은 블로그 글로는 생략...)본격적으로 프로젝트를 만들어보는 시간이다. Supabase에는 Table Editor, SQL Editor, Database, Authentication, Storage 등 어떤 기능들이 있는지 각각 자세하게 설명해주셨다.그리고 Supabase를 사용하기 위한 환경변수를 설정하고, Supabase Database를 사용할 때 쉽게 types를 추가하기 위해 script를 추가했는데 처음엔 몰랐는데 나중에 미션을 진행하다보니 자동으로 supabase에 있는 table에 대한 타입을 추가해준다는게 굉장히 편리했다."scripts": { // dev, build, start, lint "generate-types": "npx supabase gen types typescript --project-id [project_id] --schema public > types_db.ts" }그리고 이제 Supabase 사용하기 위한 client server middleware 설정도 진행했다. 혼자 공식문서를 봤으면 한참 해멨을 텐데 간단하게 설정이 완료됬다. 이 부분은 나중에 supabase로 개인 프로젝트를 진행할 때 요긴하게 사용할 듯.이제 Next.js Server Action을 구현했다.server action : api 구현 없이 함수 호출만으로도 api를 대신할 수 있는 기능getTodo, createTodo, updateTodo, deleteTodo를 만들었다. 그 중 getTodo와 createTodo를 보면 다음과 같다.export async function getTodos({ searchInput = "" }): Promise<TodoRow[]> { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("todo") .select("*") .like("title", `%${searchInput}%`) .order("created_at", { ascending: true }); if (error) { handleError(error); } return data; }from : 가져올 Table 선택 select : 가져올 부분 선택like : title에 searchInput이 앞뒤로 들어가 있는 부분이 있다면 모두 가져온다.order : 정렬export async function createTodo(todo: TodoRowInsert) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.from("todo").insert({ ...todo, created_at: new Date().toISOString(), }); if (error) { handleError(error); } return data; }created_at : 사용자가 잘못 입력할 수도 있어 데이터가 생성될 때 서버에서 자동으로 날짜가 생성되도록 구현actions들도 모두 구현했으니 이제 DOM에 이벤트를 달아줄 일만 남았다. 위에서 Tanstack Query를 사용하는 방법을 배웠듯 서버에서 데이터를 가져오는 것은 useQuery를 사용하고 데이터를 생성, 수정, 삭제하는 부분은 useMutation을 사용해 구현할 것이다.const { data, isLoading, refetch } = useQuery({ queryKey: ["todos"], queryFn: () => getTodos({ searchInput }), });const { mutate, isPending } = useMutation({ mutationKey: ["todos"], mutationFn: () => createTodo({ title: "New Todo", completed: false, }), onSuccess: () => refetch(), });useQuery useMutation을 담당하는 부분들의 일부를 한번 가져왔다. 이렇게 데이터를 get 할 때는 useQuery 그리고 create 등을 할 때 useMutation을 사용했다.강의를 모두 마치면 CRUD 기능을 모두 할 수 있는 TODO List가 완성된다.미션1주차 미션은 "생성 시간과 완료 시간"을 표시하는 것.생성 시간은 이미 존재하므로 완료 시간만 Supabase Todo Table에 completed_at 컬럼을 추가했다. 이 때 아까 위에서 말했듯 generate script를 통해 쉽게 table type을 정의할 수 있어서 간편했다.actions에 updateTodo 부분에 completed_at을 추가하고 todo.completed가 true일 경우 새 날짜를 작성하도록 구현했다.const { data, error } = await supabase .from("todo") .update({ ...todo, updated_at: new Date().toISOString(), completed_at: todo.completed ? new Date().toISOString() : null, }) .eq("id", todo.id ?? 0);그리고 시간을 표시하기위해 Date를 바꾸는 함수를 추가했다. (YYYY-MM-DD HH-MM-SS 형식)export const formatDate = (dateString: string) => { if (!dateString) return ""; const date = new Date(dateString); const formattedDate = date.toLocaleDateString("en-CA"); const formattedTime = date.toLocaleTimeString("en-GB", { hour12: false, }); return `${formattedDate} ${formattedTime}`; // 이부분 ${} 이 연속된 부분인데 현재 코드 블럭에서 이상하게 나온다. };따라서 생성 날짜와 수정된 날짜를 표시하면 다음과 같이 결과물이 완성된다.마무리 직장과 병행하며 프로그램을 수행한다는 것이 조금 어려웠지만 무사히 1주차 미션들을 달성해서 뿌듯하다. 혼자라면 중간에 포기하거나 조금 루즈해지거나 했을텐데 다들 열심히 사는 것 같아 나도 뒤쳐지지 않도록 노력했다.개인적으로 이번 강의를 모두 마치고 목표가 있다면 Supabase + Next.js를 통해 나만의 블로그를 만드는 것. 강의를 들으면서 나중에 이 기능은 어떻게 활용하면 좋겠다라는 상상도 해보고 하는 시간들이 즐거웠다. 또 강사님께서 차근차근 친절히 알려주셔서 더 쉽게 학습됬었던 듯 하다. 개인적으로 supabase이외에도 react query를 좀 더 상세하게 알 수 있었던 시간이라 (위에서도 말했듯 이전에 사용할 때에는 그냥 좋다니까 썼지 제대로 쓴다는 느낌은 받지 못했다) 굳이 fullstack 스택이 아니더라도 front-end 개발자 입장에서도 한단계 스킬 업한 느낌이었다.앞으로 3, 4주차에 좀 타이트하게 진행된다고 했는데 이번주는 스케쥴에 딱 맞게 진행해서.. 다음주에는 조금 더 신경써서 시간을 할애해봐야겠다. 스터디원들 모두 화이팅...! ❣

웹 개발웹개발풀스택SupabaseReactNext

codestudy

[인프런 워밍업 스터디 클럽 3기 풀스택] 1주차 발자국

Next.js와 Supabase를 활용한 Todo 애플리케이션 개발강의 수강 내용 요약이번 주에는 Next.js 14, TypeScript, Material Tailwind, Supabase를 연동하여 Todo 리스트 애플리케이션을 만드는 방법을 학습했습니다.사용한 기술 스택과 그 이유1. SupabasePostgreSQL 기반의 오픈소스 백엔드 서비스 플랫폼Firebase의 대안으로 관계형 DB의 장점을 활용할 수 있음자동 생성되는 API와 인증 시스템 제공장점: 오픈소스, PostgreSQL 기반, 비용 효율성, 다양한 연동 방식단점: 상대적으로 작은 커뮤니티, 문서화 부족, Firebase보다 높은 러닝커브2. Next.jsReact 기반 풀스택 웹 애플리케이션 프레임워크서버 사이드 렌더링, 정적 생성, API 라우트 등 다양한 기능 제공App Router를 통한 새로운 라우팅 시스템클라이언트/서버 컴포넌트 분리를 통한 성능 최적화3. Material TailwindMaterial UI와 Tailwind CSS를 결합한 디자인 시스템유틸리티 기반의 스타일링과 미리 구성된 컴포넌트 제공타입스크립트 지원으로 개발 경험 향상4. React Query (Tanstack Query)서버 상태 관리를 위한 강력한 라이브러리데이터 페칭, 캐싱, 동기화, 상태 업데이트 기능 제공자동 리패칭과 캐싱 기능으로 UX 향상장점: 자동 캐싱, 데이터 동기화, 개발자 경험, 성능 최적화단점: 학습 곡선, 복잡한 설정, 캐시 관리의 복잡성   🍭미션할일 CRUD 기능 구현 (feat. Server Action)Next.js의 Server Actions 활용한 데이터 조작 함수 구현getTodos, createTodo, updateTodo, deleteTodo 기능 구현React Query hooks를 활용한 서버 상태 관리Todo 컴포넌트 개발 및 CRUD 로직 연결이 과정에서 Material Tailwind 컴포넌트의 타입 불일치 문제로 여러 타입 에러가 발생했습니다. 이를 해결하기 위해 래퍼 컴포넌트 패턴을 적용했습니다:// 커스텀 IconButton 래퍼 컴포넌트 interface IconButtonProps { onClick: () => void | Promise<void>; children: ReactNode; className?: string; } function IconButton({ onClick, children, className }: IconButtonProps) { return ( <MTIconButton onClick={onClick as any} className={className} {...({} as any)} // 타입 에러 방지 > {children} </MTIconButton> ); } 또한 todoQuery와 mutation을 활용하여 데이터 관리를 구현했습니다:const updateTodoMutation = useMutation({ mutationFn: () => updateTodo({ id: todo.id, title, completed, }), onSuccess: () => { setIsEditing(false); queryClient.invalidateQueries({ queryKey: ["todos"], }); }, }); queryClient는 쿼리 관련된 요청의 캐시 역할을 해주어 React Query가 캐시를 정상적으로 동작하게 해줍니다.학습 내용 회고아쉬웠던 점타입스크립트 타입 에러 해결에 많은 시간을 소비했습니다. 라이브러리 문서를 먼저 자세히 살펴봤다면 더 효율적이었을 것입니다.프로젝트 구조 설계와 파일 경로 설정 등 기초적인 부분에서 오류가 발생했습니다.컴포넌트 재사용성과 코드 구조화 측면에서 개선할 여지가 있습니다.보완하고 싶은 점래퍼 컴포넌트를 별도 파일로 분리하여 코드의 재사용성을 높이고 싶습니다.Supabase의 인증 기능을 추가하여 사용자별 Todo 관리 시스템으로 확장하고 싶습니다.테스트 코드 작성을 통해 애플리케이션의 안정성을 높이고 싶습니다.모바일 반응형 디자인을 더 개선하여 다양한 디바이스에서의 사용성을 향상시키고 싶습니다.복습을 계속 해야할 것 같습니다.

풀스택supabasenext.jsmaterialtailwind

Jii

[인프런 워밍업 클럽 3기] 풀스택 과정 1주차 발자국

1주차 학습 범위섹션1(오리엔테이션): Supabase 및 초기 설정(VSCode 등)섹션2(기술 설명): Next.js, TailwindCSS, Recoil, React Query섹션3(연습 프로젝트): TODO LIST 만들기 📓 학습 기록Supabase개념BaaS(Backend as a Service) - 서비스형 백엔드 (예. Firebase, Supabase, AWS Amplify)Firebase의 대안으로 만들어진 오픈소스 프로젝트Firebase: NoSQL 기반인 Firestore 사용Firebase: Google이 관리하는 완전한 서버리스 Vendor Lock-in 장점오픈소스 프로젝트 - 자체 서버 구축 가능(Docker 컨테이너 배포 지원) PostgreSQL 기반 - SQL 쿼리 가능, 관계형 데이터베이스 지원 Firebase 대비 저렴다양한 연동방식 지원 - GraphQL, API, SDK, DB ConnectFirebase는 Firestore 중심이라 API 접근 방식이 제한적이고, SQL 같은 복잡한 쿼리 사용이 어려움Supabase는 PostgreSQL 기반이라 SQL, REST API, GraphQL(서드파티 지원), DB 직접 연결이 모두 가능단점아직 성숙하지 않은 커뮤니티 기반비교적 적은 기능, 적은 서비스 연동 지원부족한 문서화, 한글 문서 부족Firebase보다 높은 러닝커브 Vendor Lock-in특정 클라우드 서비스나 플랫폼을 사용하기 시작하면, 다른 서비스로 쉽게 이동할 수 없게 되는 상황 Next.jsReact 기반의 풀스택 웹 프레임워크서버 사이드 렌더링(SSR)페이지를 요청할 때 서버에서 HTML을 생성해서 반환SEO(검색 엔진 최적화) 유리, 첫 로딩 속도 빠름getServerSideProps 사용정적 사이트 생성(SSG)빌드 시 미리 HTML을 생성해 정적 파일로 제공getStaticProps 사용 파일 기반 라우팅Next.js의 폴더는 route 구조와 동일하게 작동 (폴더명 = URL)app/movies → “http://localhost:3000/movies”에 해당하는 코드 작성 가능app/movies/[id] → “http://localhost:3000/movies/1”에 해당하는 코드 작성 가능 (Dynamic Route) API 라우트 제공백엔드 없이도 /pages/api/ 혹은 /app/api/ 안에 API 파일을 만들면 백엔드 서버처럼 사용 가능route.ts 파일에 있는 코드는 웹에서 접근이 불가능 - 서버에서 돌아가는 코드보안이 중요한 코드 작성 (ex. DB 접속 등)export async function GET(request: Request) {} export async function HEAD(request: Request) {} export async function POST(request: Request) {} export async function PUT(request: Request) {} export async function DELETE(request: Request) {} export async function PATCH(request: Request) {} <Link> 사용 권장클라이언트 사이드 라우팅 (CSR)일반 <a> 태그를 사용하면 전체 페이지가 새로고침(F5)되면서 서버에서 새 HTML을 받아옴<Link>를 사용하면 Next.js가 JavaScript로 페이지를 전환하여 새로고침 없이 부드럽게 이동 가능사전 로드 (Preloading) 지원<Link>를 사용하면 Next.js가 백그라운드에서 해당 페이지 데이터를 미리 가져옴브라우저가 네트워크 요청을 최적화해서 빠른 로딩 가능SEO 최적화 & 서버 사이드 렌더링과 호환<a> 태그를 직접 사용하면 Next.js의 서버 사이드 렌더링(SSR)과 연동이 어려울 수 있음import Link from 'next/link' export default function Page() { return <Link href="/dashboard">Dashboard</Link> }  Server Actions서버에서 직접 실행되는 함수 (Next.js 14에서 도입)이전 방식 - API Route 사용 // pages/api/form.ts (또는 app/api/form/route.ts) import { NextResponse } from "next/server"; export async function POST(req: Request) { const data = await req.json(); console.log("데이터 수신:", data); return NextResponse.json({ message: "성공!", data }); } // 클라이언트에서 요청 async function submitForm(formData) { const res = await fetch("/api/form", { method: "POST", body: JSON.stringify(formData), headers: { "Content-Type": "application/json" }, }); const result = await res.json(); console.log(result); }Server Actions 사용 방식 - API Route 없이 서버에서 직접 함수 실행 가능// 서버 액션 정의 "use server"; export async function submitForm(formData: FormData) { console.log("서버에서 데이터 처리:", formData); return { message: "성공!" }; }// 클라이언트에서 바로 호출 "use client"; import { useState } from "react"; import { submitForm } from "./actions"; // 서버 함수 불러오기 export default function FormComponent() { const [message, setMessage] = useState(""); async function handleSubmit(event) { event.preventDefault(); const formData = new FormData(event.target); const result = await submitForm(formData); // API 요청 없이 직접 호출 setMessage(result.message); } return ( <form onSubmit={handleSubmit}> <input name="name" placeholder="이름 입력" /> <button type="submit">제출</button> <p>{message}</p> </form> ); }Summary외부 API와 통신해야 할 경우 → fetch를 써야 하므로 기존 API Route 방식 사용폼 제출, 데이터 처리 같은 간단한 서버 로직 → Server Actions 사용  Metadata웹 페이지의 SEO(검색 엔진 최적화)와 소셜 미디어 공유를 위해 HTML <head>에 메타 정보를 추가하는 기능동적 Metadata 설정 가능페이지마다 다른 Metadata를 적용할 수 있음SEO 최적화에 도움 (검색 엔진이 페이지별로 다르게 인식)generateMetadata() 사용export const metadata = { title: "내 멋진 웹사이트 🚀", description: "Next.js를 활용한 SEO 최적화 페이지", };// 동적 Metadata (게시글 제목 반영) import { Metadata } from "next"; export async function generateMetadata({ params }): Promise<Metadata> { const post = await fetch(`https://api.example.com/posts/${params.id}`).then((res) => res.json()); return { title: post.title, description: post.summary, }; } TailwindCSS유틸리티 퍼스트(Utility-First) 스타일링 프레임워크 미리 정의된 CSS 클래스를 조합해서 빠르게 스타일을 적용할 수 있는 CSS 프레임워크특징미리 정의된 유틸리티 클래스 사용 → text-red-500, bg-blue-300, p-4, flex 등CSS 파일 작성 불필요 → 인라인 스타일처럼 클래스만 추가하면 됨반응형 디자인 지원 → sm:, md:, lg: 같은 반응형 접두사 제공JIT (Just-In-Time) 컴파일러 → 사용된 클래스만 CSS로 빌드되어 성능 최적화  유틸리티 퍼스트(Utility-First)미리 정의된 작은 스타일 단위(유틸리티 클래스)를 조합하여 스타일을 적용하는 방식 RecoilVercel에서 만든 React를 위한 상태관리 라이브러리 다른 전역 상태관리 라이브러리(Redux, MobX)보다 사용법이 간편함React의 상태 관리 패턴과 더 유사학습 곡선이 적고 사용법이 직관적사용 예시app/recoil/atoms.ts → atom 추가app/users/update/page.tsx → 유저 데이터 업데이트app/config/RecoilProvider.tsx → 클라이언트 컴포넌트 <provider 정의>app/layout.tsx → 서버 컴포넌트 <provider 적용> atom상태 관리의 기본 단위 - 상태(state)의 일부atom에 저장된 상태는 여러 컴포넌트에서 구독할 수 있음app/recoil/atoms.ts - 모든 state를 한 공간에 모아두는(저장하는) 위치사용useRecoilState()는 상태를 읽고 쓸 수 있는 함수를 반환useRecoilValue()는 상태를 읽을 수만 있는 함수를 반환import { atom } from 'recoil'; // 'countState'라는 이름을 가진 atom을 생성 export const countState = atom({ key: 'countState', // 각 atom은 고유한 'key' 값을 가져야 함 default: 0, // atom의 초기값 });// 첫 번째 컴포넌트: 카운트 증가 const Incrementer = () => { const [count, setCount] = useRecoilState(countState); return <button onClick={() => setCount(count + 1)}>카운트 증가</button>; }; // 두 번째 컴포넌트: 카운트 표시 const DisplayCounter = () => { const count = useRecoilValue(countState); return <p>현재 카운트: {count}</p>; }; // Incrementer와 DisplayCounter 컴포넌트는 같은 atom (countState)을 공유하고 있는 것! selector기존 상태를 입력값으로 받아 그 값을 변경하거나 가공하여 새로운 값을 계산하는 순수 함수입력값에만 의존하고 외부 상태를 변경하지 않으며 결과를 반환하므로 순수 함수임파생된 상태(derived state)의 일부 기존 상태(atom이나 다른 selector 등)의 값이 변경됨에 따라 자동으로 업데이트되는 값 // atom 생성 import { atom } from 'recoil'; export const nameState = atom({ key: 'nameState', default: '홍길동', }); export const birthYearState = atom({ key: 'birthYearState', default: 1990, });// selector 생성: birthYearState 값을 바탕으로 사용자의 나이를 계산 (ageSelector) import { selector } from 'recoil'; import { birthYearState } from './atoms'; export const ageSelector = selector({ key: 'ageSelector', get: ({ get }) => { const birthYear = get(birthYearState); // atom 값 가져오기 const currentYear = new Date().getFullYear(); return currentYear - birthYear; }, }); React-Query (TanStack)비동기 데이터(fetching, caching, syncing, updating 등)를 효율적으로 관리하는 라이브러리 비동기 서버 데이터(fetch, caching, sync) 관리에 특화Client 데이터와 Server 데이터 간의 구분이 명확해 짐 [출처] 사용 예시app/config/ReactQueryProvider.tsx → 클라이언트 컴포넌트 <provider 정의>app/todos/page.tsx → 투두 데이터 조회 및 생성useQuery 사용 <데이터 가져오기>useMutation 사용 <데이터 수정하기> useQuery서버에서 데이터를 가져올 때 사용 (GET 요청)queryKey: 쿼리를 식별하는 고유 키queryFn: 실제 데이터를 불러오는 함수캐싱, 리패칭, 로딩/에러 상태 관리까지 자동으로 처리됨import { useQuery } from '@tanstack/react-query'; const fetchTodos = async () => { const res = await fetch('/api/todos'); return res.json(); }; const TodoList = () => { const { data, isLoading, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }); if (isLoading) return <p>Loading...</p>; if (error) return <p>Error loading todos</p>; return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); }; useMutation서버의 데이터를 수정할 때 사용 (POST, PUT, DELETE 요청)mutationFn: 데이터를 변경하는 함수onSuccess: 성공 후 실행할 작업(예: 데이터 다시 불러오기)onError: 실패 후 실행할 작업자동 실행되지 않음 수동 실행 → 버튼에서 mutate() 호출 필요수동 리패칭.refetch() → 특정 useQuery에 대해 수동으로 데이터를 다시 가져오는 함수queryClient.invalidateQueries([”key”]) → 전역적으로 특정 쿼리(queryKey 기준)를 전부 리패칭import { useMutation, useQueryClient } from '@tanstack/react-query'; const addTodo = async (newTodo) => { const res = await fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo), headers: { 'Content-Type': 'application/json' }, }); return res.json(); }; const AddTodo = () => { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: addTodo, onSuccess: () => { queryClient.invalidateQueries(['todos']); // todos 목록을 다시 불러옴 }, }); return ( <button onClick={() => mutation.mutate({ title: '새로운 할 일' })}> 할 일 추가 </button> ); };  📎 미션문제 해결검색(Search) 기능: 즉시 검색이 안되는 현상 발견 queryKey에 searchInput을 포함시켜 검색어가 변경될때마다 새로운 쿼리로 인식되도록 수정수정(Update): 투두 수정 시, 엔터키로 저장이 안됨input 필드에서 isEditing 상태일 때 onKeyDown을 추가하여 엔터 키를 처리 📝회고Next.js와 TailwindCSS, Firebase는 기존에 사용해 봤었고 Supabase, Recoil, React Query는 이번에 처음 배웠다.기존에 사용해보지 않았던 기술을 사용해보고 싶었는데, 이 스터디에서는 사용해본 기술과 처음 배우는 기술이 적당히 섞여있어서 좋았다. 

풀스택풀스택워밍업클럽발자국

merry

풀스택 - 1주차

강의 수강supabase 장점오픈소스 프로젝트 (자체 서버구축 가능)PostgreSQL 기반 (관계형 DB 장점을 살릴 수 있다)Firebase 대비 저렴다양한 연동방식 지원NEXT.JS 특징“use server” → 서버에서만 동작함. (DB를 연결해도 됨)fetch + REST API 조합 api/route.ts를 사용하여 api를 만들수 있음. Server Actions api 구현을 하지않아도 데이터를 구현할 수 있다. SEO를 위한 Metadata 투두리스트 CRUD 기능 구현actions/todo-action.ts"use server"; // Next.js의 서버 액션(Server Action)으로 사용하기 위해 명시해야 함. import { Database } from "types_db"; import { createServerSupabaseClient } from "utils/supabase/server"; // 📝 todo 테이블에서 조회할 때 반환되는 데이터의 타입 export type TodoRow = Database["public"]["Tables"]["todo"]["Row"]; // 📝 todo 테이블에 새로운 데이터를 추가할 때 필요한 데이터의 타입 export type TodoRowInsert = Database["public"]["Tables"]["todo"]["Insert"]; // 📝 todo 테이블에서 기존 데이터를 수정할 때 사용할 데이터의 타입 export type TodoRowUpdate = Database["public"]["Tables"]["todo"]["Update"]; // 🔹 에러 핸들링 함수 function handleError(error: any) { console.error(error); // 에러 로그 출력 throw new Error(error.message); // 에러 메시지를 포함한 예외 발생 } export async function getTodos({ searchInput = "" }) { const supabase = await createServerSupabaseClient(); // 🔹 Supabase를 통해 todo 테이블에서 데이터 조회 const { data, error } = await supabase // ✅ `await` 추가하여 비동기 데이터 처리 .from("todo") // 📌 todo 테이블에 접근 .select("*") // 📌 모든 컬럼 조회 .like("title", `%${searchInput}%`) // 📌 LIKE 연산자로 title에 searchInput이 포함된 데이터 필터링 .order("created_at", { ascending: true }); // 📌 생성 날짜 기준 오름차순 정렬 if (error) { handleError(error); // 에러 발생 시 핸들링 } return data; // 조회된 데이터 반환 } export async function createTodo(todo: TodoRowInsert) { const supabase = await createServerSupabaseClient(); // ✅ Supabase 서버 클라이언트 생성 // 🔹 Supabase를 사용하여 todo 테이블에 새로운 데이터 삽입 const { data, error } = await supabase .from("todo") // 📌 todo 테이블에 접근 .insert({ ...todo, // 📌 클라이언트에서 받은 데이터(todo) 삽입 created_at: new Date().toISOString(), // 📌 클라이언트에서 전달된 값이 이상할 수 있으므로, 서버에서 직접 현재 시간을 생성하여 추가 }); if (error) { handleError(error); // 🔹 에러 발생 시 핸들링 } return data; // ✅ 삽입된 데이터 반환 } //todo 업데이트 export async function updateTodo(todo: TodoRowUpdate) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("todo") .update({ ...todo, updated_at: new Date().toISOString(), }) .eq("id", todo.id); // 📌 id가 일치하는 행만 업데이트 if (error) { handleError(error); } return data; } //todo 삭제 export async function deleteTodo(id: number) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.from("todo").delete().eq("id", id); if (error) { handleError(error); } return data; } new Date().toISOString() 이란? 현재 시간을 ISO 8601 형식의 문자열로 변환하는 메서드입니다.1⃣ 시간 표준화 (Time Standardization)문제: 서버와 클라이언트의 시간대가 다를 수 있음사용자가 다른 시간대(한국, 미국, 유럽 등)에서 앱을 사용할 수 있음.예를 들어, 한국(KST)에서 저장한 날짜가 미국(PST)에서 보면 이상하게 보일 수 있음.해결책:모든 시간을 UTC 기준으로 저장하여 시간대를 통일하면 문제 해결!new Date().toISOString()은 항상 UTC(세계 표준시)로 변환되므로, 서버-클라이언트 간 시간 오차 없이 일관성 유지 가능.2⃣ 클라이언트와 서버 간 시간 차이 해결문제: 클라이언트에서 new Date()를 사용하면 각 기기의 로컬 시간이 저장됨만약 한국(KST)에서 new Date()를 저장하면 2025-03-02T23:30:45미국(PST)에서 new Date()를 저장하면 2025-03-02T06:30:45서버에서 보면 시간이 뒤죽박죽이 됨 😵해결책:모든 시간을 UTC 기준으로 저장(new Date().toISOString())클라이언트가 받을 때 필요한 시간대로 변환해서 사용예시:js 복사편집 const utcTime = new Date().toISOString(); console.log(utcTime); // "2025-03-02T14:30:45.123Z" (UTC) 한국(KST)에서는 2025-03-02 23:30:45로 변환해서 보여주면 됨.미국(PST)에서는 2025-03-02 06:30:45로 변환해서 보여주면 됨.3⃣ 데이터베이스와의 호환성문제: DB에서 created_at 필드를 올바르게 저장하려면?Supabase(PostgreSQL)에서는 TIMESTAMP 또는 TIMESTAMP WITH TIME ZONE 타입을 사용함.클라이언트에서 로컬 시간을 넣으면, DB에서 잘못 해석할 수 있음.해결책:ISO 8601 형식(2025-03-02T14:30:45.123Z)을 사용하면 DB에서 자동 변환 가능!Supabase에서 created_at이 TIMESTAMP 필드라면, new Date().toISOString()을 넣으면 자동으로 올바른 값이 저장됨.  미션TODO 항목 옆에 생성시간을 표시하기 테이블에서 생성시간을 뽑아오니 utc기준으로 보여지고 있었다. 한국시간으로 바꾸기위해 day.js 라이브러리를 활용하였다. 🤟🏻 created_at(UTC)을 로컬 시간으로 변환 로컬 시간(Local Time)이란 현재 사용자의 컴퓨터(브라우저) 또는 서버에서 설정된 시간대(Time Zone)에 맞는 시간을 의미**npm install dayjs** <p>{dayjs(todo.created_at).format("YYYY-MM-DD HH:mm:ss")}</p>  completed_at 필드를 추가하여 완료한 시간도 함께 저장하기supabase에서 compledte_at 컬럼 추가터미널에서 npm run generate-types 입력하면 types_db.ts 에 타입정보가 뜬다. completed_at 타입 추가하면 완성.//todo 업데이트 export async function updateTodo(todo: TodoRowUpdate) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("todo") .update({ ...todo, updated_at: new Date().toISOString(), completed_at: new Date().toISOString(), //추가 }) .eq("id", todo.id); // 📌 id가 일치하는 행만 업데이트 if (error) { handleError(error); } return data; } 검색 입력창 디바운스 적용하여 렌더링 개선.export default function UI() { const [searchInput, setSearchInput] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(searchInput); // 검색어 입력 시 `debounce` 적용 (100ms 딜레이) useEffect(() => { const handler = setTimeout(() => { setDebouncedSearch(searchInput); }, 100); return () => clearTimeout(handler); }, [searchInput]); const { data: todosQuery, isPending, refetch, } = useQuery({ queryKey: ["todos", debouncedSearch], // 검색어가 변경될 때마다 쿼리 새로 실행 queryFn: () => getTodos({ searchInput: debouncedSearch }), }); 회고퇴근 후 매일 1시간씩 꾸준히 공부한 내 자신을 칭찬하고 싶다. 완강을 목표로 하고 있지만, 한 단계 더 나아가 다양한 기능을 직접 구현하며 부딪혀 보고 익히고 싶다. 이를 통해 한 달 후에는 스스로 미니 프로젝트를 완성해 보는 것이 목표다.시간이 날 때 조금 더 개선해 보고 싶은 사항:리액트 훅을 hooks 폴더로 분리 → 비즈니스 로직을 컴포넌트에서 분리하여 코드의 가독성과 재사용성을 높이기리액트 쿼리 키를 별도로 관리 → 쿼리 키를 체계적으로 관리하여 유지보수성과 확장성을 강화하기

풀스택supabasenext.jsreact미션

leeebug

워밍업 클럽 스터디 3기 FS - 1주차 발자국

인프런 워밍업 클럽 스터디 3기 풀스택 과정을 시작하고 정신차려보니 벌써 금요일이 되었다.1주 차는 본격적으로 Next.js를 다루기 위한 워밍업 단계라고 생각한다. 이번주는 개발 환경을 어떻게 구축하는지와 Next.js의 주요 기능 등을 빠르게 학습했다. 또한, React Query, Recoil 등의 상태 관리 라이브러리의 기본적인 사용법도 강의를 들으면서 복기하였다.과제의 경우, 먼저 빠르게 해결할 수 있는 부분을 최대한 완성하고, 추가 기능을 구현하는 데 집중했다. 동시에 UI와 UX를 개선하는 것을 이번 주 목표로 삼아 보다 완성도 높은 결과물을 만들고자 했다. 📝 1주 차 학습SupabaseNoSQL 기반의 Firebase의 대체제로, PostgreSQL 기반의 BaaS를 제공하는 플랫폼서버리스 환경에서 빠르게 백엔드를 구축할 수 있도록 지원데이터베이스, 인증, 스토리지, 실시간 기능, 서버리스 함수, 관리 대시보드 지원Next.jsReact 기반의 풀스택 개발을 하기에 최적화 된 웹 프레임워크SSR, SSG를 지원API 라우트 기능을 제공하여 별도의 백엔드 서버 없이도 간단한 API 기능 수행이미지 최적화, 자동 코드 분할을 지원하여 개발 생산성을 높임서버 액션을 지원하여 API 라우트 없이도 클라이언트에서 서버 코드 호출 가능레이아웃 또는 컴포넌트별로 메타 데이터 설정 가능TailwindCSS유틸리티 퍼스트 CSS 프레임워크미리 정의된 클래스를 조합하여 스타일을 적용하는 방식 (tailwind.config.ts에서 커스텀 클래스 정의 가능)JIT 컴파일을 지원하여 실제로 사용된 클래스만 포함하여 CSS 파일 크기 최적화 적용반응형 디자인 지원(sm:, md:, lg: 등)다크 모드 및 테마 설정 지원(dark: )공식 및 커스텀 플러그인을 활용하여 다양한 추가 기능 제공React Query(Tanstack Query)데이터 패칭, 캐싱, 동기화 등을 쉽게 관리할 수 있게 도와주는 React 라이브러리로 주로 서버 상태 관리 용도로 사용자동 데이터 패칭, 자동 캐싱, 자동 리패칭을 제공하여 API 서버 호출을 최소화로 관리Mutation을 통한 데이터 수정쿼리 무효화를 통해 데이터를 최신 상태로 유지RecoilReact 상태 관리 라이브러리로 Context API보다 유연하고 확장성 있는 방법을 제공전역 상태 관리, 컴포넌트 간 상태 공유, 파생 상태 계산, 비동기 작업 처리 기능 제공2025.3.7 기준 약 2년간 업데이트가 이루어지지 않고 있기 때문에, Zustand, Jotai, RTK 등의 대체 라이브러리 추천 📋 1주 차 미션💬 깃헙 저장소🚀 과제 시연 영상 보러가기미션 해결 과정 요약1주 차 미션은 Next.js와 React Query 그리고 TailwindCSS를 사용하여 Todo List 앱 만들기였다. 기본적인 CRUD 기능을 구현하는 과제로, 목표 추가, 목표 수정, 완료 표시, 삭제 등의 작업을 통해 상태 관리와 비동기 데이터 처리를 익히는데 워밍업 미션으로 아주 적합하다고 생각한다. 강의에서 구현한 Todo List를 기반으로 앱을 구현하면서 총 세 가지에 초점을 맞춰서 진행하였다.CSS Transition을 사용하여 UX 개선CSS Transition을 활용하여 인터페이스의 상호작용을 부드럽고 직관적으로 처리스켈레톤 UI를 적용하여 UI와 UX 개선초기 렌더링 시 목표 리스트가 출력되는 부분에 스켈레톤 UI를 적용하여, 어느 부분에 컨텐츠가 표시될지에 대한 명확한 시각적 피드백 제공React Query 캐싱과 디바운스를 활용하여 불필요한 API 호출 감소초기 렌더링 시 데이터를 패칭하는 부분과 키워드 검색 시 데이터 패칭하는 부분이 동일한 액션을 공유하여 키워드 입력 시마다 불필요한 데이터 패칭이 지속적으로 이루어지는 문제를 인지하였음.export async function getTodos(): Promise<TodoRow[]> { const supabase = await createServerSupabaseClient() const { data, error } = await supabase .from('todos') .select('*') .order('created_at', { ascending: true }) if (error) { handleError(error) } return data ?? [] }해당 로직의 문제점은 키워드 입력시마다 즉시 API 요청이 발생하여 검색 결과가 매번 새로고침되는 증상을 발견하였고 키워드 검색 시 디바운스를 적용하여 의도적으로 API 요청을 지연시켜서 약간의 API 요청 감소 효과가 발생하였음 const [searchInput, setSearchInput] = useState('') const [debouncedSearchInput, setDebouncedSearchInput] = useState('') // 검색어 입력 디바운스 useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchInput(searchInput) }, 600) return () => clearTimeout(timer) }, [searchInput])의도적으로 600ms의 딜레이를 발생시켰음에도 여전히 검색 시 마다 API 요청이 발생하여, 키워드 입력 시마다 스켈레톤 UI가 표시되어 개선 방법을 고민하던 중에 키워드 검색 기능과 데이터 패칭 액션 기능을 분리시키기로 결정하였음 const filteredTodos = todosQuery.data?.filter((todo) => todo.title.toLowerCase().includes(debouncedSearchInput.toLowerCase()) )결론적으로 초기 렌더링 시 Todo List 데이터 패칭 액션으로 호출하는 방식으로 유지하고, 키워드 검색 기능은 캐싱된 데이터를 별도로 필터링하는 로직으로 분리하여 불필요한 API 요청이 감소하는 효과를 가져옴 👀 1주 차 회고첫 주부터 다사다난한 일주일을 보냈다. 그동안 사용했던 Next.js가 사실 React에 더 가까운 구조였음을 깨닫게 된 한 주였다. 다시 시작하는 마음으로, 시간 날 때마다 틈틈이 복습하고 복기하는 과정을 반복해야겠다는 생각이 든다.이번 워밍업 클럽의 목표는 Next.js의 심화 기능을 익히는 것과 그동안 신경 쓰지 않았던 UI와 UX 개선에 힘쓰는 것이다.스켈레톤 UI, CSS Transition, 캐싱을 활용한 API 호출을 최소화하는 방법 등을 활용하는 방법과 더불어서 React Suspense, Optimistic UI 등의 기능을 이번 워밍업 클럽 과제와 접목하여 실제 프로젝트에 적용해 볼 예정이다. 

풀스택워밍업클럽3기발자국회고과제미션

치현

[인프런 워밍업 스터디 클럽 3기 풀스택] 1주차 발자국

[풀스택 완성] Supbase로 웹사이트 3개 클론하기 (Next.js14)학습 내용 섹션 1. 오리엔테이션 ~ 섹션 3. 연습 프로젝트 - TODO LIST 만들기.이번 주는 라이브러리(or 프레임워크) 간략 소개 및 기본 문법이 주를 이룬다고 할 수 있다.라이브러리 사용법은 공식문서나 강의노트를 참고하면 좋을 것 같고,이 라이브러리(or 프레임워크)를 왜 사용하는 지를 적어두려고 한다. SupabaseSupabase는 위와 같은 장점과 단점을 가진다고 한다. 장점1. 오픈소스 프로젝트 (자체 서버구축 가능)완전한 오픈소스로 제공되어 무료로 사용 가능자체 서버에 직접 설치하고 운영 가능Docker를 통한 쉬운 배포와 구성커뮤니티 기반의 지속적인 발전과 버그 수정기업의 보안 정책에 맞춰 커스터마이징 가능2. PostgreSQL 기반 (관계형 DB 장점을 살릴 수 있다)강력한 PostgreSQL의 모든 기능 사용 가능복잡한 관계형 데이터 모델링 지원트랜잭션 및 ACID 속성 보장강력한 쿼리 최적화와 인덱싱풍부한 데이터 타입과 확장 기능외래 키 제약조건을 통한 데이터 무결성 보장3. Firebase 대비 저렴무료 티어의 관대한 제공사용량 기반의 합리적인 가격 정책자체 호스팅 시 비용 절감 가능예측 가능한 비용 구조과금 체계의 투명성불필요한 기능에 대한 과금 없음4. 다양한 연동방식 지원 (+ GraphQL, API, SDK, DB Connection)RESTful API 자동 생성GraphQL 인터페이스 기본 제공다양한 프로그래밍 언어를 위한 공식 SDK 제공JavaScript/TypeScriptPythonDart/Flutter기타 다양한 언어실시간 데이터 구독 기능직접적인 데이터베이스 연결 지원OAuth 및 소셜 로그인 통합자동 생성된 API 문서 단점. 아직 성숙하지 않은 커뮤니티 기반Firebase 대비 작은 커뮤니티 규모상대적으로 적은 레퍼런스와 예제 코드문제 해결을 위한 자료 부족2. 비교적 적은 기능들, 적은 서비스 연동 지원Firebase 대비 제한된 기능 세트서드파티 서비스 통합 옵션 부족일부 고급 기능 미지원3. 부족한 문서화, 한글 문서 부족불완전하거나 업데이트가 늦은 문서한국어 문서의 절대적 부족일부 기능에 대한 설명 부실4. Firebase보다 높은 러닝커브PostgreSQL 지식 필요초기 설정의 복잡성상대적으로 더 많은 학습 시간 요구Next.js Next.js는 내용이 방대하기에 정리라기보다는 공식문서의 Next.js에 대한 소개로 대체하겠다.Next.js란?풀스택 웹 애플리케이션을 구축하기 위한 React 프레임워크입니다. React의 기본 기능에 추가적인 최적화와 기능을 제공하며, 개발자가 복잡한 설정 대신 애플리케이션 개발에 집중할 수 있게 해줍니다.주요 기능1. 라우팅 시스템파일 시스템 기반 라우터레이아웃, 중첩 라우팅 지원로딩 상태, 에러 처리 기능2. 렌더링클라이언트/서버 사이드 렌더링정적/동적 렌더링Edge와 Node.js 환경에서의 스트리밍3. 데이터 페칭서버 컴포넌트에서 async/await 사용요청 메모이제이션데이터 캐싱과 재검증4. 스타일링CSS ModulesTailwind CSSCSS-in-JS 지원 5. 최적화이미지 최적화폰트 최적화스크립트 최적화6. TypeScript향상된 타입 체크효율적인 컴파일커스텀 TypeScript 플러그인라우터 종류App Router: 최신 라우터(v13.4.0 이후)서버 컴포넌트스트리밍 지원최신 React 기능 활용Pages Router: 기존 라우터서버 사이드 렌더링기존 프로젝트 지원안정성 검증됨 Tailwindcss유틸리티 우선(utility-first) 방식의 CSS 프레임워크로, 미리 정의된 클래스들을 조합하여 스타일링하는 방식을 제공한다.// 유틸리티 우선 접근 <!-- 기존 CSS 방식 --> <div class="chat-notification"> <p class="chat-title">새 메시지</p> </div> <!-- Tailwind 방식 --> <div class="flex items-center p-4 bg-white rounded-lg shadow-md"> <p class="text-lg font-semibold text-gray-700">새 메시지</p> </div>장점빠른 개발: 클래스 이름을 고민할 필요 없음일관성: 미리 정의된 디자인 시스템 제공커스터마이징: 설정 파일을 통한 쉬운 확장번들 크기: 사용한 클래스만 포함되어 최적화됨반응형 디자인: 내장된 반응형 클래스 제공단점긴 클래스명초기 학습 곡선 RecoilFacebook에서 개발한 React 전용 상태 관리 라이브러리(23년 이후로 깃허브 저장소가 업데이트되고 있지 않는 이슈가 있어, 다른 라이브러리가 권장된다...)주요 특징React 전용으로 설계된 상태 관리 도구간단하고 직관적인 API비동기 데이터 처리 지원상태 파생과 캐싱 기능TypeScript 지원1. Atom - 기본 상태 단위// atoms/todoAtom.ts import { atom } from 'recoil'; // 할 일 목록 상태 export const todosState = atom({ key: 'todosState', default: [], }); // 필터 상태 export const todoFilterState = atom({ key: 'todoFilterState', default: 'all', // 'all' | 'completed' | 'uncompleted' });2. Selector - 파생 상태// selectors/todoSelector.ts import { selector } from 'recoil'; import { todosState, todoFilterState } from '../atoms/todoAtom'; export const filteredTodosState = selector({ key: 'filteredTodosState', get: ({get}) => { const filter = get(todoFilterState); const todos = get(todosState); switch (filter) { case 'completed': return todos.filter((todo) => todo.completed); case 'uncompleted': return todos.filter((todo) => !todo.completed); default: return todos; } }, }); export const todoStatsState = selector({ key: 'todoStatsState', get: ({get}) => { const todos = get(todosState); const total = todos.length; const completed = todos.filter((todo) => todo.completed).length; const uncompleted = total - completed; return { total, completed, uncompleted, percentCompleted: total === 0 ? 0 : (completed / total) * 100, }; }, });3. 컴포넌트에서 사용 예시// components/TodoList.tsx import { useRecoilState, useRecoilValue } from 'recoil'; import { todosState, todoFilterState } from '../atoms/todoAtom'; import { filteredTodosState, todoStatsState } from '../selectors/todoSelector'; export function TodoList() { // 상태 읽기와 쓰기가 모두 필요한 경우 const [todos, setTodos] = useRecoilState(todosState); const [filter, setFilter] = useRecoilState(todoFilterState); // 읽기만 필요한 경우 const filteredTodos = useRecoilValue(filteredTodosState); const stats = useRecoilValue(todoStatsState); const addTodo = (text: string) => { setTodos([ ...todos, { id: Date.now(), text, completed: false, }, ]); }; const toggleTodo = (id: number) => { setTodos( todos.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ); }; return ( <div> {/* 필터 컨트롤 */} <div> <select value={filter} onChange={(e) => setFilter(e.target.value)}> <option value="all">모두 보기</option> <option value="completed">완료된 항목</option> <option value="uncompleted">미완료 항목</option> </select> </div> {/* 통계 표시 */} <div> <p>전체: {stats.total}</p> <p>완료: {stats.completed}</p> <p>미완료: {stats.uncompleted}</p> <p>완료율: {stats.percentCompleted.toFixed(1)}%</p> </div> {/* 할 일 목록 */} <ul> {filteredTodos.map((todo) => ( <li key={todo.id}> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} /> <span>{todo.text}</span> </li> ))} </ul> </div> ); }React Query(Tanstack Query)서버 상태 관리를 위한 강력한 라이브러리로, 데이터 페칭, 캐싱, 동기화, 업데이트를 효율적으로 처리1. 기본 사용법(Query)// 기본적인 쿼리 사용 function TodoList() { const { data, isLoading, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }); if (isLoading) return <div>로딩 중...</div>; if (error) return <div>에러 발생!</div>; return ( <ul> {data.map(todo => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); }2. 데이터 변이(Mutation)function AddTodo() { const mutation = useMutation({ mutationFn: (newTodo) => axios.post('/todos', newTodo), onSuccess: () => { // 캐시 무효화 및 쿼리 다시 가져오기 queryClient.invalidateQueries({ queryKey: ['todos'] }); toast.success('할 일이 추가되었습니다!'); }, }); return ( <button onClick={() => mutation.mutate({ title: '새로운 할 일' })}> 할 일 추가 </button> ); }3. 자동 캐싱 및 재검증// 캐시 시간 및 재시도 설정 const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, staleTime: 5 * 60 * 1000, // 5분 cacheTime: 30 * 60 * 1000, // 30분 retry: 3, // 실패시 3번 재시도 });4. 의존적 쿼리function UserTodos({ userId }) { // userId가 있을 때만 쿼리 실행 const { data: todos } = useQuery({ queryKey: ['todos', userId], queryFn: () => fetchUserTodos(userId), enabled: !!userId, }); }5. 무한스크롤 / 페이지네이션function InfiniteTodos() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ['todos'], queryFn: ({ pageParam = 0 }) => fetchTodoPage(pageParam), getNextPageParam: (lastPage) => lastPage.nextCursor, }); return ( <div> {data.pages.map((page) => ( page.todos.map(todo => <TodoItem key={todo.id} todo={todo} />) ))} <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? '로딩 중...' : hasNextPage ? '더 보기' : '더 이상 없음'} </button> </div> ); }장점자동 캐싱중복 요청 방지 데이터 신선도 관리메모리 효율적 관리서버 상태 동기화백그라운드 업데이트 자동 재시도에러 처리개발자 경험DevTools 제공TypeScript 지원직관적인 API성능 최적화 자동 가비지 컬렉션요청 중복 제거스마트 리페칭단점학습 곡선복잡한 개념들 (staleTime, cacheTime, invalidation 등)최적의 설정을 위한 깊은 이해 필요다양한 옵션들로 인한 초기 진입 장벽번들 크기기본 번들 사이즈가 큰 편 (약 12KB gzipped)작은 프로젝트에서는 과도할 수 있음 보일러플레이트 반복적인 쿼리 설정 코드전역 설정을 위한 추가 코드 필요TypeScript 사용 시 타입 정의 부분이 길어질 수 있음상태 관리 한계 클라이언트 상태 관리에는 부적합서버 상태 전용이라 별도의 상태 관리 도구 필요때로는 다른 상태 관리 라이브러리와 함께 사용해야 함캐시 관리 복잡성 복잡한 캐시 무효화 로직캐시 키 관리의 어려움잘못된 설정 시 메모리 누수 가능성미션 1TODO LIST 중 TODO 항목 옆 생성 시간 표시(선택) completed_at 필드를 추가하여 완료한 시간도 함께 저장1번은 수업중 todo 테이블을 만들었을 때 created_at을 받아와서 배치만 하면 끝이고,2번은 edit table을 해서 completed_at column을 추가하여 배치하면 사실상 끝이었다. 깃허브 저장소: https://github.com/kelvin-chihyun/next-inflearn/tree/main/apps/todo 회고사실 1주차는 맛보기와 같다.Next.js의 기본적인 기능(라우팅, 렌더링, 데이터 페칭 등)에 대해서는 알고 있는 편이라, 복습하는 느낌으로 보았다. 이번 풀스택 워밍업 클럽에서 개인적으로 얻고자 하는 것은 강의 내용은 베이스로 두고, 다른 기술을 사용한다거나 포인트를 두는 것이다.(강사님이 잘 가르쳐주시는 것은 당연하지만, 나 자신이 수동적으로 임하면 제대로 안할 것 같았다.) 이번 주에는 아직 한번도 사용해보지 않고 알고만 있던 Turbo Monorepo로 프로젝트를 구성하는 것을 목표로, TO DO LIST를 만들기보다는 모노레포 구성에서 나오는 여러 경로 인식 에러들을 처리하는 데에 힘을 썼던 것 같다. 배포는 어떻게 해야할지 아직 감도 안잡히지만, 이어지는 프로젝트를 수행하면서 배포도 해보려고한다. 구성 방법에 대해 이해가 된 건 아니지만, 어찌됐든 구성 자체는 의도대로 만들어서 뿌듯한 이번 주였다. (투두리스트의 기능이 다소 없는 것 같아 아쉬운 부분이 있는데, 이 부분은 추후에 디벨롭해보려고 한다.)

풀스택풀스택인프런워밍업스터디클럽3기NextSupabaseReact프론트엔드1주차발자국

채널톡 아이콘