프론트엔드 스터디 대면 9주차: Next.js 렌더링 진화와 웹.
Next.js가 무엇이고 왜 필요한지 React와 비교해 이해한 뒤, CSR부터 SSR, SSG, ISR, PPR까지 렌더링 전략과 Web Vitals를 정리하며 스터디를 마무리합니다.
---
사용자
-> 브라우저에서 버튼 클릭
-> 프론트엔드가 서버에 요청
-> 백엔드가 데이터베이스와 비즈니스 규칙 확인
-> 백엔드가 응답
-> 프론트엔드가 응답을 화면으로 변환const studies = await getStudies();1. 요청 URL 만들기
2. HTTP 메서드 결정하기
3. 헤더와 body 구성하기
4. 브라우저가 요청 보내기
5. 서버가 응답하기
6. 브라우저가 CORS 등 보안 규칙 확인하기
7. JSON 파싱하기
8. 성공/실패에 따라 UI 상태 바꾸기---
GET /api/studies?page=1&size=20 HTTP/1.1
Host: example.com
Accept: application/jsonHTTP/1.1 200 OK
Content-Type: application/json
{
"items": [],
"page": 1,
"totalCount": 0
}| 메서드 | 보통의 의미 | 예시 |
|---|---|---|
| GET | 데이터 조회 | 스터디 목록 조회 |
| POST | 데이터 생성 또는 명령 실행 | 스터디 신청 |
| PUT | 전체 교체 | 프로필 전체 수정 |
| PATCH | 일부 수정 | 닉네임만 수정 |
| DELETE | 삭제 | 댓글 삭제 |
| 상태 코드 | 의미 | 프론트엔드에서 자주 하는 처리 |
|---|---|---|
| 200 | 성공 | 데이터 표시 |
| 201 | 생성 성공 | 생성 완료 후 상세/목록 이동 |
| 204 | 성공했지만 body 없음 | 삭제 완료 처리 |
| 400 | 잘못된 요청 | 입력값 확인 메시지 |
| 401 | 인증 필요 | 로그인 화면 이동 또는 토큰 갱신 |
| 403 | 권한 없음 | 접근 불가 안내 |
| 404 | 대상 없음 | not found 화면 또는 빈 상태 |
| 409 | 충돌 | 이미 신청됨, 이미 사용 중인 이름 등 |
| 500 | 서버 오류 | 잠시 후 다시 시도 안내 |
GET /api/me HTTP/1.1
Authorization: Bearer access-token---
GET /api/studies?page=1&size=20{
"items": [
{
"id": 1,
"title": "프론트엔드 스터디",
"status": "OPEN",
"currentMemberCount": 12,
"maxMemberCount": 20
}
],
"page": 1,
"size": 20,
"totalCount": 42
}{
"title": "프론트엔드 스터디",
"description": "API와 통신을 공부합니다.",
"maxMemberCount": 20
}{
"code": "STUDY_ALREADY_CLOSED",
"message": "이미 마감된 스터디입니다.",
"fieldErrors": []
}{
"code": "VALIDATION_ERROR",
"message": "입력값을 확인해주세요.",
"fieldErrors": [
{
"field": "title",
"message": "제목은 필수입니다."
}
]
}검색 결과 없음 -> 200 OK + items: []
아직 작성한 글 없음 -> 200 OK + items: []
존재하지 않는 글 -> 404 Not Found
권한이 없어 볼 수 없음 -> 403 Forbidden
로그인이 필요함 -> 401 UnauthorizedGET /api/posts?page=3&size=20GET /api/posts?offset=40&limit=20처음 요청 시점
[100, 99, 98, ... 81] -> 1페이지에서 봄
[80, 79, 78, ... 61] -> 다음에 볼 예정
그 사이 새 글 3개 추가
[103, 102, 101, 100, 99, 98, ...]
offset=20으로 다음 요청
원래 기대: 80부터
실제 결과: 83부터 시작할 수 있음GET /api/posts?cursor=81&limit=20{
"items": [],
"nextCursor": "eyJpZCI6MTAwfQ==",
"hasNext": true
}---
https://example.com:443/posts/1?tab=comment
└────┬────┘ └────┬────┘ └┬┘
protocol host port
Origin = https://example.com:443https://example.com/posts
https://example.com/api/studieshttp://localhost:5173
http://localhost:8080
https://example.com
https://api.example.com
http://example.com
https://example.com프론트 코드: fetch("http://localhost:8080/api/studies")
브라우저: 요청은 보냄
서버: 응답은 보냄
브라우저: 응답 헤더 확인
브라우저: 허용되지 않은 Origin이면 JS 코드에 응답을 넘기지 않음Origin: http://localhost:5173
Access-Control-Allow-Origin: http://localhost:5173서버 관점: 요청 받음 -> 응답 보냄 -> 200 OK
브라우저 관점: 응답 받음 -> CORS 헤더 확인 실패 -> JS에 응답 전달 차단
프론트 관점: fetch 실패처럼 보임---
OPTIONS /api/studies HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorizationHTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600Content-Type: application/json으로 POST/PATCH를 보내는 경우
Authorization 헤더를 붙이는 경우
커스텀 헤더를 붙이는 경우
PUT, PATCH, DELETE 같은 메서드를 쓰는 경우
쿠키나 인증 정보를 포함하는 요청을 보내는 경우
await fetch("https://api.example.com/me", {
credentials: "include",
});Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true---
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true// NestJS 예시
app.enableCors({
origin: ["http://localhost:5173", "https://www.example.com"],
credentials: true,
});// vite.config.ts
export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true,
},
},
},
});// 브라우저에서는 같은 출처로 요청하는 것처럼 보인다.
await fetch("/api/studies");개발 환경에서 CORS 설정 때문에 막히지 않고 빠르게 화면 개발을 진행할 수 있습니다.
프론트 코드의 API base URL을 /api처럼 단순하게 유지할 수 있습니다.
로컬 개발 환경과 운영 환경의 URL 차이를 설정으로 흡수할 수 있습니다.
개발 서버 프록시는 보통 로컬 개발용입니다. 운영 CORS 정책을 대신 설계하지 않습니다. 프록시 설정이 운영 배포 구조와 다르면 로컬에서는 되는데 운영에서는 깨질 수 있습니다. 인증 쿠키, 도메인, SameSite, HTTPS 조건이 얽히면 프록시만으로 문제를 해결할 수 없습니다.
브라우저
-> https://www.example.com/api/studies
-> Nginx 또는 API Gateway
-> 내부 백엔드 서버1. 브라우저 Network 탭에서 실패한 요청을 연다.
2. Request Headers의 Origin을 확인한다.
3. Response Headers의 Access-Control-Allow-Origin을 확인한다.
4. OPTIONS 요청이 있는지 확인한다.
5. OPTIONS 응답에 Allow-Methods, Allow-Headers가 있는지 확인한다.
6. 쿠키 요청이면 credentials와 Allow-Credentials를 확인한다.
7. 서버 로그에는 성공인데 브라우저만 막는 상황인지 확인한다.---
AJAX 이전: 요청 -> 새 HTML 전체 다운로드 -> 페이지 전체 교체
AJAX 이후: 요청 -> JSON 데이터 다운로드 -> 필요한 UI만 갱신getUser(userId, function (user) {
getOrders(user.id, function (orders) {
getOrderDetail(orders[0].id, function (orderDetail) {
// 계속 중첩됨
});
});
});async function loadUserOrderInfo(userId: number) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const orderDetail = await getOrderDetail(orders[0].id);
return orderDetail;
} catch (error) {
console.error("요청 실패", error);
}
}const response = await fetch("/api/users");
const data = await response.json();404, 500 같은 HTTP 에러가 자동으로 catch로 가지 않습니다.
JSON 변환을 매번 response.json()으로 직접 해야 합니다.
timeout, 공통 헤더, 인터셉터 같은 기능을 직접 만들어야 합니다.
요청 취소는 AbortController를 알아야 합니다.
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error("HTTP error");
}
const data = await response.json();const response = await axios.get("/api/users");
const data = response.data;try {
const response = await axios.get("/api/users");
console.log(response.data);
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
console.log(error.response.status);
console.log(error.response.data);
}
}const apiClient = axios.create({
baseURL: "/api",
timeout: 10000,
withCredentials: true,
});export async function getStudies(params: GetStudiesParams) {
const response = await apiClient.get<StudyListResponse>("/studies", {
params,
});
return response.data;
}apiClient.interceptors.request.use((config) => {
// 학습을 위한 단순 예시입니다.
// localStorage에 토큰을 저장하면 XSS 공격에 노출될 수 있습니다.
const token = localStorage.getItem("accessToken");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});---
로그인 성공
-> Access Token 발급: 짧은 수명, API 요청에 사용
-> Refresh Token 발급: 긴 수명, Access Token 재발급에 사용Authorization: Bearer {accessToken}요청 A, B, C가 동시에 401을 받음
-> refresh 요청을 세 번 보내면 위험
-> 하나의 refresh만 진행하고 나머지는 그 결과를 기다리게 설계await fetch("https://api.example.com/me", {
credentials: "include",
});Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true
Set-Cookie: refreshToken=...; HttpOnly; Secure; SameSite=None---
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
axios
.get("/api/users")
.then((response) => setUsers(response.data))
.catch((error) => setError(error))
.finally(() => setLoading(false));
}, []);const { data, isLoading, error } = useQuery({
queryKey: ["users"],
queryFn: () => apiClient.get("/users").then((res) => res.data),
staleTime: 5 * 60 * 1000,
});| 구분 | Query | Mutation |
|---|---|---|
| 용도 | 데이터 읽기 | 데이터 생성/수정/삭제 |
| 주로 쓰는 HTTP | GET | POST, PUT, PATCH, DELETE |
| 실행 시점 | 컴포넌트 마운트 시 자동 | mutate 호출 시 수동 |
| 캐싱 | 자동 캐싱 | 직접 무효화/갱신 필요 |
const queryClient = useQueryClient();
const createStudy = useMutation({
mutationFn: (newStudy) => studyApi.createStudy(newStudy),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["studies"] });
},
});["studies", "list", { status: "OPEN", page: 1 }][("studies", "detail", 123)];export const studyQueries = {
all: ["studies"] as const,
lists: () => [...studyQueries.all, "list"] as const,
list: (params: GetStudiesParams) =>
[...studyQueries.lists(), params] as const,
detail: (studyId: number) =>
[...studyQueries.all, "detail", studyId] as const,
};---
src/
api/
client.ts
studies.ts
pages/
StudyListPage.tsx
components/
StudyCard.tsx// api/studies.ts
export async function getStudies(params: GetStudiesParams) {
const response = await apiClient.get<StudyListResponse>("/studies", {
params,
});
return response.data;
}src/
shared/
api/
httpClient.ts
apiError.ts
features/
studies/
api/
studyApi.ts
studyQueries.ts
model/
studyTypes.ts
studyPolicy.ts
ui/
StudyCard.tsx
StudyList.tsx---
스터디 목록 화면에서 모집 중/마감 상태에 따라 버튼이 달라집니다.
응답에 status 필드를 OPEN/CLOSED 형태로 받을 수 있을까요?
빈 목록일 때는 items: []와 totalCount: 0으로 내려오는지 확인 부탁드립니다.이 API는 로그인하지 않은 사용자도 호출할 수 있나요?
실패할 때 code 값은 어떤 목록 중 하나인가요?
빈 목록은 200과 빈 배열인가요, 아니면 404인가요?
페이지 번호는 0부터 시작하나요, 1부터 시작하나요?
정렬 기본값은 무엇인가요?
검색어가 비어 있으면 전체 목록인가요, 빈 목록인가요?
삭제 성공 시 200인가요, 204인가요?
401과 403을 어떻게 구분하나요?
refresh token 만료 시 어떤 응답이 오나요?
CORS 허용 Origin에 로컬 개발 주소와 배포 주소가 모두 들어가 있나요?
쿠키 인증이라면 credentials, SameSite, Secure, CSRF 정책은 어떻게 맞추나요?
---
---
Post Q&A
프론트엔드 스터디 대면 8주차: API와 통신 — 프론트엔드와 백엔드의 계약 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!
비동기와 async/await를 배운 뒤, 대면에서는 API를 단순 호출 방법이 아니라 프론트엔드와 백엔드 사이의 계약으로 바라봅니다. HTTP, REST, CORS, 프록시, API 클라이언트 구조, TanStack Query와 파일 배치까지 연결합니다.