frontend-backend-history
Frontend-Backend 연결 편의성의 역사
1. SOAP (2000년대 초반)
특징: XML 기반, 복잡한 스키마, WSDL 정의 필요
Backend (Java/C#)
java
@WebService
public class UserService {
@WebMethod
public GetUserResponse getUser(@WebParam(name = "userId") String userId) {
// 복잡한 XML 응답 생성
GetUserResponse response = new GetUserResponse();
response.setUser(findUserById(userId));
return response;
}
}
Frontend (JavaScript)
javascript
// SOAP 클라이언트 생성 (매우 복잡)
var soapClient = new SOAPClient();
soapClient.url = 'http://api.example.com/userservice?wsdl';
// SOAP 엔벨로프 수동 생성
var soapEnvelope = `
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetUser xmlns="http://userservice.example.com/">
<userId>123</userId>
</GetUser>
</soap:Body>
</soap:Envelope>`;
soapClient.sendRequest(soapEnvelope, function (response) {
// XML 파싱 필요
var parser = new DOMParser();
var xmlDoc = parser.parseFromString(response, 'text/xml');
// 복잡한 XML 탐색...
});
문제점: XML 파싱, 복잡한 스키마, 타입 안전성 부족
2. Non-RESTful API (2000년대 중반)
특징: HTTP + JSON, 하지만 일관성 없는 엔드포인트 설계
Backend (PHP/Node.js)
php
// 일관성 없는 엔드포인트들
// GET /getUserData.php?id=123
// POST /updateUser.php
// GET /deleteUserById.php?userId=123
function getUserData($userId) {
return json_encode([
"success" => true,
"userData" => findUser($userId),
"timestamp" => time()
]);
}
function updateUser($data) {
return json_encode([
"status" => "ok",
"user" => updateUserInDB($data)
]);
}
Frontend (JavaScript)
javascript
// 각 API마다 다른 응답 구조
function getUser(userId) {
return fetch(`/getUserData.php?id=${userId}`)
.then((res) => res.json())
.then((data) => data.userData); // 응답 구조가 다름
}
function updateUser(userData) {
return fetch('/updateUser.php', {
method: 'POST',
body: JSON.stringify(userData),
})
.then((res) => res.json())
.then((data) => data.user); // 또 다른 응답 구조
}
문제점: 일관성 없는 API 설계, 예측 불가능한 응답 구조
3. RESTful API (2010년대)
특징: HTTP 메서드 + 리소스 기반 URL, 일관된 응답 구조
Backend (Express.js)
javascript
// 일관된 RESTful 엔드포인트
app.get('/api/users/:id', (req, res) => {
const user = findUserById(req.params.id);
res.json({
data: user,
status: 'success',
});
});
app.put('/api/users/:id', (req, res) => {
const user = updateUser(req.params.id, req.body);
res.json({
data: user,
status: 'success',
});
});
app.delete('/api/users/:id', (req, res) => {
deleteUser(req.params.id);
res.json({
message: 'User deleted',
status: 'success',
});
});
Frontend (JavaScript)
javascript
class UserAPI {
static async getUser(id) {
const response = await fetch(`/api/users/${id}`);
const result = await response.json();
return result.data;
}
static async updateUser(id, userData) {
const response = await fetch(`/api/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
const result = await response.json();
return result.data;
}
static async deleteUser(id) {
await fetch(`/api/users/${id}`, { method: 'DELETE' });
}
}
개선점: 일관된 API 설계, 예측 가능한 응답 한계점: 여전히 타입 안전성 부족, 런타임 에러 가능성
4. RPC 시대
4-1. gRPC (2015년)
특징: Protocol Buffers, 강타입, HTTP/2
Protocol Buffer 정의
protobuf
// user.proto
syntax = "proto3";
message User {
string id = 1;
string name = 2;
string email = 3;
}
message GetUserRequest {
string user_id = 1;
}
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc UpdateUser(User) returns (User);
}
Backend (Go)
go
type server struct {
pb.UnimplementedUserServiceServer
}
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user := findUserById(req.UserId)
return &pb.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
}, nil
}
Frontend (TypeScript - 생성된 코드)
typescript
import { UserServiceClient } from './generated/user_grpc_web_pb';
import { GetUserRequest } from './generated/user_pb';
const client = new UserServiceClient('http://localhost:8080');
async function getUser(userId: string): Promise<User> {
const request = new GetUserRequest();
request.setUserId(userId);
return new Promise((resolve, reject) => {
client.getUser(request, {}, (err, response) => {
if (err) reject(err);
else
resolve({
id: response.getId(),
name: response.getName(),
email: response.getEmail(),
});
});
});
}
개선점: 강타입, 성능 향상, 코드 생성 한계점: 설정 복잡성, 웹 브라우저 지원 제한
4-2. tRPC (2021년)
특징: TypeScript 기반, End-to-end 타입 안전성
Backend (TypeScript)
typescript
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const userRouter = t.router({
getUser: t.procedure.input(z.string()).query(async ({ input: userId }) => {
const user = await findUserById(userId);
return {
id: user.id,
name: user.name,
email: user.email,
};
}),
updateUser: t.procedure
.input(
z.object({
id: z.string(),
name: z.string().optional(),
email: z.string().email().optional(),
})
)
.mutation(async ({ input }) => {
const user = await updateUser(input.id, input);
return user;
}),
});
export type UserRouter = typeof userRouter;
Frontend (TypeScript)
typescript
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { UserRouter } from '../server/router';
const trpc = createTRPCProxyClient<UserRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })],
});
// 완전한 타입 안전성!
async function getUser(userId: string) {
const user = await trpc.getUser.query(userId);
// user는 자동으로 { id: string, name: string, email: string } 타입
console.log(user.name); // 타입 안전!
}
async function updateUser(userData: {
id: string;
name?: string;
email?: string;
}) {
const user = await trpc.updateUser.mutate(userData);
// 컴파일 타임에 타입 체크됨
return user;
}
TypeScript 기반 통합의 혁신
기존 방식 (RESTful + Manual Types)
typescript
// 수동으로 타입 정의 (동기화 문제)
interface User {
id: string;
name: string;
email: string;
}
// 런타임에 타입 불일치 가능성
async function getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const user = await response.json(); // any 타입!
return user as User; // 타입 캐스팅으로 위험성 내포
}
현재 방식 (tRPC)
typescript
// 서버에서 스키마 정의
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
const userRouter = t.router({
getUser: t.procedure.input(z.string()).query(({ input }) => {
// 반환 타입이 자동으로 추론됨
return findUser(input);
}),
});
// 프론트엔드에서 자동 타입 추론
const user = await trpc.getUser.query('123');
// user 타입이 자동으로 { id: string, name: string, email: string }
// 컴파일 타임에 모든 타입 오류 검출!
발전 과정 요약
- SOAP: XML 복잡성, 설정 지옥
- Non-RESTful: 일관성 부족, 예측 불가능
- RESTful: 일관성 확보, 하지만 타입 안전성 부족
- gRPC: 강타입 + 성능, 하지만 설정 복잡
- tRPC: TypeScript 생태계 통합, End-to-end 타입 안전성
결론: tRPC는 TypeScript의 타입 시스템을 활용해 프론트엔드-백엔드 간 완전한 타입 안전성을 제공하며, 개발자 경험(DX)을 크게 향상시켰습니다.