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

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

       

  • 파일 기반 라우팅

  • 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>
  );
};

 

 

📎 미션

image

  • 문제 해결

    • 검색(Search) 기능: 즉시 검색이 안되는 현상 발견

      • queryKey에 searchInput을 포함시켜 검색어가 변경될때마다 새로운 쿼리로 인식되도록 수정

    • 수정(Update): 투두 수정 시, 엔터키로 저장이 안됨

      • input 필드에서 isEditing 상태일 때 onKeyDown을 추가하여 엔터 키를 처리

 

📝회고

Next.js와 TailwindCSS, Firebase는 기존에 사용해 봤었고 Supabase, Recoil, React Query는 이번에 처음 배웠다.

기존에 사용해보지 않았던 기술을 사용해보고 싶었는데, 이 스터디에서는 사용해본 기술과 처음 배우는 기술이 적당히 섞여있어서 좋았다.

 

댓글을 작성해보세요.

채널톡 아이콘