본문으로 건너뛰기

Client에서 부른 함수는 어떻게 DB까지 갈까?

지난 글에서는 Next.js가 화면을 어디서, 언제 만드는지 정리했습니다. 이번에는 화면이 나온 뒤, 사용자가 값을 바꾸는 과정을 봤습니다.

Client Component에서 updatePost를 import하는데, 그 함수 안에는 DB 수정 코드가 있었습니다. 브라우저에서 부른 함수가 어떻게 DB에 접근하는지 궁금했어요. 그 연결을 따라가다 Next.js를 실행하는 Node.js와 그 안의 V8까지 내려가게 됐습니다.

예시는 게시글 제목 수정입니다. App Router의 Node.js Runtime을 기준으로 하고, 저장하는 부분은 SQLite로 좁혔습니다.

먼저, 수정한 제목이 돌아오는 길

사용자가 제출하면 요청이 서버로 넘어갑니다. 서버가 실제 함수를 실행하고 저장한 뒤, 새 제목을 반영한 화면 결과가 돌아옵니다.

브라우저의 form 제출에서 시작해 Node.js Runtime 안에서 실제 서버 함수 찾기, 입력과 수정 권한 확인, DB에 새 제목 저장, 다시 읽고 화면 만들기가 차례로 일어나고 마지막에 새 제목이 브라우저에 표시되는 전체 왕복 그림. 화살표는 실행과 전송 순서이고 큰 상자는 실행 환경이다

그림 1. 제목 수정의 전체 왕복. ②~⑤는 Node.js Runtime 안에서 실행됩니다.

화살표는 실행·전송 순서, 큰 상자는 실행 환경입니다. ②~⑤가 모두 Node.js 상자 안에 있는 이유는 Next.js와 우리 함수가 그 환경에서 실행되기 때문입니다. V8을 다음 화살표로 이어 붙이지 않은 것도 같은 이유입니다.

이 그림의 순서대로, 먼저 브라우저에 무엇이 들어가는지부터 봤습니다.

import하면 함수 본문도 브라우저로 갈까?

아래는 Client Component에서 서버 함수를 연결하는 부분만 남긴 코드입니다. actions.ts는 파일 상단에 'use server'가 있고, 비동기 함수 updatePost를 export한다고 가정했습니다.

Client Component · import와 form 연결부
'use client'
import { updatePost } from './actions'

// 컴포넌트가 반환하는 form의 연결부
<form action={updatePost}>{/* 제목 입력과 수정 버튼 */}</form>

일반 import처럼 보여도 Client용 결과에는 원래 함수 본문 대신 서버 호출용 reference가 생깁니다. 서버 함수를 식별하고 호출 요청을 시작하는 참조입니다. React의 Server Functions 설명에서도 빌드 도구가 이 참조를 만들고, Client가 호출하면 서버로 요청을 보낸다고 설명합니다.

&#39;use server&#39;가 붙은 actions.ts 하나가 빌드될 때 브라우저에 들어갈 코드에는 호출용 reference만 남고 원래 DB 수정 본문은 포함되지 않으며, 서버에 남는 코드에는 실제 updatePost 함수 본문이 들어가 요청이 오면 실행되는 것을 좌우로 나눠 보여주는 그림

그림 2. import 문법은 같아도 Client용 빌드 결과는 서버 함수 본문과 다릅니다.

여기서 import호출은 시점이 다릅니다. import는 모듈을 연결하고, form 제출은 연결해 둔 함수를 호출하는 동작입니다. 모듈을 불러올 때 최상위 초기화 코드가 실행될 수는 있어도, 함수 안의 UPDATE가 import만으로 실행되지는 않습니다.

'use server' 함수의 Client import는 React와 Next.js가 다루는 특별한 경계입니다. 아무 서버 파일이나 import하면 이렇게 바뀌는 것은 아니고요. 이 함수를 form의 action에 연결해 쓰는 경우를 Server Action이라고 부릅니다.

함수처럼 부른 뒤에는 실제 POST가 나갑니다

이제 첫 그림의 ①→②입니다. form 값은 입력 이름과 값을 담는 FormData로 모이고, 서버가 받을 수 있는 형태로 바뀝니다. 이 변환이 직렬화입니다. 실행할 함수를 찾기 위한 식별자인 Action ID도 함께 전달됩니다.

브라우저에서 callServer가 서버 함수 ID와 인자를 담아 POST를 만들고, Node.js의 Next.js 서버에서 handleAction이 인자를 복원해 함수를 찾은 뒤 action.apply로 우리가 작성한 updatePost 본문으로 들어가는 경로를 HTTP 경계를 사이에 두고 그린 그림

그림 3. Client의 함수 호출이 요청으로 바뀌고, 서버에서 다시 실제 함수 호출로 이어집니다.

브라우저 쪽의 callServer()는 ID와 인자를 Next.js의 Client 처리 흐름으로 넘깁니다. 이어지는 fetchServerAction()에서 인자를 인코딩하고 POST를 보냅니다. 직접 fetch('/api/...')를 쓰지 않았어도 HTTP 요청은 남아 있습니다.

서버의 handleAction()은 요청을 해석하고 인자를 복원한 뒤, ID에 연결된 함수를 찾습니다. 찾아 둔 함수를 action.apply(null, args)로 호출하는 지점에서 우리 updatePost() 본문으로 들어갑니다. apply는 인자 배열을 넘겨 함수를 호출하는 JavaScript 메서드입니다.

이름은 여러 개지만 일은 두 부분입니다. 브라우저에서는 호출을 요청으로 바꾸고, 서버에서는 그 요청을 실제 함수 실행으로 연결합니다. 여기서는 JavaScript가 연결된 뒤의 호출 경로를 따라갔습니다. Next.js의 Server Action 호출은 POST를 사용합니다.

Next.js, Node.js, V8은 각각 무엇을 맡을까?

“서버에서 실행된다”는 말에서 한 단계 더 내려가 봤습니다. Runtime은 코드를 실행하고 사용할 기능을 제공하는 환경입니다. Node.js를 선택한 구성에서는 Next.js 서버 코드 자체가 그 안에서 실행됩니다.

Node.js Runtime이라는 큰 상자 안에 Next.js와 우리 코드가 실행되는 JavaScript, 그것을 실행하는 V8 엔진, node와 node 같은 Node가 제공하는 API가 함께 들어 있는 포함 관계 그림. 요청이 지나가는 순서가 아니라 구성과 역할을 표시했다

그림 4. Next.js → Node.js → V8을 요청의 다음 정거장으로 나열하지 않고, 실행 환경의 구성으로 나눴습니다.

Next.js는 요청 처리와 React 화면 갱신을 연결하는 프레임워크, Node.js는 그 서버 코드를 실행하는 Runtime입니다. 그리고 V8은 Node.js가 사용하는 JavaScript 엔진입니다. V8이 JavaScript를 실행하고, 파일·HTTP·DB 같은 기능은 해당 환경의 API와 라이브러리가 제공합니다.

그래서 Next.js → Node.js → V8 → DB를 요청이 지나가는 순서로 그리면 관계가 어긋납니다. Next.js와 updatePost()의 JavaScript는 이미 Node.js 안에서 V8으로 실행되고 있습니다.

직접 서버를 시작하는 Next.js 코드에는 http.createServer(requestListener)가 있습니다. 서버를 시작할 때 Node의 HTTP 기능에 요청 처리 함수를 연결해 두는 부분입니다. 그 뒤 요청이 오면 연결된 처리가 실행됩니다. import 한 번으로 요청이나 DB 수정이 저절로 시작되는 것과는 구분됩니다.

Node.js Runtime으로 배포한 웹에서도 같은 실행 환경을 사용합니다. 다만 요청을 처음 받는 부분은 호스팅 플랫폼이 맡을 수 있어, 위 서버 시작 코드를 모든 배포의 동일한 진입점으로 보지는 않았습니다.

Chrome도 V8이고 Edge도 V8이라면, 같은 것일까?

Chrome과 Node.js와 Cloudflare workerd 세 제품을 나란히 두고, 각각 V8에 더해 DOM과 Node API와 Workers API처럼 서로 다른 기능을 제공한다는 것을 보여주는 그림. 같은 엔진을 채택했을 뿐 제품과 제공 API는 별개다

보충 그림. 같은 V8을 사용해도 제품과 제공 API는 별개입니다.

ECMAScript는 JavaScript 언어의 규칙, V8은 그 규칙을 구현한 엔진입니다. V8은 Chrome과 Node.js가 공통으로 쓰지만, 두 제품이 제공하는 기능은 다릅니다. Chrome의 document와 Node의 node:fs를 같은 기능으로 볼 수 없는 이유입니다.

Cloudflare의 workerd도 V8을 쓰는 별도 Runtime입니다. “Edge Runtime”은 문맥에 따라 특정 제품이나 Edge용 실행 환경 계열을 가리킵니다. Node.js와 Edge를 두 종류로 묶으면 제품명과 실행 환경의 범주가 섞입니다. 이 글의 DB 예시는 Node.js에 한정했습니다.

DB를 연결한 건 Next.js일까, 우리 코드일까?

첫 그림의 ③→④입니다. Next.js는 우리 서버 함수를 찾아 실행하지만, 어떤 DB를 쓰고 어떤 데이터를 바꿀지는 앱 코드가 정합니다.

여기서는 Node가 제공하는 node:sqlite를 선택했습니다. 다른 DB라면 그 DB에 맞는 드라이버나 SDK를 사용할 수 있습니다. 우리 updatePost()가 검사를 마친 뒤 아래 saveTitle()을 호출하고, 그 안의 .run()까지 내려갑니다. 아래는 DB 준비와 저장 함수만 남긴 코드입니다. posts 테이블과 고유한 id가 이미 있고, 전달할 값과 사용자 권한은 서버에서 확인하는 전제입니다.

서버의 DB 코드 · 준비와 실행을 분리
import { DatabaseSync } from 'node:sqlite'
const db = new DatabaseSync('posts.db') // DB 연결 준비

function saveTitle(title: string, postId: string, userId: string) {
return db.prepare(`
UPDATE posts SET title = ? WHERE id = ? AND author_id = ?
`).run(title, postId, userId) // 이 호출에서 실제 저장
}

node:는 Node의 내장 모듈이라는 표시입니다. 위의 import는 Node.js 자체를 불러오는 것이 아니라, 이미 Node.js에서 실행 중인 코드가 Node가 제공하는 SQLite 기능을 가져오는 것입니다. new DatabaseSync(...)는 연결을 준비하고, saveTitle() 안의 .run()이 SQL을 실행합니다.

위쪽에 node에서 DatabaseSync를 가져와 연결을 준비하는 단계를 두고, 아래쪽에 updatePost가 호출될 때 statement.run이 Node의 C++ 연결인 StatementSync::Run을 거쳐 SQLite 엔진의 sqlite3_step으로 내려가 posts.db 파일에 쓰기까지 이어지는 경로를 나눠 그린 그림

그림 5. Node API를 가져오는 준비와, 그 API를 호출해 실제로 저장하는 작업은 다른 시점입니다.

Node.js 22.16.0에서는 node:sqlite의 JavaScript 진입점이 내부 SQLite 연결을 내보내고, statement.run()에 대응하는 C++ 함수 StatementSync::Runsqlite3_step()을 호출합니다. 이 지점부터 SQLite 엔진이 SQL을 실행하고 DB 파일을 다룹니다.

Next.js가 SQLite 저장 기능을 새로 구현한 게 아니라, 앱이 선택한 Node API를 호출해서 쓰는 구조입니다. Node의 http를 사용하는 Next.js 코드와, Node의 sqlite를 사용하는 우리 코드는 서로 다른 주체입니다.

세 import를 나란히 놓으면 역할이 더 분명합니다.

import가 있는 곳가져오는 것실제 연결
Client ComponentupdatePost from ./actionsServer Function 참조. 호출하면 서버 요청
서버의 actions.tssaveTitle from ./db같은 서버 코드 안의 저장 함수
서버의 db.tsDatabaseSync from node:sqliteNode가 제공하는 내장 SQLite API

첫 번째는 React와 Next.js가 원격 호출로 연결하는 경계이고, 뒤의 두 개는 서버 안에서 사용할 코드와 기능을 가져오는 연결입니다.

SQLite는 같은 프로세스 안의 엔진입니다. 이 그림에는 외부 DB 서버로 가는 네트워크 요청이 없습니다. PostgreSQL처럼 다른 서버에 연결하는 DB라면 그 아래 경로도 달라집니다. 또 DatabaseSync는 동기 API라서, 바깥 Server Function에 async가 있어도 DB 작업 자체가 비동기로 바뀌지는 않습니다.

저장 전에 남는 확인

서버는 로그인한 사용자를 확인하고, 제목과 게시글 ID를 검사합니다. 위 SQL은 author_id를 수정 조건에 함께 넣었습니다. 호출한 쪽에서는 수정 결과가 한 건인지도 확인한 뒤 성공으로 처리합니다.

hidden input에 둔 ID도 요청자가 바꿔 보낼 수 있습니다. Server Function의 인자와 권한 확인은 서버 코드에 남습니다. 데이터를 생성·수정·삭제하는 이 작업을 Mutation이라고 부릅니다.

저장한 제목을 화면에 다시 반영하기

이제 ⑤→⑥입니다. DB에는 새 제목이 저장돼도, 이미 만들어진 화면이나 캐시에는 이전 제목이 남아 있을 수 있습니다.

updatePost()에서 저장 성공을 확인한 뒤
revalidatePath('/posts')

현재 /posts 화면에서 제목을 수정하고 그 경로를 재검증하는 경우로 놓았습니다. revalidatePath()는 해당 경로의 캐시된 결과를 무효화하고, Server Function에서 호출할 때 현재 그 경로를 보고 있다면 UI 갱신에도 연결됩니다.

DB에는 새 제목이 저장됐지만 이미 만들어진 화면과 캐시에는 이전 제목이 남을 수 있는 상태를 위아래로 나눠 두고, revalidatePath로 재검증을 요청해 서버가 새 제목으로 React 결과를 만들고 RSC Payload로 브라우저 화면에 반영되는 흐름을 이어 그린 그림

그림 6. DB 저장 성공과 화면 갱신을 나눠서 본 뒤, revalidatePath()로 다시 연결했습니다.

서버에서 새 제목을 읽어 React 결과를 만들면 RSC Payload로 전달할 수 있습니다. 서버에서 렌더한 결과와 Client Component 연결 정보 등을 담는 데이터입니다. 브라우저는 이 결과를 현재 React 트리에 반영합니다. 모든 페이지나 외부 데이터 캐시가 자동으로 바뀌는 것은 아니어서, 다시 읽을 경로를 함께 지정했습니다.

처음 궁금했던 건 Client에서 서버 함수를 import한다는 부분이었습니다. 실제로는 호출용 참조가 요청을 만들고, Next.js가 그 요청을 우리 함수에 연결합니다. Node.js까지 내려가 본 건 그 함수와 DB 코드가 어디에서, 어떤 기능으로 실행되는지 확인하기 위해서였습니다.

참고한 공식 문서와 소스 위치

공개 동작은 React·Next.js 공식 문서를 기준으로 정리했습니다. Next.js 내부 함수 이름은 아래 원문에서 확인한 경로입니다. 공개 API와는 구분했습니다. canary 링크의 코드는 바뀔 수 있어 줄 번호 대신 파일과 함수 이름을 남겼습니다. Node의 SQLite 내부는 v22.16.0 기준입니다.

확인한 내용문서·파일읽을 부분
서버 함수 참조 생성React Server FunctionsImporting Server Functions
Client 호출 → POSTNext.js Client 소스fetchServerAction, encodeReply, fetch
서버에서 실제 함수 호출Next.js Action handlerhandleAction, action.apply
Node의 HTTP 사용Next.js 서버 시작 코드requestListener, http.createServer
Node.js와 V8Node.js 소개 · V8 소개Runtime과 엔진의 관계
node: 내장 모듈Node.js ESM 문서node: imports
SQLite import와 실행Node.js 22.16 SQLite APIDatabaseSync, prepare, run
SQLite 내부 연결Node의 sqlite.js · node_sqlite.ccinternalBinding('sqlite'), StatementSync::Run
저장 후 UI 갱신Next.js revalidatePath · Server and Client ComponentsServer Functions, RSC Payload
배포별 서버 진입점Next.js Backend for FrontendDeployment environment
V8을 쓰는 또 다른 RuntimeCloudflare WorkersV8과 격리된 실행 환경