
사이드 프로젝트를 진행하다 보면 반드시 한 번은 “API 레이어 세팅”이라는 단계를 만나게 된다.
나 역시 이번 프로젝트에서 Supabase로 만든 데이터베이스를 프론트엔드와 연결하면서,
이 과정을 실제로 구현하고 있었다.그런데 코드를 작성하다 문득 이런 생각이 들었다.
“API를 매번 쓰고 있는데… 정확히 API가 뭘 의미하는 걸까?”
axios로 요청을 보내고,GET/POST메서드를 사용하고, Swagger 문서로 엔드포인트를 확인하지만
정작 “API가 뭘 하는 놈인지” 설명하려니 말이 막혔다.그래서 이번 글에서는 API의 개념을 처음부터 다시 짚어보기로 했다.
API가 왜 필요한지, 어떻게 작동하는지,
그리고 우리가 코드에서 자주 쓰는CRUD나REST같은 개념이
이 안에서 어떤 역할을 하는지까지 하나씩 정리해보려 한다.
API는 Application Programming Interface의 약자다.
직역하면 “응용 프로그램을 위한 프로그래밍 인터페이스”인데,
말이 너무 길다.
쉽게 말해, “프로그램과 프로그램이 서로 대화할 수 있게 돕는 통로”다.
API는 “A 프로그램이 B 프로그램에게 어떤 형식으로 요청을 보내야 하는지,
그리고 어떤 형식으로 응답을 받아야 하는지”를 정해놓은 규칙집이다.
이 규칙이 있기 때문에 전혀 다른 시스템끼리도 문제없이 통신할 수 있다.
예를 들어,
내 플래너 프로젝트에서는 프론트엔드(Next.js)가
Supabase 서버에게 데이터를 요청해야 한다.
이때 “어떤 URL로 요청을 보낼지, 어떤 메서드를 쓸지, 어떤 형식으로 데이터를 보낼지”
모두 API가 정해놓은 규칙을 따라야 한다.
API를 식당으로 비유해보면 이해가 쉽다.
- 손님(프론트엔드)은 “음식을 주문하는 사람”
- 주방(서버)은 “요리를 실제로 만드는 곳”
- API는 “주문을 전달하고 결과를 가져오는 웨이터”
손님은 주방이 어떻게 요리를 하는지 알 필요가 없다.
메뉴판(=API 문서)에 적힌 대로 주문만 하면
요리가 완성돼서 나온다.
정리하자면,
API는 프로그램이 직접 서로의 코드를 몰라도,
정해진 형식의 요청과 응답만 주고받으면 협력할 수 있게 해주는 인터페이스다.즉,
“API는 소프트웨어 세계의 공용어라고 할 수 있다.”
API는 단순히 데이터를 주고받는 통로가 아니다.
서버 입장에서 보면, 요청을 해석하고, 유효성을 검사하고, 결과를 응답으로 돌려주는 하나의 프로세스다.
모든 API는 이 흐름을 따른다.
클라이언트(주로 프론트엔드)는 서버에 요청(Request)을 보낸다.
이 요청에는 “무엇을 하고 싶은지”가 명확히 담겨 있다.
예를 들어, 내 플래너 앱에서 “할 일 목록을 불러오기”를 요청한다고 하자.GET /todos이 한 줄로 “할 일 데이터를 달라”는 의미가 전달된다.
GET은 데이터를 조회(Read) 하겠다는 뜻이고,/todos는 자원(Resource) 의 경로다.
요청은 보통 HTTP 프로토콜을 통해 전송되며,
헤더(Header)에는 토큰이나 인증 정보, 바디(Body)에는 전송할 데이터가 담긴다.
서버는 요청을 받으면 우선 요청이 올바른지 검증한다.
- 요청 경로(
/todos)가 존재하는지- 인증 토큰이 유효한지
- 필요한 데이터(
title,id등)가 모두 포함되어 있는지이 검증 단계를 통과하지 못하면, 서버는 즉시 오류 응답(Error Response) 을 돌려준다.
예시:
{ "error": "Invalid request. Missing required field 'title'." }반대로 요청이 정상이라면, 서버는 본격적으로 데이터를 처리한다.
데이터베이스를 읽거나, 새로운 항목을 추가하거나, 수정·삭제 작업을 수행한다.
예를 들어 Supabase의/todosAPI는 이렇게 동작한다.
1. 요청한 사용자의user_id를 확인하고
2. 해당 유저의todos데이터를 쿼리한 뒤
3. 그 결과를 JSON으로 반환한다.
요청이 성공적으로 처리되면 서버는 응답(Response) 을 보낸다.
응답은 보통status code와body로 구성된다.
✅ 정상 응답 예시:
200 OK[ { "id": 1, "title": "블로그 글 작성", "is_done": false }, { "id": 2, "title": "API 테스트", "is_done": true } ]❌ 오류 응답 예시:
400 Bad Request{ "error": "Invalid parameters" }이처럼 서버는 단순히 데이터를 주는 것이 아니라,
요청이 유효한지 판단하고 그 결과를 명확히 응답하는 역할을 한다.
- 클라이언트(Client) → 데이터를 요청하는 쪽 (Next.js, React, 모바일 앱 등)
- 서버(Server) → 요청을 검증하고, 처리한 뒤 응답을 돌려주는 쪽 (Supabase, Node.js 등)
이 둘은 서로의 내부를 알 필요가 없다.
중요한 건, 요청(Request)과 응답(Response)의 규칙만 지키는 것이다.
이 규칙이 바로 API의 언어다.
결국 API의 동작은 이렇게 요약된다.“요청을 보내면, 서버는 그 요청을 검증하고, 결과를 응답으로 돌려준다.”
성공이면 데이터, 실패면 이유 — 이 단순한 구조가 웹 전체를 움직인다.
API는 단순히 “요청하고 응답받는 구조”로 끝나지 않는다.
그 요청들이 무엇을 하기 위한 것인지까지 구체적인 목적을 갖고 있다.
대부분의 웹 서비스는 아래 네 가지 동작으로 구성된다.
CRUD는 모든 데이터 처리를 대표하는 네 가지 약어다.
동작 설명 예시 API 요청 Create 새로운 데이터 추가 POST /todosRead 데이터 조회 GET /todosUpdate 데이터 수정 PATCH /todos/:idDelete 데이터 삭제 DELETE /todos/:id예를 들어, Supabase의
todos테이블과 연결된 REST API를 생각해보자.
프론트엔드(Next.js)는axios로 이런 요청을 보낼 수 있다:// 새로운 할 일 추가 (Create) await axios.post("/todos", { title: "블로그 글 작성" }); // 목록 조회 (Read) const { data } = await axios.get("/todos"); // 항목 수정 (Update) await axios.patch(`/todos/${id}`, { is_done: true }); // 항목 삭제 (Delete) await axios.delete(`/todos/${id}`);이 네 가지 요청만으로 할 일 관리 서비스의 기본 기능이 완성된다.
즉, CRUD는 API의 근본적인 역할 모델이다.
CRUD는 동작의 개념이라면,
REST API는 그 동작을 실제로 설계하고 표현하는 방식이다.
REST(Representational State Transfer)는
“자원을 URL로 표현하고, 행동은 HTTP 메서드로 구분한다”는 철학을 가진다.
🧱 REST의 기본 원칙
- 모든 것은 자원(Resource)이다.
→/todos,/users,/notes같은 엔드포인트는 각각 자원을 의미한다.
- 자원은 고유한 경로(URL)로 표현된다.
→/todos/123은id = 123인 특정 할 일을 가리킨다.
- 행동은 HTTP 메서드로 표현한다.
→GET,POST,PATCH,DELETE로 CRUD를 구현한다.
- 서버는 상태를 기억하지 않는다.
→ 각 요청은 독립적이며, 필요한 정보는 항상 요청에 포함되어야 한다.
🧩 CRUD ↔ REST 매핑
CRUD 동작 HTTP 메서드 예시 URL 설명 Create POST/todos새 할 일 생성 Read GET/todos전체 목록 조회 Read (단일) GET/todos/:id특정 항목 조회 Update PATCH또는PUT/todos/:id특정 항목 수정 Delete DELETE/todos/:id특정 항목 삭제
REST API에 대해서 더 깊이 알고 싶다면 아래 글을 보는 것을 추천한다!
REST API란? REST, RESTful이란?
- 구조가 단순하고 직관적이다.
- 브라우저 개발자 도구나 Postman으로 쉽게 테스트할 수 있다.
- 대부분의 백엔드 프레임워크(Supabase, Express, FastAPI 등)가 REST를 기본 지원한다.
즉, REST는 웹 서비스의 공용 문법이라고 할 수 있다.
URL과 메서드만 보면 “이 API가 무슨 일을 하는지” 바로 알 수 있다.
REST API는 항상 HTTP 상태 코드(Status Code) 를 함께 반환한다.
코드 의미 설명 200 OK성공 요청이 정상적으로 처리됨 201 Created생성 성공 새 리소스가 만들어짐 400 Bad Request잘못된 요청 파라미터 오류 등 401 Unauthorized인증 실패 로그인 필요 404 Not Found리소스 없음 요청한 자원이 존재하지 않음 500 Internal Server Error서버 오류 내부 로직 문제 이 상태 코드 덕분에 클라이언트는 “요청이 성공했는지, 실패했는지”를 일관된 방식으로 인식할 수 있다.
정리하자면,
CRUD는 API가 하는 일의 ‘종류’를 정의하고,
REST는 그 일을 ‘표현하는 방식’을 정의한다.둘은 함께 웹 서비스의 핵심 구조를 이룬다.
플래너 프로젝트에서 Supabase가 자동으로 생성해주는 엔드포인트 역시
결국 이 REST 원칙을 따르고 있다.
API라고 해서 모두 같은 형태로 동작하는 것은 아니다.
누가 사용할 수 있는지, 어떤 목적을 가지는지에 따라 여러 유형으로 나뉜다.
아래는 실제 서비스에서 자주 등장하는 세 가지 대표적인 유형이다.
누구나 사용할 수 있도록 공개된 API다.
대부분의 경우 회원가입만 하면 무료로 키(API Key) 를 발급받아 쓸 수 있다.예시
- 기상청 Open API → 날씨 데이터 제공
- 카카오 지도 API → 지도, 장소 검색 기능
- 공공데이터포털 API → 버스, 지하철, 행정 정보
- TMDB API → 영화 포스터, 평점 데이터
특징
- 접근성이 높다 (학습용, 테스트용으로 자주 사용됨)
- 사용량 제한이 있음 (예: 하루 1,000건 등)
- 인증키(API Key) 방식으로 호출
회사 내부 시스템끼리 통신할 때 사용하는 API다.
외부에는 공개되지 않고, 특정 서버 간 통신 또는 사내 대시보드용으로만 사용된다.예시
- 관리자(Admin) 대시보드 → 내부 사용자 관리
- 통계 서버 → 프론트엔드용 데이터 전달
- 마이크로서비스 간 통신 →
user-service↔todo-service
특징
- 인증 수준이 높고, 보안이 매우 중요함
- 문서가 공개되지 않음
- 변경 주기가 잦음 (내부 구조에 따라 유연하게 수정 가능)
내 플래너 프로젝트의 Supabase REST API도 기본적으로는 Private API다.
사용자 인증을 거친 뒤에만 각자의 데이터에 접근할 수 있다.
특정 파트너나 제휴사에게만 제공되는 API다.
외부와의 통합(Integration) 목적을 가지며, 계약 기반으로 관리된다.예시
- 네이버페이 / 카카오페이 결제 API
- 배민 × 토스포인트 적립 API
- Slack, Notion, GitHub 등에서 제공하는 Third-Party Integration API
특징
- 계약 및 제휴 관계가 있어야 접근 가능
- SLA(Service Level Agreement)와 보안 규약이 명확함
- 장애 대응 및 버전 관리가 체계적
최근에는 REST 외에도 다양한 통신 구조가 쓰인다.
방식 특징 예시 GraphQL 필요한 데이터만 선택적으로 요청 가능 GitHub API v4 gRPC 빠른 이진 통신, 서버 간 통신에 유리 마이크로서비스 구조 WebSocket 양방향 실시간 통신 채팅, 알림 서비스 RPC (Remote Procedure Call) 함수 호출 기반 구조 내부 시스템 통신 REST가 ‘표준 언어’라면,
GraphQL이나 gRPC는 ‘상황에 따라 더 효율적인 대화법’이다.
사실 우리는 하루에도 수십 번씩 API를 사용한다.
단지 그걸 직접 호출하지 않을 뿐,
스마트폰의 거의 모든 앱이 백엔드와 API를 통해 연결되어 있다.
스마트폰에서 “오늘 날씨”를 확인하면,
앱은 기상청 Open API에GET요청을 보낸다.GET /weather?city=seoul서버는 실시간 기상 데이터를 조회한 뒤 아래처럼 응답한다.
{ "temp": 24.3, "condition": "cloudy", "humidity": 62 }앱은 이 JSON 데이터를 받아
아이콘과 텍스트로 예쁘게 보여주는 것뿐이다.즉, “화면에 날씨가 보인다” = “API 응답을 시각화했다”는 뜻이다.
카카오맵, 네이버지도, 구글지도 모두 지도 API를 제공한다.
플래너 프로젝트에서 장소 기반 일정을 넣고 싶다면
카카오맵 API를 이렇게 불러올 수 있다.GET /maps/search?query=카페응답 예시:
[ { "name": "스타벅스 홍대입구점", "lat": 37.556, "lng": 126.925 }, { "name": "카페 어니언", "lat": 37.544, "lng": 127.055 } ]앱은 이 좌표를 기반으로 지도에 마커를 표시한다.
즉, 우리가 클릭하는 지도 역시 API 호출의 결과다.
로그인도 결국 API다.
“카카오로 로그인하기” 버튼을 누르면
앱은 카카오 인증 서버로 요청을 보낸다.POST /oauth/token카카오 서버는 사용자의 동의를 확인하고
토큰(Token)을 발급해 응답한다.{ "access_token": "abc123xyz", "expires_in": 7200 }이 토큰이 있으면,
내 서버는 “이 사용자는 카카오에서 인증된 사람”임을 신뢰할 수 있다.
즉, 로그인 역시 API로 이루어진다.
결제창이 열리고 “결제 완료” 알림이 뜨는 순간,
백엔드에서는 다음과 같은 흐름이 진행된다.
1. 클라이언트 → 결제 요청 (POST /payments)
2. 서버 → PG사(카카오페이, 토스페이 등)에 API 요청
3. PG사 → 응답 (성공/실패 여부, 승인번호)
4. 서버 → 클라이언트에 최종 응답결제 한 건을 처리하기 위해 실제로는 3~4개의 API가 연쇄적으로 작동한다.
카카오톡, 슬랙, 디스코드 같은 메신저는
서버와 클라이언트가 항상 연결된 상태를 유지해야 한다.
이때 REST 대신 WebSocket API를 사용한다.
WebSocket은 요청-응답이 아니라
서버가 먼저 “새 메시지가 왔다”고 푸시할 수 있다.즉, “알림이 바로 뜬다” = “서버가 실시간 API로 나에게 말 걸었다”는 뜻이다.
결국 우리가 날씨를 보고, 로그인하고, 결제하고, 대화하는 모든 순간 —
그 뒤에는 수많은 API 요청과 응답이 오가고 있다.
눈에 보이지 않지만,
API는 현대 서비스의 보이지 않는 혈관이다.
API는 단순히 기술 용어가 아니다.
프론트엔드와 백엔드를 이어주는 실제 연결선이다.
우리가 작성하는 코드의 대부분은 결국 “API를 어떻게 주고받을 것인가”를 중심으로 움직인다.
프론트엔드는 사용자의 행동을 요청(Request) 으로 바꿔 서버에 보낸다.
예를 들어 사용자가 “할 일 추가” 버튼을 눌렀다고 해보자.await axios.post("/api/todos", { title: "공부하기" });이 한 줄의 의미는 단순하다.
“서버야, 새로운 할 일을 만들어줘.”
서버가
201 Created를 응답하면,
프론트엔드는 이 데이터를 받아 즉시 UI에 반영한다.
클릭 → 요청 → 응답 → UI 업데이트
이 한 사이클이 API 호출의 본질이다.
백엔드는 프론트엔드로부터 요청을 받으면,
다음과 같은 순서로 동작한다.
1. 요청의 유효성 검사 (토큰, 파라미터, Body 데이터 확인)
2. 해당 로직 수행 (DB에 삽입, 조회, 수정 등)
3. 결과를 응답으로 반환
예시 — Supabase REST API의 내부 흐름:POST /todos Authorization: Bearer {user_token} Body: { "title": "공부하기" }Supabase는 이 요청을 받아 내부에서 SQL을 실행한다.
insert into public.todos (user_id, title) values (auth.uid(), '공부하기');이후 응답으로 새로 생성된 레코드를 JSON 형태로 돌려준다.
프론트엔드는 데이터를
Zustand나React Query같은 상태 관리 도구에 저장하고,
API를 통해 받은 서버 데이터와 동기화(Sync) 시킨다.
즉,
- 로컬 상태(UI 중심)는
Zustand- 서버 상태(API 중심)는
React Query
이렇게 역할을 분리하면,
“화면이 즉시 반응하면서도 서버 데이터와 일관성 있는 상태 관리”가 가능하다.const { data: todos } = useQuery(["todos"], fetchTodos);이 한 줄이 사실상 API 호출 → 응답 → 상태 반영 과정을 자동으로 처리해준다.
Supabase 같은 BaaS는 테이블을 만들면
자동으로 REST API를 생성해준다.
예를 들어todos테이블을 만들면 다음 엔드포인트가 자동으로 생긴다.
메서드 엔드포인트 기능 GET/rest/v1/todos데이터 조회 POST/rest/v1/todos데이터 추가 PATCH/rest/v1/todos?id=eq.{id}데이터 수정 DELETE/rest/v1/todos?id=eq.{id}데이터 삭제 이 구조는 우리가 직접 서버 코드를 작성하지 않아도,
즉시 CRUD API를 사용할 수 있게 해준다.
그래서 요즘은 “API를 만든다”보다 “API를 설계하고 연결한다”가 더 중요해지고 있다.
API는 모든 사용자의 요청을 받아들이면 안 된다.
로그인하지 않은 사용자가 내 데이터를 삭제할 수도 있기 때문이다.
그래서 대부분의 API는 인증(Authentication) 과 인가(Authorization) 단계를 거친다.
예를 들어 Supabase에서는
- 인증 토큰(
Bearer Token)이 없으면 →401 Unauthorized- 다른 사용자의 데이터에 접근하면 →
403 Forbidden
이런 식으로 자동 차단된다.
결국 보안이 포함된 데이터 통신 규칙이 바로 API다.
결국 개발자 입장에서 API란“UI를 실제 데이터와 연결시키는 유일한 통로이자,
사용자 행동을 서버 로직으로 변환하는 매개체”다.
API는 단순히 “요청 규칙”이 아니라,
팀 전체가 같은 규칙으로 협업할 수 있도록 정리된 공식 메뉴판이다.
이 문서 덕분에 프론트엔드와 백엔드가 “같은 언어”로 소통할 수 있다.
- OpenAPI: HTTP API를 기술하는 표준 스펙(JSON 또는 YAML 기반).
- Swagger: 이 OpenAPI 스펙을 시각적으로 보여주고, 직접 테스트(
Try it out)까지 가능한 도구 세트.
예를 들어 아래는 실제 OpenMind API의 Swagger 문서 화면이다. 👇
위 문서에는
Base URL:https://openmind-api.vercel.app- 엔드포인트 예시:
/team/questions/{id}/answers/- 메서드(
GET,POST,PATCH,DELETE)- 모델 구조(
AnswerRetrieveUpdate,Question,Reaction,Subject)
등이 명확하게 정의되어 있다.
Swagger 하단의 “Models” 영역에는 API가 주고받는 데이터 구조가 정리되어 있다.
예를 들어, 아래는AnswerRetrieveUpdate,Question,Reaction,Subject모델의 일부다. 👇
이 구조를 보면 어떤 필드가 필수인지(
required),
각 필드의 타입이 무엇인지(string,boolean,integer)를 한눈에 파악할 수 있다.
결국 이 모델 정의가 API 요청/응답의 데이터 형태를 결정한다.
- 상단의 Base URL 및 Auth 방식(
Bearer Token,API Key등) 확인- 좌측에서 엔드포인트 목록(
GET /answers/{id},POST /questions/{id}/answers/등) 확인- 각 메서드 클릭 → Request Body, Response Example 구조 확인
- 필요한 파라미터(
id,team,question_id) 체크 후 직접 “Try it out” 실행- 성공하면 Response JSON을 복사해 실제 코드에서 예시로 활용
아래는 Swagger 스펙을 그대로 코드로 옮긴 4단계 구조 예시다.
(기준: OpenMind API의POST /team/questions/{question_id}/answers/)
항목 내용 Base URL https://openmind-api.vercel.app엔드포인트 POST /team/questions/{question_id}/answers/요청 본문 { "content": "string", "isRejected": boolean }응답 모델 AnswerRetrieveUpdate인증 방식 Bearer 토큰 1)
api/client.ts— 공통 axios 인스턴스// api/client.ts import axios from "axios"; export const api = axios.create({ baseURL: "https://openmind-api.vercel.app", // Swagger Base URL headers: { "Content-Type": "application/json" }, }); // (선택) 요청 시 토큰 자동 첨부 api.interceptors.request.use((config) => { const token = localStorage.getItem("access_token"); // 프로젝트 컨벤션에 맞게 교체 if (token) config.headers.Authorization = `Bearer ${token}`; return config; });2)
types/openmind.ts— 스펙 기반 타입(필요 필드만)// types/openmind.ts export interface AnswerRetrieveUpdate { id: number; questionId: number; content: string; isRejected: boolean; createdAt: string; // ISO string } export interface Subject { id: number; title: string; // 필요 시 Swagger Models 보고 필드 추가 } export interface Question { id: number; subjectId: number; content: string; like: number; dislike: number; createdAt: string; }3)
api/subjects.ts,api/answers.ts— 엔드포인트 함수// api/subjects.ts import { api } from "./client"; import { Subject } from "../types/openmind"; // GET /{team}/subjects/ export async function listSubjects(team: string) { const { data } = await api.get<Subject[]>(`/${team}/subjects/`); return data; } // POST /{team}/subjects/ export async function createSubject(args: { team: string; body: { title: string } }) { const { team, body } = args; const { data } = await api.post<Subject>(`/${team}/subjects/`, body); return data; }// api/answers.ts import { api } from "./client"; import { AnswerRetrieveUpdate } from "../types/openmind"; // POST /{team}/questions/{question_id}/answers/ export async function createAnswer(args: { team: string; questionId: number; body: { content: string; isRejected: boolean }; }) { const { team, questionId, body } = args; const { data } = await api.post<AnswerRetrieveUpdate>( `/${team}/questions/${questionId}/answers/`, body ); return data; } // GET /{team}/answers/{id}/ export async function getAnswer(args: { team: string; id: number }) { const { team, id } = args; const { data } = await api.get<AnswerRetrieveUpdate>(`/${team}/answers/${id}/`); return data; }4) 사용 컴포넌트(간단 예시)
실전에서는 React Query 추천이지만, 여기서는 예시 간결성을 위해 기본 훅 사용
// app/examples/OpenMindExample.tsx "use client"; import { useEffect, useState } from "react"; import { listSubjects } from "@/api/subjects"; import { createAnswer } from "@/api/answers"; import type { Subject, AnswerRetrieveUpdate } from "@/types/openmind"; export default function OpenMindExample() { const team = "my-team"; // 실제 팀 식별자로 교체 const [subjects, setSubjects] = useState<Subject[]>([]); const [loading, setLoading] = useState(false); const [creating, setCreating] = useState(false); const [result, setResult] = useState<AnswerRetrieveUpdate | null>(null); // 1) 과목 목록 불러오기 useEffect(() => { let mounted = true; setLoading(true); listSubjects(team) .then((data) => mounted && setSubjects(data)) .finally(() => mounted && setLoading(false)); return () => { mounted = false; }; }, [team]); // 2) 임의 질문에 답변 달기 (예: questionId = 1) async function handleCreateAnswer() { try { setCreating(true); const data = await createAnswer({ team, questionId: 1, // 실제 질문 ID로 교체 body: { content: "안녕하세요! 예시 답변입니다.", isRejected: false }, }); setResult(data); alert("답변 생성 완료!"); } finally { setCreating(false); } } return ( <div className="p-4 space-y-4"> <h2 className="text-xl font-bold">OpenMind API Example</h2> <section> <h3 className="font-semibold mb-2">1) Subjects 목록</h3> {loading ? <p>불러오는 중...</p> : ( subjects.length ? ( <ul className="list-disc pl-5"> {subjects.map((s) => <li key={s.id}>{s.title}</li>)} </ul> ) : <p>과목이 없습니다.</p> )} </section> <section> <h3 className="font-semibold mb-2">2) 질문에 답변 생성</h3> <button className="rounded px-3 py-2 border" onClick={handleCreateAnswer} disabled={creating}> {creating ? "생성 중..." : "답변 생성"} </button> {result && ( <pre className="mt-3 text-sm bg-neutral-100 p-3 rounded"> {JSON.stringify(result, null, 2)} </pre> )} </section> </div> ); }
- Base URL →
axios.create({ baseURL })- 경로(Path) →
/team/questions/${id}/answers/- Request Body →
content,isRejected등 필드 정의- Response Model → Swagger의
AnswerRetrieveUpdate를 타입으로 정의- Auth 헤더 →
Authorization: Bearer <token>Swagger 문서만 정확히 읽을 수 있다면,
이런 axios 코드는 거의 자동으로 변환할 수 있다.
API는 데이터를 주고받는 통로이기 때문에,
보안과 버전 관리가 되어 있지 않으면 서비스 전체가 흔들린다.
특히 로그인, 사용자 데이터, 토큰이 오가는 부분에서는 기본 원칙만이라도 지켜야 한다.
“이 요청이 누구로부터 왔는가?”
가장 흔한 방식은 Bearer Token이다.
사용자가 로그인하면 서버에서 토큰을 발급하고,
이 토큰을 매 요청마다 함께 보낸다.// axios 요청 시 토큰을 헤더에 포함 api.interceptors.request.use((config) => { const token = localStorage.getItem("access_token"); if (token) config.headers.Authorization = `Bearer ${token}`; return config; });서버는 이 토큰이 유효한지 검사하고,
만료되었거나 위조된 경우엔401 Unauthorized로 응답한다.
“이 사용자가 이 행동을 해도 되는가?”
인증을 통과했다고 해서 모든 데이터에 접근할 수 있는 건 아니다.
예를 들어 다른 사용자의todo를 수정하는 건 금지되어야 한다.Supabase 같은 서비스는 RLS(Row Level Security) 로 이걸 자동으로 관리한다.
SQL 정책 예시:create policy "사용자 본인 데이터만 보기" on public.todos for select using (auth.uid() = user_id);
✅ HTTPS — “모든 데이터는 암호화되어야 한다”
- 왜 필요한가?
HTTP는 평문으로 통신하므로 토큰이나 개인정보가 유출될 수 있다.
HTTPS는 TLS 암호화로 데이터를 보호한다.
- 기본 원칙
- 배포 환경에서는 항상
https://만 사용 (Vercel, Netlify 기본 지원)- 내부 이미지나 API도 전부 HTTPS로 통일 (혼합 콘텐츠 방지)
- 실무 팁
- 개발용
http://localhost만 예외- 서버(혹은 프록시)에서 HTTPS 리다이렉트를 한 번 더 걸면 완벽하다.
✅ CORS — “내 API를 누가 부를 수 있는가”
- 개념
CORS(Cross-Origin Resource Sharing)는
“어떤 웹사이트의 JS가 내 API를 호출할 수 있는가”를 정하는 브라우저 규칙이다.
즉, 허용된 도메인만 요청 가능하도록 막는 보안 장치다.
- 예시
https://myapp.vercel.app→ ✅ 허용https://randomsite.com→ ❌ 차단 (CORS 에러 발생)
- 설정 원칙
Access-Control-Allow-Origin에 정확한 도메인만 허용
→*(와일드카드)는 인증 요청(Authorization/Cookie) 과 함께 쓰면 안 된다.- 인증이 필요한 경우
credentials: true설정 후 정확한 출처를 명시
- 자주 겪는 에러
- “CORS 정책에 의해 차단됨” → 서버 응답에
Access-Control-Allow-Origin누락- Preflight(
OPTIONS) 요청 무시 → 서버에서200/204응답 필요
👉 요약
항목 핵심 요약 HTTPS 데이터 암호화, HTTP 금지 CORS 허용 도메인만 접근 가능 Tip 인증이 있다면 *대신 정확한 도메인 명시
“기존 사용자에게 영향을 주지 않고 API를 바꾸는 법”
새로운 기능을 추가하더라도,
기존 클라이언트가 사용하는 구조는 깨지지 않아야 한다.
그래서 API는 보통 버전을 붙여 관리한다.
버전 의미 /v1/...첫 버전 /v2/...구조가 달라진 새 버전 예:
GET /v1/todos GET /v2/todos기존
/v1은 그대로 유지하고,/v2를 새로 추가한다.
나중에/v1을 제거할 땐 “Deprecated(지원 종료 예정)” 문구를 미리 안내한다.
- 로그인/민감 데이터는 반드시 토큰 인증.
- 권한은 본인 데이터만 허용 (Supabase RLS 추천).
- HTTPS + CORS 허용 도메인 지정.
- 버전은 /v1, /v2 로 구분, 기존 API는 바로 없애지 않는다.
API는 단순히 데이터를 주고받는 통로가 아니다.
사람과 시스템, 프론트엔드와 백엔드, 서비스와 서비스 사이를 연결하는 약속이다.
우리가 버튼을 클릭하면,
그 행동은Request가 되어 서버로 전달되고,
서버는 그 의미를 해석해Response를 돌려준다.
이 단순한 흐름이 반복되며, 하나의 서비스가 완성된다.
이번 프로젝트에서 API 레이어를 직접 만들면서
가장 크게 느낀 건 “모든 기능은 결국 API 위에 있다”는 사실이었다.
화면이 아무리 예뻐도,
데이터가 정확히 오가지 않으면 서비스는 동작하지 않는다.
즉, API는 서비스의 ‘실제 뼈대’다.
- 일관성 — 이름, 구조, 응답 형식이 어디서든 같다.
- 명확성 — 문서를 읽으면 바로 사용할 수 있다.
- 안정성 — 보안, 버전, 오류 처리가 정리되어 있다.
- 확장성 — 새로운 기능이 추가되어도 기존 코드는 깨지지 않는다.
이 네 가지를 지키는 게 결국 “잘 만든 API”의 기준이었다.
결국 API는 코드를 넘어서 “약속”이다.
팀이 같은 규칙으로 말하고,
시스템이 같은 언어로 소통할 수 있게 만드는,
프로덕트의 가장 중요한 연결선이다.