From c72e1e5153643b9797e66ec4efeaa04f7cb72c46 Mon Sep 17 00:00:00 2001 From: seojing Date: Thu, 14 May 2026 18:29:43 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=93=9D=20(study)=208=EC=A3=BC?= =?UTF-8?q?=EC=B0=A8=20API=20=EB=8C=80=EB=A9=B4=20=EC=9E=90=EB=A3=8C=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/clab-26-1/in-person/week11.mdx | 4 +- .../study/clab-26-1/in-person/week6-1.mdx | 2 +- .../study/clab-26-1/in-person/week8.mdx | 598 +++++++++++++++++- .../study/clab-26-1/in-person/week9.mdx | 401 +++++++++--- apps/web/content/study/clab-26-1/week9.mdx | 10 +- 5 files changed, 897 insertions(+), 118 deletions(-) diff --git a/apps/web/content/study/clab-26-1/in-person/week11.mdx b/apps/web/content/study/clab-26-1/in-person/week11.mdx index 026fade..dd6a63a 100644 --- a/apps/web/content/study/clab-26-1/in-person/week11.mdx +++ b/apps/web/content/study/clab-26-1/in-person/week11.mdx @@ -136,8 +136,8 @@ branch 작업 → PR 생성 → preview 배포 → 리뷰 → main 머지 → pr 이번 학기에는 HTML/CSS/JS/React의 기본 사용법을 비대면에서 배우고, 대면에서는 - 브라우저 렌더링, AI 시대 개발 방식, 프로젝트 구조, API 계약, 웹 보안, 팀 협업, - 배포와 운영까지 큰 그림을 봤습니다. + 브라우저 렌더링, AI 시대 개발 방식, 프로젝트 구조, API 계약, Next.js와 웹 + 렌더링, 팀 협업, 배포와 운영까지 큰 그림을 봤습니다. diff --git a/apps/web/content/study/clab-26-1/in-person/week6-1.mdx b/apps/web/content/study/clab-26-1/in-person/week6-1.mdx index 898870e..f099739 100644 --- a/apps/web/content/study/clab-26-1/in-person/week6-1.mdx +++ b/apps/web/content/study/clab-26-1/in-person/week6-1.mdx @@ -52,7 +52,7 @@ description: 4~6주차 공백 이후 진행하는 첫 번째 대면입니다. HT - 7주차: 프론트엔드 프로젝트가 복잡해지는 이유 — 구조와 책임 분리 - 8주차: API와 통신 — 프론트엔드와 백엔드의 계약 -- 9주차: 웹 보안 — 브라우저에 있는 코드는 믿으면 안 된다 +- 9주차: Next.js 렌더링 진화와 웹 퍼포먼스 - 10주차: 팀 협업 — Git, PR, 컨벤션, 리뷰 문화 - 11주차: 배포와 운영 — localhost 밖의 세계 diff --git a/apps/web/content/study/clab-26-1/in-person/week8.mdx b/apps/web/content/study/clab-26-1/in-person/week8.mdx index b023c63..72572f5 100644 --- a/apps/web/content/study/clab-26-1/in-person/week8.mdx +++ b/apps/web/content/study/clab-26-1/in-person/week8.mdx @@ -8,9 +8,10 @@ tags: - HTTP - REST - CORS + - 프록시 - 백엔드 협업 - 데이터 계약 -description: 비동기와 async/await를 배운 뒤, 대면에서는 API를 단순 호출 방법이 아니라 프론트엔드와 백엔드 사이의 계약으로 바라봅니다. HTTP, REST, CORS, 에러 응답, API 문서와 협업 방식을 다룹니다. +description: 비동기와 async/await를 배운 뒤, 대면에서는 API를 단순 호출 방법이 아니라 프론트엔드와 백엔드 사이의 계약으로 바라봅니다. HTTP, REST, CORS, 프록시, API 클라이언트 구조, TanStack Query와 파일 배치까지 연결합니다. --- 1. 이번 주 사전학습과 오늘 대면의 연결 @@ -18,8 +19,14 @@ description: 비동기와 async/await를 배운 뒤, 대면에서는 API를 단 이번 주 비대면에서는 클로저, Promise, async/await를 배웠습니다. 이 문법들이 실제 웹에서 가장 많이 쓰이는 곳은 서버와 통신할 때입니다. - 오늘은 `fetch` 문법을 다시 외우는 시간이 아니라, API를 팀 사이의 계약으로 보는 - 감각을 잡아보겠습니다. + 버튼을 눌렀을 때 서버에 요청을 보내고, 응답을 기다리고, 성공과 실패에 따라 + 화면을 바꾸는 모든 과정이 비동기 위에 있습니다. + + + + 오늘은 `fetch` 문법을 다시 외우는 시간이 아닙니다. API를{" "} + 프론트엔드와 백엔드 사이의 계약으로 바라보고, 그 계약을 + 프로젝트 안에서 어디에 배치해야 유지보수하기 쉬운지까지 연결해보겠습니다. --- @@ -43,6 +50,12 @@ GET /api/studies?page=1&size=20 올지, 빈 목록이면 어떤 응답이 올지 정해야 합니다. + + 그래서 API를 잘 다룬다는 것은 단순히 요청을 보낼 수 있다는 뜻이 아닙니다. + 화면 요구사항을 요청과 응답의 언어로 바꾸고, 백엔드와 합의한 내용을 코드에 + 안정적으로 반영할 수 있다는 뜻입니다. + + --- 3. 좋은 API 협업에서 꼭 맞춰야 하는 것들 @@ -121,41 +134,552 @@ GET /api/studies?page=1&size=20 --- -4. CORS — 브라우저가 끼어드는 보안 규칙 +4. CORS — 서버 에러처럼 보이는 브라우저 규칙 + + + CORS는 처음 보면 백엔드 에러처럼 보이지만, 사실 브라우저의 보안 정책과 + 관련이 있습니다. 브라우저는 기본적으로 같은 출처의 리소스만 자유롭게 읽을 수 + 있게 하는 SOP를 가지고 있고, CORS는 다른 출처의 리소스를 안전하게 공유하기 + 위한 예외 규칙입니다. + + + + 여기서 출처(origin)는 보통 프로토콜, 호스트, 포트의 조합으로 + 봅니다. + + +```text +http://localhost:5173 +http://localhost:8080 +``` - CORS는 처음 보면 백엔드 에러처럼 보이지만, 사실 브라우저의 보안 정책입니다. - 다른 출처의 API를 마음대로 호출하지 못하게 막는 규칙입니다. + 두 주소는 둘 다 localhost지만 포트가 다릅니다. 브라우저 입장에서는 다른 + 출처입니다. 그래서 프론트 개발 서버에서 백엔드 개발 서버로 요청을 보낼 때 + CORS 에러를 자주 만나게 됩니다. - 프론트엔드 개발자가 CORS를 알아야 하는 이유는, 이 문제가 생겼을 때 "프론트에서 - 코드 조금 바꾸면 되겠지"가 아니라 서버의 응답 헤더와 배포 환경을 함께 봐야 - 하기 때문입니다. + CORS의 핵심은 요청을 막는 주체가 서버가 아니라 브라우저라는 점입니다. 서버는 + 응답을 보냈더라도, 브라우저가 응답 헤더를 보고 "이 출처에 공개해도 된다"는 + 허가를 확인하지 못하면 JavaScript 코드에 응답을 넘기지 않습니다. + + + + 더 자세한 흐름은{" "} + + CORS란 무엇인가? (그만 괴롭혀..) + + 글의 Origin, SOP, Preflight 설명을 함께 보면 좋습니다. --- -5. API 문서는 협업 도구다 +5. CORS는 실제로 어떻게 확인되는가 + +5-1. 기본 요청 흐름 + + + 브라우저는 다른 출처로 요청을 보낼 때 요청 헤더에 Origin을 담습니다. 서버는 + 응답 헤더에 어떤 출처를 허용할지 적어 보냅니다. 브라우저는 이 둘을 비교합니다. + + +```http +Origin: http://localhost:5173 +Access-Control-Allow-Origin: http://localhost:5173 +``` + + + 값이 맞으면 브라우저는 응답을 JavaScript에 넘깁니다. 맞지 않으면 네트워크 + 요청 자체가 성공했더라도 프론트엔드 코드에서는 응답을 사용할 수 없습니다. + + +5-2. Preflight 요청 + + + 실제 요청 전에 브라우저가 OPTIONS 요청을 먼저 보내는 경우가 있습니다. 이것을 + Preflight라고 부릅니다. "이 메서드와 이 헤더로 요청해도 되는지"를 미리 물어보는 + 절차입니다. + + +```http +OPTIONS /api/studies +Origin: http://localhost:5173 +Access-Control-Request-Method: POST +Access-Control-Request-Headers: content-type, authorization +``` + + + 서버가 허용하는 origin, method, headers를 응답하면 브라우저가 그제야 실제 + 요청을 보냅니다. 우리가 흔히 쓰는 `Content-Type: application/json`이나 + `Authorization` 헤더는 Preflight를 유발하는 경우가 많습니다. + + +5-3. 인증 정보가 포함된 요청 + + + 쿠키나 인증 헤더처럼 자격 증명이 포함되는 요청은 더 엄격합니다. 프론트에서는 + `credentials: "include"` 또는 axios의 `withCredentials: true` 같은 설정이 + 필요하고, 서버도 `Access-Control-Allow-Credentials: true`를 내려줘야 합니다. + 이때 `Access-Control-Allow-Origin: *`처럼 모든 출처를 허용하는 방식은 사용할 + 수 없습니다. + + +--- + +6. CORS 해결 방법 — 서버 설정과 프록시 + +6-1. 정석은 서버가 허용할 출처를 명시하는 것 + + + 가장 정석적인 해결은 백엔드가 실제로 허용할 프론트엔드 출처를 응답 헤더에 + 명시하는 것입니다. + + +```http +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 +``` + + + 운영 환경에서는 무작정 모든 origin을 열기보다, 실제 서비스 도메인과 개발 + 도메인을 구분해서 관리해야 합니다. CORS는 귀찮은 에러가 아니라 "누가 이 응답을 + 읽을 수 있는가"를 정하는 보안 경계입니다. + + +6-2. 프론트 개발 서버 프록시 + + + 개발 환경에서는 프론트 개발 서버가 백엔드로 요청을 대신 전달하는 프록시를 + 자주 씁니다. 브라우저는 같은 출처인 프론트 개발 서버에 요청한다고 느끼고, + 프론트 개발 서버가 뒤에서 백엔드 서버로 요청을 넘깁니다. + + +```ts +// vite.config.ts +export default defineConfig({ + server: { + proxy: { + "/api": { + target: "http://localhost:8080", + changeOrigin: true, + }, + }, + }, +}); +``` + +```ts +// 브라우저에서는 같은 출처로 요청하는 것처럼 보인다. +await fetch("/api/studies"); +``` - Swagger, Postman, Notion 문서 같은 API 문서는 단순 참고 자료가 아닙니다. - 프론트와 백엔드가 같은 그림을 보고 있는지 확인하는 협업 도구입니다. + 원리는 단순합니다. CORS는 브라우저가 다른 출처의 응답을 JavaScript에 넘길지 + 판단하는 규칙입니다. 그런데 브라우저가 보는 요청 주소가 `/api/studies`처럼 + 현재 프론트 서버와 같은 출처라면, 브라우저 입장에서는 교차 출처 요청이 + 아닙니다. 실제 백엔드 호출은 브라우저 밖에 있는 개발 서버가 대신 수행합니다. -좋은 API 문서에는 적어도 다음이 있어야 합니다. +프록시 방식의 장점은 다음과 같습니다. + +- 개발 환경에서 CORS 설정 때문에 막히지 않고 빠르게 화면 개발을 진행할 수 있습니다. +- 프론트 코드의 API base URL을 `/api`처럼 단순하게 유지할 수 있습니다. +- 로컬 개발 환경과 운영 환경의 URL 차이를 설정으로 흡수할 수 있습니다. + +단점도 분명합니다. -- 요청 URL과 메서드 -- 요청 파라미터와 body -- 성공 응답 예시 -- 에러 응답 예시 -- 인증 필요 여부 -- 빈 데이터 예시 -- 필드 설명과 가능한 enum 값 +- 개발 서버 프록시는 보통 로컬 개발용입니다. 운영 CORS 정책을 대신 설계해주지는 않습니다. +- 프록시 설정이 운영 배포 구조와 다르면 로컬에서는 되는데 운영에서는 깨질 수 있습니다. +- 인증 쿠키, 도메인, SameSite, HTTPS 조건이 얽히면 프록시만으로 문제를 해결할 수 없습니다. + + + 따라서 프록시는 "CORS를 없애는 마법"이 아니라{" "} + 브라우저가 보는 요청 경로를 같은 출처처럼 만들어주는 개발/배포 구조 + 입니다. 운영에서는 Nginx, Next.js Route Handler, BFF, API Gateway 같은 계층이 + 비슷한 역할을 맡을 수 있습니다. + --- -6. 프론트엔드가 백엔드에게 잘 질문하는 법 +7. HTTP에서 현대 API 연동까지 + + + API 요청 도구를 고를 때 바로 axios, fetch, TanStack Query부터 비교하면 흐름이 + 잘 보이지 않습니다. 먼저 웹 통신이 어떤 문제를 해결하며 발전해왔는지 봐야 + 합니다. + + +7-1. HTTP — 요청과 응답의 기본 약속 + + + HTTP는 클라이언트가 요청하면 서버가 응답하는 규약입니다. 중요한 특징은 + 무상태성입니다. 서버는 기본적으로 이전 요청을 기억하지 + 않습니다. 그래서 로그인 이후의 요청에는 "내가 누구인지"를 알려주는 토큰이나 + 쿠키 같은 인증 정보가 필요합니다. + + +```http +GET /api/users/123 HTTP/1.1 +Host: example.com +Authorization: Bearer access-token +``` + + + API 협업에서 메서드도 약속입니다. 보통 GET은 조회, POST는 생성, PUT/PATCH는 + 수정, DELETE는 삭제에 사용합니다. 이 의미가 맞아야 프론트엔드와 백엔드가 같은 + 그림을 보고 개발할 수 있습니다. + + +7-2. AJAX — 페이지 전체가 아니라 데이터만 바꾸기 + + + 예전 웹은 버튼을 누르거나 링크를 클릭하면 페이지 전체를 다시 받아왔습니다. + AJAX가 등장하면서 페이지를 새로고침하지 않고 필요한 데이터만 받아와 화면의 + 일부만 바꾸는 방식이 가능해졌습니다. + + +```text +AJAX 이전: 요청 → 새 HTML 전체 다운로드 → 페이지 전체 교체 +AJAX 이후: 요청 → JSON 데이터 다운로드 → 필요한 UI만 갱신 +``` + + + 이 변화 덕분에 Gmail, Google Maps처럼 페이지 전환 없이 부드럽게 동작하는 웹 + 서비스가 가능해졌습니다. 대신 비동기 요청이 많아지면서 콜백 지옥, 에러 처리, + 공통 요청 설정 같은 새로운 문제가 생겼습니다. + + +7-3. 콜백에서 Promise, async/await으로 + + + 비동기 요청은 응답이 언제 올지 알 수 없습니다. 처음에는 콜백 함수로 응답을 + 처리했지만, 요청이 이어질수록 코드가 깊게 중첩됐습니다. + + +```js +getUser(userId, function (user) { + getOrders(user.id, function (orders) { + getOrderDetail(orders[0].id, function (orderDetail) { + // 계속 중첩됨 + }); + }); +}); +``` + + + Promise와 async/await은 이 문제를 해결했습니다. 서버 통신 코드를 동기 코드처럼 + 읽을 수 있게 만들었고, 에러 처리도 `try/catch`로 모을 수 있게 됐습니다. + + +```ts +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); + } +} +``` + +7-4. Fetch API — 브라우저 내장 HTTP 클라이언트 + + + fetch는 Promise 기반의 브라우저 내장 HTTP 클라이언트입니다. 별도 라이브러리 없이 + 사용할 수 있고, XMLHttpRequest보다 문법이 훨씬 직관적입니다. + + +```ts +const response = await fetch("/api/users"); +const data = await response.json(); +``` + +하지만 fetch에도 실무에서 불편한 지점이 있습니다. + +- 404, 500 같은 HTTP 에러가 자동으로 `catch`로 가지 않습니다. +- JSON 변환을 매번 `response.json()`으로 직접 해야 합니다. +- timeout, 공통 헤더, 인터셉터 같은 기능을 직접 만들어야 합니다. +- 요청 취소는 `AbortController`를 알아야 합니다. + +```ts +const response = await fetch("/api/users"); + +if (!response.ok) { + throw new Error("HTTP error"); +} + +const data = await response.json(); +``` + +7-5. Axios — 실무 편의 기능을 모은 HTTP 클라이언트 + + + axios는 fetch의 부족한 부분을 보완하기 위해 많이 사용됩니다. 자동 JSON 변환, + timeout, 에러 처리, 인스턴스, 인터셉터 같은 기능을 제공합니다. 브라우저와 + Node.js에서 비슷한 방식으로 쓸 수 있다는 점도 장점입니다. + + +```ts +const response = await axios.get("/api/users"); +const data = response.data; +``` + + + fetch와 달리 axios는 2xx가 아닌 HTTP 상태 코드를 에러로 처리합니다. 그래서 + `try/catch` 안에서 성공과 실패를 나누기 쉽습니다. + + +```ts +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); + } +} +``` + +7-6. 인터셉터 — 요청과 응답을 가로채는 공통 처리 + + + 인터셉터는 모든 요청이나 모든 응답 앞뒤에 공통 로직을 끼워 넣는 기능입니다. + 실무에서는 토큰 자동 추가, 401 에러 처리, 공통 에러 변환에 많이 사용합니다. + + +```ts +const apiClient = axios.create({ + baseURL: "/api", + timeout: 10000, + withCredentials: true, +}); + +apiClient.interceptors.request.use((config) => { + const token = localStorage.getItem("accessToken"); + + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + + return config; +}); +``` + + + 인터셉터를 쓰면 모든 API 함수에서 토큰을 반복해서 꺼내 붙이지 않아도 됩니다. + 공통 로직은 한 곳에 모이고, 개별 API 함수는 "무슨 요청을 보낼지"에 집중할 수 + 있습니다. + + +7-7. 토큰 인증 — HTTP의 무상태성을 보완하기 + + + HTTP는 이전 요청을 기억하지 않기 때문에, 로그인 이후 요청마다 사용자를 증명할 + 정보가 필요합니다. 이때 access token과 refresh token을 함께 쓰는 구조가 자주 + 등장합니다. + + +```text +로그인 성공 + → Access Token 발급: 짧은 수명, API 요청에 사용 + → Refresh Token 발급: 긴 수명, Access Token 재발급에 사용 +``` + + + 백엔드와 맞춰야 할 약속은 구체적이어야 합니다. 토큰을 어느 헤더에 넣는지, + `Bearer`를 붙이는지, 만료되면 어떤 상태 코드가 오는지, refresh API는 어떤 응답을 + 주는지 확인해야 합니다. + + +```http +Authorization: Bearer {accessToken} +``` + + + 401 응답이 왔을 때 인터셉터에서 토큰을 갱신하고 원래 요청을 다시 보내는 패턴도 + 자주 사용됩니다. 다만 동시에 여러 요청이 401을 받으면 refresh 요청이 중복될 수 + 있으므로, 실무에서는 중복 갱신을 막는 설계도 필요합니다. + + +7-8. TanStack Query — HTTP 통신이 아니라 서버 상태 관리 + + + 어느 순간부터 프론트엔드 개발자들은 HTTP 요청 자체보다 서버 데이터 상태 관리에 + 더 많은 시간을 쓰고 있다는 걸 알게 됐습니다. 로딩, 에러, 캐싱, 중복 요청 제거, + 백그라운드 갱신, 낙관적 업데이트 같은 문제입니다. + + +```tsx +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)); +}, []); +``` + + + TanStack Query는 이런 반복을 줄여주는 서버 상태 관리 도구입니다. axios가 서버에 + 요청을 보내고 응답을 받아오는 역할이라면, TanStack Query는 받아온 데이터를 + 캐시에 저장하고, 언제 새로 가져올지, 어떤 컴포넌트에 전달할지를 관리합니다. + + +```tsx +const { data, isLoading, error } = useQuery({ + queryKey: ["users"], + queryFn: () => apiClient.get("/users").then((res) => res.data), + staleTime: 5 * 60 * 1000, +}); +``` + + + 핵심은 역할 구분입니다. axios는 택배 기사처럼 데이터를 가져오고, + TanStack Query는 창고 관리자처럼 그 데이터를 보관하고 갱신합니다. + + +7-9. Query와 Mutation + + + TanStack Query에서는 데이터를 읽는 작업과 쓰는 작업을 구분합니다. Query는 서버 + 데이터를 읽는 작업이고, Mutation은 생성, 수정, 삭제처럼 서버 데이터를 바꾸는 + 작업입니다. + + +| 구분 | Query | Mutation | +| --- | --- | --- | +| 용도 | 데이터 읽기 | 데이터 생성/수정/삭제 | +| 주로 쓰는 HTTP | GET | POST, PUT, PATCH, DELETE | +| 실행 시점 | 컴포넌트 마운트 시 자동 | `mutate` 호출 시 수동 | +| 캐싱 | 자동 캐싱 | 직접 무효화/갱신 필요 | + +```tsx +const queryClient = useQueryClient(); + +const createUser = useMutation({ + mutationFn: (newUser) => apiClient.post("/users", newUser), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["users"] }); + }, +}); +``` + + + 새 사용자를 만든 뒤 사용자 목록 query를 무효화하면, TanStack Query가 목록을 다시 + 가져옵니다. 직접 `setUsers`로 상태를 여기저기 수정하지 않아도 서버 데이터와 + 화면을 다시 맞출 수 있습니다. + + +--- + +8. 7주차 파일 구조와 API 파일 배치 연결하기 + + + 지난 대면에서 UI, 상태, 서버 데이터, 비즈니스 규칙을 나눠서 봤습니다. API + 관련 파일도 같은 기준으로 배치하면 됩니다. 핵심은 "서버와 통신하는 코드"와 + "화면을 그리는 코드"를 섞지 않는 것입니다. + + +작은 프로젝트에서는 아래처럼 시작할 수 있습니다. + +```text +src/ + api/ + client.ts + studies.ts + pages/ + StudyListPage.tsx + components/ + StudyCard.tsx +``` + + + `client.ts`에는 base URL, credentials, 공통 헤더, 에러 변환 같은 HTTP 공통 + 정책을 둡니다. `studies.ts`에는 스터디 도메인의 API 함수들을 둡니다. 화면은 + `getStudies()` 같은 함수를 호출할 뿐, URL 문자열과 HTTP 세부 설정을 직접 알지 + 않는 편이 좋습니다. + + +```ts +// api/studies.ts +export async function getStudies(params: GetStudiesParams) { + const response = await api.get("studies", { searchParams: params }).json(); + return parseStudiesResponse(response); +} +``` + +프로젝트가 커지면 기능 단위로 가까이 두는 방식도 좋습니다. + +```text +src/ + features/ + studies/ + api/ + studyApi.ts + studyQueries.ts + model/ + studyTypes.ts + studyPolicy.ts + ui/ + StudyCard.tsx + StudyList.tsx + pages/ + StudyListPage.tsx +``` + + + 이 구조에서는 스터디 기능과 관련된 API, query key, 타입, 정책, UI가 한 기능 + 안에 모입니다. 대신 공통 HTTP 클라이언트는 여전히 바깥에 둡니다. + + +```text +src/ + shared/ + api/ + httpClient.ts + apiError.ts + features/ + studies/ + api/ + studyApi.ts + studyQueries.ts +``` + + + `studyApi.ts`는 실제 HTTP 요청 함수, `studyQueries.ts`는 TanStack Query의 query + key와 query option을 담는 식으로 나누면 역할이 선명해집니다. + + +```ts +// features/studies/api/studyQueries.ts +export const studyQueries = { + all: ["studies"] as const, + list: (params: GetStudiesParams) => ({ + queryKey: [...studyQueries.all, "list", params] as const, + queryFn: () => studyApi.getStudies(params), + }), +}; +``` + + + 이렇게 해두면 화면 컴포넌트는 "스터디 목록을 가져온다"는 의도만 드러내고, + HTTP 세부사항과 query key 규칙은 API 계층에 남습니다. + + +--- + +9. 프론트엔드가 백엔드에게 잘 질문하는 법 "API 언제 돼요?"보다 좋은 질문은 구체적입니다. @@ -170,12 +694,36 @@ GET /api/studies?page=1&size=20 개발자는 화면 요구사항을 API 계약 언어로 바꿔 말할 수 있어야 합니다. +실제로는 이런 질문들도 자주 필요합니다. + +- 이 API는 로그인하지 않은 사용자도 호출할 수 있나요? +- 실패할 때 code 값은 어떤 목록 중 하나인가요? +- 빈 목록은 200과 빈 배열인가요, 아니면 404인가요? +- 페이지 번호는 0부터 시작하나요, 1부터 시작하나요? +- CORS 허용 origin에 로컬 개발 주소와 배포 주소가 모두 들어가 있나요? + +--- + +10. 오늘 정리 + + + API는 데이터를 가져오는 기술이면서 동시에 팀 사이의 약속입니다. CORS는 그 + 약속이 브라우저 보안 규칙과 만나는 지점이고, HTTP 클라이언트와 TanStack Query는 + API 코드를 프로젝트 구조 안에 안정적으로 배치하기 위한 도구입니다. + + + + 7주차에서 배운 책임 분리를 떠올리면 오늘 내용도 같은 흐름입니다. UI는 UI답게, + API 함수는 API답게, 서버 상태 관리는 서버 상태 관리답게 두어야 프로젝트가 + 커져도 변경 범위를 좁힐 수 있습니다. + + --- -7. 다음 주 안내 +11. 다음 주 안내 - 다음 대면에서는 웹 보안을 다룹니다. 특히 브라우저에 있는 코드는 사용자가 볼 수 - 있고 조작할 수 있다는 전제에서, 프론트엔드가 어디까지 믿어도 되는지 - 이야기합니다. + 다음 대면에서는 Next.js와 웹 렌더링을 다룹니다. React로 화면을 + 만드는 것에서 한 걸음 더 나아가, 브라우저 렌더링, 서버 렌더링, 정적 생성, + 하이드레이션이 왜 등장했는지 큰 흐름을 봅니다. diff --git a/apps/web/content/study/clab-26-1/in-person/week9.mdx b/apps/web/content/study/clab-26-1/in-person/week9.mdx index 776c6a7..a1ec0e8 100644 --- a/apps/web/content/study/clab-26-1/in-person/week9.mdx +++ b/apps/web/content/study/clab-26-1/in-person/week9.mdx @@ -1,169 +1,400 @@ --- -title: "프론트엔드 스터디 대면 9주차: 웹 보안 — 브라우저에 있는 코드는 믿으면 안 된다" +title: "프론트엔드 스터디 대면 9주차: Next.js 렌더링 진화와 웹 퍼포먼스" date: 2026-05-25 tags: - 프론트엔드 - 스터디 - - 웹 보안 - - XSS - - CSRF - - 토큰 - - 인증 - - AI 보안 -description: React 전 필수 JS 문법을 비대면에서 배운 뒤, 대면에서는 브라우저 코드의 한계와 웹 보안 감각을 다룹니다. 클라이언트 검증, 토큰 저장, XSS, CSRF, API 키 노출과 AI 생성 코드의 위험을 봅니다. + - Next.js + - CSR + - SSR + - SSG + - ISR + - Web Vitals + - 웹 퍼포먼스 +description: Next.js가 제공하는 렌더링 전략의 진화를 CSR, SSR, SSG, ISR, Streaming SSR, PPR 흐름으로 정리하고, Web Vitals로 웹 퍼포먼스를 측정하는 방법을 다룹니다. --- -1. 왜 보안을 지금 이야기하는가 +1. 화면은 어디서 만들어지는가 - 이제 곧 React로 화면을 더 빠르게 만들 수 있게 됩니다. 그런데 화면을 잘 만드는 - 것과 안전한 서비스를 만드는 것은 다릅니다. 특히 프론트엔드는 사용자의 브라우저 - 위에서 실행됩니다. 이 말은 곧{" "} - 사용자가 프론트엔드 코드를 볼 수 있고, 일부는 조작할 수 있다 - 는 뜻입니다. + 지금까지 React로 화면을 만드는 방법을 배웠습니다. 그런데 React로 만든 화면은 + 어디서 실제로 그려질까요? 브라우저일 수도 있고, 서버일 수도 있습니다. 이 + 차이가 사용자가 첫 화면을 보는 속도에 직접 영향을 줍니다. -오늘의 핵심 문장은 하나입니다. + + 오늘은 렌더링 방식이 어떻게 진화해왔는지와, 퍼포먼스를 숫자로 측정하는 법을 + 봅니다. 7주차에 짧게 언급했던 Next.js가 왜 이 선택들을 제공하는지 이해하는 + 시간이기도 합니다. + + +--- + +2. CSR — 브라우저가 직접 그리는 방식 + + + React로 만든 앱을 아무 설정 없이 배포하면 CSR(Client-Side Rendering) 방식으로 + 동작합니다. 서버는 빈 HTML과 JS 파일만 보내고, 브라우저가 JS를 실행해서 직접 + 화면을 만듭니다. + + +```text +서버 → 빈 HTML 전송 → JS 다운로드 → JS 실행 → DOM 생성 → 화면 표시 +``` + +```html + + + +
+ + + +``` - 브라우저에 있는 코드는 믿으면 안 됩니다. + 사용자는 JS가 다운로드되고 실행되기 전까지 빈 화면을 봅니다. JS가 클수록, + 인터넷이 느릴수록, 화면이 늦게 뜹니다. +- 장점: 첫 로딩 후 페이지 이동이 빠르고, 서버 부하가 적음 +- 단점: 첫 화면이 늦게 뜸, 검색 엔진이 내용을 읽기 어려움(SEO) + --- - - 2. 클라이언트 검증은 UX용이지 보안의 최종선이 아니다 - +3. SSR — 서버가 먼저 그려주는 방식 - 회원가입 폼에서 비밀번호 길이를 검사하거나, 숫자만 입력하게 막는 코드는 사용자 - 경험을 좋게 만듭니다. 하지만 이것만으로 보안이 지켜지는 것은 아닙니다. + SSR(Server-Side Rendering)은 서버가 HTML을 미리 완성해서 보내줍니다. 사용자는 + JS가 로드되기 전에 이미 화면 내용을 볼 수 있습니다. -```js -if (password.length < 8) { - alert("비밀번호는 8자 이상이어야 합니다."); -} +```text +서버에서 HTML 완성 → 완성된 HTML 전송 → 화면 즉시 표시 → JS 다운로드 → Hydration ``` +| 지표 | CSR | SSR | +| --- | --- | --- | +| FCP (첫 콘텐츠가 보이는 시점) | 느림 (JS 실행 후) | 빠름 (HTML 도착 즉시) | +| SEO | 불리 (빈 HTML) | 유리 (완성된 HTML) | +| 서버 부하 | 없음 | 매 요청마다 생성 | + - 사용자는 브라우저 개발자 도구나 직접 만든 요청으로 이 코드를 우회할 수 - 있습니다. 그래서 중요한 검증은 반드시 서버에서도 해야 합니다. 프론트엔드 - 검증은 사용자를 도와주는 장치이고, 서버 검증은 시스템을 지키는 장치입니다. + SSR이 "빠르다"는 건 정확히는 사용자가 화면을 처음 보는 시점이 빠르다는 + 뜻입니다. 클릭이나 입력 같은 인터랙션이 되려면 Hydration이 끝나야 합니다. --- - - 3. 실제 사례 — AI로 만든 웹 게임을 QA하다가 생긴 일 - +4. Hydration — 정적 HTML에 생명 불어넣기 - 이전에 친구가 AI로 웹 게임을 만들어 온 적이 있습니다. 겉으로 보기에는 잘 - 동작했습니다. 버튼을 누르면 게임이 진행되고, 점수가 오르고, 랭킹도 보였습니다. - 그런데 브라우저 개발자 도구를 열어보니 내부 함수와 상태가 너무 쉽게 노출되어 - 있었습니다. + 서버가 보낸 HTML은 그림처럼 보이기만 합니다. 클릭해도 반응이 없습니다. + Hydration은 이 정적 HTML에 JS 이벤트 핸들러를 붙여서 실제로 동작하게 만드는 + 과정입니다. -```js -// 게임 내부 함수가 전역에 노출되어 있었다 -applyLaser(anyRegion, anyRegion); // 1회 호출에 점수 +38 +```text +정적 HTML (보이지만 클릭해도 반응 없음) + ↓ JS 다운로드 + Hydration +동적 HTML (클릭, 입력 등 가능) ``` - 원래라면 레이저 아이템을 실제로 가지고 있는지, 사용 가능한 상황인지 서버나 - 신뢰할 수 있는 로직에서 검증해야 합니다. 하지만 이 게임은 콘솔에서 함수를 - 호출하면 점수가 올라갔습니다. AI는 "돌아가는 게임"은 만들어줬지만, "나쁜 - 사용자가 어떻게 조작할 수 있는가"까지 먼저 챙겨주지는 않았습니다. + Hydration이 진행되는 동안 사용자는 화면은 보지만 인터랙션이 안 되는 어색한 + 구간을 경험할 수 있습니다. 이 구간을 줄이는 것이 현대 렌더링 최적화의 핵심 중 + 하나입니다. + + +--- + +5. Next.js 렌더링 전략의 진화 + + + Next.js는 SSR만 하는 게 아닙니다. 페이지마다 다른 렌더링 전략을 선택할 수 + 있고, 그 전략들이 이렇게 진화해왔습니다. +```text +CSR → SSR → SSG → ISR → Streaming SSR → PPR +``` + +SSG (Static Site Generation) — 빌드 시 미리 만들기 + + + 서버가 요청마다 HTML을 만드는 SSR과 달리, SSG는 빌드할 때 HTML을 미리 + 만들어둡니다. 요청이 오면 서버가 만들 필요 없이 미리 만든 파일을 바로 보냅니다. + + +```text +[SSR] 요청 → 서버가 HTML 생성 → 전송 (매번) +[SSG] 빌드 → HTML 생성 → CDN에 올림 → 요청 → 바로 전송 (빠름) +``` + +- 장점: 가장 빠름, 서버 부하 없음 +- 단점: 데이터가 바뀌면 전체를 다시 빌드해야 함 + +ISR (Incremental Static Regeneration) — 필요할 때만 갱신 + + + SSG의 단점을 해결합니다. 일정 시간마다 그 페이지만 조용히 재생성합니다. 1000페이지 + 중 요청이 온 1페이지만 갱신되는 식입니다. + + +```tsx +// Next.js에서 ISR 설정 +export const revalidate = 60; // 60초마다 재생성 +``` + +```text +0~60초: 캐시 그대로 반환 (SSG처럼 빠름) +60초 후: 요청이 오면 기존 캐시 반환 + 백그라운드에서 새 HTML 생성 + → 다음 요청부터 새 HTML 서빙 +``` + +Streaming SSR — 준비된 것부터 조각씩 보내기 + + + 일반 SSR은 페이지 전체가 준비될 때까지 기다렸다가 한번에 보냅니다. DB 조회가 + 느린 경우 사용자는 그만큼 기다려야 합니다. Streaming SSR은 준비된 부분부터 먼저 + 보내고, 느린 부분은 나중에 채워줍니다. + + +```tsx + +
{/* 즉시 전송 */} + }> + {/* DB 조회 끝나면 전송, 그 전엔 Skeleton 표시 */} + + +``` + + + `Suspense`는 "아직 준비 안 됐으면 대신 이걸 보여줘"를 선언하는 React + 컴포넌트입니다. Skeleton은 콘텐츠가 들어올 자리를 미리 잡아두는 빈 뼈대 UI입니다. + + +PPR (Partial Prerendering) — 컴포넌트별로 정적/동적 분리 + - 이 사례는 AI 시대에 더 중요합니다. AI가 만든 앱은 빠르게 그럴듯해집니다. - 하지만 잘 돌아가는 것처럼 보인다고 안전한 것은 아닙니다. + ISR은 페이지 전체를 정적 또는 동적으로 취급합니다. PPR은 한 페이지 안에서 + 컴포넌트 단위로 정적/동적을 나눕니다. 상품 이름·이미지는 정적으로, 가격·재고는 + 동적으로 처리할 수 있습니다. +```tsx +export default function ProductPage() { + return ( +
+ {/* 정적: 빌드 시 생성, CDN에서 즉시 전송 */} + + + + {/* 동적: 요청 시 서버에서 생성, 준비되면 Streaming */} + }> + + +
+ ); +} +``` + +전략 비교 + +| 전략 | 속도 | 데이터 신선함 | 어울리는 콘텐츠 | +| --- | --- | --- | --- | +| SSG | 가장 빠름 | 빌드 시점 고정 | 블로그, 문서, 변경 없는 페이지 | +| SSR | 느림 | 항상 최신 | 로그인 필요, 실시간 데이터 | +| ISR | SSG와 동일 | 약간의 지연 | 블로그, 상품 목록 | +| PPR | SSG급 | 컴포넌트별 분리 | 상품 상세, 대시보드 | + --- -4. 토큰 저장 — localStorage vs Cookie +6. 서버 컴포넌트 vs 클라이언트 컴포넌트 - 로그인 기능을 만들면 토큰을 어디에 저장할지 고민하게 됩니다. 대표적으로 - localStorage와 cookie가 있습니다. 둘 다 장단점이 있습니다. + Next.js App Router에서는 컴포넌트가 어디서 실행되는지를 직접 선택할 수 있습니다. + 이것이 RSC(React Server Components)입니다. -- localStorage: 사용하기 쉽지만 JavaScript로 접근 가능해서 - XSS에 취약합니다. -- httpOnly Cookie: JavaScript에서 직접 읽을 수 없어 XSS에 - 상대적으로 강하지만, CSRF 대응과 SameSite 설정을 함께 봐야 합니다. +```tsx +// Server Component (기본값) +// 서버에서만 실행됨. 브라우저로 JS가 안 감. +async function PostList() { + const posts = await db.query("SELECT * FROM posts"); + + return ( +
    + {posts.map((p) => ( +
  • {p.title}
  • + ))} +
+ ); +} +``` + +```tsx +// Client Component ('use client' 선언 필요) +// 브라우저에서 실행됨. onClick, useState 사용 가능. +"use client"; + +function LikeButton() { + const [liked, setLiked] = useState(false); + return ; +} +``` - 결론은 "무조건 이게 정답"이 아니라, 어떤 공격을 막아야 하고 서비스 구조가 - 어떤지 보고 선택해야 한다는 것입니다. 중요한 건 토큰이 곧 권한이라는 - 감각입니다. + 판단 기준은 하나입니다. 이 컴포넌트가 클릭, 입력, 상태 변화를 다루는가? 그렇지 + 않다면 Server Component로 두는 게 낫습니다. JS 번들에 포함되지 않으니 번들 + 크기가 줄고, Hydration 대상도 줄어 성능이 좋아집니다. +| 구분 | Server Component | Client Component | +| --- | --- | --- | +| 실행 위치 | 서버 | 브라우저 | +| useState, onClick | 불가 | 가능 | +| DB 직접 접근 | 가능 | 불가 | +| JS 번들 포함 | 포함 안 됨 | 포함됨 | + --- - - 5. XSS — 남의 스크립트가 내 사이트에서 실행되는 문제 - +7. Web Vitals — "빠르다"를 숫자로 말하기 - XSS는 공격자가 넣은 스크립트가 우리 사이트에서 실행되는 문제입니다. 예를 들어 - 게시글 내용에 스크립트를 넣었는데, 다른 사용자가 그 게시글을 볼 때 실행된다면 - 큰 문제가 됩니다. + Google이 정한 사용자 체감 성능의 3가지 핵심 지표입니다. 검색 순위(SEO)에도 실제로 + 영향을 줍니다. -```html - +LCP (Largest Contentful Paint) + + + 가장 큰 콘텐츠가 화면에 나타나는 시간. 메인 이미지, 큰 텍스트 블록이 해당합니다. + + +```text +페이지 요청 + │ 0.5s 헤더, 네비게이션 표시 + │ 1.2s 메인 이미지 표시 ← 이게 LCP + │ 1.8s 나머지 콘텐츠 + +좋음: ≤ 2.5s / 개선 필요: 2.5~4s / 나쁨: > 4s +``` + +- SSG나 SSR은 LCP를 빠르게 만들고, CSR은 느리게 만드는 주요 원인입니다. + +INP (Interaction to Next Paint) + +사용자가 클릭·입력한 뒤 화면이 반응하기까지의 시간. + +```text +사용자가 버튼 클릭 + │ JS 이벤트 핸들러 실행 + │ 상태 업데이트 + │ DOM 변경 + 화면 다시 그림 ← 여기까지가 INP + +좋음: ≤ 200ms / 나쁨: > 500ms ``` +- Hydration이 무거우면 JS 메인 스레드가 막혀서 INP가 나빠집니다. +- Server Component를 늘리면 Hydration 대상이 줄어 INP가 개선됩니다. + +CLS (Cumulative Layout Shift) + - React는 기본적으로 문자열을 escape해주기 때문에 많은 XSS를 막아줍니다. 하지만 - `dangerouslySetInnerHTML`처럼 HTML을 직접 넣는 기능을 쓰거나, 외부 - markdown/html을 렌더링할 때는 조심해야 합니다. + 페이지 로딩 중 레이아웃이 얼마나 흔들리는가. 광고가 갑자기 끼어들어서 읽던 + 텍스트가 밀리는 경험이 대표적입니다. +```text +좋음: ≤ 0.1 / 나쁨: > 0.25 +``` + +- 흔한 원인: 이미지 크기 미지정, 폰트 로딩 후 텍스트 크기 변화 +- 해결: `width`/`height` 명시, Skeleton으로 공간 미리 확보 + +| 지표 | 측정하는 것 | 좋음 기준 | 주요 영향 요소 | +| --- | --- | --- | --- | +| LCP | 콘텐츠 표시 속도 | ≤ 2.5s | 렌더링 방식, 이미지 크기 | +| INP | 인터랙션 반응 속도 | ≤ 200ms | JS 번들 크기, Hydration 무게 | +| CLS | 레이아웃 안정성 | ≤ 0.1 | 이미지 크기, Skeleton, 폰트 | + --- - - 6. CSRF — 사용자가 원하지 않은 요청이 전송되는 문제 - +8. 퍼포먼스 측정하는 방법 + + + Web Vitals는 실제로 어떻게 측정할까요? Chrome DevTools에 이미 다 있습니다. + + +Lighthouse + + + Chrome DevTools → Lighthouse 탭 → "Analyze page load" 버튼. LCP, INP, CLS를 + 포함한 퍼포먼스 점수와 개선 제안을 한번에 볼 수 있습니다. + + +- Mode: Navigation — 페이지 첫 로딩 시 측정 (가장 일반적) +- Device: Mobile 로 체크하면 더 엄격하게 측정됨 + +Performance 탭 - CSRF는 사용자가 로그인된 상태를 이용해, 사용자가 의도하지 않은 요청을 보내게 - 만드는 공격입니다. 쿠키 기반 인증에서는 브라우저가 쿠키를 자동으로 붙여 보내기 - 때문에 특히 신경 써야 합니다. + Chrome DevTools → Performance 탭 → 녹화 버튼 후 페이지 조작. 타임라인에서 어느 + 시점에 뭐가 실행됐는지, 어디서 병목이 생겼는지 볼 수 있습니다. +Network 탭으로 렌더링 방식 확인하기 + - SameSite Cookie, CSRF Token, Origin/Referer 검증 같은 대응이 있습니다. 오늘 - 전부 외울 필요는 없습니다. 핵심은 "브라우저가 자동으로 인증 정보를 붙여 보내는 - 구조에서는 이런 공격이 가능하다"는 감각입니다. + Network 탭에서 첫 HTML 응답을 열어보면 렌더링 방식을 가늠할 수 있습니다. +- HTML 응답에 내용이 가득 → SSR 또는 SSG +- HTML 응답에 빈 div만 → CSR + +실제로 해보기 + +1. 크롬에서 아무 사이트나 열고 DevTools → Lighthouse 실행 +2. LCP / INP / CLS 점수 확인 +3. "Opportunities" 섹션에서 개선 제안 읽기 +4. Network 탭으로 첫 HTML 응답 확인 — 내용이 있는지 없는지 + --- -7. API 키를 프론트에 넣으면 안 되는 이유 +9. 정리 — 렌더링 선택의 기준 - 프론트엔드 번들에 들어간 값은 결국 사용자에게 전달됩니다. `.env`에 넣었다고 - 안전한 것이 아닙니다. 빌드 시점에 클라이언트 코드로 들어간 API 키는 - 브라우저에서 확인될 수 있습니다. + 어떤 렌더링 방식을 쓸지는 "이 페이지의 데이터가 얼마나 자주 바뀌는가"와 + "인터랙션이 중요한가"로 판단합니다. +- 데이터가 거의 안 바뀐다 → SSG +- 실시간 데이터가 필요하다 → SSR +- 가끔 바뀌는 데이터 → ISR +- 페이지 일부만 실시간 → PPR + Streaming +- 클릭·입력이 필요한 컴포넌트만 → Client Component + - 외부 서비스 키나 숨겨야 할 토큰은 서버, BFF, 서버리스 함수 같은 신뢰할 수 있는 - 계층에서 다뤄야 합니다. + + 렌더링 방식을 잘 선택하는 것 자체가 프론트엔드 성능 최적화의 절반입니다. + --- -8. 다음 주 안내 +10. 다음 주 안내 — 10주차 "팀 협업" - 다음 대면에서는 팀 협업을 다룹니다. Git, PR, 컨벤션, 리뷰 문화처럼 혼자 공부할 - 때는 잘 와닿지 않지만 팀 프로젝트에서 바로 필요한 것들을 봅니다. + 다음 주에는 팀 협업을 다룹니다. Git, PR, 컨벤션, 리뷰 문화처럼 혼자 공부할 때는 + 잘 와닿지 않지만, 팀 프로젝트에서 바로 필요한 것들입니다. + +--- + +관련 포스팅 + +- 이번 주 학습 자료(week9) +- + vinext는 왜 빠를까? — SSR, Vite, Edge, Web Vitals까지 (참고) + diff --git a/apps/web/content/study/clab-26-1/week9.mdx b/apps/web/content/study/clab-26-1/week9.mdx index cf1f0d2..9c0e3a7 100644 --- a/apps/web/content/study/clab-26-1/week9.mdx +++ b/apps/web/content/study/clab-26-1/week9.mdx @@ -34,11 +34,11 @@ description: 다음 주부터 React를 시작합니다. React 코드에서 매 - 이번 주 대면에서는 웹 보안을 다룹니다. 비대면에서 배우는 옵셔널 체이닝, - try/catch, 모듈, API 응답 처리 문법은 단순히 React를 편하게 읽기 위한 - 도구이기도 하지만, "외부에서 들어온 값을 어디까지 믿을 수 있는가"를 판단하는 - 기초이기도 합니다. 특히 API 응답은 항상 예상한 형태로 온다고 믿지 말고, - 안전하게 접근하고 실패를 처리하는 습관을 같이 가져가면 좋습니다. + 이번 주 대면에서는 Next.js 렌더링 진화와 웹 퍼포먼스를 다룹니다. 비대면에서 + 배우는 map/filter, 구조 분해, 스프레드, 모듈 문법은 React 컴포넌트를 읽기 위한 + 기초이면서, Next.js에서 서버 컴포넌트와 클라이언트 컴포넌트를 나누고 데이터를 + 화면에 연결할 때도 계속 등장합니다. 문법을 외우기보다, 이 문법들이 데이터를 + UI로 바꾸는 과정에서 어떤 역할을 하는지 같이 보면 좋습니다. --- From f39d475bc7fa5afddb859595a26fae6a3da8a454 Mon Sep 17 00:00:00 2001 From: seojing Date: Thu, 14 May 2026 18:46:29 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20(study)=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EB=B3=B4=EC=95=88=20=EC=A3=BC=EC=9D=98?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/clab-26-1/in-person/week8.mdx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/web/content/study/clab-26-1/in-person/week8.mdx b/apps/web/content/study/clab-26-1/in-person/week8.mdx index 72572f5..ba04f47 100644 --- a/apps/web/content/study/clab-26-1/in-person/week8.mdx +++ b/apps/web/content/study/clab-26-1/in-person/week8.mdx @@ -461,6 +461,8 @@ const apiClient = axios.create({ }); apiClient.interceptors.request.use((config) => { + // 학습을 위한 단순 예시입니다. + // localStorage에 토큰을 저장하면 XSS 공격에 노출될 수 있습니다. const token = localStorage.getItem("accessToken"); if (token) { @@ -477,6 +479,15 @@ apiClient.interceptors.request.use((config) => { 있습니다. + + 다만 위 코드는 인터셉터 개념을 설명하기 위한 단순 예시입니다. 실무에서는 + access token을 localStorage에 오래 보관하는 방식을 기본값으로 두면 안 됩니다. + localStorage는 JavaScript로 읽을 수 있기 때문에 XSS가 발생했을 때 토큰이 탈취될 + 수 있습니다. 보안이 중요한 서비스라면 서버가 설정하는 HttpOnly, + Secure, SameSite 쿠키 기반 인증이나 그에 준하는 + 안전한 저장 전략을 함께 검토해야 합니다. + + 7-7. 토큰 인증 — HTTP의 무상태성을 보완하기 @@ -507,6 +518,14 @@ Authorization: Bearer {accessToken} 있으므로, 실무에서는 중복 갱신을 막는 설계도 필요합니다. + + Refresh Token은 특히 더 조심해야 합니다. 브라우저 JavaScript가 직접 읽는 저장소에 + 두기보다, 서버가 내려주는 HttpOnly + Secure 쿠키로 보관하는 방식을 많이 + 사용합니다. 이 경우 프론트엔드는 토큰 문자열을 직접 다루지 않고, 브라우저가 + 쿠키를 요청에 함께 보내도록 둡니다. 대신 쿠키 기반 인증에서는 SameSite 설정, + CSRF 대응, CORS credentials 설정을 백엔드와 함께 맞춰야 합니다. + + 7-8. TanStack Query — HTTP 통신이 아니라 서버 상태 관리