[인프런 워밍업 클럽 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) {} 사용 권장클라이언트 사이드 라우팅 (CSR)일반 태그를 사용하면 전체 페이지가 새로고침(F5)되면서 서버에서 새 HTML을 받아옴를 사용하면 Next.js가 JavaScript로 페이지를 전환하여 새로고침 없이 부드럽게 이동 가능사전 로드 (Preloading) 지원를 사용하면 Next.js가 백그라운드에서 해당 페이지 데이터를 미리 가져옴브라우저가 네트워크 요청을 최적화해서 빠른 로딩 가능SEO 최적화 & 서버 사이드 렌더링과 호환 태그를 직접 사용하면 Next.js의 서버 사이드 렌더링(SSR)과 연동이 어려울 수 있음import Link from 'next/link' export default function Page() { return Dashboard } 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 ( 제출 {message} ); }Summary외부 API와 통신해야 할 경우 → fetch를 써야 하므로 기존 API Route 방식 사용폼 제출, 데이터 처리 같은 간단한 서버 로직 → Server Actions 사용 Metadata웹 페이지의 SEO(검색 엔진 최적화)와 소셜 미디어 공유를 위해 HTML 에 메타 정보를 추가하는 기능동적 Metadata 설정 가능페이지마다 다른 Metadata를 적용할 수 있음SEO 최적화에 도움 (검색 엔진이 페이지별로 다르게 인식)generateMetadata() 사용export const metadata = { title: "내 멋진 웹사이트 🚀", description: "Next.js를 활용한 SEO 최적화 페이지", };// 동적 Metadata (게시글 제목 반영) import { Metadata } from "next"; export async function generateMetadata({ params }): Promise { 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 → 클라이언트 컴포넌트 app/layout.tsx → 서버 컴포넌트 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 setCount(count + 1)}>카운트 증가; }; // 두 번째 컴포넌트: 카운트 표시 const DisplayCounter = () => { const count = useRecoilValue(countState); return 현재 카운트: {count}; }; // 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 → 클라이언트 컴포넌트 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 Loading...; if (error) return Error loading todos; return ( {data.map((todo) => ( {todo.title} ))} ); }; 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 ( mutation.mutate({ title: '새로운 할 일' })}> 할 일 추가 ); }; 📎 미션문제 해결검색(Search) 기능: 즉시 검색이 안되는 현상 발견 queryKey에 searchInput을 포함시켜 검색어가 변경될때마다 새로운 쿼리로 인식되도록 수정수정(Update): 투두 수정 시, 엔터키로 저장이 안됨input 필드에서 isEditing 상태일 때 onKeyDown을 추가하여 엔터 키를 처리 📝회고Next.js와 TailwindCSS, Firebase는 기존에 사용해 봤었고 Supabase, Recoil, React Query는 이번에 처음 배웠다.기존에 사용해보지 않았던 기술을 사용해보고 싶었는데, 이 스터디에서는 사용해본 기술과 처음 배우는 기술이 적당히 섞여있어서 좋았다.