Next.js에서 상수와 환경 변수를 구분하는 기준부터 NEXT_PUBLIC의 주의사항, 환경별 API 주소, 로컬 스토리지 키, 공개 키와 비밀 키 관리 방법까지 실무 예제로 알아봅니다.
Next.js 프로젝트를 만들다 보면 어떤 값을 환경 변수에 넣어야 할지 고민하게 됩니다.
API 키나 데이터베이스 접속 정보처럼 민감한 값은 환경 변수로 관리해야 한다는 기준이 명확합니다. 반면 API 주소, 로컬 스토리지 키, 페이지네이션 개수처럼 보안과 관계없는 값은 판단하기 애매합니다.
특히 NEXT_PUBLIC_이 붙은 환경 변수는 브라우저에 그대로 노출됩니다. 노출되는 값이라면 무조건 환경 변수로 관리하는 것이 좋은 방법은 아닙니다. 모든 값을 환경 변수로 만들면 배포 환경, .env 파일, CI/CD 설정 등 확인해야 할 관리 포인트만 늘어날 수 있습니다.
이 글에서는 환경 변수의 사용 방법보다 한 단계 더 나아가 어떤 값을 환경 변수로 관리해야 하는지를 Next.js App Router 기준으로 알아보겠습니다.
환경 변수를 사용할지는 다음 순서로 판단하면 됩니다.
| 값의 성격 | 권장 관리 방법 | 예시 |
|---|---|---|
| 모든 환경에서 동일한 공개 값 | 코드 상수 | 로컬 스토리지 키, 페이지 크기, 고객센터 이메일 |
| 환경마다 다른 공개 값 | NEXT_PUBLIC_ 환경 변수 | Analytics ID, 공개 SDK 키, 브라우저가 직접 호출하는 API 주소 |
| 환경마다 다른 서버 설정 | 서버 환경 변수 | 내부 API 주소, 사이트 URL, 큐 이름 |
| 외부에 노출되면 안 되는 값 | 서버 환경 변수 | DB URL, API Secret, 결제 Secret Key |
| 운영 중 즉시 바꿔야 하는 값 | 원격 설정 또는 서버 API | 실시간 기능 플래그, 점검 모드 |
결국 판단 기준은 단순합니다.
같은 값이면 상수로 두고, 배포 환경에 따라 달라지면 환경 변수로 분리합니다. 비밀값은 반드시 서버 환경 변수로 관리합니다.
환경 변수는 주로 다음 두 가지 목적을 가집니다.
반대로 두 조건에 모두 해당하지 않는 값은 상수가 더 단순할 수 있습니다.
예를 들어 모든 환경에서 동일한 페이지네이션 개수를 환경 변수로 만들었다고 가정해보겠습니다.
NEXT_PUBLIC_PAGE_SIZE=20export const PAGE_SIZE = Number(process.env.NEXT_PUBLIC_PAGE_SIZE);이 값은 보안이 필요하지 않고 개발과 프로덕션에서도 동일합니다. 환경 변수로 관리하면 다음 항목을 추가로 확인해야 합니다.
.env 파일에 값이 있는지NaN이 되지 않는지이럴 때는 상수가 더 명확합니다.
export const PAGE_SIZE = 20;코드를 보는 것만으로 값과 타입을 알 수 있고, 변경 이력도 Git에서 확인할 수 있습니다.
Next.js에서 접두사가 없는 환경 변수는 기본적으로 서버에서만 사용할 수 있습니다.
DATABASE_URL="postgresql://..."
PAYMENT_SECRET_KEY="secret-key"브라우저에서도 사용해야 하는 값에는 NEXT_PUBLIC_ 접두사를 붙입니다.
NEXT_PUBLIC_ANALYTICS_ID="G-XXXXXXXXXX"
NEXT_PUBLIC_PAYMENT_PUBLISHABLE_KEY="pk_test_xxxxx"하지만 NEXT_PUBLIC_은 보안을 위한 접두사가 아닙니다. Next.js는 이 값을 next build 시점에 클라이언트 JavaScript 번들에 포함합니다. 사용자는 개발자 도구나 내려받은 JavaScript 파일에서 값을 확인할 수 있습니다.
export const analyticsId = process.env.NEXT_PUBLIC_ANALYTICS_ID;위 코드는 빌드 과정에서 다음과 비슷한 형태의 값으로 대체됩니다.
export const analyticsId = "G-XXXXXXXXXX";따라서 다음 값에는 절대로 NEXT_PUBLIC_을 붙이면 안 됩니다.
NEXT_PUBLIC_ 변수는 빌드 시점에 포함되므로 빌드가 끝난 뒤 서버의 환경 변수만 변경해도 클라이언트 값은 바뀌지 않습니다. 값을 변경하려면 다시 빌드하고 배포해야 합니다.
이 특성은 하나의 Docker 이미지를 개발, 스테이징, 프로덕션에 차례로 배포할 때 특히 주의해야 합니다. 이미지를 만들 때 들어간 NEXT_PUBLIC_ 값이 다음 환경에서도 그대로 사용되기 때문입니다.
운영 중 즉시 바꿔야 하는 공개 설정이라면 NEXT_PUBLIC_ 대신 서버 API나 원격 설정 서비스를 통해 런타임에 전달하는 방식이 더 적합합니다.
공개 환경 변수는 다음처럼 전체 이름을 직접 작성하는 것이 좋습니다.
const analyticsId = process.env.NEXT_PUBLIC_ANALYTICS_ID;동적인 접근은 빌드 시 값이 인라인되지 않습니다.
const key = "NEXT_PUBLIC_ANALYTICS_ID";
// 사용하지 않는 것을 권장
const analyticsId = process.env[key];
// 사용하지 않는 것을 권장
const env = process.env;
const anotherAnalyticsId = env.NEXT_PUBLIC_ANALYTICS_ID;다음 조건을 모두 만족한다면 환경 변수보다 코드 상수를 먼저 고려합니다.
개발과 프로덕션에서 같은 키를 사용해도 문제가 없다면 상수로 충분합니다.
export const STORAGE_KEYS = {
theme: "my-app:theme",
recentSearches: "my-app:recent-searches:v1",
} as const;import { STORAGE_KEYS } from "@/constants/storage.const";
export const saveTheme = (theme: "light" | "dark") => {
localStorage.setItem(STORAGE_KEYS.theme, theme);
};로컬 스토리지 키는 공개되어도 되는 값이고 보안 기능도 없습니다. 값이 모든 환경에서 같다면 NEXT_PUBLIC_STORAGE_KEY를 추가하는 것은 관리 포인트만 늘립니다.
저장하는 데이터 구조가 변경될 수 있다면 환경 변수 대신 v1, v2처럼 버전을 키에 포함하는 것이 좋습니다. 기존 데이터와 새로운 스키마의 충돌을 방지할 수 있습니다.
export const APP_NAME = "Recode Log";
export const DEFAULT_PAGE_SIZE = 20;
export const MAX_UPLOAD_SIZE_MB = 10;
export const SUPPORT_EMAIL = "help@example.com";이 값들이 환경에 따라 달라지지 않는다면 코드 상수가 더 읽기 쉽습니다. 특히 페이지 크기나 최대 업로드 크기가 실제 비즈니스 로직과 연결되어 있다면 코드와 함께 리뷰하고 테스트하는 편이 안전합니다.
브라우저가 같은 Next.js 서버의 Route Handler를 호출한다면 도메인을 환경 변수로 만들 필요가 없습니다.
export const getUser = async () => {
const response = await fetch("/api/user");
if (!response.ok) {
throw new Error("사용자 정보를 불러오지 못했습니다.");
}
return response.json();
};http://localhost:3000/api/user와 https://example.com/api/user를 구분하지 않고 상대 경로를 사용하면 현재 도메인에 맞춰 자동으로 요청됩니다.
# 불필요할 수 있는 설정
NEXT_PUBLIC_API_URL="http://localhost:3000/api"백엔드 주소가 모든 환경에서 완전히 같고 앞으로도 배포 설정에서 바꿀 이유가 없다면 일반 상수로 관리할 수도 있습니다.
export const EXTERNAL_API_URL = "https://api.example.com";브라우저가 백엔드 API를 직접 호출하고 환경마다 주소가 다르다면 공개 환경 변수가 적절합니다.
NEXT_PUBLIC_API_URL="http://localhost:8080"NEXT_PUBLIC_API_URL="https://api.example.com"export const apiClient = async (path: string, init?: RequestInit) => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}${path}`,
init,
);
if (!response.ok) {
throw new Error("API 요청에 실패했습니다.");
}
return response.json();
};단, API 주소를 숨기기 위해 NEXT_PUBLIC_을 제거하는 것은 해결책이 아닙니다. 브라우저가 직접 요청하는 주소는 네트워크 탭에서 확인할 수 있습니다. 공개 여부가 아니라 환경별 분리가 필요한지를 기준으로 판단해야 합니다.
Server Component, 서버 전용 서비스 또는 Route Handler에서만 사용하는 주소는 접두사 없이 관리합니다.
BACKEND_API_URL="https://internal-api.example.com"
BACKEND_API_KEY="secret-key"import "server-only";
export const getUser = async () => {
const response = await fetch(`${process.env.BACKEND_API_URL}/user`, {
headers: {
Authorization: `Bearer ${process.env.BACKEND_API_KEY}`,
},
});
if (!response.ok) {
throw new Error("사용자 정보를 불러오지 못했습니다.");
}
return response.json();
};이 방식은 내부 API 주소와 인증 키가 클라이언트 번들에 포함되는 것을 방지합니다.
Firebase 설정이나 결제 서비스의 Publishable Key처럼 공개를 전제로 한 값도 개발용 프로젝트와 운영용 프로젝트가 다르다면 환경 변수로 관리하는 것이 좋습니다.
NEXT_PUBLIC_PAYMENT_PUBLISHABLE_KEY="pk_test_xxxxx"NEXT_PUBLIC_PAYMENT_PUBLISHABLE_KEY="pk_live_xxxxx"공개 키는 노출되어도 된다는 뜻이지 아무런 보호가 필요 없다는 뜻은 아닙니다. 서비스에서 허용 도메인, API 권한, 사용량 제한 등을 설정할 수 있다면 반드시 함께 적용해야 합니다.
일반적으로 localhost와 운영 도메인은 저장 공간이 분리되므로 같은 키를 사용해도 충돌하지 않습니다. 하지만 동일한 도메인에서 개발 모드와 운영 모드를 함께 제공하는 등 명확한 분리 목적이 있다면 접두사를 환경 변수로 둘 수 있습니다.
NEXT_PUBLIC_STORAGE_PREFIX="my-app:dev"NEXT_PUBLIC_STORAGE_PREFIX="my-app:prod"const storagePrefix = process.env.NEXT_PUBLIC_STORAGE_PREFIX;
export const STORAGE_KEYS = {
theme: `${storagePrefix}:theme`,
recentSearches: `${storagePrefix}:recent-searches:v1`,
} as const;이 경우에도 값이 누락되면 undefined:theme이라는 키가 만들어질 수 있으므로 빌드 전에 유효성 검사가 필요합니다.
메타데이터, 이메일 링크, OAuth Callback URL을 만들 때 사이트의 절대 주소가 필요할 수 있습니다.
SITE_URL="https://example.com"서버에서만 절대 주소를 만들면 SITE_URL로 충분합니다. 브라우저 코드에서도 값이 필요하고 로컬, Preview, Production 주소가 다르다면 NEXT_PUBLIC_SITE_URL을 사용할 수 있습니다.
다만 브라우저에서 현재 주소만 필요하다면 환경 변수보다 window.location.origin이 더 정확할 수 있습니다. 현재 요청의 호스트를 서버에서 확인할 수 있는 경우에도 고정된 환경 변수가 꼭 필요한지 먼저 확인하는 것이 좋습니다.
배포할 때만 기능의 활성화 여부가 바뀐다면 공개 환경 변수를 사용할 수 있습니다.
NEXT_PUBLIC_ENABLE_NEW_EDITOR="true"export const isNewEditorEnabled =
process.env.NEXT_PUBLIC_ENABLE_NEW_EDITOR === "true";하지만 장애가 발생했을 때 재빌드 없이 즉시 기능을 끄고 싶다면 적합하지 않습니다. NEXT_PUBLIC_ 값은 빌드 시 고정되므로 원격 기능 플래그 서비스나 서버 API를 사용해야 합니다.
그리고 공개 기능 플래그는 화면을 숨기는 용도로만 사용해야 합니다. 다음처럼 관리자 권한을 판단하는 보안 장치로 사용하면 안 됩니다.
// 사용자가 브라우저에서 값을 확인하고 요청을 직접 보낼 수 있습니다.
const canAccessAdmin = process.env.NEXT_PUBLIC_ENABLE_ADMIN === "true";권한은 서버에서 로그인 정보와 사용자 역할을 다시 확인해야 합니다.
환경 변수는 모두 문자열입니다.
API_TIMEOUT_MS="5000"
ENABLE_CACHE="false"// "false"도 비어 있지 않은 문자열이므로 true가 됩니다.
const enableCache = Boolean(process.env.ENABLE_CACHE);명시적으로 변환하고 범위를 검사해야 합니다.
const apiTimeoutMs = Number(process.env.API_TIMEOUT_MS);
const enableCache = process.env.ENABLE_CACHE === "true";
if (!Number.isFinite(apiTimeoutMs) || apiTimeoutMs <= 0) {
throw new Error("API_TIMEOUT_MS는 0보다 큰 숫자여야 합니다.");
}| 값 | 권장 방식 | 이유 |
|---|---|---|
DATABASE_URL | 서버 환경 변수 | 비밀값이며 환경마다 다름 |
| 결제 Secret Key | 서버 환경 변수 | 브라우저 노출 금지 |
| 결제 Publishable Key | NEXT_PUBLIC_ | 브라우저 SDK에서 필요하며 환경마다 다름 |
| Analytics ID | NEXT_PUBLIC_ | 공개 값이며 환경별 프로젝트가 다를 수 있음 |
| Sentry DSN | 상황에 따라 NEXT_PUBLIC_ | 공개 사용을 전제로 하지만 환경별 분리가 필요할 수 있음 |
| 로컬 스토리지 키 | 상수 | 공개 값이며 대부분 환경 분리가 불필요 |
| 페이지당 항목 수 | 상수 | UI 또는 도메인 규칙에 가까움 |
같은 출처의 /api 경로 | 문자열 또는 상수 | 상대 경로로 환경을 자동 구분 |
| 외부 API Base URL | 환경 변수 또는 상수 | 환경별 변경 여부에 따라 결정 |
| 고객센터 이메일 | 주로 상수 | 모든 환경에서 같다면 설정 분리 불필요 |
| 실시간 기능 플래그 | 원격 설정 또는 서버 API | 재빌드 없이 변경해야 함 |
Next.js는 프로젝트 루트의 .env* 파일을 읽습니다. src 폴더를 사용하는 프로젝트도 환경 변수 파일은 src 내부가 아닌 루트에 둡니다.
| 파일 | 용도 |
|---|---|
.env | 모든 환경의 기본값 |
.env.development | 개발 환경 기본값 |
.env.production | 프로덕션 환경 기본값 |
.env.test | 테스트 환경 기본값 |
.env.local | 현재 컴퓨터의 로컬 덮어쓰기 값 |
.env.development.local | 현재 컴퓨터의 개발 환경 덮어쓰기 값 |
.env.production.local | 현재 컴퓨터의 프로덕션 환경 덮어쓰기 값 |
.env.example | 팀에 필요한 변수 이름과 형식을 알리는 예시 파일 |
Next.js는 같은 이름의 변수를 다음 우선순위로 찾고, 먼저 찾은 값을 사용합니다.
process.env.env.$(NODE_ENV).local.env.local (test 환경 제외).env.$(NODE_ENV).env예를 들어 .env와 .env.development.local에 같은 변수가 있다면 개발 환경에서는 .env.development.local의 값이 사용됩니다.
비밀값이 들어갈 수 있는 .env* 파일은 Git에서 제외하고, 필요한 변수의 목록만 .env.example로 공유하는 방식을 권장합니다.
.env*
!.env.example# server only
DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/DATABASE"
BACKEND_API_KEY=""
# client public
NEXT_PUBLIC_ANALYTICS_ID=""
NEXT_PUBLIC_PAYMENT_PUBLISHABLE_KEY=""프로덕션 값은 Vercel, GitHub Actions, AWS 등 실제 배포 환경의 Secret 또는 Environment Variable 설정에서 주입합니다.
팀에서 비밀값이 없는 .env.development 기본값을 Git으로 관리하기로 결정할 수도 있습니다. 이 경우에도 파일 정책을 명확하게 정하고, 비밀값이 포함되지 않는지 리뷰해야 합니다.
process.env.API_URL의 TypeScript 타입은 기본적으로 string | undefined입니다. ! 또는 기본값만 반복해서 사용하면 누락된 설정을 늦게 발견할 수 있습니다.
작은 프로젝트는 직접 검사해도 충분합니다.
import "server-only";
const backendApiUrl = process.env.BACKEND_API_URL;
if (!backendApiUrl) {
throw new Error("BACKEND_API_URL 환경 변수가 필요합니다.");
}
export const serverEnv = {
backendApiUrl,
};변수가 많아지면 Zod 스키마를 사용하면 타입 변환과 검증을 한곳에서 관리할 수 있습니다.
pnpm add zod server-onlyimport "server-only";
import { z } from "zod";
const serverEnvSchema = z.object({
DATABASE_URL: z.string().min(1),
BACKEND_API_URL: z.url(),
BACKEND_API_KEY: z.string().min(1),
API_TIMEOUT_MS: z.coerce.number().int().positive().default(5000),
});
const parsedServerEnv = serverEnvSchema.parse({
DATABASE_URL: process.env.DATABASE_URL,
BACKEND_API_URL: process.env.BACKEND_API_URL,
BACKEND_API_KEY: process.env.BACKEND_API_KEY,
API_TIMEOUT_MS: process.env.API_TIMEOUT_MS,
});
export const serverEnv = {
databaseUrl: parsedServerEnv.DATABASE_URL,
backendApiUrl: parsedServerEnv.BACKEND_API_URL,
backendApiKey: parsedServerEnv.BACKEND_API_KEY,
apiTimeoutMs: parsedServerEnv.API_TIMEOUT_MS,
};server-only를 사용하면 Client Component에서 이 모듈을 잘못 import했을 때 빌드 오류가 발생합니다.
import { z } from "zod";
const clientEnvSchema = z.object({
NEXT_PUBLIC_ANALYTICS_ID: z.string().min(1),
NEXT_PUBLIC_PAYMENT_PUBLISHABLE_KEY: z.string().startsWith("pk_"),
});
export const clientEnv = clientEnvSchema.parse({
NEXT_PUBLIC_ANALYTICS_ID: process.env.NEXT_PUBLIC_ANALYTICS_ID,
NEXT_PUBLIC_PAYMENT_PUBLISHABLE_KEY:
process.env.NEXT_PUBLIC_PAYMENT_PUBLISHABLE_KEY,
});공개 환경 변수도 객체를 통째로 읽지 않고 process.env.NEXT_PUBLIC_변수명 형태로 직접 작성합니다. 그래야 Next.js가 빌드할 때 값을 정상적으로 인라인할 수 있습니다.
환경 변수에 저장했다고 해서 자동으로 안전해지는 것은 아닙니다. 서버에서 읽은 값을 응답이나 Client Component의 props로 전달하면 결국 브라우저에 노출됩니다.
import "server-only";
import { serverEnv } from "@/lib/env/server";
export const createPayment = async (amount: number) => {
const response = await fetch(`${serverEnv.backendApiUrl}/payments`, {
method: "POST",
headers: {
Authorization: `Bearer ${serverEnv.backendApiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ amount }),
});
if (!response.ok) {
throw new Error("결제 생성에 실패했습니다.");
}
return response.json();
};규모가 커지면 환경 변수 접근을 Data Access Layer나 서버 전용 서비스 계층으로 제한하면 어떤 코드가 비밀값을 사용하는지 확인하기 쉬워집니다.
클라이언트 라이브러리에서 서버 비밀값이 필요한 외부 API를 직접 호출하면 안 됩니다. Route Handler를 통해 서버에서 외부 API를 호출합니다.
import { serverEnv } from "@/lib/env/server";
import { NextResponse } from "next/server";
export const GET = async () => {
// 실제 프로젝트에서는 이 위치에서 로그인과 권한을 확인합니다.
const response = await fetch(`${serverEnv.backendApiUrl}/profile`, {
headers: {
Authorization: `Bearer ${serverEnv.backendApiKey}`,
},
});
if (!response.ok) {
return NextResponse.json(
{ message: "프로필을 불러오지 못했습니다." },
{ status: response.status },
);
}
const profile = await response.json();
return NextResponse.json({
id: profile.id,
name: profile.name,
});
};export const getProfile = async () => {
const response = await fetch("/api/profile");
if (!response.ok) {
throw new Error("프로필을 불러오지 못했습니다.");
}
return response.json();
};Route Handler는 비밀 키를 숨길 수 있지만 그 자체로 인증 기능을 제공하지는 않습니다. 외부에서 /api/profile을 직접 호출할 수 있으므로 로그인, 권한, 입력값 검증, 요청 횟수 제한 등을 별도로 적용해야 합니다.
또한 외부 API의 전체 응답을 그대로 전달하지 말고 클라이언트에 필요한 필드만 선택해서 반환하는 것이 좋습니다.
Client Component에서 편하게 사용하기 위해 모든 변수에 NEXT_PUBLIC_을 붙이면 비밀값까지 번들에 포함될 수 있습니다. 클라이언트에서 정말 필요한 공개 값인지 먼저 확인해야 합니다.
.env 파일은 일반 텍스트 파일입니다. 환경 변수도 실행 중인 프로세스가 읽을 수 있는 설정일 뿐입니다. Git에서 제외하고 배포 서비스의 Secret 저장소와 접근 권한을 함께 관리해야 합니다.
next.config의 env 옵션에 정의한 값은 NEXT_PUBLIC_ 접두사 여부와 관계없이 JavaScript 번들에 포함될 수 있습니다.
const nextConfig = {
env: {
PAYMENT_SECRET_KEY: process.env.PAYMENT_SECRET_KEY,
},
};
export default nextConfig;비밀값은 next.config의 env 옵션으로 전달하지 않고 서버 코드에서 process.env.PAYMENT_SECRET_KEY로 직접 읽습니다.
Next.js의 NODE_ENV는 development, production, test 용도로 사용됩니다. 스테이징을 별도 환경으로 구분하려면 다른 변수를 사용합니다.
APP_ENV="staging"브라우저에서도 환경 이름이 필요하다면 NEXT_PUBLIC_APP_ENV를 사용할 수 있지만, 단순히 현재 개발 모드인지 확인하는 목적이라면 Next.js가 설정하는 process.env.NODE_ENV로 충분합니다.
const apiUrl = process.env.BACKEND_API_URL ?? "https://api.example.com";필수 변수에 운영 주소를 기본값으로 넣으면 로컬 개발 중 실수로 운영 API를 호출할 수 있습니다. 필수 설정은 빠르게 오류를 발생시키고, 정말 선택적인 값에만 안전한 기본값을 사용합니다.
설정 오류를 확인하기 위해 console.log(process.env)를 사용하면 배포 로그에 비밀값이 남을 수 있습니다. 값 자체 대신 존재 여부나 안전하게 가린 일부만 기록합니다.
console.log({
hasDatabaseUrl: Boolean(process.env.DATABASE_URL),
appEnv: process.env.APP_ENV,
});.env* 파일을 변경한 뒤 값이 반영되지 않는다면 개발 서버를 종료하고 다시 실행합니다. 특히 NEXT_PUBLIC_ 변수는 빌드 결과에 포함되므로 프로덕션에서는 재빌드가 필요합니다.
환경 변수를 추가하기 전에 다음 항목을 확인해봅니다.
/api 상대 경로를 사용할 수 있는가?NEXT_PUBLIC_을 제거했는가?NEXT_PUBLIC_ 값이 사용자에게 공개되어도 문제가 없는가?.env* 파일이 Git에서 제외되어 있는가?.env.example에 필요한 키와 형식이 정리되어 있는가?환경 변수를 많이 사용하는 것이 좋은 설계는 아닙니다. 환경 변수는 비밀값을 코드에서 분리하거나 배포 환경마다 다른 설정을 주입할 때 가장 가치가 있습니다.
로컬 스토리지 키, 페이지 크기처럼 공개되어 있고 모든 환경에서 동일한 값은 상수가 더 단순합니다. 반대로 개발과 프로덕션의 API 주소, 외부 서비스 프로젝트, 데이터베이스 접속 정보처럼 환경별로 달라지는 값은 환경 변수로 분리하는 것이 좋습니다.
그리고 NEXT_PUBLIC_은 보안 기능이 아니라 공개 범위를 표시하는 이름입니다. 이 기준만 명확히 이해해도 불필요한 환경 변수를 줄이고 비밀값의 노출 위험도 함께 낮출 수 있습니다.
React 웹에서 Apple 로그인 구현하기 - Sign in with Apple JS와 Node.js 검증
EAS 없이 Expo 앱 로컬 빌드해서 Play Console 내부 테스트 배포하기