How to think about data security in Next.js

김동현·2026년 3월 4일

next.js 공식문서 번역

목록 보기
22/79

React Server Components(리액트 서버 컴포넌트)는 성능을 향상시키고 데이터 패칭(가져오기)을 정말 단순하게 만들어줍니다. 하지만 동시에 데이터에 접근하는 '위치'와 '방식'을 바꿔놓았기 때문에, 기존 프론트엔드 앱에서 데이터를 다루던 전통적인 보안 가정들에도 큰 변화가 생겼어요.

💡 강사의 보충 설명: > 예전 React(SPA) 환경에서는 브라우저(클라이언트)에서 백엔드 API를 호출하는 방식이 주를 이루었죠. 하지만 Next.js App Router가 도입되면서 서버에서 직접 DB를 찌르거나 민감한 API를 호출하는 일이 잦아졌습니다. 즉, 프론트엔드 개발자도 백엔드 개발자처럼 '서버 보안'에 대해 진지하게 고민해야 하는 시대가 온 거예요!

이 가이드는 여러분이 Next.js에서 데이터 보안을 어떻게 생각해야 하는지, 그리고 모범 사례를 어떻게 구현할 수 있는지 이해하는 데 큰 도움을 줄 겁니다.

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

Next.js에서 데이터를 가져올 때 권장하는 세 가지 주요 접근 방식이 있어요. 여러분의 프로젝트 규모나 연차(진행 단계)에 따라 선택하시면 됩니다.

여기서 아주 중요한 포인트가 있어요. 이 방식들 중 하나만 선택하고, 여러 방식을 섞어 쓰는 것은 피하길 권장합니다. 하나로 통일해야 코드베이스에서 작업하는 동료 개발자들은 물론이고, 나중에 보안 감사를 하는 분들도 코드를 예측하고 파악하기가 훨씬 수월해지거든요.

🧑‍🏫 강사의 실무 팁: > 실무에서 프로젝트 일정이 바쁘다고 여기저기 다른 방식을 섞어 쓰다 보면 나중에 정말 '스파게티 코드'가 됩니다. 어디서는 컴포넌트에서 직접 DB를 부르고, 어디서는 API를 부르고... 나중에 보안 구멍(Vulnerability)을 찾으려면 밤을 새워야 할 수도 있어요. 팀 내에서 꼭 "우리는 이 방식으로 간다!"라고 규칙을 정하세요.

외부 HTTP API (External HTTP APIs)

기존 프로젝트에 서버 컴포넌트(Server Components)를 도입할 때는 반드시 Zero Trust(제로 트러스트) 모델을 따라야 해요. '아무도 믿지 마라'는 뜻이죠. 클라이언트 컴포넌트에서 했던 것과 똑같이, 서버 컴포넌트에서도 fetch 함수를 사용해 REST나 GraphQL 같은 기존 API 엔드포인트를 계속 호출할 수 있습니다.

//filename="app/page.tsx"
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', {
    headers: {
      Cookie: `AUTH_TOKEN=${token}`,
      // Other headers
    },
  })

  // ....
}

이 방식은 다음과 같은 상황에서 아주 찰떡입니다:

  • 이미 견고한 보안 관행이 회사 내에 자리 잡고 있을 때.
  • 백엔드 팀이 별도로 분리되어 있어서 다른 언어(Java, Python 등)를 사용하거나 API를 독립적으로 관리할 때.

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

신규 프로젝트라면 Data Access Layer(데이터 접근 계층, 줄여서 DAL) 이라는 전용 계층을 만드는 것을 강력하게 추천해요. 쉽게 말해, 데이터를 언제 어떻게 가져올지, 그리고 컴포넌트(렌더링 컨텍스트)에 '어떤 데이터만' 넘겨줄지를 통제하는 우리 앱 내부의 도서관 사서 같은 역할을 하는 라이브러리라고 보시면 됩니다.

데이터 접근 계층(DAL)이 갖춰야 할 조건은 다음과 같아요:

  • 오직 서버에서만 실행되어야 합니다.
  • 권한 부여(Authorization) 검사를 수행해야 합니다. (이 사람이 이 데이터를 볼 자격이 있나?)
  • 안전하고 최소한의 데이터만 담긴 DTO(Data Transfer Objects)를 반환해야 합니다.

💡 강사의 보충 설명: DTO(Data Transfer Object)란?
이름 그대로 '데이터를 전송하기 위한 객체'입니다. 데이터베이스에서 회원 정보를 가져오면 비밀번호, 주민번호 같은 민감한 정보가 다 들어있겠죠? 그걸 컴포넌트에 통째로 넘기면 큰일 납니다! 화면에 그려줄 이름과 프로필 사진 URL 정도만 딱 추려서 새로운 객체로 만드는데, 그걸 DTO라고 불러요.

이 방식을 사용하면 모든 데이터 접근 로직이 한 곳으로 모입니다(중앙 집중화). 덕분에 일관된 데이터 접근 규칙을 강제하기 쉬워지고, 권한 체크를 빼먹는 버그의 위험도 확 줄어들죠. 게다가 하나의 요청(Request) 내의 여러 부분에서 메모리 내 캐시(in-memory cache)를 공유할 수 있다는 엄청난 장점도 얻게 됩니다.

//filename="data/auth.ts"
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)
})
//filename="data/user-dto.tsx"
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
  return {
    username: canSeeUsername(currentUser) ? userData.username : null,
    phonenumber: canSeePhoneNumber(currentUser, userData.team)
      ? userData.phonenumber
      : null,
  }
}
//filename="app/page.tsx"
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);
  ...
}

알아두면 좋은 점 (Good to know): 비밀 키(Secret keys)들은 환경 변수(environment variables)에 저장해야 하는데, 오직 데이터 접근 계층(DAL)에서만 process.env에 접근하도록 하세요. 이렇게 하면 애플리케이션의 다른 부분에 비밀 키가 노출되는 것을 철저하게 막을 수 있습니다.

컴포넌트 레벨 데이터 접근 (Component-level data access)

빠르게 프로토타입을 만들고 반복해서 테스트해 볼 때는 데이터베이스 쿼리를 서버 컴포넌트 안에 직접 넣어도 괜찮아요.

하지만! 이 방식은 민감한 비공개 데이터가 클라이언트(브라우저)에 실수로 노출되기 정말 딱 좋은 구조입니다. 아래 예시를 한 번 볼까요?

//filename="app/page.tsx"
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} />
}
//filename="app/ui/profile.tsx"
'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>
  )
}

🧑‍🏫 강사의 일침:
이거 초보자분들이 정말 많이 하는 실수예요! SELECT *로 모든 정보를 다 끌어온 다음에 클라이언트 컴포넌트인 <Profile />user 프롭스로 통째로 넘겨버렸죠? 이렇게 하면 개발자 도구(Network 탭)를 열어보는 순간 사용자의 비밀번호 해시값, 전화번호 등 넘기면 안 되는 정보까지 브라우저로 다 넘어가버립니다. 절대 이렇게 하시면 안 돼요!

반드시 클라이언트 컴포넌트로 데이터를 넘기기 전에 데이터를 '세탁(정제, sanitize)' 해야 합니다.

//filename="data/user.ts"
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,
  }
}
//filename="app/page.tsx"
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을 만들어냅니다 (이걸 Pre-rendering이라고 하죠). 하지만 이 둘은 완전히 격리된 모듈 시스템에서 실행돼요. 덕분에 서버 컴포넌트는 비공개 데이터와 API에 안전하게 접근할 수 있는 반면, 클라이언트 컴포넌트는 그렇게 할 수 없도록 확실히 보장해 줍니다.

서버 컴포넌트 (Server Components):

  • 오직 서버에서만 실행돼요.
  • 환경 변수, 비밀 키(secrets), 데이터베이스, 내부 API에 아주 안전하게 접근할 수 있어요.

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

  • 미리 렌더링(pre-rendering)을 할 때 서버에서 한 번 실행되긴 하지만, 근본적으로는 브라우저에서 실행되는 코드와 똑같은 보안 기준을 따라야 해요.
  • 특권이 필요한 데이터(비밀번호 등)나 서버 전용 모듈에 절대 접근해서는 안 됩니다.

이런 구조 덕분에 Next.js 앱은 기본적으로 안전(Secure by default)합니다. 하지만 방금 전 컴포넌트 레벨 데이터 접근에서 본 것처럼, 데이터를 가져오거나 컴포넌트에 넘겨주는 과정에서 개발자의 실수로 민감한 데이터가 노출될 위험은 항상 열려있어요.

테인팅 (Tainting / 데이터 오염시키기)

클라이언트에 실수로 민감한 데이터가 노출되는 것을 막기 위해, React의 Taint(테인트) API를 사용할 수 있어요. (Taint는 '오염시키다'라는 뜻인데, 여기서는 데이터에 꼬리표를 달아서 클라이언트로 넘어가는 순간 에러를 터뜨리게 만드는 보안 장치라고 이해하시면 됩니다!)

Next.js 앱의 next.config.js 파일에서 experimental.taint 옵션을 켜주면 바로 사용할 수 있습니다.

//filename="next.config.js"
module.exports = {
  experimental: {
    taint: true,
  },
}

이렇게 하면 taint된(오염된, 즉 꼬리표가 붙은) 객체나 값이 클라이언트로 전달되는 것을 원천 차단해 줍니다. 하지만 이건 어디까지나 추가적인 보호막일 뿐이에요. 이걸 믿고 코드를 대충 짜면 안 되고, 여전히 DAL (데이터 접근 계층)에서 React의 렌더링 컨텍스트로 데이터를 넘기기 전에 데이터를 꼼꼼히 필터링하고 정제(sanitize)해야 합니다.

알아두면 좋은 점 (Good to know):

  • 기본적으로 환경 변수는 서버에서만 쓸 수 있어요. Next.js는 이름 앞에 NEXT_PUBLIC_ 이 붙은 환경 변수만 클라이언트에 노출시켜 줍니다. 자세히 알아보기.
  • 함수(Functions)나 클래스(Classes)는 기본적으로 클라이언트 컴포넌트로 전달되는 것이 아예 차단되어 있습니다.

서버 전용 코드가 클라이언트에서 실행되는 것 막기

서버에서만 돌아가야 하는 코드가 클라이언트에서 실수로라도 실행되는 것을 막으려면, 모듈 상단에 server-only 패키지를 명시해주면 됩니다.

npm install server-only
yarn add server-only
pnpm add server-only
bun add server-only
//filename="lib/data.ts"
import 'server-only'

//...

이렇게 해두면, 만약 클라이언트 환경("use client"가 선언된 곳)에서 이 모듈을 import 하려고 시도할 때 아예 빌드 에러를 내뱉습니다. 회사의 핵심 비즈니스 로직이나 보안이 중요한 코드가 철저하게 서버에만 머물도록 강제하는 아주 훌륭한 방법이죠.

데이터 변경하기 (Mutating Data)

Next.js는 Server Actions(서버 액션)을 사용해 데이터의 변경(Mutation)을 처리합니다.

내장된 Server Actions 보안 기능

기본적으로 서버 액션이 생성되고 export(내보내기) 되면, 이건 곧 공개된 HTTP 엔드포인트(API 주소) 가 하나 생기는 것과 같습니다. 따라서 일반적인 API를 만들 때와 똑같이 보안을 가정하고 권한 검사를 해야 해요. 내 코드 다른 곳에서 이 서버 액션 함수를 import 해서 쓰지 않더라도, 해커들은 여전히 외부에서 이 엔드포인트에 접근할 수 있다는 뜻입니다!

보안을 강화하기 위해 Next.js는 다음과 같은 기능들을 기본 탑재하고 있어요:

  • 보안 액션 ID (Secure action IDs): Next.js는 클라이언트가 서버 액션을 참조하고 호출할 수 있도록 암호화되고 예측 불가능한(non-deterministic) ID를 만듭니다. 이 ID들은 보안 강화를 위해 빌드할 때마다 주기적으로 재계산되어 바뀝니다.
  • 사용하지 않는 코드 제거 (Dead code elimination): 클라이언트 번들에서 사용되지 않는(ID로 참조되지 않는) 서버 액션은 외부에서 접근하지 못하도록 아예 삭제해버립니다.

알아두면 좋은 점:

이 ID들은 코드가 컴파일될 때 만들어지고 최대 14일 동안 캐시됩니다. 새 빌드가 시작되거나 빌드 캐시가 무효화되면 다시 생성되죠.
이런 보안 개선 덕분에 인증(Authentication) 계층을 깜빡 빼먹었을 때의 위험성은 조금 줄어듭니다. 하지만 여러분은 반드시 서버 액션을 공개된 HTTP 엔드포인트 다루듯이 철저히 대해야 합니다.

// app/actions.js
'use server'

// If this action **is** used in our application, Next.js
// will create a secure ID to allow the client to reference
// and call the Server Action.
export async function updateUserAction(formData) {}

// If this action **is not** used in our application, Next.js
// will automatically remove this code during `next build`
// and will not create a public endpoint.
export async function deleteUserAction(formData) {}

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

클라이언트(브라우저)에서 오는 입력값은 절대 믿어선 안 됩니다. 아주 쉽게 조작할 수 있거든요. 폼 데이터(Form data), URL 파라미터, 헤더, 검색 파라미터(searchParams) 등 모든 것을 항상 검증해야 합니다.

//filename="app/page.tsx"
// BAD: Trusting searchParams directly
export default async function Page({ searchParams }) {
  const isAdmin = searchParams.get('isAdmin')
  if (isAdmin === 'true') {
    // Vulnerable: relies on untrusted client data
    return <AdminPanel />
  }
}

// GOOD: Re-verify every time
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 />
  }
}

🧑‍🏫 강사의 실무 팁: > 위의 나쁜 예시를 보세요. 주소창에 ?isAdmin=true 라고 치기만 하면 누구나 관리자 페이지를 볼 수 있게 짜놓았네요! 황당해 보이지만 주니어 시절에 흔히 할 수 있는 실수입니다. 데이터베이스나 쿠키의 토큰을 통해 서버 측에서 직접 권한을 재확인하는 것이 진짜 보안입니다.

인증과 권한 부여 (Authentication and authorization)

어떤 액션을 수행할 때, 이 사용자가 그 행동을 할 '권한'이 있는지 항상 확인해야 합니다.

//filename="app/actions.ts"
'use server'

import { auth } from './lib'

export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('You must be signed in to perform this action')
  }

  // ...
}

Next.js의 인증(Authentication)에 대해 더 자세히 알아보세요.

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

컴포넌트 내부에 서버 액션을 정의하면 클로저(closure)가 생성됩니다. 이렇게 되면 서버 액션이 자신을 감싸고 있는 바깥쪽 함수의 스코프(변수들)에 접근할 수 있게 되죠. 예를 들어, 아래 코드에서 publish 액션은 바깥에 있는 publishVersion 변수에 접근할 수 있어요.

💡 강사의 보충 설명: 클로저란?
자바스크립트에서 어떤 함수가 생성될 당시의 주변 환경(변수 등)을 기억하는 마법 같은 기능입니다. 함수가 바깥쪽 변수를 '품고' 태어난다고 생각하시면 편해요.

//filename="app/page.tsx" switcher
export default async function Page() {
  const publishVersion = await getLatestVersion();

  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('The version has changed since pressing publish');
    }
    ...
  }

  return (
    <form>
      <button formAction={publish}>Publish</button>
    </form>
  );
}
//filename="app/page.js" switcher
export default async function Page() {
  const publishVersion = await getLatestVersion();

  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('The version has changed since pressing publish');
    }
    ...
  }

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

클로저는 렌더링 시점의 데이터 스냅샷 (예: publishVersion)을 캡처해 두었다가, 나중에 액션이 호출될 때 사용해야 할 때 아주 유용합니다.

하지만 이런 마법이 일어나려면, 캡처된 변수들이 클라이언트로 전송되었다가 액션이 실행될 때 다시 서버로 돌아와야만 합니다. 민감한 데이터가 클라이언트에 노출되는 것을 막기 위해, Next.js는 이 클로저 안의 변수들을 자동으로 암호화합니다. Next.js 앱이 빌드될 때마다 각 액션을 위한 새로운 개인 키(private key)가 생성돼요. 즉, 어떤 빌드에서 만들어진 액션은 딱 그 빌드 버전에서만 호출할 수 있다는 뜻입니다.

알아두면 좋은 점: 클라이언트에 민감한 값이 노출되는 것을 막기 위해 오직 이 '암호화'에만 의존하는 것은 권장하지 않습니다. 암호화는 거들 뿐, 근본적인 설계가 안전해야 합니다!

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

여러 대의 서버를 돌리면서 Next.js 앱을 직접 호스팅(Self-hosting) 하는 경우, 각 서버 인스턴스마다 서로 다른 암호화 키가 생성되어서 일관성이 깨지는 문제가 생길 수 있습니다.

이를 방지하기 위해 process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY 환경 변수를 사용해서 암호화 키를 강제로 덮어쓸 수 있어요. 이 변수를 지정하면 여러 번 빌드를 하더라도 암호화 키가 영구적으로 유지되고, 모든 서버 인스턴스가 똑같은 키를 사용하게 됩니다.

이 키는 반드시 base64로 인코딩된 값이어야 하고, 디코딩했을 때의 길이가 유효한 AES 키 사이즈(16, 24, 32 바이트)와 일치해야 합니다. Next.js는 기본적으로 32바이트 키를 생성합니다. 여러분이 쓰는 운영체제의 암호화 도구를 사용해서 호환되는 키를 만들 수 있어요. 예를 들면 아래처럼요:

openssl rand -base64 32

이건 여러 대의 서버에 배포할 때 암호화 동작을 일관되게 유지하는 것이 매우 중요한, '고급(Advanced)' 사용 사례에 해당합니다. 키 교체(Key rotation)나 서명(Signing) 같은 표준 보안 관행을 잘 따라주세요. 배포 시 고려해야 할 구체적인 내용은 직접 호스팅 가이드(Self-Hosting guide)를 참고하시면 됩니다.

허용된 출처 (고급 과정 - Allowed origins)

서버 액션은 <form> 태그 안에서 호출할 수 있기 때문에, CSRF 공격 (Cross-Site Request Forgery)에 노출될 위험이 있습니다.

💡 강사의 보충 설명: CSRF 공격이란?
사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(비밀번호 변경, 결제 등)를 특정 웹사이트에 요청하게 만드는 공격을 말해요. 예전엔 폼 전송을 악용한 공격이 아주 잦았죠.

하지만 걱정 마세요. 내부적으로 서버 액션은 POST 메서드를 사용하고, 오직 이 HTTP 메서드만 서버 액션을 호출하도록 허용되어 있습니다. 이것만으로도 최신 브라우저에서는 (특히 SameSite cookies가 기본값이 되면서) 대부분의 CSRF 취약점을 막아줍니다.

추가적인 보호막으로, Next.js의 서버 액션은 요청의 Origin 헤더Host 헤더 (또는 X-Forwarded-Host)를 비교합니다. 만약 두 개가 일치하지 않으면 요청을 즉시 튕겨냅니다(abort). 즉, 서버 액션을 호스팅하는 페이지와 똑같은 호스트(도메인)에서만 서버 액션을 호출할 수 있다는 뜻입니다.

리버스 프록시를 사용하거나 복잡한 다층 백엔드 아키텍처를 가진 대규모 애플리케이션의 경우 (예: 서버 API 도메인과 실제 서비스 도메인이 다를 때), next.config.js 설정에 있는 serverActions.allowedOrigins 옵션을 사용해서 '이 출처는 안전하다'고 리스트를 명시해 주는 것이 좋습니다. 문자열 배열을 넣으면 돼요.

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

보안 및 서버 액션 (Security and Server Actions) 에 대해 더 깊이 공부해보세요.

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

사용자 로그아웃, 데이터베이스 업데이트, 캐시 무효화 같은 데이터 변경 작업(Mutations)은 절대 렌더링 과정에서 발생하는 '부수 효과(Side-effect)'로 처리되어서는 안 됩니다. 서버 컴포넌트든 클라이언트 컴포넌트든 마찬가지예요. Next.js는 개발자가 의도치 않은 부수 효과를 일으키는 것을 막기 위해, 렌더링 과정 내에서 쿠키를 굽거나(설정하거나) 캐시 재검증을 유발하는 것을 명시적으로 차단하고 있습니다.

//filename="app/page.tsx"
// BAD: Triggering a mutation during rendering
export default async function Page({ searchParams }) {
  if (searchParams.get('logout')) {
    cookies().delete('AUTH_TOKEN')
  }

  return <UserProfile />
}

그 대신, 항상 서버 액션(Server Actions) 을 사용해서 이런 데이터 변경 작업을 처리해야 합니다.

//filename="app/page.tsx"
// GOOD: Using Server Actions to handle mutations
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에 비공개 데이터가 노출되지는 않았나요? 타입스크립트의 타입 시그니처가 불필요하게 너무 넓게 잡혀 있진 않은가요?
  • "use server" 파일들: 서버 액션으로 넘어오는 인자(Arguments)들이 액션 내부나 DAL 안에서 제대로 검증(Validate)되고 있나요? 액션 내부에서 사용자의 권한을 다시 한번 확인(Re-authorize)하고 있나요?
  • /[param]/. 라우팅: 대괄호([])가 쳐진 폴더는 사용자가 직접 입력하는 값입니다. 이 파라미터 값들에 대한 검증이 이루어지고 있나요?
  • proxy.tsroute.ts 파일: 이 파일들은 시스템에서 아주 큰 권한을 가집니다. 전통적인 보안 점검 기술을 동원해서 더 많은 시간을 들여 감사해야 합니다. 정기적으로 팀의 소프트웨어 개발 주기(SDLC)에 맞춰 침투 테스트(Penetration Testing)나 취약점 스캐닝을 수행하세요.

다음 단계 (Next Steps)

가이드에서 언급된 주제들에 대해 더 깊이 배워보세요.


전체 문서의 의미론적 개요를 보려면 https://nextjs.org/docs/sitemap.md를 참조하세요.

사용 가능한 모든 문서의 인덱스를 보려면 https://nextjs.org/docs/llms.txt를 참조하세요.

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

0개의 댓글