| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |
- 프론트엔드개발
- 데이터캐싱
- 프론트엔드성능
- useEffect
- 이미지최적화
- fe
- Refactoring
- TanStackQuery
- 최적화
- RTR
- JavaScript
- refresh token
- 성능최적화
- 리액트
- frontenddevelopment
- frontend
- 비동기처리
- NextJs
- Next
- 프론트엔드개발자
- reactquery
- 웹개발
- 프론트엔드
- TechBlog
- WebP
- 리팩토링
- react성능최적화
- react
- JWT
- lighthouse
- Today
- Total
juncci 님의 블로그
Next.js App Router에서 BFF 레이어를 구성한 방법과 고민 본문
프론트엔드에서 백엔드 API를 직접 호출하는 구조는 꽤 자연스러운 선택이다.
나 역시 처음에는 별다른 고민 없이 이 방식을 사용했다.
하지만 서비스가 커지면서, 프론트엔드가 단순히 UI를 그리는 역할을 넘어
여러 API를 조합하고 인증까지 처리하는 “작은 백엔드처럼 동작”하기 시작했다.
“이걸 정말 프론트에서 처리하는 게 맞을까?”
이 질문에서 출발해 도입하게 된 구조가 바로 BFF(Backend for Frontend)다.
BFF는 클라이언트와 백엔드 사이에서
API 호출을 대신하고 데이터를 가공해주는 프론트엔드 전용 백엔드 레이어다.
Next.js의 Route Handler를 활용하면
이 레이어를 비교적 자연스럽게 구성할 수 있다.
이 글에서는 Next.js 환경에서 BFF를 도입하게 된 이유와,
그 과정에서 했던 고민들을 정리해보려고 한다.
처음 아키텍처를 잡을 때는 사실 별 생각이 없었다.
프론트엔드에서 백엔드 API를 직접 호출하는 구조는 너무 익숙했고, 당연한 선택처럼 느껴졌기 때문이다.

이 구조는 단순하고 빠르다.
초기 개발 속도도 잘 나오고, 굳이 레이어를 더 나눌 필요도 없어 보였다.
그런데 서비스가 조금씩 커지면서, 이상한 지점들이 하나둘씩 눈에 들어오기 시작했다.
가장 먼저 느낀 건 “화면 하나가 생각보다 많은 API에 의존한다”는 점이었다.
예를 들어 온보딩 화면을 구성하려고 하면 jobs(직업), career-levels(경력), skills(기술 스택) 같은 API를 동시에 불러와야 했다. 자연스럽게 프론트엔드에서는 이런 코드가 늘어나기 시작한다.

요청은 세 번, 로딩 상태도 세 개, 에러 처리도 각각 따로.
처음에는 별 문제 아닌 것처럼 보이는데, 이런 패턴이 반복되기 시작하면 코드가 점점 “데이터를 그리는 코드”가 아니라 “데이터를 모으는 코드”로 변해간다.
비슷한 시기에 인증 처리도 신경 쓰이기 시작했다.
각 API 요청마다 Authorization 헤더를 붙이고, 토큰 만료를 체크하고, 필요하면 갱신까지 해야 했다.
이걸 컴포넌트 단에서 계속 처리하다 보니 자연스럽게 중복이 생겼고,
조금만 구조가 흔들리면 인증 처리 방식이 일관되지 않게 될 가능성도 보였다.
“이거… 프론트가 할 일은 아닌 것 같은데?”
결정적으로 불편했던 건, 우리가 API를 어떻게 쓰고 있는지 아무도 모른다는 점이었다.
클라이언트에서 직접 API를 호출하다 보니
- 어떤 endpoint가 많이 호출되는지
- 어디서 latency가 발생하는지
- 에러율이 높은 API가 뭔지
이런 걸 서버 레벨에서 관측하기가 어려웠다.
이쯤 되니까 구조 자체를 다시 생각해볼 필요가 있다고 느꼈다.
그래서 도입한 게 BFF(Backend for Frontend)였다.

핵심은 단순하다.
클라이언트는 더 이상 여러 API를 직접 호출하지 않고, BFF라는 중간 레이어 하나만 바라보게 만든다.
여기서 중요한 건 “왜 굳이 BFF였냐”인데,
이건 Next.js를 쓰고 있었기 때문에 훨씬 자연스러운 선택이었다.
App Router 환경에서는 Route Handler를 통해 서버 코드를 쉽게 작성할 수 있는데, 이게 사실상 “프론트 프로젝트 안에 작은 백엔드가 하나 들어온 것”과 다름없었다.
별도의 서버를 띄울 필요도 없고, 같은 코드베이스에서 관리도 가능하다 보니
“BFF를 안 쓸 이유가 없는 환경”이었다고 보는 게 더 맞다.
여기서 한 가지 고민이 있었다.
“BFF를 단순 프록시로 둘 것인가, 아니면 로직을 넣을 것인가?”
처음에는 단순히 요청을 전달하는 역할만 하게 둘 수도 있었다.

그런데 이렇게 하면 얻는 게 거의 없다.
이럴 거면 그냥 클라이언트에서 직접 호출하는 게 더 단순하기 때문이다.
그래서 방향을 바꿨다.
“어차피 레이어를 만들 거라면, 책임을 확실히 주자”
그렇게 해서 BFF에서 API aggregation을 하도록 설계했다.

여기서 BFF는 단순히 요청을 전달하는 게 아니라,
여러 API를 병렬로 호출하고 데이터를 합쳐서 하나의 응답으로 만들어준다.
이 변화 하나로 프론트 코드가 눈에 띄게 단순해졌다.
- 요청은 한 번
- 로딩 상태도 하나
- 에러 처리도 한 곳
이때 확실히 느꼈다.
“아, 이게 역할 분리구나”
인증 처리도 자연스럽게 BFF로 이동시켰다.

이 구조로 바꾸고 나니까 클라이언트에서는 토큰을 직접 다룰 일이 거의 없어졌다.
HttpOnly 쿠키를 활용할 수 있게 되면서 보안적인 측면에서도 훨씬 안정적인 구조가 됐다.
무엇보다 좋았던 건,
“인증 로직이 한 군데로 모였다”는 점이었다.
그리고 개인적으로 가장 만족스러웠던 변화는 observability였다.

- 어떤 API가 많이 쓰이는지
- 어디서 병목이 생기는지
- 어떤 요청이 실패하는지
서버 레이어에서 파악할 수 있게 됐다.
이건 실제 운영에서 생각보다 훨씬 큰 차이를 만든다.
물론 upstream API 장애도 고려해야 했다.
API가 느려지거나 죽어버리면, BFF도 같이 영향을 받기 때문이다.
그래서 최소한의 방어 로직을 넣었다.

timeout을 설정하고, 일부 데이터는 캐시 fallback을 사용하도록 했다.
완벽한 해결은 아니지만, “전체 서비스가 같이 죽는 상황”은 피할 수 있었다.
이 과정을 겪으면서 생각이 조금 바뀌었다.
처음에는 레이어를 하나 더 두는 게 오히려 복잡도를 늘린다고 생각했는데,
막상 적용해보니 오히려 반대였다.
프론트엔드는 다시 “UI와 상태”에 집중하게 되었고,
데이터를 가져오고 조합하는 책임은 BFF로 자연스럽게 이동했다.
정리하면 이 한 문장으로 설명할 수 있을 것 같다.
“복잡도를 줄이는 방법은 때로는 레이어를 하나 더 추가하는 것이다.”
Next.js 환경에서는
그 레이어를 만드는 비용이 거의 들지 않는다.
그래서 나에게 BFF는 선택이라기보다는,
서비스가 커지면서 자연스럽게 도달한 구조에 가까웠다.

'[FE]' 카테고리의 다른 글
| [Storybook] 도입과 운영 기록 (0) | 2026.02.09 |
|---|---|
| FSD 아키텍처 적용기: 코드 위치를 설계하다 (0) | 2026.02.08 |
| 🐣자바스크립트에서 덕 타이핑(Duck Typing) 이해하기 (0) | 2025.09.16 |
| 🚀실시간 통신을 위한 WebSocket, 그리고 .emit, .on 개념 정리 (0) | 2025.05.22 |
| Tailwind CSS – 헷갈렸던 포인트 정리 (0) | 2025.05.19 |