회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
프론트엔드 개발자 입장에서는 개발할 때 로컬 환경에 백엔드 서버를 일일이 빌드하는 일은 다소 번거롭게 느껴진다. 특히 빌드가 깨지기라도 하면, 그 문제를 서버 개발자에게 보고하고 오류를 해결해야만 다시 작업을 시작할 수 있다. 이러한 시간 소모가 누적되면 결국 큰 시간 낭비를 초래한다. 그래서 최근 내가 속한 팀에서는 백엔드 서버를 브랜치 단위로 배포하고, 클라이언트를 개발하는 서버에서 직접 배포된 백엔드 서버를 향해 API를 요청하기로 하였다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
프론트엔드 개발자 입장에서는 개발할 때 로컬 환경에 백엔드 서버를 일일이 빌드하는 일은 다소 번거롭게 느껴진다. 특히 빌드가 깨지기라도 하면, 그 문제를 서버 개발자에게 보고하고 오류를 해결해야만 다시 작업을 시작할 수 있다. 이러한 시간 소모가 누적되면 결국 큰 시간 낭비를 초래한다. 그래서 최근 내가 속한 팀에서는 백엔드 서버를 브랜치 단위로 배포하고, 클라이언트를 개발하는 서버에서 직접 배포된 백엔드 서버를 향해 API를 요청하기로 하였다.
그러나 웹 개발 환경에서 배포된 서버에 API를 요청하는 것은 꽤 까다로운 일이다. 일반적으로 클라이언트 개발용 서버는 HTTP 프로토콜을 사용하고 배포된 서버는 HTTPS 프로토콜을 사용하는데, 브라우저가 HTTP → HTTPS 통신을 쉽게 허락하지 않아 에러를 발생시키기 때문이다. 이는 흔히 CORS라고 부르는 브라우저 정책 위반에 의한 에러다.
CORS 에러를 해결하기 위해서는 백엔드 서버에서 강제로 특정 도메인에 대한 보안을 해제해야 한다. 하지만 서버가 허용한다고 하더라도 브라우저 자체적으로 차단하는 리소스가 있다. 쿠키(Cookie)도 그중 하나다. 쿠키는 서버가 클라이언트에 발급해 주는 것으로 클라이언트 인증에 사용하는 데이터 조각인데, 브라우저는 HTTP와 HTTPS 간 통신에서 쿠키 전송을 허용하지 않고 차단해 버린다. 자, 그럼 어떻게 해야 할까? 내가 시도한 방법을 본격적으로 공유하기에 앞서 몇 가지 개념을 짧게 언급하고 시작하겠다.
우리 팀은 빌드 도구로 Vite를 사용하고 있다. Vite는 JavaScript 및 TypeScript 애플리케이션을 위한 빌드 도구 및 개발 서버다. 매우 빠르다는 장점을 가지고 있고 여러 편리한 기능을 제공해 준다. 만약 Webpack이나 Parcel 등 다른 빌드 도구를 사용해 본 사람이라면, 직접 Vite와 성능 비교를 해봐도 흥미로울 것이다.
Vite가 제공하는 여러 편의 기능 중 하나가 바로 프록시(Proxy) 서버를 쉽게 구성할 수 있다는 점이다. vite.config.js 파일에 다음과 같이 proxy라는 키에 대한 값으로 객체를 추가해주면 끝이다.
이런 구성을 활용하면 Vite 개발 서버가 프록시 서버의 기능도 겸하게 된다. 그동안 나는 서버는 메인 서버 역할만 수행할 수 있고 프록시 서버는 별도로 구성해야 한다고 생각했는데, 이렇게 두 가지 역할을 겸할 수 있다는 부분이 처음 접했을 때 무척 신기하게 느껴졌다.
여기서 잠깐, 혹시나 프록시가 무엇인지 모르는 사람을 위해 짧게 설명하려고 한다. 프록시는 클라이언트와 서버 사이에서 데이터를 중계하는 역할을 한다. 쉽게 말해 클라이언트로부터 요청을 받아 서버로 전달하고 서버로부터 받은 응답을 클라이언트에게 반환하는 일이다. 프록시를 사용하면 사용자의 요청을 필터링하거나 수정해 보안을 강화하고, 여러 대의 서버에 요청을 분산시키는 로드밸런싱으로 네트워크 성능을 높일 수도 있다.
이때 클라이언트 측에서 동작하는 프록시를 포워드 프록시라고 부르며, 서버 측에서 동작하는 프록시를 리버스 프록시라고 부른다.
포워드 프록시(Forward Proxy): 클라이언트 측에서 동작하며 클라이언트의 요청을 외부 서버로 중계한다. 주로 익명성 보장, 보안, 캐싱 등의 목적으로 사용된다. 예를 들어, 회사나 학교에서 내부 네트워크로 인터넷에 접속할 때, 사용자의 IP 주소를 숨길 때 쓸 수 있다. Vite Proxy는 여기 해당한다.
리버스 프록시(Reverse Proxy): 서버 측에서 동작하며 클라이언트로부터 요청을 받아 백엔드 서버로 전송한다. 주로 로드 밸런싱, 보안, 캐싱, SSL 암호화 등의 목적으로 사용된다. 예를 들어, 웹 서버 앞에 배치된 리버스 프록시가 클라이언트 요청을 받아 트래픽을 여러 서버에 분산하거나 보안 기능을 제공할 수 있다.
이제 필요한 개념을 모두 소개했으니, 본격적으로 문제를 해결한 과정을 소개할까 한다. 우선 우리는 Vite Proxy 서버를 구성했다.
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
// https://vitejs.dev/config/
export default ({ mode }) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
return defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: process.env.VITE_API_ORIGIN,
changeOrigin: true,
},
},
},
resolve: {
alias: [
{
find: '@',
replacement: path.resolve(__dirname, 'src'),
},
],
},
});
};
공식 문서의 코드와 달리 환경 변수를 사용하기 위해 defineConfig 함수를 한 번 더 함수로 감쌌다. 이 함수는 defineConfig 함수 실행값을 리턴하므로 결과적으로는 똑같다. 다만 defineConfig 함수의 파라미터를 보면 server 객체 안에 proxy 객체가 있는 것을 볼 수 있다.
이제 클라이언트 개발용 서버는 ‘/api’ 로 시작하는 경로로 보내는 요청을 target에 설정된 호스트 헤더(도메인)로 전송한다. 만약 changeOrigin을 false로 설정하면 도메인이 바뀌지 않아 사실상 프록시 기능이 무효가 된다. 만약 요청 경로를 세분화해서 관리해야 한다면 최상위 경로에 대한 changeOrigin은 true로 하고, 세부 경로에 따라 false로 설정할 수도 있다. 필요에 따라 유용하게 사용하면 된다.
설정 파일 구성이 끝나고 나면 API를 호출할 차례다. API를 호출할 때는 base url없이 요청해야 한다. 그래야 Vite Proxy가 요청을 중계하고 응답을 가져다줄 것이다.
작업을 하다 보니 한 가지 예상치 못한 복병을 만났다. 바로 메인 서버 외에 인증 서버가 따로 있다는 점이었다. 우리 프로젝트는 외부 OIDC(OpenID Connect)를 통한 인증을 활용하고 있었다. OIDC는 제3의 서비스에 사용자 인증 관리를 위임하는 방식이다.
로그인 요청이 오면 백엔드 서버는 OIDC 서비스에 요청하여 redirect url을 받은 후, 사용자에게 ‘이쪽으로 로그인하라’며 쿠키와 함께 해당 url을 전달한다. 그러면 사용자는 서버로부터 받은 쿠키를 들고 이동해 OIDC 서비스에 로그인한다. OIDC 인증이 성공하면 새로운 쿠키가 발급되고, 이와 함께 사용자는 다시 기존 로그인 엔드포인트로 이동하여 백엔드 서버에 로그인을 요청한다. 백엔드 서버는 사용자가 들고 온 쿠키가 OIDC 서버에서 발급해 준 게 맞는지 확인 절차를 거친다. 문제가 없다면 로그인을 승인하고 새로운 쿠키를 발급한다. 이 마지막 쿠키가 이후 사용자의 API 요청 헤더에 담기는 쿠키가 된다.
그림에서는 프론트에 로그인을 두 번 요청하는 것처럼 표현했지만, OIDC 로그인이 성공하면 알아서 로그인 엔드포인트로 이동하기 때문에 실제 사용자 입장에서는 로그인 API를 한 번만 요청한 셈이 된다.
우리 서비스는 사용자가 새로 진입할 때마다 세션 API를 요청한다. 세션 정보가 없을 때는 로그인 페이지로 보낸다. 만약 로그인에 성공했다면, 서버에게 받은 쿠키가 요청 헤더에 담겨 있다. 따라서 세션 API 요청은 성공하고 서비스가 유저 데이터를 받아올 수 있게 된다.
이렇게 정리하고 보면 특별히 복잡하지는 않은 흐름이지만, Vite Proxy 설정과 OIDC에 대해 잘 몰랐던 탓에 개발 도중 CORS 에러에 상당히 많이 부딪혔다. 앞서 말했듯 쿠키는 민감한 보안 정보이기 때문에 HTTP 사이트에서 HTTPS 사이트로 네트워크를 전송할 때 브라우저에 의해 차단된다. 그 결과, 이런 에러 메시지가 나타났다.
또한 나는 백엔드 서버에서 발급한 쿠키가 계속 쓰인다고 착각하고 있었는데, 인증 서버와 백엔드 서버가 각각 쿠키를 발급해 주고 있다는 사실을 디버깅 과정에서 뒤늦게 알게 되었다. 생각해 보면 각각의 서버가 직접 쿠키를 발급해 주는 게 너무나 당연했다. 쿠키 하나를 여러 번 재사용하면 보안 면에서 좋지 않다.
결론적으로 처음 로그인할 때는 백엔드 서버에서 OIDC 서버에 redirect url을 ‘localhost’ 도메인으로 달라고 요청해 클라이언트에 전달했다. 이로써 클라이언트가 문제없이 redirect url에 쿠키를 들고 이동할 수 있게 되었다. 로그인에 성공한 다음 세션 API를 호출할 때 발생한 백엔드 서버와의 CORS 문제는 앞서 말한 Vite Proxy 설정을 통해 우회할 수 있었다.
이번 작업으로 Vite Proxy 설정을 활용해 굳이 API 서버를 로컬에서 실행시키지 않고도 안전한 방식으로 배포 서버와 통신하는 방법을 찾았다. 사실 개발 서버 자체를 HTTPS로 실행시키는 방법도 있는데, 별로 안전하지 않아 권장되지 않는 듯하다. HTTPS는 보안 레이어가 추가된 프로토콜인 만큼 공인된 기관에서 발급한 인증서를 사용해야 정상적으로 동작한다. 개발 서버를 간단하게 HTTPS로 빌드하고 싶다면 임시 인증서를 발급받거나 라이브러리를 사용해야 한다. 이런 방식으로 개발 서버를 띄우면 브라우저에서 안전하지 않다고, 정말로 이 사이트에 접근할 거냐고, 계속 물어본다.
웹 개발을 하는 프론트엔드 개발자들은 CORS 에러를 마주하면 불평을 쏟아 내고는 한다. 나도 그랬다. 하지만 이번 환경 세팅 과정에서 하나씩 공부하다 보니 생각이 바뀌었다. 웹은 꾸준히 사용자를 보호하는 방향으로 발전해 왔다. 이전에 없던 HTTPS 프로토콜은 사용자의 정보를 보호하기 위해 보안 레이어를 더하며 생겨났다. 또한 브라우저에서 여러 보안 정책을 만들어 주었기에 우리가 지금처럼 (비교적) 안전하게 인터넷을 사용할 수 있다. 개발 환경에서야 불필요하게 느껴지고 답답하더라도, 그 정책을 잘 이해하고 따르면서 개발할 필요가 여기 있다.
어떻게 보면 고마운 일이다. 만약 브라우저에서 보안을 위한 기능을 제공해 주지 않았다면, 새로운 프로젝트를 만들 때마다 개발자가 일일이 보안 조치를 취해야 했을 것이다. 상상만 해도 두렵지 않은가? 그러니 생각을 조금 바꾸어보자. 조금 돌아가는 편이 오히려 더 쉽고, 더 안전하다.
요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.