프로그래밍/next.js

[커리큘럼 페이지 프로젝트] Mock API를 걷어내고 Spring을 붙이며

d 0_0 b 2026. 6. 10. 21:32

6/10 작업 현황 정리

더보기
더보기

 

PR 정리

PR #1: Spring main/careers/subject detail 통합

PR:
spring-careers-api-test → spring-api-test

주요 내용:

  • /api/main Spring 프록시
  • /api/subject/table Spring 프록시
  • /api/careers/list Spring 프록시
  • /api/subjects/{subjectId} 과목 툴팁 Spring 프록시
  • normalizeSpringMainData() 추가
  • mock 기반 메인 초기 데이터 제거

결과:

  • merge 완료
  • spring-api-test에 반영

PR #2: Career side detail 통합

PR:
spring-careerside-api-test → spring-api-test

주요 내용:

  • /api/careers/detail Next 프록시 추가
  • 커리어 상세 페이지를 mock에서 Spring API 기반으로 변경
  • 커리어 대표 직무 예시와 참고 링크를 Spring 데이터로 렌더링
  • backend CareerPath에 representativeExamples, referenceLinks 필드 추가
  • 별도 CareerExample, CareerReferenceLink 엔티티/레포지토리 제거
  • seed 데이터에 커리어별 예시와 참고 링크 추가

 

 

Next.js에서 mock으로 받아오던 데이터를 Spring API로 바꾸는 작업이었다. 말로만 들으면 간단하다. fetch 주소를 바꾸고, 응답 형태를 맞추고, 화면에서 잘 나오는지 확인하면 될 것 같았다.

 

그런데 막상 해보니 이건 단순히 API 주소를 바꾸는 일이 아니었다. 화면이 믿고 있는 데이터의 기준을 옮기는 일이었다.

기존에는 프론트 안에 있는 mock 데이터가 기준이었다. 화면도 그 데이터를 보고 있었고, Next의 route.ts도 그 데이터를 내려주고 있었다. 겉으로는 API를 호출하는 구조였지만, 실제로는 프론트 프로젝트 안에서 모든 게 해결되고 있었다.

Spring을 붙인다는 건 이 기준을 바꾸는 일이었다.

 

이제 데이터의 기준은 mock이 아니라 Spring이어야 했다. 그리고 Spring 뒤에는 Controller, Service, Repository, DB가 있었다. 생각해보면 당연한 이야기인데, 처음에는 이 차이를 조금 가볍게 봤던 것 같다.

 

기존에는 Next가 임시 백엔드 역할을 하고 있었다

처음 흐름은 이랬다.

MainPageClient
  -> useCareerList
    -> fetchCareerList
      -> fetch("/api/careers/list")
        -> Next route.ts
          -> createCareerListMock()
          -> JSON 응답

MainPageClient가 화면을 만들고, useCareerList가 데이터를 관리하고, fetchCareerList가 API를 호출한다. 여기까지는 일반적인 프론트 흐름처럼 보인다.

그런데 실제 요청은 Spring으로 가는 것이 아니라 Next 안의 route.ts로 갔다. 그리고 route.ts는 mock 데이터를 만들어 JSON으로 돌려줬다.

그러니까 이 구조는 정확히 말하면 이런 상태였다.

브라우저
  -> Next 프론트엔드
    -> Next route.ts
      -> mock 데이터

프론트 개발 초기에는 이런 구조가 편하다. 백엔드가 완성되지 않아도 화면을 만들 수 있고, 필요한 데이터도 원하는 모양으로 바로 만들 수 있다.

하지만 어느 순간부터는 이 구조가 발목을 잡는다.

화면은 API를 호출하는 것처럼 보이는데, 실제 데이터는 DB가 아니라 mock에서 온다. 그러면 Spring DB의 값을 바꿔도 화면이 바뀌지 않는다. 처음에는 이상하게 느껴지지만, 사실 당연한 일이다. 화면이 Spring을 보고 있지 않았으니까.

 

Spring을 붙이면 바뀌는 것은 화면이 아니라 목적지다

Spring을 붙인 뒤의 흐름은 이렇게 바뀐다.

MainPageClient
  -> useCareerList
    -> fetchCareerList
      -> fetch("http://localhost:8081/api/careers/list")
        -> Spring Controller
          -> Service
            -> Repository
              -> DB
          -> JSON 응답

여기서 중요한 건 프론트 전체가 바뀌는 게 아니라는 점이다.

MainPageClient는 그대로 있다.
useCareerList도 그대로 있다.
fetchCareerList도 그대로 있다.
CareerSide도 그대로 데이터를 받아 렌더링한다.

바뀌는 것은 결국 fetchCareerList가 어디를 바라보느냐다.

기존에는 이랬다.

fetchCareerList
  -> Next route.ts
  -> mock 데이터

Spring을 붙이면 이렇게 된다.

fetchCareerList
  -> Spring Controller
  -> Service
  -> Repository
  -> DB

처음에는 fetch("/api/careers/list")를 fetch("http://localhost:8081/api/careers/list")로 바꾸는 정도라고 생각했다.

그런데 이 한 줄의 의미가 생각보다 컸다. URL만 바뀐 게 아니라, 화면이 믿는 데이터 출처가 바뀌는 것이었다.

 

브랜치 기준 정하기

 

작업을 하면서 먼저 정리해야 했던 것은 브랜치였다.

dev 브랜치에는 아직 mock 기반 코드가 남아 있었다. 그래서 Spring 연동 작업을 바로 dev에 섞기보다는, 따로 통합 브랜치를 두기로 했다.

처음에는 이렇게 정리했다.

dev
-> mock 기반 안정 브랜치

spring-api-test
-> Spring API 통합 기준 브랜치

spring-careers-api-test
-> 메인, 과목 테이블, 커리어 목록, 과목 툴팁 연동 브랜치

spring-careerside-api-test
-> 커리어 상세 페이지 연동 브랜치

그런데 중간에 한 번 꼬였다.

spring-api-test를 만들 때 이미 Spring 연동 작업이 들어가 있던 spring-careers-api-test에서 딴 것이 아니라, dev에서 바로 따왔다. 그러다 보니 앞에서 작업했던 메인 화면과 과목 테이블 연동 코드가 빠졌다.

결과적으로 이상한 상태가 됐다.

툴팁은 Spring을 보고 있는데, 메인 화면은 여전히 mock을 보고 있었다.

기능 브랜치를 어떻게 나누느냐도 중요하지만, 더 중요한 건 어느 브랜치를 기준으로 통합할지였다. 기준 브랜치가 흔들리면 그 위에 올라가는 작업도 같이 흔들린다.

결국 spring-careers-api-test의 내용을 PR로 spring-api-test에 병합했고, 이후에는 spring-api-test를 Spring API 통합 기준 브랜치로 고정했다.

별것 아닌 것 같지만, 이런 기준이 없으면 작업이 진행될수록 어디까지가 반영된 코드인지 점점 흐려진다.

 

 

혼자하는 프로젝트이기에 기존 기능 단위 브랜치 분기가 아닌 화면 단위의 브랜치 분기를 선정해볼 수 있었다.

 

같은 화면에서 서로 다른 데이터를 보고 있었다

이번 작업에서 가장 기억에 남는 문제는 이산수학 과목이었다. 몇몇 과목들은 이수과정이 바뀌면서 전필(전공 필수)가 전선(전공 선택) 과목이 되는 경우가 있었기에 이 전에 택했었던 DB 설계 전략이 정확히 먹혀 들어갈 수 있었다.


고심했던 문제이니, 아래의 글을 읽어봐주시면 너무너무 감사할 것 같습니당.
2026.01.01 - [프로그래밍] - [트러블 슈팅] 매년 바뀌는 전공 이수 기준, 어떻게 설계해야 깨지지 않을까?

 

Spring DB에서 이산수학의 requirementType을 MAJOR_REQUIRED로 바꿨다. 당연히 화면에서도 전공 필수로 보여야 한다고 생각했다.

그런데 툴팁에는 “전공 필수”가 잘 나왔다. 반면 메인 카드에는 M 뱃지가 보이지 않았다.

처음에는 프론트 렌더링 문제라고 생각했다. 조건문이 잘못됐나, 타입이 안 맞나, 뱃지 표시 로직이 빠졌나 싶었다.

그런데 원인은 더 단순했다.

툴팁과 메인 카드가 서로 다른 데이터를 보고 있었다.

툴팁
-> Spring API 조회

메인 카드
-> mock createMainMock() 조회

툴팁은 Spring DB의 바뀐 값을 보고 있었다. 그래서 “전공 필수”가 정상적으로 나왔다.

반면 메인 카드는 아직 mock 데이터를 보고 있었다. 그러니 Spring DB에서 아무리 값을 바꿔도 메인 카드에는 반영될 수가 없었다.

게다가 메인 카드의 M 뱃지는 requirementType을 직접 보는 구조도 아니었다. course.badges 값을 보고 렌더링하고 있었다. 즉, Spring에서 requirementType을 바꿔도 mock의 badges가 그대로라면 화면은 그대로였다.

여기서 알게 된 것은 하나다.

API는 하나만 붙인다고 끝나는 게 아니다. 같은 화면을 구성하는 데이터는 같은 출처를 봐야 한다.

툴팁만 Spring으로 바꾸는 건 부분적으로는 성공처럼 보인다. 하지만 사용자가 보는 화면 기준에서는 오히려 모순이 생긴다. 사용자에게는 “여기는 mock이고 여기는 Spring이라서 그렇다”는 설명이 의미가 없다. 그냥 화면이 이상한 것이다.

결국 /api/main, /api/subject/table까지 Spring으로 연결했다. 메인 화면과 과목 카드가 Spring 데이터를 보도록 맞췄다.

normalizeSpringMainData가 필요했던 이유

Spring 응답을 바로 프론트 컴포넌트에 넣을 수도 있었다.

하지만 그렇게 하면 백엔드 응답 구조가 조금만 바뀌어도 UI 컴포넌트들이 같이 흔들릴 수 있다. 기존 컴포넌트들은 이미 MainData라는 형태에 맞춰져 있었다.

그래서 중간에 normalizeSpringMainData()를 두었다.

Spring 응답
  -> normalizeSpringMainData()
    -> 기존 MainData 형태
      -> 기존 UI 컴포넌트

이 구조가 꽤 중요했다.

Spring에서 내려주는 데이터는 백엔드 기준의 데이터다. 반면 프론트 컴포넌트가 원하는 데이터는 화면 기준의 데이터다. 둘이 항상 같을 수는 없다.

그래서 중간에서 한 번 변환해주는 계층이 필요했다. 덕분에 기존 UI를 크게 흔들지 않고 데이터 출처만 Spring으로 바꿀 수 있었다.

이번 작업을 하기 전에는 이런 변환 계층을 단순한 매핑 정도로 생각했다. 그런데 실제로 해보니 이 계층은 프론트와 백엔드 사이의 완충지대에 가까웠다.

백엔드는 백엔드답게 응답하고, 프론트는 프론트답게 화면을 그리기 위해 필요한 지점이었다.

 

검증은 화면만 보면 부족했다

작업 중에는 프론트 타입 검사를 계속 돌렸다.

npm run typecheck

백엔드 테스트도 확인했다.

./gradlew test

그리고 로컬 Spring API 응답도 확인했다.

/api/main 200
/api/subject/table 200
/api/subjects/{subjectId} 200
/api/careers/detail 연동 구조 확인

이번에는 화면이 잘 나온다고 끝낼 수 없었다. 화면은 정상처럼 보여도 내부에서는 여전히 mock을 보고 있을 수 있기 때문이다.

그래서 실제로 어떤 API를 호출하는지, 응답이 어디서 오는지, 프론트 타입은 깨지지 않는지, 백엔드 테스트는 통과하는지를 같이 봐야 했다.

예전에는 화면에서 잘 보이면 어느 정도 된 것이라고 생각했다. 그런데 이번 작업에서는 그 생각이 조금 바뀌었다.

API 전환 작업에서는 화면이 아니라 데이터 출처를 확인해야 한다.

 

 

이번 작업에서 배운 것

이번 작업의 핵심은 Spring API를 붙였다는 사실 자체가 아니었다.

더 중요했던 것은 mock 기반으로 만들어진 화면을 실제 API 기준으로 전환할 때, 무엇을 기준으로 삼아야 하는지였다. 개발자로 보면 API 연결 작업이지만, PM 관점에서 보면 전환 범위를 정하고, 기준을 맞추고, 사용자에게 보이는 화면의 일관성을 관리하는 일이었다.

 

 

첫째, 전환은 API 단위가 아니라 화면 단위로 봐야 한다.

처음에는 특정 API가 Spring과 연결되었는지가 중요해 보였다. 하지만 실제로 문제가 된 것은 API 하나의 연결 여부가 아니었다. 같은 화면 안에서 어떤 영역은 Spring 데이터를 보고, 어떤 영역은 mock 데이터를 보고 있다는 점이었다.

툴팁은 Spring을 보고 있었고, 메인 카드는 mock을 보고 있었다. 그래서 이산수학의 requirementType을 Spring DB에서 MAJOR_REQUIRED로 바꿨을 때, 툴팁에는 “전공 필수”가 보였지만 메인 카드에는 M 뱃지가 보이지 않았다.

개발 중에는 이런 상태가 잠깐 생길 수 있다. 하지만 사용자 입장에서는 내부 사정을 알 수 없다. 사용자는 툴팁과 메인 카드를 하나의 화면으로 본다. 한쪽에서는 전공 필수라고 하고, 다른 쪽에서는 전공 필수 표시가 없다면 그것은 단순한 개발 과정이 아니라 화면의 불일치로 느껴진다.

그래서 PM 관점에서는 “어떤 API를 붙였는가”보다 “이 화면은 이제 어떤 데이터 기준으로 동작하는가”를 먼저 봐야 한다. API 전환 범위도 화면 단위로 정의하는 편이 더 안전하다.

 

둘째, 데이터 구조의 차이는 일정과 QA에 영향을 준다.

Spring 응답을 프론트가 바로 쓰게 만들 수도 있다. 하지만 그 방식은 프론트와 백엔드를 강하게 묶는다. 백엔드 응답 구조가 조금만 바뀌어도 화면 컴포넌트가 같이 흔들릴 수 있다.

이번에는 normalizeSpringMainData()를 통해 Spring 응답을 기존 프론트의 MainData 형태로 변환했다. 이 작업은 단순한 코드 정리가 아니었다. 기존 UI를 최대한 유지하면서 데이터 출처만 바꾸기 위한 완충 장치였다.

PM 관점에서 이런 변환 계층은 꽤 중요하다. 백엔드와 프론트가 동시에 완벽히 맞아떨어지는 경우는 많지 않다. 응답 필드명, 데이터 묶음 방식, 화면에서 필요한 값이 조금씩 다를 수 있다. 이 차이를 어디서 흡수할지 정하지 않으면, 작은 응답 변경도 화면 수정과 QA 범위 확대로 이어진다.

따라서 API 통합을 관리할 때는 단순히 “응답이 온다”가 아니라 “이 응답이 기존 화면 구조에 어떤 영향을 주는가”를 함께 봐야 한다.

 

 

셋째, 통합 브랜치는 작업의 기준선이다.

이번에 한 번 꼬였던 부분은 브랜치 기준이었다. spring-api-test가 처음부터 Spring 연동 코드가 반영된 브랜치에서 파생되지 않고, mock 기반의 dev에서 만들어지면서 이미 진행된 작업 일부가 빠졌다.

그 결과 어떤 화면은 Spring 기준으로 전환되어 있고, 어떤 화면은 다시 mock 기준으로 남아 있는 상태가 되었다.

이 문제를 겪고 나서 느낀 것은, 통합 브랜치는 단순히 코드를 모으는 장소가 아니라 작업의 기준선이라는 점이다. 기준 브랜치가 명확해야 기능 브랜치도 의미가 있다. 어느 브랜치에 무엇이 들어가야 하는지 정해져 있어야 PR의 목적도 분명해진다.

PM 입장에서는 기능이 얼마나 개발되었는지만 볼 것이 아니라, 그 기능이 어느 기준 브랜치에 반영되어 있는지도 확인해야 한다. 개발은 되었지만 통합 기준에 올라오지 않은 작업은 아직 제품 흐름 안에 들어온 것이 아니다.

 

 

넷째, 전환 작업은 기능 개발보다 관리 업무에 가깝다.

mock을 걷어내고 Spring을 붙이는 일은 겉으로 보기에는 개발 작업이다. 하지만 실제로는 기준을 맞추는 일이 더 많았다.

API 주소가 맞아야 했다.
Spring 포트가 맞아야 했다.
응답 구조가 맞아야 했다.
seed 데이터가 맞아야 했다.
화면 문구와 fallback 상태도 맞아야 했다.
브랜치 기준도 맞아야 했다.

이 중 하나만 어긋나도 화면은 정상적으로 보이지 않는다. 더 어려운 점은, 어떤 문제는 화면만 봐서는 바로 드러나지 않는다는 것이다. 화면은 정상처럼 보이지만 내부적으로는 아직 mock을 보고 있을 수도 있다.

그래서 전환 작업에서는 체크리스트가 필요하다. 단순히 “API 연결 완료”라고 표시하는 것이 아니라, 해당 화면의 데이터 출처, 호출 API, 응답 형태, 예외 처리, seed 데이터, 테스트 여부까지 함께 확인해야 한다.

이번 작업을 통해 API 통합은 기술 작업이면서 동시에 운영 관리 작업이라는 것을 느꼈다. 특히 mock에서 실제 API로 넘어가는 시점에는 더 그렇다. 이 구간에서는 개발 속도보다 기준의 일관성이 더 중요하다.

 

 

현재 상태

현재 spring-api-test에는 다음 작업이 병합되어 있다.

메인 화면 Spring 연동
학기별 과목 테이블 Spring 연동
커리어 목록 Spring 연동
과목 툴팁 Spring 연동
커리어 상세 페이지 Spring 연동

이제 메인 화면과 커리어 사이드의 주요 흐름은 Spring API 기준으로 맞춰졌다.

남은 방향은 admin API나 추가 세부 화면도 같은 방식으로 Spring에 붙이는 것이다. 앞으로는 spring-api-test를 기준으로 기능별 브랜치를 따서 작업하면 된다.

이번 작업을 하고 나서 Spring API 통합을 조금 다르게 보게 됐다.

Spring을 붙인다는 건 단순히 mock을 지우는 일이 아니다. 화면이 신뢰하는 데이터의 출처를 바꾸는 일이다.

그리고 데이터의 출처를 바꾸려면 코드만 바꿔서는 부족하다. 브랜치도 맞아야 하고, 포트도 맞아야 하고, 응답 형태도 맞아야 하고, seed 데이터도 맞아야 한다.

결국 개발은 기능을 하나씩 붙이는 일이기도 하지만, 기준을 하나씩 정리하는 일이기도 한 것 같다.

 

PR 정리

PR #1: Spring main/careers/subject detail 통합

PR:
spring-careers-api-test → spring-api-test

주요 내용:

  • /api/main Spring 프록시
  • /api/subject/table Spring 프록시
  • /api/careers/list Spring 프록시
  • /api/subjects/{subjectId} 과목 툴팁 Spring 프록시
  • normalizeSpringMainData() 추가
  • mock 기반 메인 초기 데이터 제거

결과:

  • merge 완료
  • spring-api-test에 반영

PR #2: Career side detail 통합

PR:
spring-careerside-api-test → spring-api-test

주요 내용:

  • /api/careers/detail Next 프록시 추가
  • 커리어 상세 페이지를 mock에서 Spring API 기반으로 변경
  • 커리어 대표 직무 예시와 참고 링크를 Spring 데이터로 렌더링
  • backend CareerPath에 representativeExamples, referenceLinks 필드 추가
  • 별도 CareerExample, CareerReferenceLink 엔티티/레포지토리 제거
  • seed 데이터에 커리어별 예시와 참고 링크 추가