웹 아키텍처의 진화: PHP의 추억과 CORS의 악몽

“그때는 파일 하나면 충분했는데…”

학부 시절에 다뤘던 PHP는 정말 편했다. index.php 파일 하나에 HTML 태그도 넣고, 그 사이에 <?php ?>를 열어서 DB 조회 코드도 넣었다. 프론트엔드와 백엔드의 구분이 없었다. 변수 하나 선언하면 HTML 어디서든 갖다 쓸 수 있었고, 데이터 통신 때문에 에러가 날 일도 없었다.

하지만 실무에서 ‘Vue.js(프론트)’와 ‘Spring Boot(백엔드)’로 기술 스택을 갈아타면서 지옥이 시작됐다.

“분명 로컬에서 스프링 서버 띄웠고, Vue 화면도 띄웠는데 왜 데이터가 안 넘어오지?”

크롬 개발자 도구의 콘솔 창은 새빨간 에러 메시지로 도배되어 있었다.

Access to XMLHttpRequest at ... has been blocked by CORS policy

도대체 ‘CORS’가 뭐길래 내 데이터를 막는 걸까? 그리고 왜 우리는 파일 하나면 되던 편한 방식을 버리고, 굳이 프론트와 백엔드를 찢어놓는 고생을 사서 하는 걸까?

옛날엔 한 지붕 아래 살았지만, 지금은 서로 다른 집에 살며 검문을 통과해야 한다.

요리를 누가 할 것인가? (SSR vs CSR)

이 변화를 이해하려면, 웹 페이지(HTML)라는 ‘요리’를 누가 만드느냐의 차이를 알아야 한다. 이것이 바로 ‘SSR(Server Side Rendering)’과 ‘CSR(Client Side Rendering)’의 차이다.

1. SSR: 다 만들어진 도시락 (PHP, JSP)

  • 방식: 서버(주방)에서 밥, 반찬, 국을 다 조리하고 도시락통(HTML)에 예쁘게 담아서 보낸다.
  • 클라이언트(브라우저): 그냥 도시락 뚜껑 열고 보여주기만 하면 된다.
  • 장점: 첫 화면 로딩이 빠르다. 검색엔진(SEO)이 내용을 읽기 좋다.
  • 단점: 반찬 하나만 바꾸려 해도 도시락 전체를 다시 받아야 한다. (화면 깜빡임)

2. CSR: 밀키트 배송 (React, Vue)

  • 방식: 서버는 요리를 안 한다. 그냥 재료(JSON 데이터)와 조리법(JavaScript)만 보낸다.
  • 클라이언트(브라우저): 받은 재료를 가지고 내 컴퓨터(브라우저)에서 직접 지지고 볶아서 화면을 만든다.
  • 장점: 반찬만 쏙쏙 바꿀 수 있어서 화면 전환이 엄청 부드럽다(앱 같다). 서버 부하가 줄어든다.
  • 단점: 처음에 조리법(JS 파일)을 다운받느라 초기 로딩이 느리다.

[Tip] CSR과 SPA, 같은 말 아니에요?

흔히 혼용해서 쓰지만 엄밀히는 다르다.

  • CSR (Client Side Rendering): ‘방법(How)’이다. 화면을 브라우저가 그리는 방식을 말한다.
  • SPA (Single Page Application): ‘형태(What)’다. 페이지가 한 개뿐인 애플리케이션을 말한다.

우리는 보통 SPA를 만들기 위해 CSR 방식을 사용한다. 즉, 페이지를 하나만 써서 깜빡임을 없애고 싶으니까(SPA), 브라우저가 직접 부분부분 고쳐 그리는 방식(CSR)을 택한 것이다.

우리가 Vue.js나 React를 쓰는 이유는 ‘사용자 경험(UX)’ 때문이다. 웹사이트가 아니라 마치 스마트폰 앱처럼 부드럽게 동작하게 만들기 위해, 요리의 주체를 서버에서 브라우저로 옮긴 것이다.

불청객을 막는 경비원, CORS

프론트엔드(Vue)와 백엔드(Spring)가 분리되면서 우리는 ‘두 개의 집’을 갖게 되었다.

  • 프론트엔드 집: localhost:8080 (Vue 서버)
  • 백엔드 집: localhost:3000 (Spring 서버)

여기서 문제가 발생한다. 웹 브라우저는 보안상의 이유로 “다른 출처(Origin)에서 온 리소스는 함부로 믿지 말라”는 기본 원칙을 가지고 있다. 이것이 ‘SOP(Same Origin Policy)’다.

생각해 보라. 내가 네이버에 로그인해 있는데, 해커가 만든 사이트에서 몰래 네이버 서버로 “내 개인정보 내놔”라고 요청을 보낸다면? 브라우저가 이걸 막지 않으면 내 정보는 다 털린다.

그래서 브라우저는 기본적으로 ‘출처(도메인, 포트)’가 다르면 요청을 차단한다. 그런데 우리는 개발을 위해 의도적으로 포트를 나눴다. 내 데이터 내가 쓰겠다는데 브라우저라는 경비원이 “당신 출신 성분(Port)이 다르잖아! 출입 금지야!”라고 막아서는 상황, 이것이 바로 ‘CORS(Cross-Origin Resource Sharing) 에러’의 정체다.

[Code Verification] 출입증 발급하기

이 문제를 해결하는 방법은 간단하다. 서버(백엔드)가 경비원(브라우저)에게 “이 친구는 내가 초대한 손님이니까 들여보내세요”라고 출입증을 써주면 된다.

스프링 부트에서는 설정 파일 하나로 이 출입증을 발급할 수 있다.

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 1. 모든 API 경로에 대해
                .allowedOrigins("http://localhost:8080") // 2. 이 주소에서 오는 요청은 허용해라
                .allowedMethods("GET", "POST", "PUT", "DELETE") // 3. 이런 방식의 요청을 허용한다
                .allowCredentials(true); // 4. 쿠키/인증 정보도 포함 가능하다
    }
}

분석:

  1. allowedOrigins: 가장 중요하다. 여기에 프론트엔드 서버의 주소(localhost:8080)를 적어주면, 스프링 서버가 응답 헤더에 Access-Control-Allow-Origin: http://localhost:8080이라는 도장을 찍어서 보낸다.
  2. 브라우저는 이 도장을 확인하고 “아, 서버 주인이 허락했구나” 하고 데이터를 프론트엔드에 넘겨준다.

주의할 점은 귀찮다고 allowedOrigins("*")로 모든 곳을 다 열어버리면 안 된다는 것이다. 그건 집 대문을 활짝 열어두고 “도둑님 환영합니다”라고 써 붙이는 것과 같다.

실무 조언: 언제 무엇을 쓸까?

그렇다면 무조건 CSR(Vue/React)이 정답일까? 아니다. 실무에서는 목적에 따라 선택해야 한다.

  1. 관리자 페이지(Admin), 대시보드:
    • 추천: CSR (Vue, React)
    • 이유: 데이터 상호작용이 많고, 검색엔진 노출이 필요 없다. 앱처럼 빠릿빠릿한 반응성이 중요하다.
  2. 뉴스 사이트, 블로그, 쇼핑몰 메인:
    • 추천: SSR (Next.js, Nuxt.js)
    • 이유: 구글 검색에 잘 걸려야 하고(SEO), 고객이 들어왔을 때 흰 화면 대신 내용이 바로 떠야 한다.
  3. 작은 개인 프로젝트:
    • 추천: Thymeleaf (스프링 부트 내장 SSR)
    • 이유: 혼자 개발하는데 굳이 프론트/백엔드 나누고 CORS 설정하느라 힘뺄 필요 없다. PHP 시절처럼 하나로 합쳐서 개발하는 게 생산성은 최고다.

마치며: 경계를 넘어서

PHP 시절의 ‘한 지붕 대가족’은 편했지만 복잡해질수록 관리가 힘들었다. 지금의 ‘분가한 두 가족(프론트/백)’은 통신(CORS)하느라 귀찮지만, 각자의 역할에 집중할 수 있게 되었다.

CORS 에러를 만났을 때 짜증 내지 말자. 그건 브라우저가 우리를 괴롭히는 게 아니라, 우리 집(서버)을 지켜주려는 ‘경비원의 철저한 검문’이었음을 이제는 아니까.

자, 이제 프론트엔드와 백엔드의 통로가 뚫렸다. 데이터가 오갈 수 있게 되었다. 그런데 이 데이터들은 서버 어디에 저장될까? 예전에는 그냥 쿼리 날려서 가져오면 됐는데, 스프링에서는 ‘JPA’니 ‘Entity’니 하는 것들 때문에 SQL을 직접 못 쓰게 한다.

다음 시간에는 ‘데이터베이스 설계의 정석(정규화)’이 실무에서 어떻게 발목을 잡는지, 그리고 ‘JPA’가 왜 SQL을 감추려 하는지에 대해 알아보자.

“웹 아키텍처의 진화: PHP의 추억과 CORS의 악몽”에 대한 2개의 생각

  1. 여기 블로그는 SSR 인가요? CSR인가요?
    제 생각에는 SSR로 되어 있는 거 같네요

    PHP로 한 페이지 안에 DB 조회, 화면 표시가 된다면 PHP는 SSR인가요 CSR인가요

    응답
    • 이 블로그(워드프레스)는 PHP를 기반으로 작동하는 전형적인 SSR(Server Side Rendering) 사이트입니다.

      워드프레스는 서버(PHP)가 DB에서 글 내용을 조회하고 HTML을 완성해서 브라우저로 보내줍니다. 그래서 ‘페이지 소스 보기’를 하면 글 내용이 다 들어있죠.

      질문하신 것처럼 PHP 파일 하나에서 DB 조회와 HTML 생성을 모두 마친 뒤, ‘완성된 HTML’을 브라우저에게 던져주는 방식이 바로 SSR의 교과서적인 예시입니다. (브라우저는 요리할 필요 없이 차려진 밥상을 받기만 하면 되니까요.)

      좋은 질문 남겨주셔서 감사합니다! 덕분에 다른 분들도 개념 잡는 데 도움이 될 것 같네요. 🙂

      응답

댓글 남기기