Framework/Nextjs

백엔드 개발자가 다시 프론트엔드로: 2024년 생태계 변화 총정리

Joonfluence 2025. 8. 24.

요약

2021-2022년과 비교했을 때, 2024년 프론트엔드 생태계는 서버 중심 사고로의 회귀, 타입 안전성 강화, 그리고 개발자 경험(DX) 개선을 중심으로 급격한 변화를 겪었습니다. React 18의 Concurrent Features, Next.js App Router의 Server Components, 그리고 TanStack Query의 서버 상태 관리 패러다임은 백엔드 개발자에게 친숙한 개념들로 프론트엔드와 백엔드의 경계를 모호하게 만들고 있습니다. 이러한 변화는 특히 풀스택 개발 경험이 있는 개발자들에게는 오히려 더 직관적이고 이해하기 쉬운 환경을 제공합니다.

핵심 기능/개념 정리

주요 기술 스택 변화 타임라인

연도 기술/패턴 변화 내용 백엔드 개발자 관점
2021 React 17, SWR CSR 중심, 클라이언트 상태 관리 복잡 프론트엔드 != 백엔드 사고방식
2022 React 18 출시 Concurrent Features, Suspense 비동기 처리 패턴 유사성 증가
2023 Next.js 13 App Router Server Components, 스트리밍 서버사이드 렌더링 회귀
2024 Next.js 15, TanStack Query v5 완전한 풀스택 프레임워크 백엔드 개념과 거의 동일

2024년 현재 핵심 기술 스택

1. React 18+ Concurrent Features

// 2021년: 복잡한 상태 관리
const [loading, setLoading] = useState(false)
const [data, setData] = useState(null)

useEffect(() => {
  setLoading(true)
  fetchData().then(setData).finally(() => setLoading(false))
}, [])

// 2024년: Suspense + Server Components
function MyComponent() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <DataComponent /> {/* 서버에서 데이터 로딩 */}
    </Suspense>
  )
}

2. Next.js App Router vs Pages Router

측면 Pages Router (2021) App Router (2024)
라우팅 파일 기반, 클라이언트 라우팅 디렉토리 기반, 서버/클라이언트 혼합
데이터 페칭 getServerSideProps, useEffect Server Components, fetch
레이아웃 _app.js, _document.js layout.tsx 중첩 구조
로딩/에러 수동 구현 loading.tsx, error.tsx 자동

3. 상태 관리 패러다임 변화

// 2021년: Redux + RTK Query
const { data, isLoading, error } = useGetUsersQuery()

// 2024년: TanStack Query + Server State
const { data, isLoading, error } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  staleTime: 10 * 60 * 1000, // 10분간 fresh
})

사용 예시 및 코드 스니펫

1. Server Components와 Client Components 분리

// app/products/page.tsx (Server Component)
import { ProductGrid } from './product-grid'
import { getProducts } from 'app/lib/api'

export default async function ProductsPage() {
  // 서버에서 실행, 빌드 타임 또는 요청 시 데이터 페칭
  const products = await getProducts()

  return (
    <div>
      <h1>상품 목록</h1>
      <ProductGrid products={products} />
    </div>
  )
}

// app/products/product-grid.tsx (Client Component)
'use client'

import { useState } from 'react'
import { useAddToCart } from 'app/hooks/use-cart'

export function ProductGrid({ products }) {
  const [selectedProduct, setSelectedProduct] = useState(null)
  const { addToCart } = useAddToCart()

  // 클라이언트에서만 실행되는 인터랙티브 로직
  return (
    <div className="grid grid-cols-4 gap-4">
      {products.map(product => (
        <div key={product.id}>
          {/* 상품 UI */}
          <button onClick={() => addToCart(product.id)}>
            장바구니 추가
          </button>
        </div>
      ))}
    </div>
  )
}

2. TanStack Query v5 서버 상태 관리

// app/lib/queries/use-products.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

// Query Keys 중앙 관리 (백엔드의 API 엔드포인트와 유사)
export const QUERY_KEYS = {
  PRODUCTS: ['products'],
  PRODUCT_DETAIL: (id: string) => ['products', id],
  USER_CART: ['user', 'cart'],
} as const

// 상품 목록 조회
export function useProducts(filters?: ProductFilters) {
  return useQuery({
    queryKey: [...QUERY_KEYS.PRODUCTS, filters],
    queryFn: () => fetchProducts(filters),
    staleTime: 5 * 60 * 1000, // 5분간 캐시 유지
    gcTime: 10 * 60 * 1000,   // 10분 후 가비지 컬렉션
  })
}

// 장바구니 추가 (Mutation)
export function useAddToCart() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (productId: string) => addToCartAPI(productId),
    onSuccess: () => {
      // 관련 쿼리 무효화 (백엔드 캐시 무효화와 동일한 개념)
      queryClient.invalidateQueries({ queryKey: QUERY_KEYS.USER_CART })
      queryClient.invalidateQueries({ queryKey: QUERY_KEYS.PRODUCTS })
    },
    onError: (error) => {
      console.error('장바구니 추가 실패:', error)
    }
  })
}

3. TypeScript + GraphQL Codegen 타입 안전성

// codegen.ts - 설정 파일
import { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
  schema: 'https://store-api.bigcommerce.com/graphql',
  documents: ['app/**/*.graphql'],
  generates: {
    'app/gql/': {
      preset: 'client',
      plugins: [
        'typescript',
        'typescript-operations',
        'typescript-react-query'
      ]
    }
  }
}

// 사용 예시 - 자동 생성된 타입과 훅
import { useGetProductsQuery, Product } from 'app/gql/graphql'

function ProductList() {
  const { data, loading, error } = useGetProductsQuery({
    variables: { first: 20 }
  })

  // data.products는 완전한 타입 안전성 보장
  return (
    <div>
      {data?.site?.products?.edges?.map(({ node }) => (
        <div key={node.id}>{node.name}</div>
      ))}
    </div>
  )
}

4. 현대적 폼 처리 (React Hook Form + Zod)

// app/components/forms/user-form.tsx
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

// 백엔드 개발자에게 친숙한 스키마 검증
const userSchema = z.object({
  email: z.string().email('유효한 이메일을 입력하세요'),
  name: z.string().min(2, '이름은 2글자 이상이어야 합니다'),
  age: z.number().min(18, '18세 이상만 가입 가능합니다'),
})

type UserFormData = z.infer<typeof userSchema>

export function UserForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<UserFormData>({
    resolver: zodResolver(userSchema)
  })

  const onSubmit = (data: UserFormData) => {
    // 타입 안전한 폼 데이터 처리
    console.log(data) // UserFormData 타입 보장
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}

      <input {...register('name')} />
      {errors.name && <span>{errors.name.message}</span>}

      <input {...register('age', { valueAsNumber: true })} type="number" />
      {errors.age && <span>{errors.age.message}</span>}

      <button type="submit">제출</button>
    </form>
  )
}

실제 사용 시 주의점 / Best Practice

1. Server Components vs Client Components 구분 원칙

Server Components 사용 시기:

  • 데이터베이스 직접 접근
  • 보안이 중요한 API 호출 (API 키 사용)
  • 큰 의존성 라이브러리 사용
  • SEO가 중요한 정적 콘텐츠

Client Components 사용 시기:

  • 사용자 인터랙션 (onClick, onChange 등)
  • 브라우저 전용 API (localStorage, sessionStorage)
  • React 상태 훅 (useState, useEffect)
  • 실시간 데이터 업데이트

2. TanStack Query 최적화 전략

// QueryClient 설정 최적화
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,    // 5분
      gcTime: 10 * 60 * 1000,     // 10분 (구 cacheTime)
      retry: (failureCount, error) => {
        // 백엔드 개발자 친화적 에러 처리
        if (error.status === 404) return false
        return failureCount < 3
      },
      refetchOnWindowFocus: false,  // 개발 시 불필요한 리페치 방지
    },
    mutations: {
      retry: 1,
      onError: (error) => {
        // 전역 에러 처리
        console.error('API 오류:', error)
      }
    }
  }
})

3. 타입 안전성 보장을 위한 패턴

// API 응답 타입 정의
interface ApiResponse<T> {
  data: T
  message: string
  success: boolean
}

// Generic을 활용한 API 클라이언트
async function apiCall<T>(endpoint: string): Promise<ApiResponse<T>> {
  const response = await fetch(endpoint)
  return response.json()
}

// 사용 시 타입 추론
const userResponse = await apiCall<User[]>('/api/users') // ApiResponse<User[]> 타입

4. 성능 최적화 실무 팁

// Dynamic Import로 코드 스플리팅
const HeavyComponent = dynamic(() => import('./heavy-component'), {
  loading: () => <Skeleton className="h-32 w-full" />,
  ssr: false // 클라이언트에서만 로드
})

// React.memo로 불필요한 리렌더링 방지
const ProductCard = React.memo<ProductCardProps>(({ product }) => {
  return (
    <div>
      {product.name} - {product.price}원
    </div>
  )
})

// useMemo로 비싼 계산 최적화
const expensiveValue = useMemo(() => {
  return products.reduce((sum, product) => sum + product.price, 0)
}, [products])

5. 에러 처리 및 로깅 전략

// Error Boundary 설정
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Sentry 등 에러 트래킹 서비스 연동
    console.error('Global error:', error)
  }, [error])

  return (
    <html>
      <body>
        <h2>문제가 발생했습니다!</h2>
        <button onClick={() => reset()}>다시 시도</button>
      </body>
    </html>
  )
}

 

참고자료 / 공식 문서 출처

공식 문서

마이그레이션 가이드

실무 참고 자료

반응형

댓글