[CORS] Solved.ac API 연동과 자체 프록시 서버 구축기

2026. 2. 16. 03:30·웹 프로그래밍
반응형

프로젝트 CoTe-Coach를 개발하던 중, 분명 간단해 보였던 외부 API 연동이 발목을 잡았다.

나를 괴롭혔던 CORS의 실체와, 수많은 시행착오 끝에 자체 프록시 서버를 구축해 해결한 과정을 아주 상세하게 기록해본다. (내용이 좀 길지만, 저와 같은 삽질을 하는 분들께 도움이 되길 바라며 하나도 빠짐없이 다 적어봤다!)

 

 


기획 의도: 외부 API 연동의 필요성

CoTe-Coach 프로젝트의 핵심 기능 중 하나는 사용자의 Solved.ac 티어와 추천 문제를 실시간으로 보여주는 것이다.

사용자가 자신의 현재 위치를 파악하고 동기부여를 얻을 수 있도록 외부 API(Solved.ac) 연동은 필수적이었다. 이를 위해 Solved.ac API를 활용하기로 했다.


초기 구현: corsproxy.io

프로젝트 초기, API 연동을 구현할 때 나는 `corsproxy.io`를 사용했다.

// Axios 인터셉터로 프록시 URL 적용
solvedAcApi.interceptors.request.use((config) => {
  const fullUrl = axios.getUri(config);
  config.url = `https://corsproxy.io/?url=${encodeURIComponent(fullUrl)}`;
  return config;
});

사용한 이유는 별 게 없었다. 별도의 서버 구축이나 복잡한 설정 없이, API URL 앞에 주소만 붙이면 즉시 해결되는 가성비 방식이었기 때문이다. 실제로 개발 초기에는 로컬과 배포 환경 모두에서 티어 정보가 완벽하게 출력되었다.

굿~


 

🚨 문제 발생: "undefined 6"

하지만 배포 후 갑자기 문제가 생겼다.

잘 돌아가던 사이트의 티어 정보가 "undefined 6"으로 표시되기 시작한 것이다. 추천 문제 목록 역시 아예 로딩되지 않았다. 분명 코드를 수정한 게 없는데...😱

 

브라우저 콘솔을 열어보니 아래와 같은 에러 메시지가 떴다.😪

GET https://corsproxy.io/?url=https%3A%2F%2Fsolved.ac%2Fapi%2Fv3%2Fuser%2Fshow%3Fhandle%3D... 403 (Forbidden)

 


문제 해결 방식 설명 전 필요한 배경 지식: CORS와 프록시

1. CORS가 대체 뭐야?

CORS는 브라우저가 적용하는 보안 정책이다. 내 앱에서 요청을 보내면, 브라우저가 응답을 받기 전에 "잠깐! 상대방 서버가 너한테 응답 줘도 된다고 허락했어?"라고 확인한다.

출처(Origin)란 프로토콜 + 도메인 + 포트의 조합인데

  • https://cote.timeqlife.com (내 앱)
  • https://solved.ac (외부 API)

이 둘은 출처가 다르다(Cross-Origin). 그런데 solved.ac 서버는 응답에 "너한테 데이터 줘도 돼"라는 허가 헤더(Access-Control-Allow-Origin)를 안 보내준다. 그러니까 브라우저가 중간에서 차단해버리는 것! ❌

 

❗ 이때 주의 해야 할 사항: CORS는 서버가 아닌 브라우저가 적용하는 정책이다. 서버끼리의 통신에는 CORS가 없다!

 

 

2. 로컬 상식: 로컬에서는 왜 됐을까?

배포 버전과 달리, 로컬에서는 문제없이 사용자 정보가 떴었기에 그 원인도 찾아봤다.

이유는 다음과 같다. 우선 로컬에서는 Vite 설정에 server.proxy라는 걸 넣어뒀다.

// vite.config.ts
proxy: {
  '/api/v3': {
    target: '[https://solved.ac](https://solved.ac)',
    changeOrigin: true,
  }
}

이걸 설정하면 브라우저는 localhost:5173/api/v3/...에 요청을 던지는 것으로 착각하고(보안 통과), 실제로는 Vite 개발 서버가 대리인(Proxy)이 되어 대신 solved.ac에 가서 데이터를 가져온다. 하지만 GitHub Pages는 정적 서버라 이런 대리인 기능이 없다. 그래서 배포만 하면 브라우저가 직접 solved.ac에 가려다 차단당하는 거였다.

 

추가로, 프록시를 이용해 사용자 정보를 가져오는 함수는

/**
 * 사용자 정보 가져오기
 */
export const fetchSolvedAcUser = async (handle: string): Promise<SolvedAcUser> => {
  try {
    if (import.meta.env.PROD) {
      return await prodFetch<SolvedAcUser>('/user/show', { handle });
    }
    const { data } = await solvedAcApi.get('/user/show', { params: { handle } });
    return data;
  } catch (error: any) {
    console.error('[Solved.ac] fetchSolvedAcUser 실패:', {
      status: error.response?.status,
      message: error.message,
    });
    throw error;
  }
};

`import.meta.env.PROD`를 통해 배포 버전에서만 실행되도록 구현해 두었다.


 

기술적 도전: 해결 시도 과정

브라우저가 직접 못 가니까, 중간에 대리인(프록시 서버)을 세워야 한다. corsproxy.io도 이미 만들어진 무료 프록시였다.

 

원래 코드: corsproxy.io — 실패 (403 Forbidden)

// Axios 인터셉터로 프록시 URL 적용
solvedAcApi.interceptors.request.use((config) => {
  const fullUrl = axios.getUri(config);
  config.url = `https://corsproxy.io/?url=${encodeURIComponent(fullUrl)}`;
  return config;
});
  • 결과: 403 Forbidden 에러.
  • 원인: 프록시 서버 자체가 solved.ac 도메인으로 가는 요청을 막아두고 있었다. 내 서버가 아니니 해결 방법이 없었다.

 

시도 1: allorigins.win/raw — 실패 (CORS 차단)

다른 서비스를 찾아 나섰다. allorigins.win의 /raw 엔드포인트를 써봤다.

  • 결과: CORS 에러가 떴다.
  • 원인 분석: /raw 방식은 프록시가 응답을 원본 그대로 넘겨준다. solved.ac의 응답엔 원래 CORS 헤더가 없으니, 프록시가 넘겨받은 데이터에도 헤더가 없다. 브라우저는 "헤더 없네?" 하고 또 차단해버린 것... 🤯

 

시도 2 : allorigins.win/get — 실패 (522 서비스 다운)

이번엔 응답을 JSON으로 감싸서 프록시 자체 헤더를 붙여준다는 /get 방식을 썼다.

엔드포인트 동작 CORS 헤더
/raw?url= 원본 응답 그대로 전달 ❌ 원본 것 그대로
/get?url= 응답을 { contents: "..." } JSON으로 감싸서 반환 ✅ AllOrigins가 추가
  • 결과: net::ERR_FAILED 522 에러.
  • 원인: HTTP 522는 Cloudflare의 "Connection Timed Out"이다. 프록시 서비스 자체가 과부하로 죽어있었다. (공짜의 한계...)

 

시행착오 요약 정리

시도 프록시 에러 실패 원인
원본 corsproxy.io 403 프록시가 solved.ac 도메인 차단
1차 allorigins.win/raw CORS 원본 응답 그대로 전달 → 헤더 없음
2차 allorigins.win/get 522 프록시 서비스 자체 다운

 

핵심 교훈: 무료 CORS 프록시 서비스는 근본적으로 불안정하다. 운영 환경에 쓰기엔 너무 위험하다!


최종 해결: Supabase Edge Function으로 자체 프록시 구축

결국 "내가 직접 프록시 서버를 만들자"로 결론을 내렸다. 이때 내가 눈여겨 봐둔 프록시 구축 방식은 Supabase Edge Function이었다.

Supabase Edge Function 이란, 쉽게 설명하면 "내가 필요할 때만 잠깐 켜서 쓰는 초소형 서버리스 함수"다. 이미 프로젝트에서 Supabase를 쓰고 있었기에 인프라를 추가할 부담이 전혀 없었고, 무엇보다 무료 티어 범위가 넉넉해서 비용 측면에서도 아주 합리적이었다(월 50만건 무료). 별도의 거대한 백엔드 서버를 운영하려면 돈도 관리 리소스도 많이 드는데, Supabase Edge Function은 딱 필요한 로직만 실행하니까 훨씬 효율적이었다.

서버 간 통신은 CORS 제약이 없다는 점을 이용해서, 내가 직접 응답 헤더에 "이거 허용해줄게!"(Access-Control-Allow-Origin: *)라고 딱지를 붙여주는 나만의 전용 대리인을 만들었다. TypeScript를 기본으로 지원하는 Deno 환경이라 설정도 깔끔하고, 유저와 가까운 서버에서 실행되니 속도까지 빨라서 아주 만족스러웠다.

더보기

여기에 내가 느낀 Edge Function의 매력을 좀 더 덧붙여보자면 다음과 같다.

1. 서버 간 통신의 마법 (CORS의 구세주) 🛡️ : 브라우저에서 직접 가면 막히지만, 서버끼리의 대화는 제약이 없다는 점을 완벽히 이용했다. Edge Function이 Solved.ac에 대신 가서 데이터를 받아온 뒤, 우리 브라우저에 배달해 주는 든든한 대리인이 되어준다.
2. Edge(가장자리)에서 오는 미친 속도 ⚡ : 이름 그대로 전 세계 도처에 흩어진 서버 중 유저와 가장 가까운 곳에서 실행된다. 덕분에 프록시를 거치는데도 응답 속도가 아주 빠릿빠릿해서 대만족이었다.
3. Deno 런타임의 간결함 🦕 : Node.js와 달리 TypeScript를 기본으로 지원해서 설정이 정말 깔끔했다. 냅다 URL로 모듈을 가져와 쓸 수 있어서 귀찮은 npm install 과정도 필요 없었다!
4. 철저한 보안 🔐 : 아무나 내 프록시를 쓰면 곤란한데, Supabase 보안 시스템과 연동되어 우리가 설정한 Anon Key가 있는 요청만 선택적으로 처리할 수 있어 마음이 놓였다.

 

기술 스택 선정 이유 정리

대안 장점 단점 채택
무료 프록시 계속 시도 즉시 적용 가능 불안정, 언제든 중단 가능 ❌
Cloudflare Worker 높은 안정성 새 인프라 추가 필요 ❌
별도 백엔드 서버 완전한 제어 관리 부담 및 비용 발생 ❌
Supabase Edge Function 기존 인프라 활용, 무료, 안정적 Deno 런타임 학습 ✅

 

 

1. 전용 프록시 서버 구현 (서버 사이드)

내가 직접 응답 헤더에 "이거 허용해줄게!"라고 써주면 된다.

// supabase/functions/solved-ac-proxy/index.ts
const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
};

serve(async (req) => {
  if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });

  const targetUrl = `https://solved.ac/api/v3${url.searchParams.get("path")}`;
  const response = await fetch(targetUrl); // 서버 간 통신은 CORS 제약 없음!
  const data = await response.text();

  return new Response(data, {
    headers: { ...corsHeaders, "Content-Type": "application/json" },
  });
});

 

2. 401 Unauthorized 에러 (보안 이슈 해결)

프록시를 배포하고 호출했더니 이번엔 { "code": 401, "message": "Missing authorization header" }가 떴다. 원인은 Supabase 함수의 보안 설정! 아무나 내 프록시를 쓰면 안 되니까, 요청 헤더에 Supabase Anon Key를 꼭 넣어줘야 했다.

// src/api/solvedac.ts (프론트엔드 수정본)
const response = await fetch(
  `${SUPABASE_PROXY_URL}?path=${encodeURIComponent(fullPath)}`,
  {
    headers: {
      'Authorization': `Bearer ${import.meta.env.VITE_SUPABASE_ANON_KEY}`,
    },
  },
);

최종 아키텍처

브라우저(cote.timeqlife.com)가 내 서버에 요청을 보냄
-> Supabase Edge Function (자체 프록시)가 받음
-> CORS 제약 없이 solved.ac로 서버 간 통신 수행
-> solved.ac가 데이터를 응답함
-> Edge Function이 "CORS 허용 헤더"를 붙여서 브라우저에 최종 배달

-> 통과! 👍


회고

오류를 잡는 과정을 통해 단순히 "프록시를 쓰면 된다"는 사고방식보다 특정 방식은 실패하고 다른 방식은 성공하는지를 깊게 이해할 수 있었다.

  • 환경 차이 이해: 로컬(Vite Proxy)과 운영(정적 서빙)의 구조적 차이를 완벽히 파악
  • 체계적 디버깅: 가설→시도→분석→수정의 반복적 사이클을 통한 문제 해결 능력 증명
  • 인프라 설계 판단력: 무료 서비스에 의존하지 않고 자체 인프라를 구축하는 장기적 관점 확보
  • 새로운 기술 활용: Supabase Edge Function과 Deno를 처음 써보며 빠르게 적응하여 솔루션 구현

이 문제 때문에 몇 시간이나 허비했지만 해결하고 나니 굉장히 뿌듯하다!

반응형
저작자표시 비영리 변경금지 (새창열림)

'웹 프로그래밍' 카테고리의 다른 글

[프론트 CI/CD 스터디 1주차] Github Action 환경에서의 AWS CF, S3 배포 설정 및 배포자동화(React CSR)  (1) 2024.04.07
'웹 프로그래밍' 카테고리의 다른 글
  • [프론트 CI/CD 스터디 1주차] Github Action 환경에서의 AWS CF, S3 배포 설정 및 배포자동화(React CSR)
청량리 물냉면
청량리 물냉면
프로그래밍 공부를 하고 있습니다. 공부 내용 정리 겸 정보 공유를 목적으로 합니다.
    반응형
  • 청량리 물냉면
    노력중인 블로그
    청량리 물냉면
  • 전체
    오늘
    어제
    • 분류 전체보기 (506)
      • 프로그래밍 (41)
        • Programming (1)
        • C | C++ (6)
        • Java (28)
        • Python (5)
      • 웹 프로그래밍 (2)
        • HTML | CSS (5)
        • JavaScript | TypeScript (41)
        • React (25)
        • Vue.js (0)
        • Next.js (18)
        • Spring & Spring Boot (13)
        • JSP & Servlet (1)
        • DB (4)
      • 웹 프로젝트 (77)
        • 웹 프로젝트 (22)
        • 🥨스낵몰 (3)
        • 👨‍👨‍👧‍👧소셜 가계부 (26)
        • 🌜꿈 일기장 (11)
        • 🔮포트폴리오 사이트 (11)
        • 🏃‍♂️팀 프로젝트: 일정관리 프로그램 (0)
        • 📈팀 프로젝트: AI기반 주식 분석 플랫폼 (0)
        • 😺Just Meow It: 조언 사이트 (2)
        • 📕Workly: 교대근무 다이어리 (1)
      • 앱 프로그래밍 (26)
        • Flutter (24)
        • Kotlin (2)
      • Problem Solving (166)
        • 백준 (52)
        • 프로그래머스 (79)
        • SWEA (29)
      • Computer Science (40)
        • 알고리즘 (14)
        • 컴퓨터 네트워크 (18)
        • 이산수학 (8)
      • Developer (47)
        • 후기 (4)
        • 자료정리 (4)
        • 취업 | 취준 (9)
        • SSAFY (1)
        • 웹개발 교육 프로그램 (9)
        • TIL (20)
  • 블로그 메뉴

    • 홈
    • Github
  • 공지사항

    • 프로그래밍 공부 중😊
  • 인기 글

  • 태그

    자바스크립트
    리액트
    AWS
    Til
    파이썬
    컴퓨터네트워크
    ZeroCho
    mysql
    블로그 제작
    React
    웹사이트
    spring boot
    백준
    뉴렉처
    알고리즘
    포트폴리오
    프로젝트
    강의내용정리
    클론 프로젝트
    bfs
    Jiraynor Programming
    플러터
    자바
    d3
    구현
    프로그래머스
    타입스크립트
    공식문서
    Next.js
    SWEA
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
청량리 물냉면
[CORS] Solved.ac API 연동과 자체 프록시 서버 구축기
상단으로

티스토리툴바