Laravel + Vue (Inertia) + gRPC: gRPC User 서비스와 통신하는 간단한 BFF 구축
Source: Dev.to
Laravel + Vue + Inertia + gRPC: 간단한 BFF 만들기 (gRPC 사용자 서비스와 연동)
개요
이번 포스트에서는 Laravel을 백엔드 프레임워크로, Vue와 Inertia.js를 프론트엔드 레이어로, 그리고 gRPC를 통해 별도의 사용자 서비스와 통신하는 BFF (Backend‑For‑Frontend) 를 구축하는 과정을 단계별로 살펴보겠습니다.
- Laravel: API 라우팅, 인증, 비즈니스 로직 담당
- Vue + Inertia.js: 서버‑사이드 렌더링 없이 SPA‑같은 경험 제공
- gRPC: 고성능 바이너리 프로토콜을 이용한 마이크로서비스 간 통신
목표: 프론트엔드에서 직접 gRPC 호출을 하지 않고, Laravel BFF가 중간에서 gRPC 클라이언트를 사용해 데이터를 받아 Vue 컴포넌트에 전달하도록 구현합니다.
사전 준비
| 항목 | 버전 | 비고 |
|---|---|---|
| PHP | ^8.1 | Laravel 10 이상 권장 |
| Laravel | 10.x | |
| Composer | 2.x | |
| Node.js | >=18 | |
| npm / Yarn | >=8 | |
| Vue | 3.x | |
| Inertia.js | ^1.0 | |
protobuf compiler (protoc) | 3.21+ | |
grpc/grpc PHP extension | 1.55+ | (선택 사항, grpc/grpc 패키지도 사용 가능) |
gRPC 서비스 정의 (proto)
syntax = "proto3";
package user;
service UserService {
rpc GetUser (GetUserRequest) returns (GetUserResponse);
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
}
message GetUserRequest {
string id = 1;
}
message GetUserResponse {
string id = 1;
string name = 2;
string email = 3;
}
message ListUsersRequest {}
message ListUsersResponse {
repeated GetUserResponse users = 1;
}
위 파일을 proto/user.proto 로 저장하고, 아래 명령어로 PHP와 JavaScript용 스텁을 생성합니다.
# PHP 스텁
protoc --php_out=app/Grpc --grpc_out=app/Grpc \
--plugin=protoc-gen-grpc=$(which grpc_php_plugin) \
proto/user.proto
# JavaScript (Vue) 스텁 – 필요 시 사용
protoc --js_out=import_style=commonjs,binary:resources/js/grpc \
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:resources/js/grpc \
proto/user.proto
Laravel 프로젝트 설정
1. 새 Laravel 프로젝트 생성
composer create-project laravel/laravel bff-grpc-demo
cd bff-grpc-demo
2. 필요한 패키지 설치
composer require guzzlehttp/guzzle
composer require spiral/roadrunner # (옵션) 고성능 서버
composer require spiral/roadrunner-grpc # gRPC 클라이언트 지원
composer require inertiajs/inertia-laravel
composer require laravel/ui # Vue 스캐폴딩용
3. Inertia와 Vue 설정
php artisan ui vue
npm install @inertiajs/inertia @inertiajs/inertia-vue3
npm install
npm run dev
resources/js/app.js 를 다음과 같이 수정합니다.
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/inertia-vue3';
createInertiaApp({
resolve: name => import(`./Pages/${name}.vue`),
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el);
},
});
4. gRPC 클라이언트 래퍼 만들기
app/Services/Grpc/UserService.php
<?php
namespace App\Services\Grpc;
use App\Grpc\User\UserServiceClient;
use App\Grpc\User\GetUserRequest;
use App\Grpc\User\ListUsersRequest;
use Grpc\UnaryCall;
class UserService
{
protected UserServiceClient $client;
public function __construct()
{
// gRPC 서버 주소 (예: localhost:50051)
$this->client = new UserServiceClient('127.0.0.1:50051', [
'credentials' => \Grpc\ChannelCredentials::createInsecure(),
]);
}
public function getUser(string $id)
{
$request = new GetUserRequest();
$request->setId($id);
/** @var UnaryCall $call */
$call = $this->client->GetUser($request);
[$response, $status] = $call->wait();
if ($status->code !== \Grpc\STATUS_OK) {
throw new \Exception($status->details, $status->code);
}
return $response;
}
public function listUsers(): array
{
$request = new ListUsersRequest();
/** @var UnaryCall $call */
$call = $this->client->ListUsers($request);
[$response, $status] = $call->wait();
if ($status->code !== \Grpc\STATUS_OK) {
throw new \Exception($status->details, $status->code);
}
return $response->getUsers();
}
}
Tip:
UserServiceClient클래스는protoc로 생성된 파일에 포함됩니다. 네임스페이스와 경로가 다를 경우composer.json의autoload섹션을 조정해 주세요.
5. 라우트와 컨트롤러
routes/web.php
use App\Http\Controllers\UserController;
use Inertia\Inertia;
Route::get('/', fn() => Inertia::render('Dashboard'));
Route::prefix('users')->group(function () {
Route::get('/', [UserController::class, 'index'])->name('users.index');
Route::get('/{id}', [UserController::class, 'show'])->name('users.show');
});
app/Http/Controllers/UserController.php
<?php
namespace App\Http\Controllers;
use App\Services\Grpc\UserService;
use Inertia\Inertia;
use Illuminate\Http\Request;
class UserController extends Controller
{
protected UserService $grpc;
public function __construct(UserService $grpc)
{
$this->grpc = $grpc;
}
public function index()
{
$users = $this->grpc->listUsers();
// Laravel 컬렉션으로 변환 후 Vue에 전달
return Inertia::render('Users/Index', [
'users' => collect($users)->map(fn($u) => [
'id' => $u->getId(),
'name' => $u->getName(),
'email' => $u->getEmail(),
]),
]);
}
public function show($id)
{
$user = $this->grpc->getUser($id);
return Inertia::render('Users/Show', [
'user' => [
'id' => $user->getId(),
'name' => $user->getName(),
'email' => $user->getEmail(),
],
]);
}
}
Vue 페이지 구현
1. resources/js/Pages/Users/Index.vue
<template>
<div>
<h1 class="text-2xl font-bold mb-4">사용자 목록</h1>
<ul class="space-y-2">
<li v-for="user in users" :key="user.id" class="p-2 border rounded">
<Link :href="route('users.show', user.id)">
{{ user.name }} ({{ user.email }})
</Link>
</li>
</ul>
</div>
</template>
<script setup>
import { Link } from '@inertiajs/inertia-vue3';
defineProps({
users: Array,
});
</script>
2. resources/js/Pages/Users/Show.vue
<template>
<div>
<h1 class="text-2xl font-bold mb-4">사용자 상세</h1>
<div class="p-4 border rounded">
<p><strong>ID:</strong> {{ user.id }}</p>
<p><strong>이름:</strong> {{ user.name }}</p>
<p><strong>이메일:</strong> {{ user.email }}</p>
</div>
<Link class="mt-4 inline-block text-blue-600" :href="route('users.index')">
← 목록으로 돌아가기
</Link>
</div>
</template>
<script setup>
import { Link } from '@inertiajs/inertia-vue3';
defineProps({
user: Object,
});
</script>
gRPC 사용자 서비스 (예시)
여기서는 Node.js 기반의 간단한 gRPC 서버를 예시로 보여줍니다. 실제 프로젝트에서는 Go, Java, Python 등 원하는 언어로 구현할 수 있습니다.
// server.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDef = protoLoader.loadSync('proto/user.proto', {});
const userProto = grpc.loadPackageDefinition(packageDef).user;
const users = [
{ id: '1', name: '홍길동', email: 'hong@example.com' },
{ id: '2', name: '김철수', email: 'kim@example.com' },
];
function getUser(call, callback) {
const user = users.find(u => u.id === call.request.id);
if (user) {
callback(null, user);
} else {
callback({
code: grpc.status.NOT_FOUND,
details: 'User not found',
});
}
}
function listUsers(_, callback) {
callback(null, { users });
}
const server = new grpc.Server();
server.addService(userProto.UserService.service, {
GetUser: getUser,
ListUsers: listUsers,
});
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
console.log('gRPC 서버가 50051 포트에서 실행 중...');
server.start();
});
주의: 프로덕션 환경에서는 TLS 인증서를 사용해
createSsl()로 보안 채널을 구성해야 합니다.
테스트 및 디버깅
Laravel → gRPC 호출 확인
php artisan tinker
>>> $service = app(App\Services\Grpc\UserService::class);
>>> $service->listUsers();
위 명령이 정상적으로 사용자 배열을 반환한다면 BFF와 gRPC 서비스 간 연결이 성공한 것입니다.
프론트엔드 확인
npm run dev
브라우저에서 http://localhost:8000/users 로 접속하면 사용자 목록이 표시되고, 각 항목을 클릭하면 상세 페이지가 나타납니다. 네트워크 탭에서 Inertia 요청이 정상적으로 반환되는지 확인하세요.
마무리
- Laravel이 gRPC 클라이언트 역할을 수행함으로써 프론트엔드에서는 전통적인 HTTP/JSON 인터페이스만 사용하게 됩니다.
- Inertia.js 덕분에 Vue 컴포넌트는 라우팅과 상태 관리가 서버‑사이드 라우트와 자연스럽게 연결됩니다.
- gRPC는 마이크로서비스 간에 낮은 레이턴시와 강력한 타입을 제공하므로, 복잡한 비즈니스 로직을 별도 서비스로 분리하고 싶을 때 이상적인 선택입니다.
다음 단계로는:
- 인증 – Laravel Sanctum 혹은 Passport 로 API 토큰을 발급하고, gRPC 호출에 메타데이터로 전달.
- 에러 핸들링 – gRPC 상태 코드를 Laravel 예외로 매핑해 사용자에게 친절한 오류 메시지 제공.
- CI/CD –
protoc자동 실행, Docker‑Compose 로
Laravel + Vue 프로젝트에서 gRPC를 사용하는 이유
현대적인 Laravel + Vue 앱을 구축할 때 기본적인 선택은 보통 REST/JSON입니다.
gRPC는 다릅니다: API를 .proto 파일에 계약으로 정의하고, 강타입 클라이언트/서버 코드(스텁) 를 생성합니다. HTTP/2 위에서 동작하며 Protocol Buffers를 사용해 압축된 직렬화를 제공합니다.
Note: 브라우저에서 실행되는 Vue 앱은 추가 인프라(gRPC‑Web + Envoy와 같은 프록시) 없이는 gRPC를 직접 사용할 수 없습니다.
실용적인 패턴은 다음과 같습니다.✅ Vue (browser) → Laravel (HTTP/JSON) → gRPC microservice (internal)
Laravel은 BFF(Backend‑For‑Frontend) 역할을 합니다. 이 저장소는 바로 그 방식을 시연합니다.
목표
Vue 페이지에 사용자를 표시하되, Laravel을 통해 gRPC 서버를 호출하도록 합니다.
흐름
- Vue 페이지(
UserShow.vue)가GET /api/users/{id}를 호출합니다. - Laravel API 라우트가
App\Services\UserGrpc를 실행합니다. UserGrpc는 생성된 gRPC 클라이언트를 사용해UserService.GetUser를 호출합니다.- Node gRPC 서버가 사용자 데이터를 반환합니다(데모에서는 Laravel이 사용하는 동일한 SQLite DB를 읽습니다).
폴더 레이아웃 (하이라이트)
proto/user/v1/user.proto → contract
generated/App/Grpc/... → generated PHP stubs (client classes)
grpc-server/ → demo gRPC server (Node)
app/Services/UserGrpc.php → Laravel gRPC client wrapper
resources/js/Pages/UserShow.vue → Vue / Inertia page
새 프로젝트의 경우, Laravel의 Vue 스타터 키트(Inertia)가 훌륭한 기반이 됩니다.
1️⃣ 계약 정의 (proto/user/v1/user.proto)
syntax = "proto3";
package user.v1;
service UserService {
rpc GetUser (GetUserRequest) returns (GetUserResponse);
}
message GetUserRequest {
string id = 1;
}
message GetUserResponse {
string id = 1;
string name = 2;
}
.proto 파일은 단일 진실의 원천입니다.
2️⃣ PHP 클라이언트 스텁 생성
buf는 Protobuf 워크플로와 생성 설정(buf.yaml, buf.gen.yaml)을 표준화합니다.
npx buf generate
생성된 파일은 generated/App/Grpc/... 아래에 위치합니다.
Tip: 많은 팀이 생성된 코드를 커밋하지 않습니다(CI에서 생성합니다).
데모와 빠른 설정을 위해 커밋해도 괜찮습니다.
Important: PHP 클라이언트 스텁을 생성해도 서버는 생성되지 않습니다. 여전히 gRPC 서버 구현이 필요합니다(Go, Node, Java, PHP, …). 이 저장소에서는 데모 서버가
grpc-server/아래에 있습니다.
3️⃣ Demo gRPC 서버 (Node)
cd grpc-server
npm install
Cannot find module 'dotenv' 혹은 Cannot find module 'better-sqlite3' 와 같은 오류가 발생하면, 누락된 패키지를 설치합니다:
npm install dotenv better-sqlite3 @grpc/grpc-js @grpc/proto-loader
grpc-server/server.js
require("dotenv").config();
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const PROTO_PATH = __dirname + "/../proto/user/v1/user.proto";
const packageDef = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const userProto = grpc.loadPackageDefinition(packageDef).user.v1;
// Demo handler
function GetUser(call, callback) {
const { id } = call.request;
// Minimal demo response
callback(null, { id, name: `User #${id}` });
}
function main() {
const server = new grpc.Server();
server.addService(userProto.UserService.service, { GetUser });
const addr = process.env.GRPC_ADDR || "0.0.0.0:50051";
server.bindAsync(
addr,
grpc.ServerCredentials.createInsecure(),
() => {
console.log(`gRPC server listening on ${addr}`);
server.start();
}
);
}
main();
서버를 시작합니다:
node server.js
Laravel에서 Connection refused 가 발생한다면, gRPC 서버가 실행되지 않았거나 예상한 주소/포트에서 수신 대기하고 있지 않은 것입니다.
4️⃣ Laravel gRPC 클라이언트 래퍼 (app/Services/UserGrpc.php)
client = new UserServiceClient(
env('USER_SVC_ADDR', '127.0.0.1:50051'),
['credentials' => ChannelCredentials::createInsecure()]
);
}
public function getUser(string $id): array
{
$req = new GetUserRequest();
$req->setId($id);
/** @var \App\Grpc\User\V1\GetUserResponse $resp */
[$resp, $status] = $this->client->GetUser($req)->wait();
if ($status->code !== STATUS_OK) {
throw new \RuntimeException($status->details, $status->code);
}
return [
'id' => $resp->getId(),
'name' => $resp->getName(),
];
}
}
일반적인 문제: Grpc\ChannelCredentials를 찾을 수 없음
이는 gRPC PHP 확장이 현재 실행 중인 PHP에 활성화되지 않았음을 의미합니다 (CLI와 FPM이 서로 다를 수 있음).
php -m | grep grpc # "grpc"가 목록에 표시되어야 합니다
php --ini # 로드된 php.ini 파일을 확인합니다
올바른 php.ini에 확장을 활성화하십시오:
extension=grpc.so
5️⃣ 클래식 JSON API 엔드포인트 노출
routes/api.php
use Illuminate\Support\Facades\Route;
use App\Services\UserGrpc;
Route::get('/users/{id}', function (string $id, UserGrpc $userGrpc) {
return response()->json($userGrpc->getUser($id));
});
routes/web.php (Inertia 페이지 라우트)
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::get('/users/{id}', function (string $id) {
return Inertia::render('UserShow', [
'id' => $id,
]);
});
6️⃣ Vue 페이지 (resources/js/Pages/UserShow.vue)
Inertia‑friendly한 깔끔한 버전은 props를 사용합니다 (Vue Router는 필요하지 않음).
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
type User = { id: string; name: string };
const props = defineProps<{ id: string }>();
const id = computed(() => props.id);
const loading = ref(true);
const error = ref<string | null>(null);
const user = ref<User | null>(null);
onMounted(async () => {
try {
const resp = await fetch(`/api/users/${id.value}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
user.value = await resp.json();
} catch (e: any) {
error.value = e.message;
} finally {
loading.value = false;
}
});
</script>
<template>
<div>
<h2>User {{ id }}</h2>
<div v-if="loading">Loading…</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>
<p><strong>ID:</strong> {{ user?.id }}</p>
<p><strong>Name:</strong> {{ user?.name }}</p>
</div>
</div>
</template>
요약
| 단계 | 수행 내용 |
|---|---|
| 1 | .proto 계약을 작성합니다. |
| 2 | PHP 클라이언트 스텁을 생성합니다 (buf generate). |
| 3 | gRPC 서버를 실행합니다 (Node 데모). |
| 4 | 생성된 클라이언트를 Laravel 서비스(UserGrpc)에 래핑합니다. |
| 5 | 서비스를 호출하는 JSON API 엔드포인트를 노출합니다. |
| 6 | Inertia/Vue 페이지가 JSON 엔드포인트를 가져옵니다. |
이 설정을 통해 얻을 수 있는 장점:
- 강력한 타입을 서비스 전반에 걸쳐 제공합니다 (Protobuf 덕분).
- 효율적인 바이너리 전송을 백엔드 내부에서 사용합니다 (HTTP/2 + protobuf).
- 깨끗한 BFF가 브라우저를 gRPC‑Web 복잡성으로부터 보호합니다.
즐거운 코딩 되세요! 🚀
추가 Vue 예제 (대체 구현)
<script setup lang="ts">
import { ref, onMounted } from "vue";
const id = ref(/* route param or similar */);
const user = ref<{ id: string; name: string }>({ id: "", name: "" });
const loading = ref(true);
const error = ref<string | null>(null);
onMounted(async () => {
try {
const res = await fetch(`/api/users/${encodeURIComponent(id.value)}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
user.value = await res.json();
} catch (e: any) {
error.value = e?.message ?? "Unknown error";
} finally {
loading.value = false;
}
});
</script>
<template>
<div>
<h2>User</h2>
<div v-if="loading">Loading…</div>
<div v-else-if="error">{{ error }}</div>
<div v-else>
<p><strong>ID:</strong> {{ user.id }}</p>
<p><strong>Name:</strong> {{ user.name }}</p>
</div>
</div>
</template>
서비스 실행
터미널 1 — gRPC 서버
cd grpc-server
node server.js
터미널 2 — Laravel + Vite
composer install
cp .env.example .env
php artisan key:generate
npm install
npm run dev
php artisan serve
이 설정을 사용할 때
- 내부 서비스 간에 타입이 지정된 계약을 원한다.
- 다양한 언어로 된 여러 백엔드가 있다.
- 팀 간에 공유되는 안정적인 인터페이스(proto)가 필요하다.
- 성능과 스트리밍 기능을 원한다(gRPC는 스트리밍을 지원한다).
앱이 작은 모놀리식이고 공개 API만이 유일한 관심사라면, REST가 여전히 더 간단할 수 있다.