React + TypeScript 프로젝트에서 발견한 문제점들을 고친 과정을 정리했습니다.
TypeScript 5.5부터 moduleResolution: "bundler" 모드에서 baseUrl이 deprecated 처리되어 빨간 줄이 생겼습니다.
또한 baseUrl 없이 paths를 사용하려면 값이 ./로 시작하는 명시적 상대경로여야 합니다.
// tsconfig.json
{
"compilerOptions": {
"moduleResolution": "bundler",
/* Path alias */
"baseUrl": ".", // ← deprecated 경고
"paths": {
"@/*": ["src/*"] // ← baseUrl 없으면 에러
}
}
}
// tsconfig.json
{
"compilerOptions": {
"moduleResolution": "bundler",
/* Path alias */
"paths": {
"@/*": ["./src/*"] // baseUrl 제거, 상대경로로 명시
}
}
}
moduleResolution: "bundler" 에서는 baseUrl 없이 paths만으로 경로 별칭이 동작합니다.baseUrl이 없을 때 paths 값은 반드시 ./로 시작하는 상대경로여야 합니다.프로젝트에 baseURL과 withCredentials가 설정된 공통 axios 인스턴스(src/api/axios.ts)가 있었지만,
일부 페이지에서 이를 무시하고 axios를 직접 import해서 사용하고 있었습니다.
그 결과:
withCredentials)이 누락됨VITE_API_BASE_URL)가 무시됨src/api/axios.ts)const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'https://www.api.moonsunpower.com',
withCredentials: true, // 쿠키 기반 인증에 필요
});
export default axiosInstance;
import axios from 'axios'; // ← 외부 라이브러리 직접 import
const handleCafeClick = async (cafe) => {
const response = await axios.post('http://43.200.174.78:8080/api/cafes/save', {
// ↑ baseURL 없음 + IP 하드코딩 + withCredentials 없음
kakaoPlaceId: String(cafe.id),
name: cafe.name,
});
};
import api from '@/api/axios'; // ← 공통 인스턴스 import
const handleCafeClick = async (cafe) => {
const response = await api.post('/api/cafes/save', {
// ↑ baseURL 자동 적용 + 쿠키 자동 적용 + 환경변수 반영
kakaoPlaceId: String(cafe.id),
name: cafe.name,
});
};
import React, { useState } from 'react'; // ← React 미사용 import
import axios from 'axios'; // ← 직접 import
const handleSubmit = async () => {
const response = await axios.post('/api/reviews', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
};
import { useState } from 'react'; // React 제거
import api from '@/api/axios'; // 공통 인스턴스 사용
const handleSubmit = async () => {
const response = await api.post('/api/reviews', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
};
import axios from 'axios' | import api from '@/api/axios' | |
|---|---|---|
| baseURL | 없음 (전체 URL 직접 입력) | 자동 적용 |
| 쿠키(withCredentials) | 없음 | 자동 적용 |
| 환경변수 | 무시됨 | 반영됨 |
OAuth 로그인 후 처리하는 AuthSuccess.tsx에서 오류 발생 시 /login으로 리다이렉트하고 있었습니다.
그런데 실제 로그인 페이지 라우트는 /loginpage로 정의되어 있어 존재하지 않는 페이지로 보내는 버그였습니다.
// 실제 로그인 페이지 경로
<Route path="/loginpage" element={<LoginPage />} />
// AuthSuccess.tsx
if (isNewUser === 'true') {
navigate('/signup', { replace: true });
} else if (isNewUser === 'false') {
navigate('/category', { replace: true });
} else {
navigate('/login', { replace: true }); // ← 존재하지 않는 경로!
}
// AuthSuccess.tsx
if (isNewUser === 'true') {
navigate('/signup', { replace: true });
} else if (isNewUser === 'false') {
navigate('/category', { replace: true });
} else {
navigate('/loginpage', { replace: true }); // ← 실제 경로로 수정
}
CafeCard.tsx와 map.tsx 두 파일에서 각자 Cafe라는 이름의 인터페이스를 정의하고 있었습니다.
이름은 같은데 필드 구조가 달라서 혼란스러웠고, 특히 CafeCard.tsx의 review 필드는
실제로 주소(address)를 담고 있어 필드명 자체가 잘못되어 있었습니다.
// CafeCard.tsx 파일 안에 로컬로 선언
interface Cafe {
id: number;
name: string;
tags: string[];
review: string; // ← 실제로는 주소인데 이름이 review
imageUrl: string;
}
// map.tsx 파일 안에 로컬로 선언
interface Cafe { // ← CafeCard의 Cafe와 이름은 같지만 구조가 다름
id: number;
name: string;
address: string; // ← 같은 주소인데 필드명이 다름
latitude: number;
longitude: number;
thumbnail: string; // ← 같은 이미지인데 필드명이 다름 (imageUrl vs thumbnail)
}
두 타입을 역할에 맞는 이름으로 구분해 공유 파일로 분리했습니다.
// src/types/cafe.ts
// 카카오 Places API 검색 결과 (목록 화면용)
export interface KakaoCafe {
id: string;
name: string;
address: string; // review → address로 필드명 교정
tags: string[];
imageUrl: string;
placeUrl: string;
lat: number;
lng: number;
}
// 우리 백엔드 DB에 저장된 카페 (지도 마커용)
export interface DbCafe {
id: number;
name: string;
address: string;
latitude: number;
longitude: number;
thumbnail: string;
}
// 기존: 파일 안에 직접 정의
// interface Cafe { ... } ← 삭제
// 변경: 공유 타입 import
import type { KakaoCafe } from '@/types/cafe';
interface CafeCardProps {
cafe: KakaoCafe; // Cafe → KakaoCafe
}
// review → address
<span>{cafe.address}</span>
// 기존: 파일 안에 직접 정의
// interface Cafe { ... } ← 삭제
// 변경: 공유 타입 import
import type { DbCafe } from '@/types/cafe';
const [cafes, setCafes] = useState<DbCafe[]>([]); // Cafe[] → DbCafe[]
import type { KakaoCafe } from '@/types/cafe';
// any[] → KakaoCafe[]
const [cafes, setCafes] = useState<KakaoCafe[]>([]);
// any → KakaoCafe
const handleCafeClick = async (cafe: KakaoCafe) => { ... };
// 카카오 API 결과 매핑 시 필드명 교정
const mappedData = data.map((place) => ({
...
address: place.address_name, // review: place.address_name → address로 교정
...
}));
| 전 | 후 | |
|---|---|---|
| 타입 위치 | 각 파일 안에 따로 선언 | src/types/cafe.ts 한 곳에 |
| 타입 이름 | 둘 다 Cafe로 동일 | KakaoCafe / DbCafe로 역할 구분 |
| 잘못된 필드명 | review에 주소 저장 | address로 교정 |
| 유지보수 | 파일마다 수정 필요 | types/cafe.ts 하나만 수정 |