03a 동기·비동기·Ajax 📚 전체 맵 04 프로젝트 코드
Series 03 · 웹/통신 기초 · 2/2

CORS
AXIOS
PATHVAR

CORS 오류 원인과 해결 · ResponseEntity 응답 포맷
axios interceptor 토큰 자동 첨부 · @PathVariable·@RequestParam

03a 동기·비동기·Ajax·Promise 03b CORS·axios·@PathVariable
01 Java 기초 02 Spring 기초 03 웹/통신 04 프로젝트 코드 05 심화 06 Security 07 JVM 08 DI
이 파일의 학습 목적
03b — CORS · ResponseEntity · axios interceptor · @PathVariable

React에서 Spring Boot API를 처음 호출하면 CORS policy 에러가 뜬다. JWT 토큰을 매번 헤더에 수동으로 붙이면 코드가 엄청 지저분해진다. 03b는 이 실전 문제들을 해결하는 4가지 — CORS 설정·ResponseEntity·axios interceptor·@PathVariable을 정리한다. 웹/통신 시리즈의 마지막이자 04 프로젝트 코드로 넘어가기 직전 파일이다.

이 파일에서 배울 4가지
  • CORS — 왜 에러 나는지, @CrossOrigin·CorsConfig 차이
  • ResponseEntity — 상태코드·헤더·바디를 직접 제어
  • axios interceptor — 모든 요청에 JWT 자동 첨부
  • @PathVariable · @RequestParam — URL 변수 처리
프로젝트 코드 연결
  • · 2차 CorsConfig.java → 3차 SecurityConfig cors()
  • · 2·3차 axios interceptor — localStorage JWT 자동 첨부
  • · @PathVariable Long id — 게시글 수정·삭제·상세
시리즈 순서: 03a 동기·비동기·Ajax → 03b ← 지금 여기 → 04a 1차 프로젝트 코드 해설 → 04b 2차 → 04c 3차
01 — CORS란? 왜 필요한가?
React(3000)와 Spring Boot(8080) — 포트가 달라서 생기는 브라우저 보안 정책
🚧 Cross-Origin Resource Sharing Browser Security
🏢 비유 — 다른 건물 출입 통제
React 앱 = A 건물 (포트 3000)
Spring Boot = B 건물 (포트 8080)
브라우저 = 경비원 → "A 건물 직원이 B 건물에 무단 출입하면 막겠다!"
Spring Boot에서 "React는 허용됨"이라고 명시해줘야 통과
React
Frontend
localhost:3000
→ ✗
CORS 차단
브라우저
설정 없으면 막음
→ ✓
허용 설정
Spring Boot
localhost:8080
❌ CORS 설정 없음 → 오류
// 브라우저 콘솔 에러:
Access to XMLHttpRequest at
'http://localhost:8080/api/...'
from origin
'http://localhost:3000'
has been blocked by CORS policy

// React axios 요청이 Spring Boot에
// 도달하기도 전에 브라우저가 차단
✅ CORS 설정 후 → 통과
// Spring Boot에서 React 허용 설정
// → 브라우저가 통과시켜 줌
// → axios 요청 정상 처리

// 응답 헤더에 자동 추가됨:
Access-Control-Allow-Origin:
http://localhost:3000
Java — 2차 프로젝트 CORS 설정 — WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 모든 URL에 적용
.allowedOrigins("http://localhost:3000") // React 허용
.allowedMethods("GET","POST","PUT","DELETE")
.allowedHeaders("*") // 모든 헤더 허용
.allowCredentials(true); // 쿠키·인증 헤더 포함 허용
}
}
Java — 3차 프로젝트 CORS 설정 — SecurityConfig 내부
// Spring Security 사용 시 WebConfig의 CORS 설정이 무시될 수 있음
// → SecurityConfig에서 별도로 설정해야 함
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET","POST","PUT","DELETE"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
⚠️ Docker(4차) 환경에서 CORS 변경
Docker에서는 nginx가 React와 Spring Boot를 같은 포트(80)로 묶어줌
→ 포트가 달라서 생기던 CORS 문제 자체가 사라짐
baseURL: '' (빈 문자열) — nginx가 /api/ 요청을 Spring Boot로 프록시
02 — ResponseEntity
HTTP 응답을 직접 제어 — 상태코드 + 헤더 + 바디 한 번에
📬 ResponseEntity — 응답 봉투 HTTP Response
📦 비유 — 택배 상자
단순히 물건(데이터)만 보내는 게 아니라
상태코드(배송 성공/실패) + 헤더(운송장) + 바디(물건) 를 한 번에 제어
→ 성공이면 200 + 데이터 / 실패면 400 + 에러 메시지
200 OK
요청 성공 · 데이터 포함
201 Created
생성 성공 (POST)
400 Bad Request
요청 데이터 오류
401 Unauthorized
인증 필요 · 토큰 없음
403 Forbidden
권한 없음 (로그인은 됨)
500 Server Error
서버 내부 오류
Java — ResponseEntity 실제 사용 패턴
// 성공 — 데이터 포함
return ResponseEntity.ok(boardList);
// → HTTP 200 + JSON 바디

// 생성 성공 — 201
return ResponseEntity.status(HttpStatus.CREATED).body("등록됐어요");
// → HTTP 201 + 문자열 바디

// 실패 — 에러 메시지
return ResponseEntity.badRequest().body("이미 존재하는 아이디입니다.");
// → HTTP 400 + 에러 메시지

// 실전 패턴 — try/catch와 함께
@PostMapping("/api/user/register")
public ResponseEntity<String> register(@RequestBody UserDto dto) {
try {
userService.register(dto);
return ResponseEntity.ok("회원가입 성공");
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
💡 React에서 응답 상태코드 활용
axios는 2xx 상태코드면 .then(), 그 외엔 .catch()로 넘어감
→ Spring에서 상태코드를 명확히 내려줘야 React에서 성공/실패 분기가 쉬워짐
03 — axios interceptor
모든 요청·응답을 가로채서 공통 처리 — JWT 토큰 자동 첨부의 핵심
🔀 요청/응답 중간 가로채기 Middleware
🛂 비유 — 공항 출입국 심사
요청 interceptor = 출국 심사 — 모든 여권(토큰)을 자동 확인·첨부하고 보냄
응답 interceptor = 입국 심사 — 모든 응답을 체크, 401이면 로그인 페이지로 강제 이동
→ 각 컴포넌트마다 토큰 붙이는 코드 작성 안 해도 됨
요청
axios.get('/api/board/list') 호출
→ 요청 interceptor 실행 → localStorage에서 token 꺼냄
Authorization: Bearer eyJhb... 헤더에 자동 첨부
→ Spring Boot로 전송
응답
Spring Boot 응답 수신
→ 응답 interceptor 실행 → 상태코드 확인
→ 200이면 그대로 반환 / 401이면 token 삭제 + 로그인 페이지 이동
JavaScript — 우리 프로젝트 api.js — interceptor 전체 코드
import axios from 'axios';

// axios 인스턴스 생성 (baseURL 공통 설정)
const api = axios.create({
baseURL: 'http://localhost:8080', // Spring Boot 주소
});

// ── 요청 interceptor ──────────────────────
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token'); // 토큰 꺼내기
if (token) {
config.headers.Authorization = `Bearer ${token}`;
// 모든 요청 헤더에 토큰 자동 첨부!
}
return config;
});

// ── 응답 interceptor ──────────────────────
api.interceptors.response.use(
(response) => response, // 성공(2xx)이면 그대로 반환
(error) => {
if (error.response?.status === 401) {
// 401 Unauthorized → 토큰 만료 또는 없음
localStorage.removeItem('token');
window.location.href = '/login'; // 로그인 페이지로
}
return Promise.reject(error);
}
);

export default api;

// 사용 시: 토큰 신경 쓸 필요 없음
api.get('/api/board/list'); // 헤더에 토큰 자동 첨부
api.post('/api/board/write', data); // 여기도 자동
04 — @PathVariable · @RequestParam
URL에서 값을 꺼내는 두 가지 방법 — 경로 변수 vs 쿼리 파라미터
🔍 URL에서 값 꺼내기 URL Params
@PathVariable — 경로에 값 포함
// URL: /api/board/detail/5
// ↑ 값이 경로 안에

@GetMapping("/api/board/detail/{id}")
public ResponseEntity<?> detail(
@PathVariable Long id) {
// id = 5
}

// React에서:
api.get(`/api/board/detail/${boardId}`)
@RequestParam — 쿼리스트링으로 전달
// URL: /api/board/list?page=2
// ↑ ?뒤에 붙는 값

@GetMapping("/api/board/list")
public ResponseEntity<?> list(
@RequestParam(defaultValue="0") int page) {
// page = 2 (없으면 기본값 0)
}

// React에서:
api.get('/api/board/list', {params:{page:2}})
구분@PathVariable@RequestParam
URL 형태/board/detail/5/board/list?page=2
사용 상황특정 리소스 식별 (id)옵션·필터·페이징
기본값 설정불가 (경로에 반드시 있어야 함)defaultValue 설정 가능
프로젝트 예시/api/board/detail/{id}/api/board/list?page=0
Java + JavaScript — 우리 프로젝트 실제 사용 패턴
// Spring Boot — BoardController
@GetMapping("/api/board/detail/{id}") // PathVariable
public ResponseEntity<?> detail(@PathVariable Long id) {
return ResponseEntity.ok(boardService.getDetail(id));
}

@GetMapping("/api/board/list") // RequestParam
public ResponseEntity<?> list(
@RequestParam(defaultValue = "0") int page) {
return ResponseEntity.ok(boardService.getList(page));
}

// React — 호출 방법
api.get(`/api/board/detail/${id}`); // PathVariable
api.get('/api/board/list', {params:{page:currentPage}}); // RequestParam
api.get(`/api/board/list?page=${currentPage}`); // 직접 쿼리스트링도 됨
웹/통신 기초 시리즈 — 전체 정리
03a + 03b — React ↔ Spring Boot 통신의 전체 그림
흐름 — React에서 게시글 목록 요청 → Spring Boot → 응답 전 과정
// 1. React 컴포넌트 (async/await — 03a)
const res = await api.get('/api/board/list', {params:{page:0}});

// 2. axios interceptor 실행 (03b)
// → localStorage에서 token 꺼내서 Authorization 헤더에 자동 첨부

// 3. 브라우저 CORS 검사 (03b)
// → Spring Boot가 Access-Control-Allow-Origin 헤더 응답에 포함
// → 브라우저 통과

// 4. Spring Boot Controller 수신 (02a)
// @GetMapping("/api/board/list")
// @RequestParam int page → 0

// 5. Service → Repository → DB (02b)
// boardRepository.findAll(pageable)

// 6. ResponseEntity.ok(boardList) 반환 (03b)
// → HTTP 200 + JSON 바디

// 7. React useState 업데이트 (03a)
setBoards(res.data); // 화면 자동 렌더링
03a 동기·비동기·Ajax 📚 전체 맵 04 프로젝트 코드