How to think about data security in Next.js

김동현·2026년 3월 5일

next.js 공식문서 번역

목록 보기
46/79

안녕하세요 여러분! 프론트엔드 개발의 세계로 오신 것을 환영합니다. 오늘은 Next.js 공식 문서를 보면서 '데이터 보안'에 대해 함께 공부해보려고 해요.

React 서버 컴포넌트(React Server Components)는 앱의 성능을 크게 개선하고 데이터 패칭(가져오기)을 아주 단순하게 만들어 주었죠. 하지만 동시에 데이터에 접근하는 '위치'와 '방식'이 서버 쪽으로 옮겨가면서, 우리가 기존 프론트엔드 앱에서 데이터를 다룰 때 당연하게 생각했던 보안에 대한 전제 조건들이 꽤 많이 바뀌게 되었습니다. 클라이언트에서만 데이터를 다루던 시절과는 생각하는 방식 자체가 달라져야 해요.

이 가이드는 여러분이 Next.js 환경에서 데이터 보안을 어떻게 생각해야 하는지, 그리고 현업에서 어떤 모범 사례들을 적용해야 하는지 이해하는 데 아주 큰 도움이 될 거예요. 자, 그럼 본격적으로 시작해볼까요?


데이터 패칭 접근 방식 (Data fetching approaches)

여러분의 프로젝트 규모나 얼마나 오래된 프로젝트인지에 따라, Next.js에서 데이터를 가져오는 데 권장하는 세 가지 주요 접근 방식이 있습니다:

이 중에서 프로젝트에 맞는 하나의 데이터 패칭 방식을 선택하고, 여러 방식을 섞어 쓰는 것은 피하시길 권장합니다. 이렇게 기준을 하나로 정해두면 함께 일하는 동료 개발자들은 물론이고, 나중에 보안 감사를 하는 전문가들도 코드를 보고 어떤 흐름인지 정확히 예측할 수 있기 때문이죠.

외부 HTTP API (External HTTP APIs)

기존 프로젝트에 서버 컴포넌트를 도입할 때는 제로 트러스트(Zero Trust, 아무것도 신뢰하지 않는 보안 모델) 방식을 따라야 합니다. 클라이언트 컴포넌트에서 하던 것과 완전히 똑같이, 서버 컴포넌트 안에서도 fetch 함수를 사용해서 기존에 쓰시던 REST나 GraphQL 같은 API 엔드포인트를 계속 호출하시면 됩니다.

import { cookies } from 'next/headers'

export default async function Page() {
  const cookieStore = cookies()
  const token = cookieStore.get('AUTH_TOKEN')?.value

  const res = await fetch('[https://api.example.com/profile](https://api.example.com/profile)', {
    headers: {
      Cookie: `AUTH_TOKEN=${token}`,
      // Other headers
    },
  })

  // ....
}

💡 강사의 팁: 이 방식은 이미 튼튼한 백엔드 API가 구축되어 있는 회사에 입사했을 때 가장 많이 보게 될 패턴이에요.

이 방식은 다음과 같은 상황에서 아주 잘 작동합니다:

  • 이미 탄탄한 보안 관행과 인프라가 갖춰져 있을 때.
  • 별도의 백엔드 팀이 다른 언어(Java, Python 등)를 사용하거나 API를 독립적으로 관리하고 있을 때.

데이터 접근 계층 (Data Access Layer)

신규 프로젝트를 시작한다면, 전용 데이터 접근 계층(Data Access Layer, DAL) 을 만드는 것을 적극 추천합니다! DAL은 데이터가 언제, 어떻게 패칭되는지, 그리고 렌더링 컨텍스트(화면을 그리는 부분)로 어떤 데이터가 전달될지를 통제하는 내부 라이브러리 역할을 합니다.

제대로 된 데이터 접근 계층(DAL)이라면 다음 원칙을 지켜야 해요:

  • 오직 서버에서만 실행되어야 합니다.
  • 사용자가 이 데이터에 접근할 권한이 있는지 인가(Authorization) 검사를 수행해야 합니다.
  • 안전하고 꼭 필요한 데이터만 담긴 최소한의 데이터 전송 객체(Data Transfer Objects, DTOs) 를 반환해야 합니다.

이 방식을 사용하면 데이터를 가져오는 모든 로직이 한 곳으로 집중(Centralized)됩니다. 덕분에 일관성 있게 데이터 접근 규칙을 강제하기가 훨씬 쉬워지고, 권한 검사를 빼먹는 등의 보안 버그가 발생할 위험이 뚝 떨어지죠. 게다가 하나의 요청(Request) 내에서 여러 곳에서 같은 데이터를 호출하더라도 메모리 캐시를 공유할 수 있다는 엄청난 성능적 이점도 얻을 수 있습니다.

import { cache } from 'react'
import { cookies } from 'next/headers'

// Cached helper methods makes it easy to get the same value in many places
// without manually passing it around. This discourages passing it from Server
// Component to Server Component which minimizes risk of passing it to a Client
// Component.
export const getCurrentUser = cache(async () => {
  const token = cookies().get('AUTH_TOKEN')
  const decodedToken = await decryptAndValidate(token)
  // Don't include secret tokens or private information as public fields.
  // Use classes to avoid accidentally passing the whole object to the client.
  return new User(decodedToken.id)
})
import 'server-only'
import { getCurrentUser } from './auth'

function canSeeUsername(viewer: User) {
  // Public info for now, but can change
  return true
}

function canSeePhoneNumber(viewer: User, team: string) {
  // Privacy rules
  return viewer.isAdmin || team === viewer.team
}

export async function getProfileDTO(slug: string) {
  // Don't pass values, read back cached values, also solves context and easier to make it lazy

  // use a database API that supports safe templating of queries
  const [rows] = await sql`SELECT * FROM user WHERE slug = ${slug}`
  const userData = rows[0]

  const currentUser = await getCurrentUser()

  // only return the data relevant for this query and not everything
  // [https://www.w3.org/2001/tag/doc/APIMinimization](https://www.w3.org/2001/tag/doc/APIMinimization)
  return {
    username: canSeeUsername(currentUser) ? userData.username : null,
    phonenumber: canSeePhoneNumber(currentUser, userData.team)
      ? userData.phonenumber
      : null,
  }
}
import { getProfile } from '../../data/user'

export async function Page({ params: { slug } }) {
  // This page can now safely pass around this profile knowing
  // that it shouldn't contain anything sensitive.
  const profile = await getProfile(slug);
  ...
}

💡 강사의 팁: 제가 현업에서 Next.js App Router를 써보니, 컴포넌트 안에서 직접 DB를 찌르는 코드가 많아지면 나중에 유지보수가 지옥이 됩니다. 위 코드처럼 src/datasrc/services 같은 폴더를 따로 파서 DAL을 구축하세요. 그리고 DTO 패턴을 꼭 적용해서, DB에서 가져온 민감한 정보(비밀번호, 주민번호 등)가 실수로라도 화면 단으로 넘어오지 않게 커팅해주는 작업이 필수입니다!

알아두면 좋은 점 (Good to know): 비밀 키(Secret keys)들은 반드시 환경 변수(environment variables)에 저장해야 합니다. 그리고 오로지 이 '데이터 접근 계층(DAL)'에서만 process.env에 접근하도록 하세요. 이렇게 하면 애플리케이션의 다른 부분에 비밀 키가 노출되는 것을 완벽하게 차단할 수 있습니다.

컴포넌트 수준의 데이터 접근 (Component-level data access)

빠르게 프로토타입을 만들거나 코드를 테스트해 볼 때는, 데이터베이스 쿼리를 서버 컴포넌트 안에 직접 작성할 수도 있습니다. 진짜 빠르고 직관적이긴 하죠.

하지만, 이 방식은 자칫 잘못하면 클라이언트 쪽에 사용자의 개인 정보를 실수로 노출시킬 위험이 굉장히 높아집니다. 아래의 나쁜 예시를 한 번 볼까요?

import Profile from './components/profile.tsx'

export async function Page({ params: { slug } }) {
  const [rows] = await sql`SELECT * FROM user WHERE slug = ${slug}`
  const userData = rows[0]
  // EXPOSED: This exposes all the fields in userData to the client because
  // we are passing the data from the Server Component to the Client.
  return <Profile user={userData} />
}
'use client'

// BAD: This is a bad props interface because it accepts way more data than the
// Client Component needs and it encourages server components to pass all that
// data down. A better solution would be to accept a limited object with just
// the fields necessary for rendering the profile.
export default async function Profile({ user }: { user: User }) {
  return (
    <div>
      <h1>{user.name}</h1>
      ...
    </div>
  )
}

위의 예시를 보면 sql 쿼리로 유저 테이블의 모든 데이터(SELECT *) 를 가져와서, 클라이언트 컴포넌트인 <Profile>에 통째로 넘겨주고 있어요. 화면에는 user.name만 그리는데, 네트워크 탭을 열어보면 비밀번호 해시값이나 이메일 같은 것들이 전부 클라이언트로 넘어가고 있을 겁니다. 이건 정말 끔찍한 보안 사고죠!

반드시 데이터를 클라이언트 컴포넌트로 전달하기 전에 데이터를 정제(sanitize) 해야 합니다. 아래처럼요:

import { sql } from './db'

export async function getUser(slug: string) {
  const [rows] = await sql`SELECT * FROM user WHERE slug = ${slug}`
  const user = rows[0]

  // Return only the public fields
  return {
    name: user.name,
  }
}
import { getUser } from '../data/user'
import Profile from './ui/profile'

export default async function Page({
  params: { slug },
}: {
  params: { slug: string }
}) {
  const publicProfile = await getUser(slug)
  return <Profile user={publicProfile} />
}

데이터 읽기 (Reading data)

서버에서 클라이언트로 데이터 전달하기

처음 페이지가 로딩될 때(Initial load), 서버 컴포넌트와 클라이언트 컴포넌트 둘 다 서버에서 실행되어 HTML을 만들어냅니다 (이걸 SSR, 서버 사이드 렌더링이라고 하죠). 하지만 두 컴포넌트는 완전히 격리된 모듈 시스템에서 실행돼요. 이를 통해 서버 컴포넌트는 프라이빗 데이터나 내부 API에 마음껏 접근할 수 있으면서도, 클라이언트 컴포넌트는 그런 민감한 정보에 접근할 수 없도록 확실히 보장하는 겁니다.

서버 컴포넌트 (Server Components):

  • 오직 서버에서만 실행됩니다.
  • 환경 변수, 비밀 키, 데이터베이스, 내부 API 등에 안전하게 접근할 수 있습니다.

클라이언트 컴포넌트 (Client Components):

  • 초기 렌더링 시 서버에서 한 번 실행되긴 하지만, 기본적으로 브라우저에서 실행되는 코드와 동일한 보안 수준을 가정하고 작성해야 합니다.
  • 절대 권한이 필요한 데이터나 서버 전용 모듈에 접근해서는 안 됩니다.

이런 구조 덕분에 앱은 기본적으로(by default) 안전합니다. 하지만 방금 앞의 예시에서 본 것처럼, 데이터를 가져오거나 컴포넌트에 넘겨주는 과정에서 개발자의 실수로 민감한 데이터가 클라이언트로 유출될 가능성은 여전히 존재합니다.

데이터 오염 방지 API (Tainting)

실수로 개인 정보나 민감한 데이터가 클라이언트로 넘어가는 것을 원천 봉쇄하기 위해, React의 Taint(오염) API를 사용할 수 있습니다:

이 기능을 쓰려면 Next.js 앱의 next.config.js 파일에서 experimental.taint 옵션을 켜주면 됩니다:

module.exports = {
  experimental: {
    taint: true,
  },
}

이 기능을 적용하면 Taint 처리된 객체나 값이 클라이언트로 넘어가는 순간 React가 에러를 뿜으며 막아줍니다. 하지만 이건 어디까지나 추가적인 보호막(안전장치) 일 뿐이에요. 이것만 믿지 마시고, 앞서 배운 데이터 접근 계층 (DAL)에서 React 렌더링 쪽으로 데이터를 넘기기 전에 꼭 데이터를 필터링하고 정제(Sanitize)하는 습관을 들이셔야 합니다!

알아두면 좋은 점:

  • 기본적으로 환경 변수는 서버에서만 접근 가능합니다. 만약 클라이언트(브라우저)에서도 특정 환경 변수를 쓰고 싶다면 이름 앞에 NEXT_PUBLIC_을 붙여야 해요. 환경 변수에 대해 더 알아보기
  • 함수(Functions)나 클래스(Classes)는 애초에 클라이언트 컴포넌트로 전달되는 것이 기본적으로 차단되어 있습니다. (직렬화할 수 없기 때문이죠.)

클라이언트에서 서버 전용 코드 실행 방지하기

서버에서만 돌아야 하는 중요한 코드가 실수로 클라이언트 쪽에 섞여 들어가는 걸 막으려면, server-only라는 패키지를 사용해서 해당 모듈을 마킹해두면 됩니다.

npm install server-only
yarn add server-only
pnpm add server-only
bun add server-only
import 'server-only'

//...

💡 강사의 팁: 이거 정말 꿀팁입니다. DB 접속 객체나 비밀키를 다루는 유틸리티 파일 맨 위에 import 'server-only' 한 줄만 적어두세요. 만약 다른 팀원이 실수로 클라이언트 컴포넌트('use client')에서 이 파일을 import 하려고 하면 빌드 과정에서 즉시 에러가 납니다. 대형 보안 사고를 미리 막아주는 효자템이에요!

이 패키지는 사내 보안 코드나 내부 비즈니스 로직이 클라이언트 환경에서 임포트될 경우 아예 빌드 에러를 발생시켜서, 코드가 무조건 서버에만 머물도록 꽉 잡아줍니다.


데이터 변경하기 (Mutating Data)

Next.js에서는 데이터를 수정, 추가, 삭제할 때 서버 액션(Server Actions)을 사용합니다.

내장된 서버 액션 보안 기능들

정말 중요한 내용입니다! 기본적으로 여러분이 서버 액션을 만들고 export 하는 순간, 보이지 않는 공개 HTTP 엔드포인트(API 주소)가 하나 생성되는 것과 같습니다. 따라서 일반 API를 만들 때와 똑같은 수준의 보안 의식과 인가(Authorization) 검사가 필요해요. 다시 말해, 여러분이 만든 서버 액션이나 유틸리티 함수를 앱 내 다른 곳에서 한 번도 가져다 쓰지(import) 않았더라도, 악의적인 사용자가 밖에서 이 엔드포인트를 찔러볼 수 있다는 뜻입니다.

보안을 강화하기 위해 Next.js는 다음과 같은 내장 기능들을 제공합니다:

  • 안전한 액션 ID (Secure action IDs): Next.js는 클라이언트가 서버 액션을 참조하고 호출할 수 있도록 암호화되고 비결정적인(매번 바뀌는) ID를 만듭니다. 이 ID들은 보안 강화를 위해 빌드와 빌드 사이에 주기적으로 다시 계산됩니다.
  • 죽은 코드 제거 (Dead code elimination): 코드 상에서 아무데서도 사용되지 않는(ID가 참조되지 않는) 서버 액션은 아예 클라이언트 번들에서 삭제해버립니다. 외부에서 접근할 길 자체를 없애는 거죠.

알아두면 좋은 점:
액션 ID들은 컴파일(빌드) 중에 생성되며 최대 14일 동안 캐싱됩니다. 새로운 빌드가 시작되거나 빌드 캐시가 무효화될 때 새롭게 재생성돼요.
이런 보안 개선 사항 덕분에 인증 레이어를 실수로 빼먹었을 때의 위험은 조금 줄어들긴 합니다만, 그래도 서버 액션은 무조건 퍼블릭 HTTP 엔드포인트라고 생각하고 꼼꼼히 방어해야 합니다.

// app/actions.js
'use server'

// 만약 이 액션이 앱 어딘가에서 '사용된다면', Next.js는
// 클라이언트가 이 서버 액션을 호출할 수 있도록 안전한 ID를 만듭니다.
export async function updateUserAction(formData) {}

// 만약 이 액션이 앱에서 '사용되지 않는다면', Next.js는
// `next build` 과정에서 이 코드를 자동으로 삭제해버리고
// 공개 엔드포인트도 만들지 않습니다.
export async function deleteUserAction(formData) {}

클라이언트 입력값 검증하기 (Validating client input)

클라이언트(사용자)가 보내는 입력값은 너무나 쉽게 조작될 수 있기 때문에 절대로 그대로 믿으시면 안 됩니다. 폼(form) 데이터, URL 파라미터, HTTP 헤더, 그리고 searchParams 같은 것들은 사용하기 전에 항상 깐깐하게 검증해야 해요.

// BAD: searchParams를 아무 의심 없이 믿는 나쁜 코드
export default async function Page({ searchParams }) {
  const isAdmin = searchParams.get('isAdmin')
  if (isAdmin === 'true') {
    // 취약점: 신뢰할 수 없는 클라이언트 데이터에 의존하고 있습니다.
    // 누구나 URL 뒤에 ?isAdmin=true 만 붙이면 관리자 페이지를 볼 수 있어요!
    return <AdminPanel />
  }
}

// GOOD: 서버에서 매번 새롭게 권한을 확인하는 좋은 코드
import { cookies } from 'next/headers'
import { verifyAdmin } from './auth'

export default async function Page() {
  const token = cookies().get('AUTH_TOKEN')
  const isAdmin = await verifyAdmin(token)

  if (isAdmin) {
    return <AdminPanel />
  }
}

💡 강사의 팁: 입력값을 검증할 때는 ZodYup 같은 스키마 검증 라이브러리를 적극 활용하세요. 데이터 타입이 맞는지, 이메일 형식이 맞는지 등을 아주 우아하고 안전하게 체크할 수 있습니다.

인증과 인가 (Authentication and authorization)

어떤 액션을 수행하기 전에, 현재 사용자가 그 행동을 할 '권한(권리)'이 있는지 항상 확인해야 합니다.

'use server'

import { auth } from './lib'

export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('이 작업을 수행하려면 먼저 로그인해야 합니다.')
  }

  // ... 아이템 추가 로직
}

Next.js에서의 인증(Authentication)에 대해 더 깊이 알고 싶다면 인증 가이드 문서를 참고해 보세요.

클로저와 암호화 (Closures and encryption)

서버 액션을 컴포넌트 '안'에서 정의하면 클로저(closure)가 생성됩니다. 쉽게 말해, 액션 함수가 자기를 감싸고 있는 바깥쪽 함수의 변수들을 기억하고 가져다 쓸 수 있게 되는 거죠. 예를 들어, 아래 코드에서 publish 액션은 바깥에 있는 publishVersion 변수에 접근할 수 있습니다:

export default async function Page() {
  const publishVersion = await getLatestVersion();

  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('버튼을 누른 사이에 버전이 변경되었습니다!');
    }
    ...
  }

  return (
    <form>
      <button formAction={publish}>Publish</button>
    </form>
  );
}

이런 클로저 기능은 화면을 그리는(렌더링) 시점의 데이터 상태(예: publishVersion)를 찰칵! 사진 찍듯 스냅샷으로 캡처해 두었다가, 나중에 사용자가 버튼을 눌러 액션을 실행할 때 그대로 사용할 수 있게 해줘서 매우 유용합니다.

하지만 이런 마법이 가능하려면, 캡처된 변수들이 몰래 클라이언트로 내려갔다가 액션이 실행될 때 다시 서버로 올라와야 합니다. 이 과정에서 민감한 데이터가 클라이언트로 유출되는 것을 막기 위해, Next.js는 클로저에 갇힌(closed-over) 변수들을 자동으로 암호화합니다. Next.js 앱이 빌드될 때마다 각 액션에 대한 새로운 개인 키(private key)가 생성돼요. 즉, 특정 빌드에서 만들어진 액션은 그 빌드 환경에서만 정상적으로 호출될 수 있다는 뜻입니다.

알아두면 좋은 점: 그렇다고 하더라도, 민감한 정보(주민번호, 비밀번호 등)가 클라이언트에 노출되는 것을 막기 위해 '암호화'에만 전적으로 의존하는 것은 권장하지 않습니다. 보안은 다중으로 철저하게 하는 것이 좋습니다.

암호화 키 덮어쓰기 (고급 과정 - Overwriting encryption keys)

여러 대의 서버 인스턴스를 두고 Next.js 앱을 직접 호스팅(Self-hosting)하는 환경이라면 문제가 될 수 있습니다. 각 서버 인스턴스마다 서로 다른 암호화 키가 생성되어버려서, 1번 서버에서 렌더링된 폼을 2번 서버가 처리하려고 할 때 암호를 풀지 못해 에러가 날 수 있거든요.

이 문제를 해결하기 위해, process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY 환경 변수를 사용해서 암호화 키를 수동으로 지정(덮어쓰기)할 수 있습니다. 이렇게 하면 빌드를 새로 하더라도 키가 고정되고, 모든 서버 인스턴스가 똑같은 키를 공유하게 됩니다.

이 키는 반드시 base64로 인코딩된 값이어야 하고, 디코딩했을 때의 길이가 유효한 AES 키 사이즈(16, 24, 또는 32 바이트)와 정확히 일치해야 합니다. Next.js는 기본적으로 32바이트 키를 생성합니다. 리눅스나 맥의 터미널에서 아래 명령어로 호환되는 키를 간단히 생성해볼 수 있어요:

openssl rand -base64 32

이 설정은 여러 대의 서버를 돌릴 때 일관된 암호화 동작이 꼭 필요한 아주 고급 사용 사례에 해당합니다. 실무에 적용할 때는 주기적으로 키를 교체(Key rotation)하는 등 표준 보안 관행을 잘 지켜주세요. 배포와 관련된 자세한 내용은 직접 호스팅 가이드 (Self-Hosting guide)를 꼭 읽어보시길 권합니다.

허용된 오리진 (고급 과정 - Allowed origins)

서버 액션은 <form> 태그 안에서 호출될 수 있기 때문에, 태생적으로 CSRF 공격 (크로스 사이트 요청 위조)에 노출될 수 있습니다.

이 공격을 방어하기 위해 Next.js 서버 액션은 내부적으로 오직 POST 메서드만 허용합니다. 모던 브라우저에서는 쿠키 설정이 기본적으로 SameSite로 되어 있기 때문에, POST 요청만 허용하는 것만으로도 대부분의 CSRF 취약점을 훌륭하게 방어해냅니다.

여기에 추가적인 보호막으로, Next.js의 서버 액션은 HTTP 요청의 Origin 헤더Host 헤더 (또는 X-Forwarded-Host)를 꼼꼼히 비교합니다. 만약 두 주소가 다르면 그 요청은 가차 없이 중단(Abort)시켜 버립니다. 한마디로, 서버 액션을 품고 있는 페이지가 떠 있는 호스트(도메인)와 정확히 일치하는 곳에서만 액션을 호출할 수 있다는 뜻이에요.

하지만 만약 리버스 프록시를 쓰거나 백엔드 아키텍처가 복잡한 대규모 앱의 경우 (실제 서비스되는 도메인과 서버 API 도메인이 다른 경우 등), 기본 방어막 때문에 정상적인 요청도 막힐 수 있습니다. 이럴 때는 next.config.js 파일에서 serverActions.allowedOrigins 옵션을 사용해 "안전하다고 믿을 수 있는 외부 도메인 목록"을 직접 지정해주어야 합니다.

/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
    },
  },
}

이와 관련된 더 딥한 이야기는 보안과 서버 액션 블로그 글에서 확인하실 수 있습니다.

렌더링 중 발생하는 부수 효과(Side-effects) 피하기

데이터를 변경하는 작업(예: 사용자 로그아웃 처리, 데이터베이스 업데이트, 캐시 삭제 등)은 절대, 네버! 서버나 클라이언트 컴포넌트가 화면을 그리는 '렌더링(Rendering)' 과정 중에 발생해서는 안 됩니다. 의도치 않은 버그나 무한 루프를 막기 위해, Next.js는 렌더링 함수 내부에서 쿠키를 설정하거나 캐시를 무효화(revalidate)하는 행위를 명시적으로 꽉 막아두었습니다.

// BAD: 렌더링 도중에 데이터를 변경(mutation)하려고 하는 나쁜 예시
export default async function Page({ searchParams }) {
  if (searchParams.get('logout')) {
    // 렌더링 중에 쿠키를 삭제하려고 하면 Next.js가 에러를 발생시킵니다!
    cookies().delete('AUTH_TOKEN')
  }

  return <UserProfile />
}

화면을 그리는 도중에 데이터를 건드리지 말고, 사용자가 버튼을 클릭하는 등 이벤트가 발생했을 때 서버 액션을 호출해서 데이터를 변경하도록 하세요.

// GOOD: 데이터 변경 처리는 깔끔하게 서버 액션에게 맡기는 좋은 예시
import { logout } from './actions'

export default function Page() {
  return (
    <>
      <UserProfile />
      <form action={logout}>
        <button type="submit">Logout</button>
      </form>
    </>
  )
}

알아두면 좋은 점: Next.js는 데이터 변경을 처리할 때 오직 POST 요청만을 사용합니다. 이것은 단순 조회를 위한 GET 요청에서 실수로 데이터가 변경되는 부수 효과를 막아주고, 결과적으로 CSRF 공격의 위험을 크게 낮춰줍니다.


보안 감사 (Auditing)

만약 여러분이 Next.js로 만든 프로젝트의 보안을 꼼꼼하게 점검(Audit)해야 한다면, 강사로서 다음 항목들을 특히 주의 깊게 살펴보시길 추천합니다:

  • 데이터 접근 계층 (Data Access Layer): 격리된 DAL을 구성하고 사용하는 규칙이 팀 내에 잘 자리 잡혀 있나요? 데이터베이스 연결 패키지나 환경 변수가 DAL 바깥쪽 컴포넌트에서 무분별하게 import 되고 있지는 않은지 꼭 확인하세요.
  • "use client" 파일들: 클라이언트 컴포넌트가 받는 Props에 숨겨야 할 개인 데이터가 섞여 있지는 않나요? 타입 정의(Type signatures)가 필요 이상으로 너무 광범위하게 잡혀서 통째로 데이터를 넘기고 있지는 않은지 점검하세요.
  • "use server" 파일들: 서버 액션으로 들어오는 인자(Arguments)들이 액션 내부나 DAL 안에서 철저하게 검증(Validation)되고 있나요? 액션이 실행될 때 사용자의 권한(인가)을 다시 한 번 꼼꼼히 체크하고 있나요?
  • /[param]/ 폴더들: 대괄호로 묶인 폴더명은 사용자가 마음대로 입력할 수 있는 URL 파라미터입니다. 이 파라미터 값들이 안전한지 검증하고 계신가요?
  • proxy.tsroute.ts 파일들: 이 파일들은 굉장히 강력한 권한을 가지고 동작합니다. 이런 파일들은 기존 백엔드를 감사할 때 쓰는 전통적인 보안 점검 기법들을 사용해 각별히 시간을 들여 검토하셔야 합니다. 모의 해킹(Penetration Testing)이나 취약점 스캐닝을 정기적으로 수행하거나, 팀의 소프트웨어 개발 주기(SDLC)에 맞춰 진행하는 것을 권장합니다.

다음 단계 (Next Steps)

오늘 가이드에서 언급된 주제들에 대해 더 깊이 파고들고 싶다면 아래 문서들을 참고해 보세요!


전체 문서의 맥락적 개요를 보시려면 사이트맵 (https://nextjs.org/docs/sitemap.md)을 확인해 주세요.

이용 가능한 모든 문서의 색인(Index)은 LLM용 텍스트 파일 (https://nextjs.org/docs/llms.txt)에서 확인하실 수 있습니다.


오늘 강의는 여기까지입니다. 내용이 조금 길고 어려울 수 있지만, 프론트엔드 개발자로서 서버 사이드 렌더링 환경의 보안을 이해하는 것은 이제 선택이 아닌 필수랍니다! 꼭 직접 코드를 쳐보고 복습해 보세요. 수고하셨습니다! 😊

profile
프론트에_가까운_풀스택_개발자

0개의 댓글