Vue와 Spring하다가 생긴 오류

키요·2025년 9월 15일

공부

목록 보기
23/32

프롤로그: "이거 금방 만들겠는데?"

🚀 오늘의 목표: Vue.js와 Spring Boot, WebSocket을 사용해서 친구와 내 위치를 Google 지도 위에서 실시간으로 공유하는 멋진 기능을 만들어보자!

친구와 내 위치가 지도 위에 마커로 찍히고, 실시간으로 스르륵 움직이는 상상. 정말 멋지지 않나요? Geolocation API로 위치를 얻고, WebSocket으로 쏘고, DB에 저장하고, 다시 뿌려주면 되니 금방 할 줄 알았습니다.

하지만 현실은... 예상치 못한 에러들의 연속이었습니다. 오늘 하루 종일 저를 괴롭혔던 문제들과 해결 과정을 미래의 저와 동료 개발자들을 위해 기록으로 남겨봅니다.

🐞 Level 1: 기본 중의 기본, 400 Bad Request

가장 먼저 만난 건 API 호출 실패를 알리는 400 Bad Request 에러였습니다. 프론트엔드에서 사용자가 선택한 시작 시간과 종료 시간을 백엔드로 보내는 간단한 요청이었죠.

😱 문제 현상

분명히 startTimeendTime 값을 담아 보냈는데, 서버는 요청이 잘못되었다며 계속 400 에러를 반환했습니다.

✅ 원인과 해결

원인은 너무나도 간단했습니다. URL 쿼리 파라미터에 대한 기초적인 실수였죠.

// 문제의 코드 😨
const apiUrl = `/api/points?startTime=${start}?endTime=${end}`;

// 올바른 코드 ✅
const apiUrl = `/api/points?startTime=${start}&endTime=${end}`;

URL 파라미터는 첫 번째는 ?로 시작하고, 두 번째부터는 &로 연결해야 한다는 사실을 잠시 잊고 있었습니다. 커피 한 잔 마시고 정신을 차렸습니다.

🐞 Level 2: 보이지 않는 유령의 요청, undefined

첫 번째 문제를 해결하자마자 페이지를 열자마자 또다시 400 에러가 발생했습니다. 이번엔 콘솔에 찍힌 URL이 .../points?startTime=undefined&endTime=undefined 였습니다.

😱 문제 현상

사용자가 아무것도 클릭하지 않았는데, 페이지 로드와 동시에 undefined 값이 담긴 요청이 자동으로 보내지고 있었습니다.

✅ 원인과 해결

Vue 컴포넌트의 onMounted 훅 안에서 페이지가 준비되자마자 데이터를 조회하는 함수를 호출했던 것이 원인이었습니다. startTimeendTime 변수는 아직 사용자의 선택을 받기 전이라 당연히 undefined였죠.

해결책은 간단했습니다. onMounted에서 자동으로 API를 호출하는 로직을 제거하고, 사용자가 명시적으로 '조회' 버튼을 눌렀을 때만 함수가 실행되도록 변경했습니다.

🐞 Level 3: 침묵의 실패, 200 OK는 왔지만 데이터가 없다

이제 API 통신은 성공적으로 200 OK 응답을 받기 시작했습니다. 하지만 기쁨도 잠시, 응답 본문은 항상 텅 빈 배열([])이었습니다. 분명 DB에는 해당 시간의 데이터가 있었는데 말이죠.

😱 문제 현상

프론트엔드와 백엔드 API 서버 간의 통신은 정상. 하지만 백엔드가 DB에서 데이터를 가져오지 못하는 상황. 이제 문제는 백엔드 깊숙한 곳에 있었습니다.

✅ 원인과 해결

가장 유력한 용의자는 타임존(Timezone) 불일치였습니다.

  • Java 애플리케이션(Spring Boot): Asia/Seoul (KST, UTC+9) 타임존에서 실행 중
  • 데이터베이스 서버: 기본값인 UTC 타임존으로 설정되어 있었음

Java에서 09:00시로 조회하면, DB는 이것을 09:00 UTC로 인식합니다. 한국 시간으로는 18:00 KST이니, 당연히 운영 시간 내의 데이터가 조회되지 않았던 것입니다.

이 문제는 application.properties의 JDBC 연결 URL에 타임존 설정을 추가하여 해결했습니다.

# application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Seoul

🐞 Level 4: TypeScript의 반란, googlePromise를 찾아서

이제 실시간 위치 공유 로직을 Vue 컴포넌트에 통합할 차례. 여기서부터 TypeScript와의 본격적인 싸움이 시작되었습니다.

😱 문제 현상 1: Cannot find namespace 'google'

let map: google.maps.Map; 같은 코드에서 TypeScript가 google이라는 타입을 전혀 알지 못했습니다.

✅ 해결

google 객체는 외부 스크립트로 동적으로 로드되기 때문에, TypeScript가 정적으로 코드를 분석하는 시점에는 존재하지 않습니다. TypeScript에게 google 객체의 타입을 알려주는 타입 정의 파일을 설치해주었습니다.

npm install --save-dev @types/google.maps

😱 문제 현상 2: Promise only refers to a type...

Promise, async/await 같은 최신 문법에서 에러가 발생했습니다. TypeScript가 옛날 버전의 JavaScript 기준으로 코드를 검사하고 있었던 거죠.

✅ 해결

tsconfig.json 파일에서 TypeScript가 최신 JavaScript 문법을 이해하도록 targetlib 옵션을 수정했습니다.

// tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "lib": ["ESNext", "DOM", "DOM.Iterable"]
  }
}

😱 문제 현상 3: tsconfig.json을 수정해도 에러가 사라지지 않는다!

설정을 바꿨는데도 에러가 계속 발생했습니다. 알고 보니 tsconfig.json 파일의 구조 자체를 잘못 작성하고 있었습니다.

// 잘못된 구조 😨
{
  "compilerOptions": { ... },
  "target": "ESNext", // 이런! compilerOptions 밖에 있잖아!
  "lib": ["ESNext", "DOM"]
}

targetlib 옵션을 compilerOptions 객체 안으로 옮겨주니 거짓말처럼 문제가 해결되었습니다.

에필로그: 오늘의 핵심 교훈

오늘의 길고 긴 삽질 여정을 통해 몇 가지 중요한 교훈을 얻었습니다.

📚 오늘의 교훈

  1. 기본을 놓치지 말자. ?& 같은 URL의 기본 구조부터 다시 생각하게 되었다.
  2. 코드의 실행 시점을 항상 의식하자. onMounted에서 undefined 변수를 사용한 실수는 타이밍의 중요성을 일깨워줬다.
  3. 에러 코드는 위대한 힌트다. 400 에러는 프론트엔드의 요청 자체를, 200인데 데이터가 없는 경우는 백엔드 로직을 의심하게 했다.
  4. TypeScript와 환경 설정은 한 몸이다. tsconfig.json 파일의 구조와 옵션 하나하나가 코드 전체에 영향을 미친다는 것을 깨달았다.
  5. "껐다 켜라." 설정 파일을 변경한 후에는 반드시 개발 서버를 재시작하자. 이것은 진리다.

오늘의 삽질이 미래의 나에게, 그리고 이 글을 읽는 누군가에게 작은 도움이 되길 바랍니다.


profile
운도 실력

0개의 댓글