🎁 모든 강의 30% + 무료 강의 선물🎁

블로그

이정환 Winterlood

한 입 크기로 잘라먹는 Next.js 사전 등록 이벤트

안녕하세요 이정환입니다 😃오늘은 돌아오는 월요일인 8월 19일(월) 출시되는 제 신규 강의한 입 크기로 잘라먹는 Next.js(15+)를 여러분께 소개 드림과 동시에오픈 직전 진행하는 사전 등록 이벤트를 안내드리려고 이렇게 글을 쓰게 되었습니다.간략하게 이벤트 부터 안내 드리고 그 이후에 강의를 소개하는 순서로 진행하겠습니다이벤트 안내사전등록 이벤트 | 한입 크기로 잘라먹는 Next.js8월 18일(일) 자정까지 신청하실 수 있습니다.신청해 주신 모든 분들에게 50%(반값) 할인 쿠폰을 지급해드립니다.추첨을 통해 50분께는 100%(무료) 쿠폰을 지급해드립니다.히든 이벤트 참가 방법을 안내해드립니다. 강의 소개한입 시리즈의 3번째 강의 한 입 크기로 잘라먹는 Next.js는약 15시간의 분량으로 Page Router부터 App Router 버전까지 Next.js의 모든 개념을 자세히 살펴보는 강의입니다.강의 특징특징 1. 직관적인 애니메이션 시각자료!Next.js는 복잡한 매커니즘을 갖는 기능이 많은 편입니다.따라서 쉬운 이해를 위해 애니메이션을 활용한 직관적인 시각자료를 준비했습니다!특징 2. 2배속으로 들어도 다 들리는 딕션과 발성!오랜 시간 제 목소리를 들으셔야 하는 만큼 좋은 발성과 딕션을 위해 항상 노력하고 있습니다.이를 위해 2배속으로 재생해도 다 알아 들으실 수 있게끔 강의를 제작했습니다.한번 들어보세요! 아래는 2배속으로 재생되는 한입 Next 소개 영상입니다.https://www.youtube.com/watch?v=bOpc-HU-v3Y커리큘럼 소개🌱 Section 1. Next.js를 소개합니다기술을 잘 이해하려면 그것이 어떤 배경에서 탄생했는지 알 필요가 있습니다.1섹션에서는 본격적인 학습에 앞서 Next.js라는 기술은 무엇인지오늘날 왜 이렇게 많은 관심을 받고 있는지 살펴봅니다.🌱 Section2. Page Router 핵심 정리(선택 수강) 프로젝트와 함께 Next.js 출시 초기부터 제공된 Page Router에 대해 빠르게 살펴봅니다.동시에 Page Router에 어떠한 불편함과 기술적 한계점 들이 있는지도 함께 살펴봅니다.🌱 Section 3. App Router 시작하기화제의 중심! App Router에 대해 살펴봅니다.App Router란 무엇인지, 어떤 기술적 차이가 있는지 알아보며 기본적인 사용법에 대해서도 함께 살펴봅니다.🌱 Section 4. 데이터 페칭서버 컴포넌트를 활용한 데이터 페칭에 대해 살펴봅니다.더불어 Next에서 제공하는 다양한 캐싱(데이터 캐시, 리퀘스트 메모이제이션)에 대해서도 함께 살펴봅니다.🌱 Section 5. 페이지 캐싱App Router 버전의 페이지 캐싱인 풀 라우트 캐시와 클라이언트 라우터 캐시에 대해 자세히 살펴봅니다.더불어 페이지의 캐싱 옵션을 강제로 설정하는 라우트 세그먼트 옵션에 대해서도 추가로 살펴봅니다🌱 Section 6. 스트리밍 & 에러 핸들링페이지에서 빠르게 준비되는 부분부터 바로 렌더링 하도록 도와주는 스트리밍에 대해 살펴봅니다.Loading 파일을 이용한 방식과 Suspense를 이용한 두가지 방식을 모두 살펴봅니다.또한 Error 파일을 이용한 에러 핸들링 및 페이지 복구 방법에 대해서도 살펴봅니다.🌱 Section 7. 서버 액션공개당시 뜨거운 반응을 불러일으킨 서버액션에 대해 살펴봅니다.서버액션을 이용해 데이터를 추가하거나 삭제하며 로딩 상태와 에러 상태를 처리하는 방법까지 살펴봅니다.🌱 Section 8. 고급 라우팅 패턴(패럴랠, 인터셉팅)App Router에서 새롭게 제공되는 패럴랠(병렬) 라우트와 인터셉팅(가로채기) 라우트에 대해 살펴봅니다.이를 통해 페이지 이동시에 사용자의 탐색을 방해하지 않도록 특정 페이지를 모달로 보여주는 기능을 구현합니다.🌱 Section 9. 최적화와 배포이미지, 메타데이터, 페이지, 리전 등의 최적화 방법을 살펴봅니다.최적화 된 프로젝트를 Vercel에 배포하고 한번 더 성능을 개선하는 작업을 진행합니다.닫는 말마지막으로 신규 강의를 끝까지 마무리 할 수 있도록 계속해서 응원해주신제 기존 강의의 수강생 분들 그리고 주변 지인 분들께 모두 감사드립니다.또 이 글을 보고 관심을 가져주신 모든 벨로그 여러분께도 감사드립니다.사전등록 이벤트 | 한입 크기로 잘라먹는 Next.js

프론트엔드Nextjs넥스트NextAppRouterNext13

이정환 Winterlood

한입 FE 완강 챌린지 2기를 모집합니다

🏃 시작부터 완강까지! 함께합니다.한입 FE 챌린지는 수강생 여러분들의 완강을 도와드리고자 하는 목적으로 기획되었습니다.매일 매일(일요일 및 공휴일 제외) 조금씩 강의를 완강하실 수 있도록 진도표를 제공해드리며당일 배운 내용을 바로 복습하실 수 있도록 매일 미션도 함께 제공해 드립니다.미션 검사도 당연히 제공됩니다 🫡 챌린지 강의 목록한입 FE 챌린지 2기는 다음 2개의 강의로 진행됩니다.한 입 크기로 잘라먹는 Next.js  한 번에 끝내는 자바스크립트: 바닐라 자바스크립트로 SPA 개발까지자바스크립트 학습을 염두에 두셨던 분들이라면 “한 번에 끝내는 자바스크립트” 챌린지에Next.js 학습을 염두에 두셨던 분들이라면 “한 입 크기로 잘라먹는 Next.js”에 참여하시는걸 추천드립니다. 상세 안내참여 혜택참여 리워드참가하시기만 해도 받으실 수 있는 리워드입니다.완강 의지를 불태우기 위한 특별 강의 할인 쿠폰을 제공합니다. (미 구매자 한정)완주 리워드모든 미션을 완료해야 받을 수 있는 리워드입니다.한입 FE 멘토들의 다른 강의 할인 쿠폰을 제공합니다.향후 챌린지 개설시 프리패스로 참여하실 수 있습니다.기간 및 일정모집 기간 :09월 1일 ~ 09월 07일(토) 자정까지활동 기간 :한 번에 끝내는 자바스크립트 : 09.09(월) ~ 09.27(금), 전체 기간 3주, 미션 수행일 14일한 입 크기로 잘라먹는 Next.js : 09.09(월) ~ 10.05(토), 전체 기간 4주, 미션 수행일 20일매주 일요일, 공휴일(추석 연휴 기간 포함)에는 쉽니다 😴활동 내용진도표에 맞춰 강의 수강하기하나의 강의를 선택해 완강합니다.매일 매일 체계적으로 수강하실 수 있도록 강의별 진도표를 제공합니다.커뮤니티를 통해 매일 인증합니다.퀴즈 및 과제 수행하기당일 배운 내용을 복습할 수 있는 퀴즈(or 과제)를 매일 제공합니다.커뮤니티를 통해 매일 인증합니다.커뮤니티에서 지식&경험 공유하기챌린지 참여자분들과 함께 한입 FE Discord 채널에서 소통합니다.미션 제출, 수강 인증, 스몰톡 등의 활동을 진행합니다.접수 방법https://bit.ly/4cJqGgZ위 링크로 신청해주세요 문의onebite.fe@gmail.com

프론트엔드챌린지스터디완강JSNext.jsJavaScriptNextjsNext

치현

[인프런 워밍업 스터디 클럽 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주차발자국

치현

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

학습 내용인프런 워밍업 클럽 스터디 3주차로,이번 주는 넷플릭스 프로젝트를 다루는 시간이었다.useInfiniteQuery와 Jotai(recoil 대체 전역상태 라이브러리)를 사용해볼 수 있었다.미션 3 구현 내용과제 구현 저장소Netflix 중 찜하기 관련 기능 포인트 1: favorites 테이블 추가actions/favoriteActions.ts"use server"; import { createServerSupabaseClient, handleError, PostgrestError, } from "@next-inflearn/supabase"; // Movie 타입 정의 export type Movie = { id: number; image_url: string; overview: string; popularity: number; release_date: string; title: string; vote_average: number; // Movie 타입에 favorites 필드 추가 favorites?: { // optional field로 추가 id: number; } | null; }; // SearchMoviesResponse 타입 정의 export type SearchMoviesResponse = { data: Movie[]; page: number; pageSize: number; hasNextPage: boolean; }; // 에러 케이스를 위한 타입 정의 type SearchMoviesError = { data: never[]; count: number; page: null; pageSize: null; error: PostgrestError; }; // 성공 케이스를 위한 타입 정의 type SearchMoviesSuccess = { data: Movie[]; page: number; pageSize: number; hasNextPage: boolean; }; export async function searchMovies({ search, page, pageSize, }: { search: string; page: number; pageSize: number; }): Promise<SearchMoviesSuccess> { const supabase = await createServerSupabaseClient(); // 현재 사용자 정보 가져오기 const { data: { user }, } = await supabase.auth.getUser(); // 쿼리 설정 const query = supabase .from("movie") .select( user ? ` *, favorites!left ( id ) ` : "*", // 로그인하지 않은 경우 favorites 정보를 가져오지 않음 { count: "exact" } ) .ilike("title", `%${search}%`) .order("popularity", { ascending: false }); // 로그인한 경우 현재 사용자의 즐겨찾기만 조회하도록 필터링 if (user) { query.eq("favorites.user_id", user.id); } const { data, count, error } = await query.range( (page - 1) * pageSize, page * pageSize - 1 ); const hasNextPage = count ? count > page * pageSize : false; if (error) { return { data: [], page, pageSize, hasNextPage: false, }; } // 반환된 데이터를 Movie 타입에 맞게 변환 const moviesWithFavorites = (data || []).map((movie: any) => ({ ...movie, favorites: movie.favorites?.[0] || null, // 즐겨찾기 정보 포함 })); return { data: moviesWithFavorites, page, pageSize, hasNextPage, }; } export async function getMovie(id: string) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("movie") .select("*") .eq("id", id) .maybeSingle(); handleError(error); return data; } 포인트 2: profiles 테이블 추가utils/AuthProvider.tsx"use client"; import { useEffect } from "react"; import { createBrowserSupabaseClient } from "@next-inflearn/supabase"; import { useSetAtom } from "jotai"; import { userAtom } from "@/utils/jotai/atoms"; export function AuthProvider({ children }: { children: React.ReactNode }) { const setUser = useSetAtom(userAtom); const supabase = createBrowserSupabaseClient(); useEffect(() => { // 현재 세션 확인 supabase.auth.getSession().then(({ data: { session } }) => { setUser(session?.user ?? null); }); // Auth 상태 변경 구독 const { data: { subscription }, } = supabase.auth.onAuthStateChange((_event, session) => { setUser(session?.user ?? null); }); return () => subscription.unsubscribe(); }, [supabase, setUser]); return children; } 포인트 3: movies / favorites / profiles 테이블 연결favorites 테이블 내 profiles 테이블 내 회고 시간이 부족하다는 핑계로,더 디벨롭할 수 있는 부분이 많음에도 불구하고 생각했던 것들을 다 구현하진 못했던 것 같다.다만, 이렇게 틀을 갖춰놧으니 추후에 추가적인 기능을 정의해서 구현해보기 너무 좋을 것 같다.  

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

이수진

[인프런 워밍업 클럽 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

치현

[인프런 워밍업 스터디 클럽 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주차발자국

채널톡 아이콘