블로그

codestudy

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

4주차 학습 내용강의 및 학습 내용React/Next.js 기본 레이아웃 구성Flex 속성 활용: flex, justify-center, items-center 등의 특성 학습컴포넌트 구조화 방법과 레이아웃 관리테일윈드 CSS를 활용한 그라데이션 배경 적용bg-gradient-to-r from-cyan500 to-blue500 등의 문법 활용로그인/회원가입 화면 구현SetView 상태관리를 통한 화면 전환 구현사용자가 로그인과 회원가입 사이를 전환할 수 있도록 기능 개발이메일과 패스워드 입력폼 설계 및 유효성 검사 구현Supabase 활용Supabase Auth를 통한 인증 시스템 구현메시지 테이블 설계와 컬럼(id, message, sender, receiver, is_deleted, created_at) 구성Supabase Realtime을 활용한 실시간 채팅 기능 구현RLS(Row Level Security) 정책 설정으로 데이터 보안 강화채팅 기능 구현메시지 전송 및 수신 기능 개발메시지 상태(읽음/안읽음) 관리를 위한 is_read 필드 활용메시지 삭제 기능 구현(is_deleted 필드 활용)UUID를 사용한 사용자 식별 및 메시지 보내기/받기 구현디자인 및 UI 개선Material Tailwind 활용className 속성을 통한 스타일링 관리반응형 디자인 구현(모바일 최적화)유저 아바타 구현을 위한 랜덤 이미지 API 활용학습 회고Instagram 클론코딩 4주차에서는 채팅 기능 고도화를 진행했습니다.Supabase의 실시간 데이터 처리 기능을 활용해 메시지 전송, 수신, 상태 관리를 구현했습니다.로그인/회원가입 페이지의 UI를 개선하고 테일윈드 CSS로 그라디언트 배경 효과를 적용했습니다.React와 Next.js, Supabase를 조합해 백엔드 인프라 없이도 강력한 기능을 빠르게 구현하는 현대적인 웹 개발 방식을 경험했습니다.실시간으로 채팅이 된다는 점이 너무 재밌었습니다.🛠 미션 해결 과정채팅 기능 고도화 미션 구현구현 과정데이터베이스 테이블 설계:message 테이블 확장: is_read, is_deleted 필드 추가blocked_users 테이블 생성: blocker_id, blocked_id 필드 포함reports 테이블 생성: reporter_id, reported_id, message_id, reason 필드 포함각 테이블에 적절한 외래 키(Foreign Key) 설정Row Level Security 정책 구현:메시지 읽기, 쓰기, 업데이트 권한 설정"사용자가 수신한 메시지 읽음 표시 가능" 정책 추가차단 및 신고 기능에 대한 사용자별 접근 권한 설정메시지 삭제 기능 구현:Message 컴포넌트에 삭제 버튼 추가is_deleted 필드를 업데이트하는 서버 액션 구현삭제된 메시지는 UI에서 필터링하여 표시하지 않도록 처리메시지 읽음/안읽음 표시 기능:is_read 필드를 이용해 메시지 상태 관리메시지를 읽을 때 자동으로 상태 업데이트UI에 읽음 상태 표시 (읽음/안읽음)사용자 차단 기능 구현:사용자 차단 버튼 및 확인 모달 추가blocked_users 테이블에 차단 정보 저장차단된 사용자와의 메시지 교환 제한메시지 신고 기능 구현:부적절한 메시지 신고 버튼 및 신고 사유 입력 모달 추가reports 테이블에 신고 정보 저장신고 후 사용자 피드백 제공실시간 업데이트 기능 개선:Supabase Realtime을 통한 INSERT와 UPDATE 이벤트 동시 구독메시지 상태 변경 시 실시간으로 UI 업데이트새 메시지 알림 구현로그인/회원가입 페이지 버그 수정:화면 전환 로직 오류 수정회원가입/로그인 버튼 기능 정상화 미션 해결 회고구현 내용데이터베이스 테이블 설계: message, blocked_users, reports 테이블 구성Row Level Security 정책 구현: 메시지 읽기/쓰기/업데이트 권한 설정메시지 기능 구현:메시지 삭제 기능읽음/안읽음 표시 기능사용자 차단 기능메시지 신고 기능Supabase Realtime을 통한 실시간 업데이트 기능 개선로그인/회원가입 페이지 버그 수정배운 점Supabase RLS(Row Level Security) 정책 설정을 통한 데이터베이스 보안 구현 방법WebSocket 기반 Supabase Realtime을 활용한 실시간 UI 업데이트 구현TypeScript를 활용한 타입 안전성 확보Supabase를 활용한 서버리스 백엔드 구축의 효율성실시간 기능이 사용자 경험에 미치는 중요성 

풀스택supabase워밍업3기

이수진

[인프런 워밍업 클럽 Full-Stack 3기] 4주차 발자국 - 인스타그램 클론코딩

이번주는 강의 볼륨이 제일 많았던 인스타그램 클론코딩을 진행했다. 로그인, 회원가입을 위한 Supabase Auth와 실시간 채팅을 구현하기 위한 Supabase Realtime Database 배포까지 구현해볼 수 있었다. 이번시간엔 거의 다른 주차에 비해 2배정도 되는? 양인거 같아 월요일부터 틈틈히 해서 겨우 시간을 맞출 수 있었다. 수강 내용Section 6 인스타그램 클론코딩 - Supabase 인증 구현 Part 1이번 챕터에서는 Supabase 인증 시스템을 구현해보는 것이었다. 다음과 같은 기능을 구현했다.이메일 인증을 통한 회원가입OTP 인증을 통한 회원가입로그인(추가) 카카오 소셜 로그인이는 Supabase Auth를 통해 구현했다. 강의에서는 위의 내용들만 구현했지만 Supabase Auth에는 더 다양한 기능들을 제공하고있었다. JWTSession 등을 지원하기도했고, 권한관리 등도 제공했다.그리고 강의를 들으면서 느꼈던 것이지만 React Query를 잘 사용하셔서 React Query의 활용성에 대해서도 한번 더 성장한다는 느낌을 받았다.Section 7 인스타그램 클론코딩 - 인스타 DM 채팅 기능 구현 Part 2인증 기능을 모두 마친 뒤에는 실시간 채팅 기능을 구현했다. 이 시간에 Supabase Realtime Database를 학습할 수 있었다. 사실 그냥 Database랑 어떤 차이인지는.. 잘 모르겠지만 해당 기능을 켰을 때 실시간으로 채팅 기능을 수현할 수 있었다. 이때에도 React Query를 이용해 데이터를 캐싱하고 다시 불러오는 작업을 함으로써 좀 더 효율적으로 Supabase Table 데이터들을 관리할 수 있었다. 그리고 전에도 얘기했던것이지만 Database라서 그런지 메서드 자체가 SQL문을 그대로 가져와서 학부시절 DB를 배워놨었던 부분이 이해하기도, 적응하기에도 좀 더 편했었다.export async function getAllMessages({ chatUserId }: { chatUserId: string }) { const supabase = await createServerSupabaseAdminClient(); const { data, error } = await supabase.auth.getSession(); if (error && !data?.session) { throw new Error("User is not authenticated"); } const { data: messages, error: messagesError } = await supabase .from("message") .select("*") .or(`receiver.eq.${chatUserId},receiver.eq.${data?.session?.user.id}`) .or(`sender.eq.${chatUserId},sender.eq.${data?.session?.user.id}`) .order("created_at", { ascending: true }); if (messagesError) { throw new Error("Failed to get messages"); } return messages; } const { data: messages, error: messagesError, isLoading: messagesLoading, refetch, } = useQuery({ queryKey: ["messages", selectedIndexState], queryFn: async () => { const allMessages = await getAllMessages({ chatUserId: selectedIndexState, }); return allMessages; }, });useEffect(() => { const channel = supabase .channel("message_postgres_changes") .on( "postgres_changes", { event: "INSERT", schema: "public", table: "message", }, (payload) => { console.log(payload); } ) .subscribe(); return () => { channel.unsubscribe(); }; }, []);그리고 메시지를 주고받고 한 다음 채팅을 그냥 보여주기만 하면 실시간으로 동기화가 안된다는 문제점이 있어 (새로고침을 해야 보여진다.) 그 때를 위한 동기화 작업을 supabase channel기능을 사용했다.Section 8 웹사이트 배포하기 - 도메인 등록, Vercel, AWS 배포Vercel 배포같은 경우는 개인적으로 포트폴리오 배포할 때 유용하게 사용했었고 AWS배포도 한번쯤은 해보는것이 좋다고 해서 해본적이 있어서 좀 가볍게 들었다. 그리고 배포 시 빌드 에러를 최소화하기 위해 항상 push하기 전에 build한번 하고 push를 해서 빌드 오류도 없이 무난하게 모든 프로젝트를 배포할 수 있었다.미션4주차 미션은 다음과 같았다.작성한 모든 프로젝트 배포하기채팅 메시지 삭제 기능 구현채팅 읽음, 안읽음 표시 기능 구현삭제기능, 읽음/안읽음 표시 기능을 위해 supabase data table에 is_readis_deleted 항목을 추가하고 각 항목들을 update하는식으로 구현했다. 그리고 동기화를 위해 channel에 update 항목을 추가해주는걸로. 인터넷에서 찾아보니 *을 이요해 모든 이벤트 INSERTDELETEUPDATE 등을 포함할 수 있다고 했지만 그냥 업데이트만 하나 추가해주었다. ui는 그렇다 치고.. 일단 삭제 기능이랑 읽음 기능까지는 구현해봤다.마무리인스타그램 클론코딩을 끝으로 이렇게 4주간의 대장정이 마무리가됬다. Supabase 프로젝트 한번 해보겠다고 들을까 말까 했던 강의였는데 이렇게 다같이 스터디할 수 있는 기회가 생겨서 나 혼자 했으면 흐지부지 되었을텐데 이렇게 끝까지 완강할 수 있어서 개인적으로 뜻깊었던 스터디 기간이었다.강의 내용도 부실하지 않고 적당히 그리고 자세하게 알려주셔서 내용을 이해하기도 쉬웠다. 또한 Supabase 자체에 국한되지 않고 Next.js나 React Query같은 프론트엔드 지식도 함께 쌓아 더 괜찮은 시간이었다. 지금 이 토대를 기반으로 다음에 할 Supabase를 통한 개인 프로젝트도 화이팅해야겠다!

웹 개발웹개발프론트엔드백엔드supabase

찬우 이

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

4주차 학습 내용Part 1. Git Repository 생성 및 초기 설정 진행이번 프로젝트는 기존 강의에서 학습했던 방식대로 GitHub에 레포지토리를 생성하고, 초기 설정을 진행하며 시작했다.폴더 구조와 기본적인 세팅은 이전과 동일하게 구성하여 빠르게 시작할 수 있었다.새롭게 만든 message 테이블에는 다음과 같은 컬럼들을 추가했다.id: 기본 키 (primary key)message: 메시지 내용sender: 보낸 사람의 UUIDreceiver: 받는 사람의 UUIDis_deleted: 삭제 여부 (boolean)created_at: 생성 시간 (timestamp) Part 2. 회원가입, 로그인 화면 제작이전에 하던대로 틀 잡고, components로 구분지으면서 제작했다.새롭게 배운 부분으로는 배경색을 그라데이션으로 준 부분이였다.  테일윈드 css로 편하게 gradient값을 줬다.사용법bg-gradient-to-r: 그라디언트를 오른쪽 방향으로 흐르게 함 / ex) to-r = to right  from-{color}: 그라디언트의 시작 색상 지정 / ex) from-green-400 to-{color}: 그라디언트의 끝 색상 지정 / ex) to-blue-500via-{color}: 그라디언트의 중간 색상 지정 / ex) via-pink-500  Part 3. Supabase Auth 소개 및 인증방식 기획Supabase Auth는 이메일 기반 인증부터 소셜 로그인까지 다양한 인증 방식을 제공하며, 회원가입, 로그인, 세션 유지 등을 쉽게 처리할 수 있는 백엔드 인증 서비스다.1. Confirmation URL 방식사용자 이메일로 인증 링크를 전송하고, 사용자가 해당 링크를 클릭함으로써 인증이 완료되는 방식이다.사용자가 회원가입(또는 로그인)을 하면, 이메일로 인증 링크가 전송되고사용자가 해당 링크를 클릭하면 Supabase가 사용자의 인증을 완료함  2. 6-Digit OTP 방식사용자 이메일로 6자리 숫자 코드(OTP)를 전송하고 사용자가 해당 코드를 입력해서 인증하는 방식  사용자가 이메일을 입력하면, Supabase는 해당 이메일로 6자리 OTP 코드를 전송하고사용자는 입력창에 이 코드를 입력하고 인증을 완료하게 된다.  Part 6. 채팅 화면 구현1. 유저 아바타 랜덤 이미지 사용https://randomuser.me/photos API를 활용해 유저 프로필 사진을 랜덤으로 가져옴별도 이미지 업로드 없이 테스트용 아바타를 빠르게 구현할 수 있음  const randomImage = https://randomuser.me/api/portraits/men/${index}.jpg; 2. javascript-time-ago 라이브러리채팅 메시지 시간 표시를 "1분 전", "3시간 전"처럼 사람이 보기 쉬운 형태로 변환국제화(i18n)도 지원함 (예: 한글/영어/중국어 등)import TimeAgo from "javascript-time-ago"; import ko from "javascript-time-ago/locale/ko.json"; TimeAgo.addDefaultLocale(ko); const timeAgo = new TimeAgo("ko"); timeAgo.format(new Date()) // → "방금 전" Part 7. Supabase Realtime 소개 & 채팅목록 구현 Supabase Realtime은 PostgreSQL 데이터베이스의 변경 사항을 실시간으로 감지해서 클라이언트에 push해주는 기능.기본적으로 WebSocket을 기반으로 작동하고, 내부적으로는 PostgreSQL의 logical replication 기능을 활용한다. 🔧 작동 방식클라이언트가 WebSocket으로 채널 생성.channel() 메서드를 이용해서 원하는 테이블과 이벤트 종류를 구독함예: message 테이블의 INSERT 이벤트Supabase 서버에서 PostgreSQL의 변경 스트림 감지PostgreSQL에서 발생하는 INSERT, UPDATE, DELETE 이벤트를 감지이를 wal2json 등의 logical decoding plugin을 통해 JSON으로 변환Supabase Realtime 서버가 이를 브로드캐스트WebSocket 연결된 클라이언트에게 변경된 데이터(payload)를 push로 전달함프론트에서 실시간 UI 반영받은 payload로 상태를 업데이트하거나 refetch()를 통해 데이터를 다시 불러와서 렌더링Part 9. 배포하기 vercel 배포Add New 파일을 열고 프로젝트를 선택해준다. 나의 깃허브와 연동시켜주고 배포를 원하는 프로젝트를 선택해준다 Deploy를 누르면 배포가 되게 되는데 그전에 프로젝트의 이름을 정하고,프로젝트에서 npm run build를 해서 배포시에 발생할 에러가 있는지 미리 체크한다.그리고 환경변수에는 프로젝트에 있는 .env 파일에 있는 코드를 복사해서 넣어줘야한다. 사진과 같은 에러가 발생했고, 현재 에러를 해결하는 방법은 2가지가 있다.하나는 직접 에러의 원인을 찾아 제거하는 방식 = <Spinner onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />)}둘째는 any로 지정하는것 처럼 무시하고 지나가라는식의 방법으로 해결하는 방법이다.루트에 index.d.ts를 만들고, declare module "@material-tailwind/react";를 해줌으로써 material-tailwind로 발생하는 에러는 넘겨주는 것. 배포가 끝났고 이제 휴대폰에서도 정상적으로 동작한다!  미션: 그동안 만든 4개 프로젝트 배포하기선택 미션에는 채팅 메세지 삭제 기능 이나 채팅 읽음, 안 읽음 표시 등 추가 작업인데 우선 후순위로 미루고 배포에 집중함.  미션 이외로미션 이외로 나는 휴대폰으로 채팅하는 모습을 확인하고 싶어서 배포를 진행하고 휴대폰을 확인했다.하지만 이메일 인증을 하고 이동하는 코드는 아래처럼 되어 있어서 연결이 깨지게 되어 있다. const signupMutation = useMutation({ mutationFn: async () => { const { data, error } = await supabase.auth.signUp({ email, password, options: { emailRedirectTo: "http://localhost:3000/signup/confirm", }, }); if (data) setConfirmationRequired(true); if (error) alert(error.message); }, });그렇다고 여기서 저부분을 배포 주소로 바꾸자니, 나중에 버그나 새로운 기능을 넣기 위해 작업할때는 또 번거롭게 바꿔줘야 하기때문에 이 문제를 어떻게 해결할지에 대한 고민을 했다. const redirectUrl = process.env.NEXT_PUBLIC_REDIRECT_URL || (process.env.NODE_ENV === "development" ? "http://localhost:3000" : "https://supabase-instagram-clone.vercel.app"); const signupMutation = useMutation({ mutationFn: async () => { const { data, error } = await supabase.auth.signUp({ email, password, options: { emailRedirectTo: `${redirectUrl}/signup/confirm`, // ✅ 변수 사용 }, }); if (data) setConfirmationRequired(true); if (error) alert(error.message); }, });우선 리다이렉트를 구분하기 위해 로컬호스트에서는 로컬호스트를 보내고 수퍼베이스에서는 수퍼베이스만 보내도록 수정함.버셀에서 환경변수를 추가해주고수퍼베이스에서도 배포한 링크를 연결시켜주니 해결했다.말은 쉬운데 사실 해결하려고 몇시간을 싸매고 한 결과다..ㅠ성공한 사진!!🎉🎉UI적으로는 다듬지 못해서 엉망인 모습을 보이고, 남들이 보기엔 별거 아닐지라도 나한테는 휴대폰과 노트북으로 서로 소통을 하는게 이렇게 돌아가는구나 라는것을 느끼고 되게 신기했다.---📝 마지막 회고4주차는 지금까지 중 가장 힘든 한 주였던 것 같다.평소엔 기능을 하나씩 구현해왔는데 이번 주차는 로그인과 채팅 기능을 함께 다루다 보니 정신없고 벅찼다.점점 기능 난이도가 깊어지고 있다는 느낌도 들어서, 생각보다 어렵게 다가왔다.특히 이번 주에는 코드를 따라 치긴 했지만 이해가 부족했던 순간들도 많았고주말에 큰 약속이 있어서 과제를 빨리 끝내야 한다는 부담감 때문에 급하게 하다 보니 놓친 부분도 많았던 것 같다.그렇게 어느새 4주차.. 이 스터디의 마지막 주차가 되어버렸다.돌이켜보면 한 달 전의 나와 비교해 분명 많이 성장한 시간이었다. 📌 이번 한 달간, 내가 얻은 것들처음엔 next.js와 tailwind를 조금만 다룰 줄 알았고 전역 상태 관리도 zustand 정도만 써봤었다.하지만 이번 강의를 통해→ next.js의 프로젝트 구조 잡는 방식→ tailwind를 더 효율적으로 사용하는 법→ 그리고 새롭게 배운 recoil, supabase, server actions, vercel 배포까지!정말 다양한 기술들을 직접 써보면서 익힐 수 있었다. 🙃 좋았던 순간, 힘들었던 순간강사님 코드랑 똑같이 썼는데도 에러가 나서 괜히 억울했던 순간,반대로 내가 직접 구현한 기능이 잘 작동해서 뿌듯했던 순간도 있었다.물론 한 번 배웠다고 해서 금방 능숙하게 다루진 못하지만이 강의를 통해 전반적인 흐름을 훑을 수 있었고이제는 Supabase를 활용해 어떤 기능을 만들어볼까? 상상해보는 재미도 생겼다. 🎉 마지막으로다음엔 이번 주차에서 만들었던 인스타그램 클론을 좀 더 확장해서게시물 기능까지 구현해보고 싶은 욕심도 있다.좋은 아이디어가 생긴다면? 더 멋있는 기능을 섞어서 구현해보고 싶다...어쨌든, 이번 스터디는 정말 기억에 남는 한 달이었다.한 달 동안 함께 완주해온 스터디 분들 정말 수고 많았고,좋은 강의 만들어주신 강사님께도 진심으로 감사합니다! 🎉

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

찬우 이

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

3주차 학습 내용Part 1. Git Repository 생성 및 초기 설정 진행이전과 동일하게  npx create-next-app@14 inflearn-supabase-netflix-clone 해주고, 2주차의 코드를 가져왔다.테이블의 column을 지정해주고, 강사님께서 가져오신 영화 데이터를 받아 DB를 구성했다. Part 2. UI 작업이번에는 다이나믹 라우트를 사용해서 포스터를 클릭하면 해당 포스터의 id를 들고 상세페이지로 이동한다. 사용법은 간단하게 대괄호를 열고 닫은 폴더명을 사용하면 됨.그거 말고는 전체적으로 예제와 같게 UI작업을 했고, 이전 드롭박스때 처럼 grid로 간단하게 반응형을 구현해줌.  Part 3. 영화 검색 기능 & 영화 개별 상세페이지 구현🌀 Recoil 사용 방법✅ 1. 설치먼저 Recoil을 설치한다: npm install recoil✅ 2. 서버 컴포넌트(layout.tsx)에 직접 쓰면 ❌Recoil은 클라이언트 사이드 전용 라이브러리이기 때문에layout.tsx에서 바로 사용하면 에러가 발생한다.✅ 3. RecoilProvider 따로 만들어 감싸기config/RecoilProvider.tsx 파일을 만들고, 아래와 같이 구성한다// config/RecoilProvider.tsx "use client"; import { RecoilRoot } from "recoil"; export default function RecoilProvider({ children }: React.PropsWithChildren) { return <RecoilRoot>{children}</RecoilRoot>; } ✅ 4. 전역 상태 정의 (atoms.ts)utils/recoil/atoms.ts에 전역 상태를 선언한다:// utils/recoil/atoms.ts import { atom } from "recoil"; export const searchState = atom({ key: "searchState", default: "", }); ✅ 5. 컴포넌트에서 사용하기import { useRecoilState } from "recoil"; import { searchState } from "utils/recoil/atoms"; const [search, setSearch] = useRecoilState(searchState); Part 4. 무한 스크롤 기능 구현하기 & 더 나은 검색을 위한 SEO 작업하기 핵심 포인트react-query의 useInfiniteQuery 사용react-intersection-observer로 마지막 요소 감지해서 추가 데이터 불러오기🔧 구현 흐름useInfiniteQuery에서 pageParam으로 현재 페이지 관리searchMovies()를 호출해서 검색어와 페이지 정보를 넘김getNextPageParam으로 다음 페이지 조건 처리마지막 아이템에 ref 붙여서 화면에 보이면 자동 로딩 "use client"; import { useInfiniteQuery } from "@tanstack/react-query"; import MovieCard from "./movie-card"; import { searchMovies } from "actions/movieActions"; import { Spinner } from "@material-tailwind/react"; import { useRecoilValue } from "recoil"; import { searchState } from "utils/recoil/atoms"; import { useInView } from "react-intersection-observer"; import { useEffect } from "react"; export default function MovieCardList() { const search = useRecoilValue(searchState); const { data, isFetching, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteQuery({ initialPageParam: 1, queryKey: ["movie", search], queryFn: ({ pageParam }) => searchMovies({ search, page: pageParam, pageSize: 12 }), getNextPageParam: (lastPage) => lastPage.page ? lastPage.page + 1 : null, }); const { ref, inView } = useInView({ threshold: 0, }); useEffect(() => { if (inView && hasNextPage && !isFetching && !isFetchingNextPage) { fetchNextPage(); } }, [inView, hasNextPage]); useEffect(() => { console.log(inView); }, [inView]); return ( <div className="grid gap-1 md:grid-cols-4 grid-cols-3 w-full h-full"> {isFetching || (isFetchingNextPage && <Spinner />)} <> {data?.pages ?.map((page) => page.data) ?.flat() ?.map((movie) => ( <MovieCard key={movie.id} movie={movie} /> ))} <div className="h-1" ref={ref}></div> </> </div> ); } 🔧 구현 흐름Next.js의 generateMetadata()를 사용해서페이지별로 동적으로 메타 태그 생성. 💡 포인트페이지 타이틀에 영화 제목 자동 반영설명은 영화 overview에서 가져옴OG 이미지도 함께 등록해서 링크 공유 시 썸네일 출력됨export async function generateMetadata({ params, searchParams }) { const movie = await getMovie(params.id); return { title: movie.title, description: movie.overview, openGraph: { images: [movie.image_url], }, }; }  미션: 북마크 기능 만들기미션: 찜하기 기능을 만들어서, 찜한 영화를 영화 리스트 화면의 최상단에 보여주기원래는 영화 리스트 화면에서 북마크한 영화를 최상단에 보여주는 미션이었지만,실제 사용자 입장에서 불편할 것 같아서 헤더에 "bookmark" 메뉴를 따로 만들고해당 페이지에서 북마크한 영화만 모아보는 방식으로 약간 변형해서 구현했습니다. 1. Supabase 테이블에 bookmark 추가해주기Supabase의 movie 테이블에 bookmark라는 boolean 컬럼을 추가하고,기본값을 false로 설정하기 위해 SQL Editer에서 UPDATE movie SET bookmark = false WHERE bookmark IS NULL;를 입력해줌 2. 북마크 토글 함수 생성// actions/movieActions.ts export async function toggleBookmark(id: number, current: boolean) { const supabase = await createServerSupabaseClient(); const { error } = await supabase .from("movie") .update({ bookmark: !current }) // 현재 값 반대로 토글 .eq("id", id); // 해당 ID만 업데이트 handleError(error); } movieActions에서 북마크 토글을 지원하는 함수를 만들어줌. 3. 상세 페이지에서 북마크 버튼 만들기// app/movies/[id]/ui.tsx "use client"; import { toggleBookmark } from "actions/movieActions"; import { useState, useTransition } from "react"; import { BookmarkIcon } from "@heroicons/react/24/outline"; import { BookmarkIcon as BookmarkSolidIcon } from "@heroicons/react/24/solid"; export default function MovieDetail({ movie }) { const [bookmarked, setBookmarked] = useState(movie.bookmark); const [isPending, startTransition] = useTransition(); const handleClick = () => { setBookmarked((prev) => !prev); // UI 먼저 변경 startTransition(async () => { await toggleBookmark(movie.id, bookmarked); // 서버에 실제 반영 }); }; return ( <div> <h1>{movie.title}</h1> <button onClick={handleClick} disabled={isPending}> {bookmarked ? ( <BookmarkSolidIcon className="w-6 h-6 text-yellow-500" /> ) : ( <BookmarkIcon className="w-6 h-6 text-gray-500" /> )} </button> </div> ); } npm install @heroicons/react을 설치해서 토글 아이콘을 생성함.2번에서 만든 함수를 연결해서 북마크를 키고 끄면 DB에도 연동되게 만들었음. 4. 북마크한 영화만 보여주는 페이지 만들기// actions/movieActions.ts export async function getBookmarkedMovies() { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("movie") .select("*") .eq("bookmark", true) // bookmark가 true인 영화만 가져옴 .order("id", { ascending: true }); // id 순으로 정렬 handleError(error); return data; }movieActions에서 북마크한 영화만 보여주는 함수를 만들어줌. // app/movies/bookmark/page.tsx import MovieCard from "@/components/movie-card"; import { getBookmarkedMovies } from "actions/movieActions"; export default async function BookmarkPage() { const movies = await getBookmarkedMovies(); return ( <div className="grid grid-cols-3 gap-4"> {movies.map((movie) => ( <MovieCard key={movie.id} movie={movie} /> ))} </div> ); }header에서 북마크를 클릭하면 북마크 페이지로 이동하게 만들었고위에서 만든 북마크 영화 함수를 사용해서 북마크한 영화만 보여주도록 만들었음. 아쉬운 부분무한스크롤이 아직 조금 어려워서 북마크 페이지에서는 아직 구현해지 못했다.추후에 좀 더 공부해보고 넣어주면 좋을 것 같다!회고이번 주차 수업 중에서는 무한 스크롤이 가장 어려웠지만,과제는 이전 주차처럼 단순히 '딸깍하면 되는 문제'가 아니어서"어떻게 구현할까?" 부터 고민하는 재미가 있었다.Supabase에서 테이블을 직접 작성하고,서버 액션에서 원하는 기능을 하는 함수를 만들고,그 함수를 페이지에 붙여서내가 의도한 대로 동작하는 걸 확인했을 때 정말 재밌었다.뭔가 시간이 지나면서 점점 supabase와 친해지는 느낌을 받았다.엄청 어렵지는 않지만,약간만 생각하면 풀리는 적당한 난이도라서 더 좋았던 과제였다.

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

codestudy

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

Netflix 클론코딩 - 영화 검색 서비스 1. 프로젝트 기본 설정 및 구조기술 스택Next.js 기반 프레임워크React 컴포넌트 시스템TypeScript 타입 정의Tailwind CSS 스타일링Supabase 데이터베이스TMDB API (영화 데이터 소스)프로젝트 구조config, app, components, utils 폴더 구성페이지 라우팅 설정환경 변수(.env) 구성2. 데이터베이스 구축Supabase 설정TMDB에서 가져온 60개의 영화 데이터 활용CSV 파일 업로드 방식으로 데이터 임포트영화 테이블 스키마 구성:제목, 설명, 이미지 URL, 평점, 인기도, 개봉일 등데이터 타입 설정 (float, string, nullable 등)3. UI 컴포넌트 개발헤더 컴포넌트상단 고정 네비게이션 바로고 및 네비게이션 메뉴 (Movies, Dramas)검색 기능 UI 구현푸터 컴포넌트하단 고정 레이아웃저작권 및 출처 정보 표시영화 카드 컴포넌트그리드 시스템으로 반응형 레이아웃 구현MD 사이즈에서 4개, 기본 사이즈에서 3개 카드 표시호버 효과 및 트랜지션 적용영화 상세 페이지동적 라우팅을 통한 개별 영화 페이지영화 포스터, 제목, 설명, 평점, 인기도, 개봉일 표시4. 데이터 관리 및 API 연동React Query 활용useQuery hook으로 데이터 페칭 및 캐싱로딩 상태 및 에러 처리Recoil 상태 관리atom을 활용한 검색어 상태 관리useRecoilState, useRecoilValue 등의 훅 활용Supabase API 연동영화 목록 조회 (getMovies)개별 영화 상세 정보 조회 (getMovie)검색 기능 구현 (SearchMovies)5. 고급 기능 구현무한 스크롤React Intersection Observer 활용useInfiniteQuery로 페이지네이션 구현Range Query를 통한 효율적인 데이터 로딩페이지 단위로 12개 항목씩 로드검색 기능실시간 검색어 상태 관리Supabase LIKE 쿼리를 활용한 검색 기능검색 결과 렌더링데이터 최적화flatten을 통한 배열 데이터 처리페이지 상태 관리 (hasNextPage, isFetching 등)6. SEO 최적화메타데이터 관리generateMetadata 함수 구현동적 메타데이터 생성영화 제목, 설명, 이미지 정보 포함소셜 미디어 공유 최적화오픈 그래프 태그 추가트위터 카드 설정카카오톡 공유 시 이미지와 설명 표시 최적화7. 학습 회고이 Netflix 클론 프로젝트를 통해 Next.js의 App Router와 TypeScript, Supabase를 결합한 풀스택 개발을 경험했습니다. 컴포넌트 아키텍처: 서버 컴포넌트와 클라이언트 컴포넌트를 적절히 분리하는 Next.js의 패턴을 익힘데이터 페칭 최적화: React Query를 활용한 효율적인 데이터 관리와 무한 스크롤 구현 방법 서버리스 백엔드: Supabase를 활용한 데이터베이스 관리와 서버 액션으로 별도 백엔드 없이 기능 구현복습을 꾸준히 하고 supabase의 다양한 기능을 습득하도록 해야겠습니다.  🛠 미션 해결 과정찜 기능 구현 과정 및 회고구현 과정Supabase 테이블 설계:favorites 테이블 생성: id, movie_id, device_id, created_at 필드 포함Foreign key는 설정하지 않고 간단한 구조로 진행클라이언트-사이드 식별자 구현:deviceId.ts 유틸리티 생성: 사용자 브라우저를 식별하기 위한 고유 ID 생성 및 로컬 스토리지에 저장로그인 없이도 각 사용자의 즐겨찾기를 식별할 수 있는 방법 제공서버 액션 구현:favoriteActions.ts: 즐겨찾기 추가/제거 및 목록 조회 기능 구현searchMovies 함수 수정: 즐겨찾기 정보 포함 및 즐겨찾기 항목 상단 정렬 로직 추가UI 컴포넌트 수정:MovieCard 컴포넌트에 하트 아이콘 추가: Font Awesome과 Material Tailwind 사용찜 상태에 따라 아이콘 스타일 변경 (빨간색/회색)클릭 이벤트 처리 및 비동기 동작 구현타입 관리:기본 Movie 타입 확장하여 MovieWithFavorite 인터페이스 정의TypeScript 타입 오류 해결을 위한 전략 수립미션 해결 회고로그인 없이 사용자별 데이터를 관리하기 위해 기기 식별자를 활용한 접근법을 알게되어 활용해보았습니다. Supabase로 찜한 정보를 저장하고 관리하는 과정에서 서버리스 데이터베이스의 강력함을 경험했습니다.타입스크립트의 인터페이스 확장을 통해 is_favorite 속성을 추가하며 타입 시스템의 중요성에 대해 또 느꼈습니다.원하는 기능이 있을때마다 supabase에 테이블을 추가하여 활용하는 것이 신기했습니다.

풀스택인프런워밍업풀스택supabase

이수진

[인프런 워밍업 클럽 Full-Stack 3기] 3주차 발자국 - Netflix 클론코딩

이번주는 Netflix 클론코딩에 관한 내용이었다. Netflix 클론코딩이라던지 무한스크롤 구현이라던지 하는 내용은 이전에도 다른 강의에서 자주 나왔던 항목이어서 그런지 들을 때 좀 가볍게 들었던 것 같다. 다음 주에 있는 인스타그램 클론코딩의 강의+과제 볼륨이 거의 2주치가 되어서 이번주는 쉬어가는 듯 가볍게 듣고 다음 챕터 강의에 집중하려했다.이번 강의의 핵심은 supabase table에서 데이터 가져오기, 무한 스크롤 구현, SEO 최적화하기였다. 수강 내용Section 5. Netflix 클론코딩 - 영화검색 서비스 제작하기이번 챕터에선 Netflix 클론코딩을했다. 어느 때와 마찬가지로 tmdb에서 데이터를 가져와 영화를 뿌려주고, 해당 영화에 대한 정보를 보여주는 것이었다. 기술 스택은 지난번과 마찬가지로 Next.js, tailwind css를 기반으로했고 이번 프로젝트에서는 특히 상태관리가 필요해 zustand 라이브러리를 선택했다. 강의에서는 recoil을 사용했지만 리코일의경우 Next.js 15버전과 호환이 잘 되지 않기도 하고 평소에 가벼운 zustand를 자주 사용해서 해당 라이브러리로 상태관리를 진행했다.영화 목록 전체 불러오기우선 영화 데이터는 강의에서 준비해서 supabase 테이블에 모두 넣고 해당 테이블의 데이터들을 모두 불러오는 코드를 작성했다. const { data, error } = await supabase .from("movie") .select("*")검색 기능 추가하기위에서 말한 상태관리 라이브러리는 이 검색 기능을 활용하기 위해 사용한다. Header 컴포넌트에 있는 SearchInput 에서 검색을 하면 다른 곳에서도 해당 검색어를 사용하기 위해 검색어를 전역 변수로 지정했다.const { data, error } = await supabase .from("movie") .select("*") .like("title", `%${search}%`)search 키워드를 받아와 supabase table 내 title 컬럼에서 search 키워드가 포함된 항목들을 검색한다. 보면서 느끼는건데 확실히 SQL을 알고있으면 이런 키워드를 이해하는 데 좀 더 쉬운거같단 생각이 든다. table이라 그런지 다 SQL문법 사용하네.. 아무튼 이렇게 하면 supabase table에서도 검색어를 손쉽게 찾을 수 있다.무한 스크롤 구현무한 스크롤 구현하는 방법은 매우 다양하다. intersection observer을 사용한다던지.. 이번 강의에서는 가볍게 tanstack-query와 react-intersection-observer 라이브러리를 통해 무한 스크롤을 구현했다.const { ref, inView } = useInView({ threshold: 0 });위는 react-intersection-observer에서 사용하는 hooks이다.ref : 참조할 요소를 지정한다. inView : 요소를 불러와야 할 경우를 true false로 판별한다.threshold : 얼만큼 겹쳤을 경우 inView를 변경할 지 설정한다.이를 통해 데이터를 불러오는 곳 하단에 <div ref={ref} /> 을 작성하면 하단에 닿았을 경우의 트리거가 완성된다. 그리고 그 다음에는 tanstack-query에 있는 useInfiniteQuery를 사용한다.const { data, isFetchingNextPage, isFetching, hasNextPage, fetchNextPage } = useInfiniteQuery({ initialPageParam: 1, queryKey: ["movie", keyword], queryFn: ({ pageParam }) => searchMovies({ search: keyword, page: pageParam, pageSize: 12 }), getNextPageParam: (lastPage, allPages) => { return lastPage.page ? lastPage.page + 1 : undefined; }, });대충 이런식으로.. useQuery랑은 비슷하지만 hasNextPage, fetNextPage등 무한 스크롤 구현에 유용한 기능들이 포함되어있다. 강의를 들으면 깔끔하게 무한 스크롤까지 구현이 가능해진다.SEO (Next.js generateMetadata)Next.js에서는 동적으로 metadata를 생성해주는 기능을 제공한다. dynamic page같은 경우 각 페이지별로 메타데이터를 설정해주려고 하면 예를들어 id를 1, 2, 3 이렇게 다 따로 만들 수 없으니 이 때 generateMetadata를 사용하면 된다.export async function generateMetadata({ params }: any) { // Next.js에서는 params를 await 해야 함 const { id } = await params; const movie = await getMovie(Number(id)); return { title: movie?.title, description: movie?.overview, openGraph: { images: [movie?.image_url], }, }; }강의에서는 Next.js 14 버전이라 별 문제가 없었지만 나는 Next.js 15 버전을 사용해서 params를 가져올 때 async-await을 사용해서 가져왔다. 15버전에서 그냥 가져오면 동작은 하지만 에러가 발생한다. 이렇게까지 하면 가볍게 Netflix 클론코딩은 클리어. 미션3주차미션은 "찜하기" 기능을 구현하는 것이다. 유저 정보도 아직 없고 어떻게 구현할까 하다가 역시 로컬에 저장하는건 localstorage가 답이다 생각했다. 하지만 Next.js 는 SSR이라 localstorage를 그저 React처럼 사용한다면 기능이 정상적으로 동작하지 않는다. 따라서 zustand의 persist를 통해 로컬스토리지에 데이터를 쉽게 사용할 수 있도록 구현했다.마무리 이번주도 주말까지 무사히 일정을 잘 맞췄다. 몇번 했던 기술들이었지만 한번 더 복습 겸 꼼꼼히 들었다. 예전에는 javascript api 중 intersection observer을 이용해 무한스크롤을 깡으로 구현했었는데 확실히 라이브러리를 통해 구현하니까 많이 간편했다. 다음 인스타 클론코딩은 거의 2주치 분량이던데 다음주는 진짜 미리미리 듣고 추가미션까지 해낼 수 있도록 노력해야겠다. 마지막 4주차도 화이팅!

웹 개발웹개발프론트엔드풀스택supabase

hee j

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

목차Dropbox Clone ProjectDrag & Drop 할 영역 설정 및 서버에 파일 전송supabase의 Storage에 첨부파일 업로드첨부파일 검색, 삭제 2주차 미션파일의 마지막 수정(업로드) 시간을 표시하기파일명을 UUID로 변경하여 업로드하기Dropbox Clone Project1. Drag & Drop 할 영역 설정Drag & Drop 라이브러리 설치npm i --save react-dropzonefile-dragdropzone.tsx 파일div: 파일을 받는 영역 태그input: 파일 정보를 받는 태그isDragActive: 어떤 파일을 드래그 앤 드롭할 때 영역에 무엇을 보여줄지 정할 수 있게 해주는 값 ex) 드래그를 했다면? 파일을 여기에 드롭: 아니라면 파일을 드래그 앤 드롭을 해라 라는 문구 출력formData에 파일 이름과 파일 정보를 담아서 전송multiple을 true로 작성하여 여러 파일을 받을 수 있게 한다 export default function FileDragDropZone() { const uploadImageMutation = useMutation({ mutationFn: uploadFile, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["images"], // images로 시작하는 것들 전부 리셋 }); }, }); const onDrop = useCallback(async (acceptedFiles) => { // 10개 이하의 파일만 업로드함 if (acceptedFiles.length > 0 && acceptedFiles.length <= 10) { const formData = new FormData(); acceptedFiles.forEach((file) => { formData.append(file.name, file); }); await uploadImageMutation.mutate(formData); } }, []); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, multiple: true, }); return ( <div {...getRootProps()} className="w-full border-4 border-dotted border-blue-700 flex flex-col items-center justify-center py-20 cursor-pointer" > <input {...getInputProps()} /> {uploadImageMutation.isPending ? ( <Spinner /> ) : isDragActive ? ( <p>파일을 놓아주세요.</p> ) : ( <p>파일을 여기에 끌어다 놓거나 클릭하여 업로드 하세요.</p> )} </div> ); }  2. supabase의 Storage에 첨부파일 업로드.env 파일에 Storage 명 추가NEXT_PUBLIC_STORAGE_BUCKET=miniboxroot/actions 폴더 생성storageActions.ts 파일에 storage에 업로드할 함수 작성export async function uploadFile(formData: FormData) { const files = Array.from(formData.entries()).map( ([name, file]) => file as File ); // all(): 여러 파일을 한 번에 업로드 진행하기 위해 사용 const results = await Promise.all( files.map((file) => { supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .upload(file.name, file, { upsert: true }); }) ); return results; 3. 첨부파일 검색, 삭제storageActions.ts 파일에 파일 검색, 파일 삭제 함수 추가export async function searchFiles(search: string = "") { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .list(null, { search }); // path, options, parameters handleError(error); return data; } export async function deleteFile(fileName: string) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .remove([fileName]); handleError(error); return data; } 2주차 미션[미션 완성 이미지][파일의 마지막 수정(업로드) 시간을 표시하기]dropbox-image.tsxstorage에서 받아오는 정보 내에 마지막 수정 시간을 담고 있는 updated_at을 가져와 표시formData로 날짜와 시간의 가독성을 높여줌const formData = (dateString: string) => { return format(new Date(dateString), "yyyy-MM-dd HH:mm:ss"); }; return ( ... {/* FileName */} <div>{image.name}</div> <div>수정된 시간: {formData(image.updated_at)}</div> ... ) [파일명을 UUID로 변경하여 업로드하기]- 한글명 파일이 업로드 되지 않아 uuid로 파일 명을 변경하여 업로드 해보았음- 업로드는 잘 되나 같은 파일을 업로드 하여도 파일 명이 변경되어 업로드 되기 때문에 새로 업로드 되어 업로드 시간이 변경되지 않아 적용하지는 않음uuid 설치하기npm install uuidstorageActions.tsext 변수에 첨부 파일의 확장자 추출fileName 변수에 uuid+.확장자를 합쳐 파일명 생성const results = await Promise.all( files.map((file) => { const ext = file.name.split(".").pop(); // 확장자 추출 const fileName = `${uuidv4()}.${ext}`; // UUID 기반 파일명 생성 supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .upload(fileName, file, { upsert: true }); }) );

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

찬우 이

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

2주차 학습 내용Part 1 - Git Repository 생성 및 초기 설정 진행create-next-app을 통해 초기 세팅을 했으며,이전에 TODO에서 했던 코드들을 일부 가져와서 빠르게 세팅함. Part 2 - UI 작업알게된 사실 - page.tsx에는 클라이언트 컴포넌트를 사용하면 좋지않다.그 이유는 나중에 메타데이터를 쓰고 하는데 그건 서버 컴포넌트에서만 돌아가기 때문에 피해줘야 한다.나는 평소 flex만 사용하고 grid는 잘 사용하지 않는다. 항상 쓰던것만 써서 그렇기도 하고 grid로 편하게 구현하는 것도 flex로 구현은 가능하기 때문에 그렇게 했다. 강의에서는 grid를 사용했고 디테일하진 않지만 간단하게 3단계로 반응형도 쉽게 구현되는 모습을 보고 grid를 다시보게 됨className="grid md:grid-cols-3 lg:grid-cols-4 grid-cols-2"그 이후로는 컴포넌트 별로 분리해서 퍼블리싱 작업을 구현했다. Part 3 - 파일 업로드 구현(Supabase Storage) 사진을 업로드 하는데 알 수 없는 에러가 있었고, 분명 코드도 다른부분이 없는데 문제가 생겨서 오랫동안 붙잡고 있었다.원인은 사진이름이 한글로된 경우 안되는 부분이였고, 잠시 오류 수정으로 고쳐서 한글이름으로 된 사진도 업로드가 가능하게 했다. 하지만 한글이름이 아닌 a__a__a같은 이름으로 저장되는 문제가 발생해서 이 문제는 추후 고쳐봐야 할 문제 같다.// actions/storageActions.ts function sanitizeFileName(fileName: string) { return fileName .normalize("NFKD") // 유니코드 정규화 .replace(/[^\w.-]/g, "_") // 특수 문자 제거 .replace(/\s+/g, "_") // 공백을 `_`로 변경 .toLowerCase(); // 소문자로 변환 } export async function uploadFile(formData: FormData) { const supabase = await createServerSupabaseClient(); const file = formData.get("file") as File | null; if (!file) { console.error("❌ 업로드할 파일이 없습니다."); throw new Error("파일이 없습니다."); } // 파일 이름을 안전한 형식으로 변환 const safeFileName = sanitizeFileName(file.name); console.log("✅ 변환된 파일 이름:", safeFileName); const { data, error } = await supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET!) .upload(safeFileName, file, { upsert: true }); if (error) { console.error("❌ Supabase 업로드 실패:", error.message); throw new Error(error.message); } return data; }  Part 4 - 파일제거 구현, Darg & Drop, 멀티파일 업로드 구현Darg & Drop을 위해 설치해줌.  npm i --save react-dropzone이번주 미션 - 파일의 마지막 수정(업로드) 시간을 표시하는 기능을 추가하기!// components/dropbox-image.tsx <div>생성일: {formatDate(image.updated_at)}</div> 간단하게 이 부분 넣어서 해결함. 미션 이외로...- 강사님과 같은 코드로 하니까 생긴 에러가 있어서 코드를 약간 수정함.수정한 부분은 actions/storageActions.ts 의 uploadFile 부분임1⃣ 파일 필터링 (undefined 값 제거)✅ 첫 번째 코드 (위 코드) → undefined 또는 잘못된 파일 제거const files = Array.from(formData.entries()) .map(([name, file]) => file as File) .filter((file) => file instanceof File && file.name); // ✅ undefined 제거 filter()를 사용하여 undefined 또는 비정상적인 파일을 제거파일이 null이거나 undefined면 upload()에서 에러 발생 가능성이 있음 → 이를 방지❌ 두 번째 코드 (아래 코드) → 필터링 없음const files = Array.from(formData.entries()).map(([name, file]) => file as File); undefined 파일이 포함될 가능성이 있음 → 업로드 시 오류 발생 가능 2⃣ 파일명 변환 (sanitizeFileName)✅ 첫 번째 코드 (위 코드) → 파일명 변환 추가function sanitizeFileName(fileName: string) { return fileName .normalize("NFC") // ✅ 한글 깨짐 방지 .replace(/[^a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣_.-]/g, "_"); // ✅ 특수 문자 제거 } const safeFileName = sanitizeFileName(file.name); 특수 문자, 공백 제거 (file.name을 정리)한글 깨짐 방지 (NFC 정규화)→ Supabase는 일부 특수 문자나 공백이 포함된 파일명을 허용하지 않으므로 안정적❌ 두 번째 코드 (아래 코드) → 원본 파일명 그대로 사용supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .upload(file.name, file, { upsert: true }); 파일명을 그대로 사용하기 때문에, 특수 문자나 공백이 포함되면 Supabase에서 오류 발생 가능 3⃣ async 처리 및 오류 핸들링✅ 첫 번째 코드 (위 코드) → async 사용 및 오류 처리const results = await Promise.all( files.map(async (file) => { // ✅ async 사용 const safeFileName = sanitizeFileName(file.name); const { data, error } = await supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .upload(safeFileName, file, { upsert: true }); if (error) { // ✅ 오류 처리 console.error("❌ Supabase 업로드 실패:", error.message); throw new Error(error.message); } return data; }) ); 각 파일 업로드가 비동기(async)로 처리됨오류 발생 시 console.error로 출력하고 예외 처리각 파일 업로드 후 결과(data) 반환❌ 두 번째 코드 (아래 코드) → 오류 핸들링 없음const results = await Promise.all( files.map((file) => supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .upload(file.name, file, { upsert: true }) ) ); async 키워드 없이 바로 upload() 실행오류가 발생해도 catch되지 않으며, 전체 업로드가 실패할 가능성이 있음업로드 성공 여부를 확인할 방법 없음 (data를 반환하지 않음)  2주차 회고 2주차에는 중간점검을 하는 시간을 가졌다. 수강생들이 하고 싶었던 질문을 하나하나 답변해주시는 시간을 가져서 꽤 유용한 시간이였고, 더 열심히 하자는 마음을 다지는 계기가 되었다.첫주때보단 수퍼베이스에 적응을 하는거같다. 아직 친해지기에는 시간이 더 많이 필요할꺼 같긴한데 정처기 준비하고 CS 스터디 하고, 다른 프젝도 마무리 하고, 매일 알고리즘 문제도 풀고 있다보니 시간이 많이 부족한 것 같다.중간점검때 강사님께서 시간관리에 대한 얘기도 했었는데, 매우 동감하는 부분...시간 관리나 스케줄 관리를 잘 해야 할 것 같다.. 의욕만 앞서서 살짝 망하는거 같기도함.그래도 뭐 흥미있고 재미있으니까 만족한다.

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

codestudy

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

🚀 Dropbox 클론코딩 - 파일 스토리지 서비스 제작하기📚 강의 수강일주일 동안 학습한 내용 요약🔧 프로젝트 설정 (섹션 4-2)Next.js 프로젝트 초기 설정 방법Tailwind CSS와 TypeScript 환경 구성Material-Tailwind, React Query 등 필요한 라이브러리 설치프로젝트 페이지 구조와 레이아웃 설정환경 변수(.env) 구성🎨 UI 구축 (섹션 4-3)Dropbox 클론의 직관적인 인터페이스 구현logo.tsx, dropbox-image, search-component 등 재사용 컴포넌트 개발Next.js의 이미지 처리와 정적 파일 관리Tailwind CSS의 flex, grid 시스템을 활용한 반응형 디자인useState 훅을 활용한 검색 기능 상태 관리🗄 Supabase Storage 설정 및 파일 업로드 (섹션 4-4)Supabase Storage 버킷 생성 및 설정Form 요소와 onSubmit 이벤트를 활용한 파일 업로드 기능FormData 객체를 활용한 파일 데이터 처리React Query의 useMutation 훅을 통한 파일 업로드 상태 관리업로드된 이미지 URL 생성 및 표시🔄 고급 기능 구현 (섹션 4-5)Supabase Storage의 remove 함수를 활용한 파일 삭제 기능react-dropzone 라이브러리를 활용한 드래그 앤 드롭 구현useDropZone 훅 활용 및 onDrop 콜백 함수 설정multiple 옵션을 활용한 여러 파일 동시 업로드 기능Promise.all을 활용한 비동기 파일 업로드 병렬 처리로딩 상태 표시 및 사용자 피드백 개선학습 내용 회고이번 주 학습을 통해 Next.js와 TypeScript를 활용한 모던 웹 애플리케이션 개발 과정을 경험할 수 있었습니다. TypeScript의 타입 시스템을 적극 활용하면서 인터페이스 정의와 컴포넌트 타입 적용에 많은 배움이 있었지만, Material Tailwind 컴포넌트와의 타입 호환성 문제를 해결하는 과정에서 any 타입을 사용한 것은 아쉬움으로 남습니다.  🛠 미션 해결 과정TypeScript 타입 문제 해결Material Tailwind 컴포넌트와 TypeScript의 타입 호환성 문제에 직면했을 때, 다음과 같은 과정으로 해결했습니다:문제 정의: IconButton, Input, Spinner 등의 컴포넌트에서 다음과 같은 TypeScript 타입 에러가 발생했습니다.'{ value: string; onChange: (e: ChangeEvent<HTMLInputElement>) => void; label: string; icon: Element; }' 형식에 'Pick<InputProps, ... 284 more ... | "shrink">' 형식의 onPointerEnterCapture, onPointerLeaveCapture 속성이 없습니다. 해결 방안 탐색: 여러 가지 해결 방법을 고려했습니다:@ts-ignore 또는 @ts-expect-error 주석 사용타입 단언(Type Assertion) 사용props 객체와 any 타입 활용효율적인 접근법 선택: 코드 가독성과 유지보수성을 고려하여 props 객체와 any 타입을 활용하는 방법을 선택했습니다:const inputProps: any = { value: searchInput, onChange: (e: ChangeEvent<HTMLInputElement>) => setSearchInput(e.target.value), label: "Search Images", icon: <i className="fa-solid fa-magnifying-glass" /> }; return <MaterialInput {...inputProps} />; 일관된 패턴 적용: 비슷한 문제가 발생하는 다른 컴포넌트에도 동일한 패턴을 적용하여 코드의 일관성을 유지했습니다. 예를 들어 IconButton, Spinner 등의 컴포넌트에도 같은 방식으로 타입 문제를 해결했습니다.로딩 상태 처리: boolean 값 처리 문제도 해결했습니다.// 경고: Received `false` for a non-boolean attribute `loading` // 해결: loading 속성 제거 후 조건부 렌더링으로 처리 const iconButtonProps: any = { onClick: () => { deleteFileMutation.mutate(image.name); }, color: "red", children: deleteFileMutation.isPending ? ( <Spinner {...spinnerProps} /> ) : ( <i className="fas fa-trash" /> ) }; 외부 라이브러리와의 통합 과정에서는 때로는 타입 시스템을 부분적으로 우회해야 할 필요가 있다는 것을 배웠습니다.미션 해결 회고기능에 집중을 하다가도...아무래도 빨간줄 에러표시가 뜨는 것이 저는 너무 신경이 쓰였던 것 같습니다. 중간중간마다 타입에러를 해결하는데 시간투자를 했었습니다. (ai에게도 많은 질문을 했습니다)저는 결국, 에러 무시하는 방법 혹은 TypeScript를 사용하여 타입 안전한 코드를 작성하는 방법 중 타입스크립트를 적용해보는 것을 선택했습니다.틈틈이 타입스크립트 공부도 해야할 것 같습니다.이번 풀스택 강의가 끝나고 나서 컴포넌트 안에서 공통으로 적용하여 재사용가능한 타입으로 빼서 리팩토링 할 시간도 가져보면 좋을 것 같습니다.그리고 프로젝트를 진행하면서 업로드 날짜 표시 기능과 한글 파일명 변환하는 작업은 하지 못한 점은 아쉬움으로 남습니다. 다른 수강생분들은 강의 따라하는 것 외에 새로운 추가 기능도 넣기도 한 것 같은데 저는 기본을 따라 가는 것에도 벅차서 추가적인 기능은 넣지 못했습니다..복습을 좀 더 해서 빠르게 지나쳤던 용어나 코드들을 재점검해야 할 것 같습니다... 감사합니다.

프론트엔드dropboxsupabase

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워밍업발자국

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주차 발자국

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

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

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미션

채널톡 아이콘