작년 여름, 백엔드 개발자 재원이형과 화상 미팅을 하면서 30분을 날렸다.
신규 주문 상세 페이지를 만들고 있었는데, API 응답에서 items가 배열 안에 객체로 오는 줄 알았다. Figma 보고 당연히 그렇겠거니 했다. 그런데 실제 응답은 items가 pagination된 별도 endpoint였고, 주문 정보에는 itemIds만 달랑 들어있었다. 나는 "이거 왜 items가 안 와요?"를 반복했고, 재원이형은 "아니 그건 원래 따로 호출하는 건데..."를 반복했다. 서로 다른 그림을 보면서 같은 단어를 쓰고 있었다.
미팅이 끝나고 나서야 깨달았다. 나는 REST API가 어떤 원칙으로 설계되는지를 전혀 몰랐다. Resource 단위로 endpoint가 나뉜다는 개념 자체가 없었으니, 왜 한 번에 다 안 주는지를 이해할 수 없었던 거다.
HTTP, 생각보다 모르고 쓰고 있었다
프론트엔드 개발하면서 fetch나 axios를 매일 쓰지만, HTTP 자체를 제대로 이해하고 있는 건 아니었다.
상태 코드부터 그랬다. 200이면 성공, 400이면 에러, 500이면 서버 문제. 이 정도만 알고 있었다. 그런데 어느 날 API가 201 Created를 반환하는데 내 에러 핸들링 코드에서 status === 200만 체크하고 있어서 생성 성공인데 에러 토스트가 뜨는 버그가 났다. 그때 상태 코드를 제대로 공부했다.
// 이렇게 쓰고 있었다
if (response.status === 200) {
showSuccess();
} else {
showError(); // 201도 여기로 빠짐
}
// 이렇게 바꿨다
if (response.ok) { // 200-299 전부 커버
showSuccess();
}작은 차이인데 이걸 모르면 버그가 된다.
그리고 CORS. 프론트엔드 개발자라면 한 번쯤 로컬에서 API 호출하다가 빨간 CORS 에러를 본 적 있을 거다. 나도 처음엔 "아 CORS 에러 났어요, 백엔드에서 풀어주세요"만 말할 줄 알았다. 왜 CORS가 존재하는지, preflight 요청이 뭔지, Access-Control-Allow-Origin 헤더가 어떻게 동작하는지는 몰랐다.
CORS는 브라우저의 보안 정책이다. 서버가 "이 origin에서 오는 요청은 허용해"라고 명시적으로 알려줘야 브라우저가 응답을 읽을 수 있다. 이걸 이해하면 "백엔드에서 풀어주세요"가 아니라 "staging 서버의 origin을 allow list에 추가해주실 수 있나요?"라고 구체적으로 요청할 수 있다. 백엔드 개발자 입장에서도 이렇게 말하는 프론트엔드가 훨씬 일하기 편하다.
헤더도 마찬가지다. Content-Type, Authorization, Cache-Control — 이런 기본 헤더들이 뭘 하는지 알면 디버깅 속도가 확 달라진다. Network 탭에서 request/response 헤더를 읽을 수 있게 되면, 문제가 프론트에 있는지 백엔드에 있는지를 스스로 판단할 수 있다.
REST API 설계를 이해하면 협업이 달라진다
처음에 말한 재원이형과의 30분 헛소리 사건 이후로, REST API 설계 원칙을 공부했다. Roy Fielding의 논문까지 읽진 않았고, 실무에서 자주 쓰이는 패턴 위주로.
핵심은 resource 중심 사고다. REST에서 URL은 동사가 아니라 명사다.
# 이런 게 아니라
POST /getOrderDetail
POST /createNewOrder
# 이런 식이다
GET /orders/123
POST /orders
GET /orders/123/items
이걸 알고 나니까 API 문서를 읽는 속도가 빨라졌다. endpoint를 보면 "아, 주문이라는 resource 아래에 items가 sub-resource로 있구나"라는 게 바로 보인다. 새 페이지를 만들 때도 어떤 API를 호출해야 하는지 감이 온다.
그리고 한 가지 더. 백엔드가 왜 한 API에 모든 데이터를 안 담아주는지를 이해하게 됐다. 예전에는 "그냥 한 번에 다 주면 프론트에서 편한데 왜 나눠놓는 거야"라고 불만이 있었다. 근데 이유가 있다.
하나의 API에 너무 많은 데이터를 넣으면 그 API의 응답 시간이 느려진다. 주문 상세를 호출하는데 배송 추적, 리뷰, 추천 상품까지 전부 join해서 한 방에 주면 DB 쿼리가 무거워진다. 프론트에서는 화면에 보이는 영역만 먼저 로딩하고, 나머지는 lazy하게 호출하는 게 UX도 더 좋다. 백엔드의 설계 의도를 이해하면 "왜 이렇게 했어요?"가 "이렇게 나눠주셨으니 프론트에서 이렇게 호출할게요"로 바뀐다. 이 차이가 크다.
인증은 프론트엔드의 영역이기도 하다
JWT, 세션, OAuth. 이런 단어를 들으면 "그건 백엔드 일 아닌가?"라고 생각할 수 있다. 나도 그랬다. 근데 아니다. 토큰을 어디에 저장할지 결정하는 건 프론트엔드다.
한번은 로그인 기능을 구현하면서 JWT 토큰을 localStorage에 저장했다. 동작은 잘 됐다. 코드 리뷰에서 시니어분이 물었다.
"XSS 공격으로 localStorage 털리면 어떻게 돼?"
대답을 못 했다. localStorage는 JavaScript로 자유롭게 접근할 수 있으니까, XSS 취약점이 하나라도 있으면 토큰이 통째로 노출된다. httpOnly 쿠키에 저장하면 JavaScript에서 접근이 불가능하니까 XSS에 안전하다. 대신 CSRF를 신경 써야 한다.
// localStorage 방식 - XSS에 취약
const token = localStorage.getItem('accessToken');
fetch('/api/data', {
headers: { Authorization: `Bearer ${token}` }
});
// httpOnly 쿠키 방식 - 브라우저가 자동으로 쿠키를 보냄
fetch('/api/data', {
credentials: 'include' // 이것만 하면 된다
});OAuth 흐름도 마찬가지다. "카카오 로그인 구현해주세요"라는 요구사항을 받으면, authorization code flow가 어떻게 돌아가는지를 알아야 한다. redirect URI 설정, authorization code를 받아서 백엔드에 넘기기, 백엔드가 access token을 받아서 유저 정보를 가져오는 흐름. 이걸 전체적으로 알아야 프론트에서 어디를 담당하는지가 명확해진다.
refresh token rotation이 뭔지, access token 만료 시 어떻게 갱신하는지, 401이 왔을 때 자동 refresh 후 원래 요청을 재시도하는 interceptor를 어떻게 짜는지. 이런 건 프론트엔드가 직접 구현해야 하는 영역이다.
DB를 조금만 알면 질문이 달라진다
DB를 알아야 한다고 해서 SQL 마스터가 되라는 뜻이 아니다. 기본 개념 몇 가지만 알면 된다.
N+1 문제를 예로 들어보자. 주문 목록 API가 있다. 주문 10개를 가져온다. 그런데 각 주문마다 유저 정보를 별도로 조회한다. 결과적으로 쿼리가 1 + 10 = 11번 나간다. 주문이 100개면 101번. 이게 N+1 문제다.
프론트엔드 개발자가 이걸 알면 뭐가 좋은가? 주문 목록 페이지가 갑자기 느려졌을 때 "이 API 느린데 확인해주세요"가 아니라 "혹시 주문 목록 조회할 때 유저 정보를 N+1으로 가져오는 건 아닌가요?"라고 물을 수 있다. 백엔드 개발자 입장에서 이런 질문을 받으면 "오, 이 친구 좀 아는구나"가 되면서 소통 비용이 확 줄어든다.
인덱스 개념도 알아두면 좋다. "이 검색 API가 왜 2초나 걸려요?"라고 물을 때, 인덱스가 안 걸려있을 수 있다는 가능성을 떠올릴 수 있으면 대화가 빨라진다.
pagination도 마찬가지다. offset 기반과 cursor 기반의 차이를 알면, 무한 스크롤 구현할 때 왜 cursor 방식이 더 안정적인지 이해할 수 있다. 데이터가 실시간으로 추가되는 피드에서 offset 기반 pagination을 쓰면 같은 아이템이 중복으로 보이는 문제가 생긴다. 이런 걸 알면 설계 미팅에서 의견을 낼 수 있게 된다.
Docker, 로컬에서 백엔드 한번 띄워봐라
이건 의외로 추천하고 싶은 건데, Docker로 로컬에서 백엔드를 띄워보는 경험이 프론트엔드 개발에 꽤 도움이 된다.
우리 팀은 docker-compose.yml 파일이 있어서 docker-compose up 하면 백엔드 서버, DB, Redis가 로컬에 뜬다. 처음에 이걸 모르고 항상 staging 서버에 붙어서 개발했다. staging이 불안정할 때마다 내 작업도 멈췄다. 어느 날 백엔드 동료가 "로컬에서 띄우면 되는데?"라고 하길래 한번 해봤다.
# 이게 전부다
git clone backend-repo
docker-compose up -d
# 로컬 백엔드가 localhost:8080에 뜬다
# 프론트 .env에서 API_URL만 바꾸면 끝Docker를 깊이 알 필요는 없다. image, container, volume이 뭔지 정도. Dockerfile을 읽을 수 있는 정도면 충분하다. 이걸로 얻는 이점은 명확하다. staging 서버 상태에 의존하지 않고 독립적으로 개발할 수 있다. API 응답을 직접 수정해볼 수도 있다. 백엔드 코드에서 에러가 나면 로그를 직접 볼 수 있다.
docker logs -f backend-container 한 줄이면 백엔드 로그가 실시간으로 보인다. "이 API 호출하면 500 에러 나요"를 말하면서 동시에 "로그 보니까 NullPointerException이 이 라인에서 나네요"라고 말할 수 있으면 문제 해결 속도가 완전 다르다.
여기서 멈춰라
여기까지 읽으면 "오 그러면 Spring Boot도 배워볼까?" 같은 생각이 들 수 있다. 하지 마라.
나도 한때 그런 적 있다. 백엔드가 재밌어 보여서 NestJS로 사이드 프로젝트를 시작했다. Controller, Service, Repository 패턴을 배우고, TypeORM으로 DB 연동하고, JWT 인증까지 직접 구현했다. 2주 정도 재밌었다. 그런데 그 2주 동안 프론트엔드 실력은 1도 안 늘었다. 회사에서 맡은 성능 최적화 작업은 진전이 없었고, 팀에서 논의되던 디자인 시스템 구축에도 참여를 못 했다.
프론트엔드 개발자로서 가치를 만들어야 하는 영역은 프론트엔드다. 접근성, 성능 최적화, 상태 관리 아키텍처, 테스트 전략, 디자인 시스템. 이런 것들을 잘하는 게 먼저다. 백엔드 지식은 이 위에 얹는 양념이다.
내가 위에서 말한 것들 — HTTP 기본, REST 설계 원칙, 인증/인가 기초, DB 개념, Docker 로컬 환경 — 이 정도만 알면 된다. 깊이 팔 필요 없다. 각 주제별로 하루 이틀이면 실무에 필요한 수준은 충분히 채울 수 있다.
백엔드를 깊이 파는 프론트엔드 개발자보다, 백엔드를 적당히 이해하면서 프론트엔드를 깊이 파는 개발자가 팀에 훨씬 도움이 된다. 재원이형과의 30분 헛소리 이후로 백엔드 기초를 익히는 데 투자한 시간은 총 2주 정도였다. 그 2주의 투자로 이후 1년간의 협업 속도가 체감상 2배는 빨라졌다.
그러니까 결론은 이거다. 백엔드를 "할 줄 알" 필요는 없다. 백엔드 개발자가 하는 말을 "알아들을 수 있으면" 된다. 그 차이는 생각보다 작은 노력으로 만들 수 있다.
