[인프런 워밍업 클럽 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 Connect
Firebase는 Firestore 중심이라 API 접근 방식이 제한적이고, SQL 같은 복잡한 쿼리 사용이 어려움
Supabase는 PostgreSQL 기반이라 SQL, REST API, GraphQL(서드파티 지원), DB 직접 연결이 모두 가능
단점
아직 성숙하지 않은 커뮤니티 기반
비교적 적은 기능, 적은 서비스 연동 지원
부족한 문서화, 한글 문서 부족
Firebase보다 높은 러닝커브
Vendor Lock-in
특정 클라우드 서비스나 플랫폼을 사용하기 시작하면, 다른 서비스로 쉽게 이동할 수 없게 되는 상황
Next.js
React 기반의 풀스택 웹 프레임워크
서버 사이드 렌더링(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)
미리 정의된 작은 스타일 단위(유틸리티 클래스)를 조합하여 스타일을 적용하는 방식
Recoil
Vercel에서 만든 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는 이번에 처음 배웠다.
기존에 사용해보지 않았던 기술을 사용해보고 싶었는데, 이 스터디에서는 사용해본 기술과 처음 배우는 기술이 적당히 섞여있어서 좋았다.
댓글을 작성해보세요.