1. 일반적인 웹 어플리케이션의 동작 원리: 풀 스택 (Full Stack)
프론트 엔드와 백 엔드가 구분되지 않는 일반적인 웹 어플리케이션의 동작 원리는 어떠할까? 파이썬 기반의 장고(Django), 자바 기반의 스프링(Spring) 등이 대표적인 웹 어플리케이션이라고 볼 수 있다. 이러한 웹 어플리케이션의 동작 원리는 아주 단순하다. 클라이언트가 요청(Request)을 보내면 서버에 존재하는 웹 어플리케이션이 데이터베이스 서버와 통신하여 필요한 데이터들을 전부 가져오고, 이것들을 가지고 완전한 HTML, CSS, JavaScript 파일들을 만들어서 클라이언트에게 제공한다. 그러면 클라이언트는 그렇게 전달받은 파일들을 그대로 브라우저에서 사용하기만 하면 된다. 이와 같은 방식으로 웹을 개발하는 사람들을 통상 풀 스택 개발자(Full Stack Developer)라고 부른다. 그러나 최근에는 프론트 엔드와 백 엔드가 분리되는 경향이 짙어지면서 프론트 엔드 개발자와 백 엔드 개발자가 나뉘었고, 이때 분리된 프론트 엔드와 백 엔드를 둘 다 개발하는 사람들도 마찬가지로 풀 스택 개발자라고 부른다.
주문을 하면 요리까지 전부 직접 다 해서 제공해주는 식당에 비유할 수 있다.
2. 프론트 엔드의 분리: SPA (Single Page Application)
React, Vue, Angular 등은 JavaScript 라이브러리 혹은 프레임워크이다. 즉, 이것들은 클라이언트의 브라우저 단에서 실행되는 JavaScript를 멋지게 코딩할 수 있도록 도와주는 수단일 뿐이다. 특히 React나 Vue의 경우 JavaScript 코드를 작성할 때 필요한 만큼만 적재적소에 사용할 수 있기 때문에 이것들을 사용한다고 해서 프론트 엔드와 백 엔드가 구분되는 것은 절대 아니다. 다만 이것들을 활용하면 프론트 엔드와 백 엔드를 완전히 구분하여 두 기능을 독립적으로 개발할 수 있게 된다. 그렇다면 그것이 어떻게 가능한 것인지, 그 동작 원리를 한 번 살펴보도록 하자.
개발자가 React 라이브러리를 활용하여 클라이언트에게 제공할 JavaScript 파일들을 ES6 + JSX 문법으로 코딩하면, Babel 등의 컴파일러가 그것들을 모든 브라우저에서 호환이 가능한 문법(ES5)의 코드로 변환해준다. 또한 Webpack 등의 모듈 번들러가 HTML, CSS, JavaScript 파일들을 효율적인 방식으로 적절히 번들링 하여 준비해둔다.
create-react-app
으로 생성한 React 프로젝트의 개발 환경에서는 npm start
로 실행되는 개발 서버(webpack-dev-server
)가 파일이 수정될 때마다 이러한 변환 작업들을 자동으로 수행해준다. 그리고 실제 배포 시에는 번들링 한 파일들을 프론트 엔드 서버의 다큐먼트 루트에 미리 준비해두고 웹 서버가 그 파일들을 제공할 수 있도록 설정해야 한다.이후 클라이언트가 요청을 보내면 프론트 엔드 서버는 미리 준비해둔 HTML, CSS, JavaScript 파일들을 클라이언트에게 제공한다. 그러면 클라이언트의 브라우저는 전달받은 JavaScript 파일을 실행하여 페이지에 렌더링을 시작한다. 즉, 앞서 React 라이브러리를 활용하여 코딩했던 JavaScript 코드는 동적으로 DOM에 렌더링을 해주기 위한 코드였던 것이다. 만약 렌더링 과정에서 DB 데이터가 필요한 경우에는 백 엔드 서버에게 API 요청을 보내서 필요한 데이터를 요청한다. 이러한 맥락에서 백 엔드 서버를 API 서버라고 부르기도 한다. 필요한 데이터를 전달해주는 역할만 수행할 뿐, 페이지 렌더링에 필요한 정적 리소스들을 제공해주는 것이 아니기 때문이다.
이후, 페이지를 리로드 하지 않고 부분적으로만 리렌더링이 필요해지는 경우에는 사용자의 버튼 클릭과 같은 액션을 감지하여 JavaScript가 마찬가지 방식으로 백 엔드 서버에게 API 요청을 보내게 된다. 이때는 딱 해당 부분의 리렌더링에 필요한 데이터만 요청하게 된다. 그러면 JavaScript가 그 응답 데이터를 가지고 필요한 부분만 리렌더링을 해주게 된다. 이것 또한 개발자가 React 라이브러리를 활용하여 미리 코딩해둔 JavaScript 코드의 역할인 것이다.
이러한 맥락에서 React, Vue, Angular 등을 SPA(Single Page Application)라고 부른다. 기본적인 HTML, CSS, JavaScript 파일들만 제공받은 다음에 렌더링이 필요한 부분에 대해서만 직접 백 엔드 서버에게 API 요청을 보내서 리렌더링을 하는 방식이기 때문이다. 한 번 받은 페이지를 깜빡 거리는 일 없이 동적으로 제어가 가능해진다.
주문을 하면 각종 재료와 불판만 제공해주고 요리는 알아서 해 먹으라는 식당에 비유할 수 있다.
이와는 다르게 백 엔드 서버의 기능과 프론트 엔드 서버의 기능을 하나의 서버에서 서비스하도록 할 수도 있다. 즉 웹 서버, 웹 어플리케이션 서버, 웹 어플리케이션을 하나의 서버에 두는 것이다. 이 경우, 웹 서버가 웹 어플리케이션 서버에 대한 프록시 서버로 동작하도록 함으로써 API 요청을 처리할 수 있다. 그러나 이는 해당 서버가 마비되면 백 엔드 서버의 기능과 프론트 엔드 서버의 기능이 모두 마비된다는 문제점 때문에 사용되지 않는 경우도 많은 것 같다.
CORS 및 쿠키 문제
백 엔드 서버와 프론트 엔드 서버가 서로 다른 도메인을 가지는 경우, 다음과 같은 두 가지 문제점이 발생한다. 첫째, 기본적인 CORS 정책은 다른 오리진의 자원에 접근하는 것을 막는다는 문제이다. 따라서 백 엔드 서버에게 API 요청을 보내면 CORS 정책에 의해 요청이 거부당할 것이다. 이를 해결하려면 백 엔드 서버 쪽에서 CORS를 허용하기 위한 별도의 설정을 해줘야 한다. 다음으로, 다른 오리진에 대한 요청 시에는 쿠키를 전송 혹은 수신할 수 없다는 문제이다. 즉 백 엔드 서버에게 API 요청을 보내더라도 쿠키가 설정되지 않기 때문에 로그인 등의 기능을 구현하는 것이 어려워진다. 이를 해결하려면 백 엔드 서버에게 API 요청을 보낼 때 JavaScript 단에서 특정 설정(
XMLHttpRequest.withCredentials
옵션을 true
로 설정하거나, fetch
API라면 credentials
옵션을 include
로 설정)을 해줘야 하며, 백 엔드 서버 쪽에도 응답의 헤더에서 Access-Control-Allow-Credentials
옵션을 true
로 설정해줘야 한다. (참고)한편 이것과 별개로 쿠키의
SameSite
옵션에도 주의를 기울여야 하는데, 기본적으로 Lax
로 설정이 되기 때문에 GET 요청과 같은 안전한 요청이 아닌 이상 동일한 도메인일 때만 쿠키가 전송될 수 있다. 여기서 말하는 동일한 도메인이란 1차 도메인과 2차 도메인까지만을 말한다. 즉, a.naver.com
과 b.naver.com
은 동일한 도메인으로 취급된다. (참고)오리진 (Origin)
프로토콜, 도메인, 포트를 합친 부분이다. 예를 들어, URL이
https://www.naver.com:8000/users/123
이면 오리진은 https://www.naver.com:8000
이다. 참고로, 서브 도메인이 있는 오리진과 없는 오리진은 다른 오리진으로 취급된다.본 글은 아래 링크의 내용을 참고하여 학습한 내용을 나름대로 정리한 글임을 밝힙니다.
https://youtu.be/iE29lbjbow0