카카오_구름/리액트

4기 카카오 프론트 전체 스터디 - 리액트 4주차 (SPA 구조 + Next.js & TS 프리뷰)

코딩의 세계 2025. 5. 17. 23:42

아마 마지막의 카카오 프론트 전체 스터디 글이 될 거 같다. (4기 한정)

이번에는 리액트의 SPA 구조 및 Next.js에 대한 이야기를 할 것이다. (next.js에서는 타입스크립트로 진행함.)

목표는 다음과 같다.

1. SPA의 구조와 "가짜 페이지 전환" 흐름 이해
2. Next.js 구조(app/, page.tsx)와 타입스크립트 적용 맛보기

시작하자.


1. SPA의 구조와 "가짜 페이지 전환" 흐름 이해

1. 싱글 페이지 애플리케이션(SPA) 구조와 “가짜 페이지 전환” 동작 원리

1-1. 전통적 MPA와 SPA의 차이는 다음과 같다.

항목 전통적 MPA(멀티 페이지) / SPA(싱글 페이지)

페이지 전환 서버가 새로운 HTML 전체를 응답 → 브라우저가 문서 완전 교체 첫 요청에 단일 HTML + JS 번들을 로드 → 이후 전환은 클라이언트 라우터가 JS 로직으로 DOM 일부만 교체
새로고침 감각 필수 (Flicker 발생) URL만 바뀌고 화면은 즉시 변해 매끄러운 UX
초기 로드 속도 빠름 (HTML만 수신) 상대적으로 느림 (JS 번들 다운로드)
SEO 서버가 완전한 문서 제공 → 즉시 크롤링 가능 JS 렌더 필요 → SSR/SSG, 프레임워크 지원이 필요

1-2. “가짜 페이지 전환”의 기술적 구성은 다음과 같다.

  1. Browser History API
    history.pushState() · replaceState() · popstate 이벤트를 이용해 URL을 바꿔도 브라우저는 네트워크 요청을 보내지 않는다.
  2. 클라이언트 라우터
    • 예: React Router의 <BrowserRouter> / Next.js next/router 내부 라우터.
    • URL 변화를 구독해 현재 경로에 대응하는 컴포넌트 트리를 재렌더링한다.
  3. 코드 스플리팅 & 프리패칭
    • import(/* webpackChunkName */) 또는 Next.js의 자동 코드 스플리팅.
    • 전환 직전 미리 JS 청크를 받아 두면 순간 로딩 스피너조차 안 보인다.
  4. 전역 상태 유지
    한 문서 안에서 전환이 일어나므로, Recoil/Zustand/Context API로 글로벌 저장소를 두면 탭 간 데이터 손실이 없다.
흐름 시퀀스 다이어그램
사용자 클릭 /posts/42
        │
        ▼
Router intercepts event ───┐
        │                  │
history.pushState('/posts/42') 	(주소줄 갱신·뒤로가기 히스토리 추가)
        │                  │
동적 import(PostPage) 로딩?───┘
        │
PostPage 컴포넌트 렌더링

2. Next.js 구조(app/, page.tsx)와 타입스크립트 적용 맛보기

공식 문서 링크 >

https://nextjs.org/

 

Next.js by Vercel - The React Framework

Next.js by Vercel is the full-stack React framework for the web.

nextjs.org

2-1. 프로젝트 생성

# 최신 버전 설치
npm create next-app@latest my-blog \
  --typescript \
  --tailwind \
  --eslint \
  --app --src-dir --import-alias "@/*"
cd my-blog
npm dev

옵션 설명 >

옵션 의미

--typescript tsconfig.json 자동 생성 & 파일 확장자를 .tsx로
--app app router 사용 (pages/ 대신 app/)
--src-dir 소스가 src/ 아래로 들어가 구조가 깔끔
--import-alias "@/*" 절대 경로 import → import Button from "@/components/Button"

2-2. 파일 구조 해부

my-blog/
├─ src/
│  ├─ app/
│  │  ├─ layout.tsx      ← 모든 페이지 공통 레이아웃
│  │  ├─ page.tsx        ← "/"
│  │  ├─ posts/
│  │  │  ├─ [slug]/
│  │  │  │  └─ page.tsx  ← 동적 라우트 "/posts/:slug"
│  │  │  └─ page.tsx     ← "/posts"
│  ├─ components/
│  ├─ hooks/
│  └─ styles/
├─ public/
├─ .eslintrc.json
└─ tailwind.config.ts

중요한 개념 >

파일 / 폴더 역할

layout.tsx 페이지에 공통 UI를 래핑(헤더, Footer, Context Providers)
page.tsx 라우트 도착 시 SSR/SSG/ISR(선택) → React 서버 컴포넌트로 실행
[slug]/page.tsx 동적 세그먼트. params.slug를 받아 빌드 시 SSG 가능
(.) () 폴더는 URL 경로에 포함되지 않는 그룹핑용

2-3. TypeScript 엄격 설정

// tsconfig.json

{
  "compilerOptions": {
    "target": "es2022",
    "strict": true,
    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] },
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

strict 모드는 초기엔 귀찮지만, 런타임 에러·null 체크를 컴파일 단계에서 잡아낼 수 있다.

2-4. 예시 페이지 흐름

/posts 리스트 페이지 (서버 컴포넌트)

// src/app/posts/page.tsx
import { PostCard } from '@/components/PostCard'
import { fetchPosts } from '@/lib/api'

export default async function PostsPage() {
  const posts = await fetchPosts()          // 서버에서 직접 DB/API 호출
  return (
    <section className="grid gap-4">
      {posts.map((p) => (
        <PostCard key={p.id} post={p} />
      ))}
    </section>
  )
}
  • 서버 컴포넌트이므로 데이터 패칭이 서버에서 실행 → 클라이언트 번들 크기 0 KB 증가
  • fetchPosts() 내부에서 axios로 외부 API 요청을 해도 API 키가 노출되지 않는다.

/posts/[slug] 상세 페이지 (서버 → 클라이언트 혼합)

// src/app/posts/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { MDXRemote } from 'next-mdx-remote/rsc'
import { fetchPost } from '@/lib/api'
import { LikeButton } from '@/components/client/LikeButton' // 클라 컴포넌트

export default async function PostDetail({ params }: { params: { slug: string } }) {
  const post = await fetchPost(params.slug)
  if (!post) return notFound()

  return (
    <article className="prose mx-auto">
      <h1>{post.title}</h1>
      <MDXRemote source={post.body} />
      {/* 클라이언트 전용 상호작용 */}
      <LikeButton postId={post.id} />
    </article>
  )
}

클라이언트 컴포넌트 선언

// src/components/client/LikeButton.tsx
'use client'

import { useState } from 'react'
import { likePost } from '@/lib/api'

export function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false)

  const handleClick = async () => {
    await likePost(postId)
    setLiked(true)
  }

  return (
    <button onClick={handleClick} className="btn">
      {liked ? 'Liked ❤️' : 'Like'}
    </button>
  )
}
  • 'use client' 지시어가 있으면 Next.js가 별도 JS 청크로 번들링하여 브라우저에만 보낸다.

2-5. 데이터 패칭 전략 비교

전략 메서드 특징

SSG export const dynamic = 'force-static'빌드 시 HTML 생성 빌드-타임 데이터 고정, Ultra Fast
SSR 기본(서버 컴포넌트) 요청마다 fresh 데이터, BigQuery 같은 실시간 데이터
ISR revalidate = 60 (초) N 초 후 백그라운드 재생성

2-6. 글로벌 상태 & “가짜 전환” 유지

Next.js 15에서도 내부적으로 여전히 SPA 클라이언트 라우터가 동작한다.

  1. 같은 layout.tsx 안에서 전환 → React state 보존
  2. 외부 Link 컴포넌트를 쓰면 history pushState로 처리되어 새로고침 없음
  3. 서버 컴포넌트만 변경되면 Next.js는 partial hydration으로 필요한 부분만 DOM에 패치 → 성능 향상

2-7. PortOne(아임포트) 결제 모듈 삽입 예시

  1. SDK 설치
npm add @iamport/react-native-iamport
  1. 결제 페이지 (클라이언트 컴포넌트)
'use client'

import { useRouter } from 'next/navigation'
import { Iamport } from '@iamport/react-native-iamport'

export default function Checkout() {
  const router = useRouter()
  const userCode = process.env.NEXT_PUBLIC_PORTONE_CODE!

  const data = {
    pg: 'html5_inicis',
    pay_method: 'card',
    merchant_uid: `mid_${Date.now()}`,
    name: '1달 구독',
    amount: 20000,
    buyer_email: 'test@domain.com',
    buyer_name: '홍길동'
  }

  const callback = (response: any) => {
    if (response.success) router.push('/success')
    else router.push('/fail')
  }

  return <Iamport userCode={userCode} data={data} callback={callback} />
}
  • 환경 변수 분리는 .env.local → NEXT_PUBLIC_ prefix면 클라이언트로 노출됨.

3. 프로젝트 시작 → 배포 전체 플로우

  1. 레포 생성 & CI 연결
    • GitHub → npm dlx @vercel/cli@latest link 또는 Vercel Dashboard 연결
  2. 개발: npm dev (포트 3000)
    Hot-reload, ESLint error overlay 확인
  3. 커밋 & 푸시 → Vercel Preview Deployment
  4. 프로덕션 배포 (main 브랜치)
    • Vercel이 npm install && pnpm build 후 /.vercel/output 배포
  5. 도메인 연결 · HTTPS 자동
  6. 모니터링 → Vercel Analytics, Lighthouse

마무리

  • SPAHistory API + 클라이언트 라우터로 “가짜 페이지”를 만든다.
  • Next.js 15는 app/ 라우터를 통해 서버·클라이언트 경계를 명시적으로 나누고, 기본적으로 여전히 SPA 방식을 사용하되 SEO · 성능까지 잡는다.
  • TypeScript + Tailwind + ESLint는 create next-app 옵션만으로 즉시 세팅 가능.
  • 결제·인증 같은 브라우저-전용 로직은 클라이언트 컴포넌트에 'use client'를 선언해 분리한다.