diff --git a/apps/web/content/study/clab-26-1/in-person/week7.mdx b/apps/web/content/study/clab-26-1/in-person/week7.mdx
index 6d9d070..49933f0 100644
--- a/apps/web/content/study/clab-26-1/in-person/week7.mdx
+++ b/apps/web/content/study/clab-26-1/in-person/week7.mdx
@@ -1,211 +1,710 @@
---
-title: "프론트엔드 스터디 대면 7주차: 프로젝트가 복잡해지는 이유 — 구조와 책임 분리"
+title: "프론트엔드 스터디 대면 7주차: React 입문과 프로젝트 구조"
date: 2026-05-11
tags:
- 프론트엔드
- 스터디
+ - React
+ - 컴포넌트
+ - Virtual DOM
- 프로젝트 구조
- 책임 분리
+ - 폴더 구조
- 상태 관리
- - 컴포넌트 설계
- - 아키텍처
-description: 7주차 비대면에서 불변성·프로토타입·타입 체크를 배운 뒤, 대면에서는 프론트엔드 프로젝트가 왜 복잡해지는지와 UI·상태·서버 데이터·비즈니스 규칙을 나누는 감각을 다룹니다.
+description: 바닐라 JS의 한계에서 React가 등장한 이유까지, 컴포넌트와 렌더링 방식을 이해하고 프로젝트 구조 감각을 만들어가는 대면입니다.
---
-1. 이번 주 사전학습과 오늘 대면의 연결
+1\. 저번 주 연결 — 바닐라 JS로 화면 그리기의 한계
- 이번 주 비대면에서는 불변성, 프로토타입,
- 타입 체크를 배웠습니다. 세 개 모두 처음에는 "JS가 왜 이렇게
- 이상하게 생겼지?" 싶은 개념입니다. 그런데 이 개념들은 프로젝트가 커질수록 실제
- 버그와 구조 문제로 이어집니다.
+ 저번 주에는 바닐라 JS로 화면을 직접 그리는 방법을 봤습니다.{" "}
+ document.querySelector, innerHTML,{" "}
+ addEventListener 같은 DOM API를 써서 화면을 제어했죠.
+```js
+const button = document.querySelector("#submit");
+const countEl = document.querySelector("#count");
+let n = 0;
+
+button.addEventListener("click", () => {
+ n++;
+ countEl.textContent = n;
+ if (n >= 10) button.disabled = true;
+});
+```
+
+
+ 간단한 경우에는 잘 동작합니다. 그런데 화면이 복잡해지고 상태가 여러 개 생기면
+ 어떻게 될까요?
+
+
+- 상태(`n`)가 바뀔 때마다 영향받는 DOM 요소를 직접 찾아서 바꿔야
+ 합니다.
+- 같은 UI 조각을 여러 곳에 쓰려면 코드를 복사해야 합니다.
+- 상태가 어디서 어떻게 바뀌는지 코드를 전부 읽어야만 알 수 있습니다.
+
- 그래서 오늘 대면에서는 문법을 다시 설명하기보다, 프론트엔드 프로젝트가 왜
- 복잡해지는지 이야기해보겠습니다. 처음에는 HTML, CSS, JS 파일 몇 개면 될 것
- 같은데, 왜 React가 나오고, 컴포넌트가 나오고, 상태 관리가 나오고, 폴더 구조와
- API 계층이 생기는지 큰 그림을 잡는 시간입니다.
+ 이 문제를 해결하려고 나온 것이 React입니다. React는 "상태가 바뀌면 화면도
+ 알아서 바뀐다"는 약속을 가지고 있습니다.
---
-2. 프론트엔드 프로젝트는 왜 복잡해지는가
+2\. React가 등장한 이유 — UI를 선언적으로
- 작은 화면 하나를 만들 때는 간단합니다. 버튼을 누르면 텍스트가 바뀌고, CSS로
- 예쁘게 꾸미면 됩니다. 문제는 화면이 많아지고, 데이터가 많아지고, 여러 사람이
- 동시에 작업하기 시작할 때 생깁니다.
+ 바닐라 JS 방식은 명령형(imperative)입니다. 화면을 어떻게
+ 바꿀지, 단계별로 지시해야 합니다.
+```js
+// 명령형: 어떻게 바꿀지 직접 지시
+countEl.textContent = n;
+button.disabled = n >= 10;
+badge.classList.toggle("hidden", n === 0);
+```
+
- 복잡도는 보통 코드 양보다 책임이 섞일 때 터집니다.
+ React는 선언형(declarative)입니다. 화면이 어떻게 보여야
+ 하는지를 작성하면, 상태가 바뀔 때 React가 화면을 알아서 맞춥니다.
-- 버튼 컴포넌트 안에 API 호출이 들어간다.
-- API 응답 가공 로직이 화면 여기저기에 흩어진다.
-- 로그인 여부 판단이 여러 파일에 복붙된다.
-- 서버에서 내려준 값을 화면마다 다르게 해석한다.
-- 디자이너가 바꾼 UI 하나 때문에 비즈니스 로직까지 같이 흔들린다.
+```tsx
+// 선언형: 상태에 따라 화면이 어떻게 보여야 하는지 작성
+function Counter() {
+ const [n, setN] = useState(0);
+
+ return (
+
+ {n > 0 && }
+
{n}
+
+
+ );
+}
+```
- 코드는 많아도 책임이 잘 나뉘어 있으면 고칠 수 있습니다. 반대로 코드가 적어도
- 책임이 섞여 있으면, 한 줄을 바꾸는 순간 어디가 깨질지 알 수 없습니다.
+ 개발자는 "지금 상태가 이렇다면 화면은 이렇게 보여야 한다"만 작성하면 됩니다.
+ 상태를 어떻게 DOM에 반영할지는 React가 담당합니다.
+> 명령형은 "어떻게 바꿀지"를 써야 하고, 선언형은 "어떻게 보여야 하는지"를 씁니다.
+
---
-3. 나눠서 봐야 하는 네 가지 책임
+3\. 컴포넌트란 무엇인가 — UI 조각을 함수로 만들기
-3-1. UI — 어떻게 보이는가
+
+ React에서 UI는 컴포넌트(Component) 단위로 만들어집니다.
+ 컴포넌트는 간단하게 말하면 UI 조각을 반환하는 함수입니다.
+
+
+```tsx
+function Button({ label, onClick, disabled }) {
+ return (
+
+ );
+}
+```
- UI는 사용자에게 보이는 모양입니다. 버튼, 카드, 모달, 입력창, 리스트 같은
- 것들이 여기에 들어갑니다. 좋은 UI 컴포넌트는 가능하면 "어떻게 보여줄지"에
- 집중하고, 데이터를 어디서 가져오는지는 덜 알아야 합니다.
+ 함수처럼 props(매개변수)로 데이터를 받고, JSX로 화면을
+ 반환합니다. 그리고 함수를 호출하듯이 화면에서 사용합니다.
```tsx
-
+
```
+컴포넌트를 쓰면 세 가지가 좋아집니다.
+
+- 재사용 — 버튼, 카드, 입력창 같은 UI 조각을 여러 화면에서 꺼내
+ 씁니다.
+- 조합 — 작은 컴포넌트를 쌓아서 더 큰 화면을 만듭니다.
+- 책임 분리 — 버튼은 버튼처럼 생긴 것만 신경 씁니다. 나머지는
+ 다른 곳에서 담당합니다.
+
+컴포넌트 트리
+
- 이 컴포넌트는 이름, 역할, 온라인 여부를 받아 보여주는 일만 하면 됩니다. 이
- 값이 API에서 왔는지, 임시 데이터인지, 로그인한 사용자 정보인지는 몰라도
- 됩니다.
+ 화면은 컴포넌트를 트리처럼 쌓아서 만들어집니다. 부모 컴포넌트가 자식
+ 컴포넌트에게 props를 내려줍니다.
+
+
+```tsx
+function StudyPage() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+```
+
+---
+
+4\. React의 렌더링 방식 — 언제 다시 그리나
+
+
+ 컴포넌트는 상태(state)가 바뀌거나 props가
+ 바뀌면 다시 실행됩니다. 다시 실행되면 새로운 화면을 반환하고, React가 그
+ 결과를 실제 화면에 반영합니다.
+
+
+```tsx
+function Counter() {
+ const [count, setCount] = useState(0);
+
+ return (
+
+
{count}
+
+
+ );
+}
+```
+
+
+ 버튼을 누르면 setCount가 호출됩니다. 상태가 바뀌었으니 React는
+ Counter를 다시 실행해서 새로운 화면을 만들고, <p> 안의
+ 숫자만 바꿔줍니다.
+
+
+Virtual DOM — 왜 빠른가
+
+
+ React는 실제 DOM을 바로 바꾸지 않습니다. 대신 메모리 안에{" "}
+ 가상 DOM(Virtual DOM)을 만들어서 이전 상태와 비교한 뒤,
+ 달라진 부분만 실제 DOM에 반영합니다.
+
+
+- 전체를 다시 그리지 않고, 변경된 부분만 업데이트합니다.
+- DOM 조작은 비용이 비쌉니다. 필요한 만큼만 건드리는 것이 빠릅니다.
+- 개발자는 이 과정을 직접 제어하지 않아도 됩니다. React가 처리합니다.
+
+
+ 이것이 바닐라 JS와의 핵심 차이입니다. 바닐라 JS에서는 DOM을 개발자가 직접
+ 찾아서 바꿔야 했지만, React에서는 상태를 바꾸면 화면은 따라옵니다.
+
+
+---
+
+5\. 프론트엔드 프로젝트는 왜 복잡해지는가
+
+
+ React를 써도 프로젝트는 복잡해집니다. 코드 양이 많아서가 아니라,{" "}
+ 책임이 섞일 때 터집니다.
+
+
+
+ 예를 들어 버튼 하나가 있다고 해볼게요. 버튼은 화면에 보여야 하고, 클릭되면
+ 상태가 바뀌어야 하고, 서버에 요청을 보내야 하고, 로그인한 사용자 권한에 따라
+ 막히기도 해야 합니다. 여기까지는 자연스럽습니다. 그런데 이 모든 판단이 버튼
+ 컴포넌트 하나 안에 들어가면, 그때부터 문제가 시작돼요.
+
+
+```tsx
+function ApplyButton() {
+ // UI도 있고, 서버 요청도 있고, 권한 규칙도 있고, 상태 변경도 있음
+ // 버튼 하나를 고치려 했는데 서비스 전체 규칙을 건드리게 됩니다.
+}
+```
+
+
+ 처음에는 빠릅니다. 파일 하나에 다 있으니까요. 하지만 조금만 지나면 "버튼 색만
+ 바꾸고 싶었는데 API가 깨지고", "문구만 바꾸고 싶었는데 권한 조건이 바뀌고",
+ "서버 응답 구조가 바뀌었는데 화면 파일 20개를 고쳐야 하는" 상황이 옵니다.
+
+
+> 코드 양이 아니라, 책임이 섞일 때 프로젝트가 터집니다.
+
+---
+
+6\. 나눠서 봐야 하는 4가지 책임
+
+
+ 프론트엔드 코드를 볼 때 처음부터 완벽한 폴더 구조를 떠올리려고 하면
+ 어렵습니다. 대신 먼저 책임을 네 가지로 나눠보면 훨씬 쉬워집니다.
+
+
+- UI — 무엇을 어떻게 보여줄 것인가
+- 상태 — 지금 화면이나 앱이 어떤 상태인가
+- 서버 데이터 — 서버에서 무엇을 가져오고, 어떻게 캐시하고, 언제
+ 다시 가져올 것인가
+- 비즈니스 규칙 — 우리 서비스에서만 성립하는 판단 기준은
+ 무엇인가
+
+6-1\. UI — 보여주는 책임
+
+
+ UI 컴포넌트의 첫 번째 책임은 보여주는 것입니다. 버튼이면
+ 버튼처럼 보이고, 카드면 카드처럼 보이고, 리스트면 리스트처럼 보이면 됩니다.
+ 문제는 UI가 서비스 규칙까지 알기 시작할 때 생깁니다.
-3-2. 상태 — 지금 화면이 기억해야 하는 값
+Bad
+
+```tsx
+function StudyApplyButton({ study, user }) {
+ const disabled =
+ !user ||
+ user.role !== "member" ||
+ study.status !== "OPEN" ||
+ study.appliedUserIds.includes(user.id) ||
+ study.currentCount >= study.maxCount;
+
+ return (
+
+ );
+}
+```
+
+왜 깨지나
- 상태는 화면이 기억해야 하는 값입니다. 모달이 열려 있는지, 어떤 탭이
- 선택됐는지, 입력창에 무슨 값이 들어있는지 같은 것들입니다.
+ 이 버튼은 단순히 버튼이 아니라 스터디 신청 정책까지 알고
+ 있습니다. 나중에 "운영진은 마감 후에도 신청 가능" 같은 규칙이 추가되면 버튼
+ 컴포넌트를 수정해야 합니다. 같은 규칙을 다른 화면에서도 써야 하면
+ 복사·붙여넣기가 생기고, 어느 한쪽만 고치면서 버그가 납니다.
+Good
+
+```tsx
+function StudyApplyButton({ disabled }: { disabled: boolean }) {
+ return (
+
+ );
+}
+
+const disabled = !canApplyStudy({ study, user });
+
+;
+```
+
+
+ UI는 "비활성화 여부"만 받아서 그립니다. "왜 비활성화인지"는 다른 책임으로
+ 분리합니다. 이렇게 하면 버튼은 재사용 가능해지고, 신청 규칙은 따로 테스트할 수
+ 있습니다.
+
+
+6-2\. 상태 — 지금 무엇이 선택되어 있는가
+
- 상태가 무서운 이유는 시간이 지나며 바뀌기 때문입니다. 값이 바뀌면 화면도
- 바뀌고, 다른 컴포넌트에도 영향을 줄 수 있습니다. 그래서 상태는 "어디에 둘지"가
- 중요합니다.
+ 상태는 서버에 영구 저장된 데이터가 아니라,{" "}
+ 지금 화면에서 사용자가 보고 있거나 조작 중인 값입니다. 예를
+ 들면 모달이 열려 있는지, 어떤 탭이 선택되어 있는지, 검색어 입력값이 무엇인지
+ 같은 것들이에요.
-- 한 컴포넌트에서만 쓰면 그 컴포넌트 안에 둡니다.
-- 부모와 자식이 같이 쓰면 공통 부모에 둡니다.
-- 여러 페이지에서 쓰면 전역 상태나 서버 상태 관리가 필요해질 수 있습니다.
+Bad
+
+```tsx
+function MemberPage() {
+ const [members, setMembers] = useState([]);
+ const [selectedId, setSelectedId] = useState(null);
+
+ useEffect(() => {
+ fetch("/api/members")
+ .then((res) => res.json())
+ .then(setMembers);
+ }, []);
+
+ const selectedMember = members.find((member) => member.id === selectedId);
+
+ return ;
+}
+```
+
+왜 깨지나
+
+
+ 여기서 `members`는 서버 데이터이고, `selectedId`는 화면 상태입니다. 둘이 같은
+ 컴포넌트 안에 있으면 처음엔 괜찮지만, 캐싱·로딩·에러·재요청이 붙는 순간
+ 복잡해져요. 서버 데이터가 비어 있을 때 선택된 ID를 어떻게 처리할지, 재요청
+ 중에는 어떻게 할지 같은 문제가 화면 컴포넌트에 계속 쌓입니다.
+
+
+Good
+
+```tsx
+function MemberPage() {
+ const [selectedId, setSelectedId] = useState(null);
+ const { data: members = [] } = useMembersQuery();
+
+ const selectedMember = members.find((member) => member.id === selectedId);
+
+ return ;
+}
+```
+
+
+ 서버 데이터는 `useMembersQuery` 같은 훅으로 빼고, 화면은 "무엇이 선택되어
+ 있는가"만 관리합니다. 상태를 바꿀 때는 직접 수정하지 말고,{" "}
+ 새 상태를 만들어 넘기는 패턴을 기본으로 가져가야 합니다.
+
- 3-3. 서버 데이터 — 내가 만든 값이 아니라 받아온 값
+ 6-3\. 서버 데이터 — 가져오고, 캐시하고, 다시 가져오는 책임
- 서버 데이터는 프론트엔드가 소유한 값이 아닙니다. 사용자 목록, 게시글, 알림,
- 주문 내역처럼 서버가 진짜 원본을 가지고 있는 데이터입니다. 그래서 로딩, 실패,
- 재요청, 캐싱, 권한 같은 문제가 따라옵니다.
+ 서버 데이터는 화면 상태와 다릅니다. 서버 데이터는 내가 만든 값이 아니라{" "}
+ 서버가 소유한 값의 프론트엔드 복사본입니다. 그래서 로딩,
+ 에러, 캐시, 재요청, 만료 시간이 같이 따라옵니다.
+Bad
+
+```tsx
+function NoticeList() {
+ const [notices, setNotices] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ setLoading(true);
+ fetch("/api/notices")
+ .then((res) => res.json())
+ .then(setNotices)
+ .catch(setError)
+ .finally(() => setLoading(false));
+ }, []);
+
+ // ...
+}
+```
+
+왜 깨지나
+
- 이걸 일반 상태와 똑같이 다루면 점점 힘들어집니다. 화면 상태는 "내가 지금 어떤
- 탭을 보고 있나"에 가깝고, 서버 데이터는 "서버의 최신 값이 무엇인가"에
- 가깝습니다. 둘은 성격이 다릅니다.
+ 화면마다 이 패턴을 반복하면 로딩·에러·재시도·캐시 규칙이 전부 흩어집니다. A
+ 화면에서는 새로고침하면 다시 가져오고, B 화면에서는 캐시가 남고, C 화면에서는
+ 에러 메시지가 다르게 나오는 식으로 서비스 경험이 흔들립니다.
-3-4. 비즈니스 규칙 — 제품이 정한 판단
+Good
+
+```tsx
+function NoticeList() {
+ const { data: notices = [], isLoading, error } = useNoticesQuery();
+
+ if (isLoading) return ;
+ if (error) return ;
+
+ return ;
+}
+```
+
+```tsx
+function useNoticesQuery() {
+ return useQuery({
+ queryKey: ["notices"],
+ queryFn: () => noticeApi.getNotices(),
+ });
+}
+```
- 비즈니스 규칙은 제품이 정한 판단입니다. 예를 들어 "관리자만 승인 버튼을 볼 수
- 있다", "마감 시간이 지나면 신청할 수 없다", "무료 사용자는 3개까지만 만들 수
- 있다" 같은 규칙입니다.
+ 서버 데이터 책임은 React Query, SWR 같은 도구가 잘 맡아줍니다. CLAB member
+ app도 `@tanstack/react-query`를 사용합니다. 핵심은 "서버에서 가져온 값"과
+ "화면에서 잠깐 쓰는 값"을 같은 상태로 취급하지 않는 것입니다.
+6-4\. 비즈니스 규칙 — 우리 서비스의 판단 기준
+
- 이런 규칙이 화면 코드 곳곳에 흩어지면 나중에 정책이 바뀔 때 지옥이 열립니다.
- 그래서 규칙은 이름을 붙이고, 가능하면 한 곳에서 관리해야 합니다.
+ 비즈니스 규칙은 서비스마다 다른 판단입니다. "누가 신청할 수 있는가", "마감된
+ 모집을 보여줄 것인가", "어떤 상태에서 버튼을 숨길 것인가" 같은 규칙이에요.
+Bad
+
+```tsx
+function RecruitmentCard({ recruitment, user }) {
+ return (
+
+ {recruitment.status === "OPEN" &&
+ user?.role === "member" &&
+ !recruitment.applicants.includes(user.id) && }
+
+ );
+}
+```
+
+왜 깨지나
+
+
+ 이 조건은 화면 코드 안에 숨어 있습니다. 같은 "지원 가능 여부"를 상세 페이지,
+ 목록 페이지, 관리자 페이지에서 모두 써야 하면 조건이 퍼집니다. 나중에 규칙이
+ 바뀌면 어디를 고쳐야 하는지 찾기 어려워져요.
+
+
+Good
+
```ts
-function canApprove(user, item) {
- return user.role === "ADMIN" && item.status === "PENDING";
+export function canApplyRecruitment({ recruitment, user }) {
+ if (!user) return false;
+ if (user.role !== "member") return false;
+ if (recruitment.status !== "OPEN") return false;
+ if (recruitment.applicants.includes(user.id)) return false;
+
+ return true;
+}
+```
+
+```tsx
+function RecruitmentCard({ recruitment, user }) {
+ const canApply = canApplyRecruitment({ recruitment, user });
+
+ return {canApply && };
}
```
- 중요한 건 함수 하나를 만들었다는 사실이 아니라, "승인 가능 여부"라는 판단에
- 이름을 붙였다는 점입니다. 좋은 구조는 이런 식으로 팀이 말하는 개념을 코드에
- 남깁니다.
+ 규칙을 함수로 빼면 이름이 생깁니다. 이름이 생기면 토론할 수 있고, 테스트할 수
+ 있고, 바뀔 때 한 군데를 고치기 쉬워집니다. 구조를 잘 잡는다는 건 결국{" "}
+ 바뀔 가능성이 큰 판단에 이름을 붙이는 일이기도 합니다.
---
-
- 4. 좋은 구조는 예쁜 폴더가 아니라 변경에 강한 구조다
-
+7\. 폴더 구조의 종류 — 책임을 어디에 둘 것인가
- 프로젝트 구조 이야기를 하면 폴더 이름부터 떠올리기 쉽습니다. `components`,
- `hooks`, `utils`, `api` 같은 폴더를 어떻게 나눌지 고민합니다. 물론 중요합니다.
- 하지만 폴더 이름보다 더 중요한 질문은 이겁니다.
+ 폴더 구조는 정답 맞히기 문제가 아닙니다. "책임을 어디에 둘 것인가"에 대한 팀의
+ 합의입니다. 아래 이름들은 외우기보다, 어떤 상황에서 어울리는지 감으로 가져가면
+ 됩니다.
+| 구조 | 한 줄 정의 | 어울리는 상황 |
+| :------ | :------------------------------------------------------------------- | :----------------------------------------------------- |
+| flat | 파일을 얕게 나열하는 구조 | 작은 과제, 페이지 수가 적은 토이 프로젝트 |
+| feature | 기능 단위로 관련 파일을 묶는 구조 | 도메인/기능이 분명한 서비스형 프로젝트 |
+| layered | `components`, `hooks`, `api`, `utils`처럼 기술 계층별로 나누는 구조 | 초반 학습용, 팀원이 구조를 빨리 이해해야 하는 프로젝트 |
+| atomic | atoms/molecules/organisms처럼 UI 조립 단위로 나누는 구조 | 디자인 시스템, 공통 UI 컴포넌트가 중요한 프로젝트 |
+| FSD | app/pages/widgets/features/entities/shared로 책임 레벨을 나누는 구조 | 큰 규모, 여러 기능 팀이 같이 만지는 프로젝트 |
+
-
- "요구사항이 바뀌었을 때, 어디를 고치면 되는지 바로 알 수 있는가?"
-
+ CLAB member app은 완전한 FSD라기보다는 layered + feature 혼합
+ 에 가깝습니다. `api`, `components`, `pages`, `hooks`, `model` 같은 계층이
+ 있고, 그 안에서 `community`, `activity`, `library`, `auth`처럼 기능별
+ 디렉터리가 다시 나뉩니다.
+```txt
+apps/member/src
+├─ api
+│ ├─ member
+│ ├─ community
+│ ├─ recruitment
+│ └─ auth
+├─ components
+│ ├─ home
+│ ├─ community
+│ ├─ activity
+│ ├─ library
+│ └─ common
+├─ pages
+│ ├─ home
+│ ├─ community
+│ ├─ activity
+│ └─ my
+├─ app
+│ ├─ layout
+│ └─ route
+├─ hooks
+├─ model
+├─ types
+└─ utils
+```
+
+---
+
+8\. CLAB platforms로 보는 프론트 영역 지도
+
- 좋은 구조는 변경이 들어왔을 때 흔들리는 범위가 작습니다. 디자인만 바뀌면 UI
- 컴포넌트만 바꾸고, API 응답만 바뀌면 데이터 변환 계층만 바꾸고, 정책만 바뀌면
- 비즈니스 규칙만 바꿀 수 있어야 합니다.
+ 이번 장은 한 페이지짜리 지도처럼 보면 됩니다. 프론트엔드 프로젝트는 코드만
+ 있는 게 아니라, 패키지 매니저·빌드 도구·스타일링·상태 관리·테스트 같은 여러
+ 영역이 함께 움직입니다.
+```json
+{
+ "dependencies": {
+ "@tanstack/react-query": "^5.90.21",
+ "jotai": "^2.18.0",
+ "ky": "^1.7.3",
+ "react": "^19.2.0",
+ "react-router": "^7.1.5",
+ "tailwindcss": "^4.1.18",
+ "zustand": "^5.0.11"
+ }
+}
+```
+
+| 영역 | 왜 필요하나 | CLAB member app |
+| :------------ | :---------------------------------------- | :------------------ |
+| 패키지 매니저 | 의존성 설치와 버전 고정 | pnpm |
+| 빌드 | 개발 코드를 브라우저용 결과물로 변환 | Vite |
+| 모노레포 | 여러 앱/패키지를 한 저장소에서 관리 | Turbo + workspace |
+| 언어 | 타입으로 실수를 미리 잡음 | TypeScript |
+| 스타일링 | UI를 일관되게 표현 | Tailwind CSS v4 |
+| HTTP | 서버와 통신 | ky |
+| 서버 상태 | 서버 데이터 캐싱·로딩·에러 관리 | React Query |
+| 전역 상태 | 여러 화면이 공유하는 클라이언트 상태 관리 | Jotai + Zustand |
+| 라우팅 | URL과 화면을 연결 | React Router |
+| 린트/포맷 | 코드 스타일과 실수를 자동 점검 | ESLint + Prettier |
+| 테스트 | 변경 후 기존 동작 확인 | Vitest + Playwright |
+
- 처음부터 완벽한 구조를 만들 필요는 없습니다. 다만 코드가 조금만 커져도 "이
- 코드는 UI인가, 상태인가, 서버 데이터인가, 정책인가"를 묻는 습관이 중요합니다.
+ 각 도구가 어떤 책임을 대신 맡고 있는지를 보는 것입니다.
+ 프로젝트 구조는 폴더만이 아니라, 이런 도구 선택까지 포함합니다.
---
-5. AI 시대에는 구조 감각이 더 중요해진다
+9\. 페어 워크 — Bad → Good 리팩터
- AI는 파일 하나 안에서 그럴듯한 코드를 빠르게 만들어줍니다. 하지만 프로젝트의
- 책임 경계를 자동으로 잘 지켜주지는 않습니다. "이 로직은 컴포넌트 안에 있으면
- 안 되고 hook으로 빼야 한다", "이건 서버 데이터라 캐싱 전략이 필요하다", "이건
- 정책 함수로 이름 붙여야 한다" 같은 판단은 사람이 해야 합니다.
+ 이제 6장에서 본 Bad 코드 중 하나를 골라 페어로 리팩터해보겠습니다. 목표는
+ 완벽한 정답을 만드는 것이 아니라,{" "}
+ 책임을 어디까지 나눌지 토론하는 것입니다.
+진행 방식
+
+1. 6장의 Bad 코드 중 하나를 고릅니다.
+2. 이 코드 안에 섞여 있는 책임을 표시합니다.
+ - UI
+ - 화면 상태
+ - 서버 데이터
+ - 비즈니스 규칙
+3. 최소 2개 이상의 책임을 분리합니다.
+4. 특히 서버 상태 vs 화면 상태를 구분해봅니다.
+5. 리팩터 후 "바뀌기 쉬운 지점"이 어디로 이동했는지 설명합니다.
+
+예시 미션
+
+```tsx
+function StudyPage() {
+ const [studies, setStudies] = useState([]);
+ const [selectedCategory, setSelectedCategory] = useState("ALL");
+
+ useEffect(() => {
+ fetch("/api/studies")
+ .then((res) => res.json())
+ .then(setStudies);
+ }, []);
+
+ const visibleStudies = studies.filter((study) => {
+ if (selectedCategory !== "ALL" && study.category !== selectedCategory)
+ return false;
+ if (study.status !== "OPEN") return false;
+ return true;
+ });
+
+ return (
+
+
+ {visibleStudies.map((study) => (
+
+ ))}
+
+ );
+}
+```
+
- 그래서 앞으로는 코드를 많이 치는 사람보다,{" "}
-
- AI가 만든 코드를 프로젝트 구조 안에 제대로 배치할 수 있는 사람
-
- 이 더 중요해질 수 있습니다.
+ 이 코드에는 서버 데이터 가져오기, 화면 상태, 필터링 규칙, UI 렌더링이 섞여
+ 있습니다. 페어로 아래처럼 나눠보세요.
+
+
+- `useStudiesQuery()` — 서버 데이터 책임
+- `selectedCategory` — 화면 상태 책임
+- `getVisibleStudies()` — 비즈니스/필터 규칙 책임
+- `StudyCard`, `CategoryTabs` — UI 책임
+
+
+ 리팩터 후에는 서로에게 설명해보세요. "이제 카테고리 정책이 바뀌면 어디를
+ 고치면 되나요?" "API 경로가 바뀌면 어디를 고치면 되나요?" 이 질문에 바로 답할
+ 수 있으면 좋은 방향입니다.
---
-6. 같이 이야기해볼 질문
-
-
-
-
-
+10\. Next.js — React로 만든 프레임워크
+
+
+ 오늘 다룬 React는 UI를 만드는 라이브러리입니다. React만으로는 라우팅, 서버
+ 렌더링, 파일 기반 페이지 구성 같은 것들을 직접 세팅해야 합니다.{" "}
+ Next.js는 이 위에 쌓인 프레임워크입니다.
+
+
+- React를 기반으로, 라우팅·빌드·배포·렌더링 방식까지 미리 구성해줍니다.
+- 페이지를 서버에서 렌더할지, 클라이언트에서 렌더할지 선택할 수
+ 있습니다.
+- 지금 이 사이트도 Next.js로 만들어져 있습니다.
+
+
+ "서버에서 뭘 그리고, 클라이언트에서 뭘 그리냐"는 질문은 앞으로 점점 중요해지는
+ 감각입니다. 자세한 내용은 나중에 웹 퍼포먼스를 다루는 편에서 제대로 볼
+ 예정이고, 지금은 "React를 더 편하게 쓰기 위한 틀" 정도로
+ 이해해두면 충분합니다.
+
---
-7. 다음 주 안내
+11\. 다음 주 안내 — 8주차 "API와 통신"
- 다음 대면에서는 프론트엔드와 백엔드가 만나는 지점인{" "}
- API와 통신을 다룹니다. API는 그냥 데이터를 받는 수단이
- 아니라, 팀 사이의 계약입니다.
+ 다음 주에는 오늘 살짝 본 HTTP와 서버 상태{" "}
+ 영역을 메인으로 다룹니다. 오늘은 "서버 데이터는 화면 상태와 다르게 봐야
+ 한다"는 감각을 잡았다면, 다음 주에는 실제로{" "}
+ API를 호출하고 응답을 다루는 방법을 더 깊게 보게 됩니다.
+
+
+ 특히 `fetch`, `axios`, `ky` 같은 HTTP 클라이언트 선택지와, Promise/async-await
+ 흐름이 자연스럽게 연결됩니다. 서버에서 데이터가 "나중에" 오기 때문에 비동기가
+ 필요하고, 그 데이터를 화면에서 안전하게 쓰기 위해 서버 상태 관리가 필요해지는
+ 구조입니다.
+
+
+다음 주 예습
+
+- 다음 주 학습 자료(week8)의
+ API/비동기 관련 구간 읽기
+- 콘솔에서 `fetch("https://jsonplaceholder.typicode.com/todos/1")`를 실행해보고
+ Promise가 어떻게 보이는지 확인하기
+- 오늘 자료에서 서버 상태와 화면 상태를 구분한 예시를 하나 다시
+ 읽어오기
+
+관련 포스팅
+
+-
+ 이번 주 학습 자료(week7) — 불변성, 프로토타입, 타입 체크
+
+-
+ 다음 주 학습 자료(week8) — API와 통신
+
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..869e4a7 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,406 @@
---
-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: CSR부터 SSR, SSG, ISR, PPR까지 Next.js 렌더링 전략의 진화를 이해하고, Web Vitals로 웹 퍼포먼스를 숫자로 측정하는 방법을 다룹니다.
---
-1. 왜 보안을 지금 이야기하는가
+1\. 화면은 어디서 만들어지는가
- 이제 곧 React로 화면을 더 빠르게 만들 수 있게 됩니다. 그런데 화면을 잘 만드는
- 것과 안전한 서비스를 만드는 것은 다릅니다. 특히 프론트엔드는 사용자의 브라우저
- 위에서 실행됩니다. 이 말은 곧{" "}
- 사용자가 프론트엔드 코드를 볼 수 있고, 일부는 조작할 수 있다
- 는 뜻입니다.
+ 지금까지 React로 화면을 만드는 방법을 배웠습니다. 그런데 React로 만든 화면은{" "}
+ 어디서 실제로 그려질까요? 브라우저일 수도 있고, 서버일 수도
+ 있습니다. 이 차이가 사용자가 첫 화면을 보는 속도에 직접 영향을 줍니다.
-오늘의 핵심 문장은 하나입니다.
+
+ 오늘은 렌더링 방식이 어떻게 진화해왔는지와, 퍼포먼스를 숫자로 측정하는 법을
+ 봅니다. 7주차에 짧게 언급했던 Next.js가 왜 이 선택들을 제공하는지 이해하는
+ 시간이기도 합니다.
+
+
+---
+
+2\. CSR — 브라우저가 직접 그리는 방식
+
+
+ React로 만든 앱을 아무 설정 없이 배포하면{" "}
+ CSR(Client-Side Rendering)
+ 방식으로 동작합니다. 서버는 빈 HTML과 JS 파일만 보내고, 브라우저가 JS를
+ 실행해서 직접 화면을 만듭니다.
+
+
+```
+서버 → 빈 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자 이상이어야 합니다.");
-}
+```
+서버에서 HTML 완성 → 완성된 HTML 전송 → 화면 즉시 표시 → JS 다운로드 → Hydration
```
+| 지표 | CSR | SSR |
+| :---------------------------- | :---------------- | :-------------------- |
+| FCP (첫 콘텐츠가 보이는 시점) | 느림 (JS 실행 후) | 빠름 (HTML 도착 즉시) |
+| SEO | 불리 (빈 HTML) | 유리 (완성된 HTML) |
+| 서버 부하 | 없음 | 매 요청마다 생성 |
+
- 사용자는 브라우저 개발자 도구나 직접 만든 요청으로 이 코드를 우회할 수
- 있습니다. 그래서 중요한 검증은 반드시 서버에서도 해야 합니다. 프론트엔드
- 검증은 사용자를 도와주는 장치이고, 서버 검증은 시스템을 지키는 장치입니다.
+ SSR이 "빠르다"는 건 정확히는{" "}
+ 사용자가 화면을 처음 보는 시점이 빠르다는 뜻입니다. 클릭이나
+ 입력 같은 인터랙션이 되려면 Hydration이 끝나야 합니다.
---
-
- 3. 실제 사례 — AI로 만든 웹 게임을 QA하다가 생긴 일
+4\. Hydration — 정적 HTML에 생명 불어넣기
+
+
+ 서버가 보낸 HTML은 그림처럼 보이기만 합니다. 클릭해도 반응이 없습니다.{" "}
+ Hydration은 이 정적 HTML에 JS 이벤트 핸들러를 붙여서 실제로
+ 동작하게 만드는 과정입니다.
+
+
+```
+정적 HTML (보이지만 클릭해도 반응 없음)
+ ↓ JS 다운로드 + Hydration
+동적 HTML (클릭, 입력 등 가능)
+```
+
+
+ Hydration이 진행되는 동안 사용자는 화면은 보이지만 인터랙션이 안 되는 어색한
+ 구간을 경험할 수 있습니다. 이 구간을 줄이는 것이 현대 렌더링 최적화의 핵심 중
+ 하나입니다.
+
+
+---
+
+5\. Next.js 렌더링 전략의 진화
+
+
+ Next.js는 SSR만 하는 게 아닙니다. 페이지마다 다른 렌더링 전략을 선택할 수
+ 있고, 그 전략들이 이렇게 진화해왔습니다.
+
+
+```
+CSR → SSR → SSG → ISR → Streaming SSR → PPR
+```
+
+
+ SSG (Static Site Generation) — 빌드 시 미리 만들기
- 이전에 친구가 AI로 웹 게임을 만들어 온 적이 있습니다. 겉으로 보기에는 잘
- 동작했습니다. 버튼을 누르면 게임이 진행되고, 점수가 오르고, 랭킹도 보였습니다.
- 그런데 브라우저 개발자 도구를 열어보니 내부 함수와 상태가 너무 쉽게 노출되어
- 있었습니다.
+ 서버가 요청마다 HTML을 만드는 SSR과 달리, SSG는{" "}
+ 빌드할 때 HTML을 미리 만들어둡니다. 요청이 오면 서버가 만들
+ 필요 없이 미리 만든 파일을 바로 보냅니다.
-```js
-// 게임 내부 함수가 전역에 노출되어 있었다
-applyLaser(anyRegion, anyRegion); // 1회 호출에 점수 +38
```
+[SSR] 요청 → 서버가 HTML 생성 → 전송 (매번)
+[SSG] 빌드 → HTML 생성 → CDN에 올림 → 요청 → 바로 전송 (빠름)
+```
+
+- 장점: 가장 빠름, 서버 부하 없음
+- 단점: 데이터가 바뀌면 전체를 다시 빌드해야 함
+
+
+ ISR (Incremental Static Regeneration) — 필요할 때만 갱신
+
- 원래라면 레이저 아이템을 실제로 가지고 있는지, 사용 가능한 상황인지 서버나
- 신뢰할 수 있는 로직에서 검증해야 합니다. 하지만 이 게임은 콘솔에서 함수를
- 호출하면 점수가 올라갔습니다. AI는 "돌아가는 게임"은 만들어줬지만, "나쁜
- 사용자가 어떻게 조작할 수 있는가"까지 먼저 챙겨주지는 않았습니다.
+ SSG의 단점을 해결합니다.{" "}
+ 일정 시간마다 그 페이지만 조용히 재생성합니다. 1000페이지 중
+ 요청이 온 1페이지만 갱신되는 식입니다.
+```tsx
+// Next.js에서 ISR 설정
+export const revalidate = 60; // 60초마다 재생성
+```
+
+```
+0~60초: 캐시 그대로 반환 (SSG처럼 빠름)
+60초 후: 요청이 오면 기존 캐시 반환 + 백그라운드에서 새 HTML 생성
+ → 다음 요청부터 새 HTML 서빙
+```
+
+Streaming SSR — 준비된 것부터 조각씩 보내기
+
- 이 사례는 AI 시대에 더 중요합니다. AI가 만든 앱은 빠르게 그럴듯해집니다.
- 하지만 잘 돌아가는 것처럼 보인다고 안전한 것은 아닙니다.
+ 일반 SSR은 페이지 전체가 준비될 때까지 기다렸다가 한번에 보냅니다. DB 조회가
+ 느린 경우 사용자는 그만큼 기다려야 합니다. Streaming SSR은{" "}
+ 준비된 부분부터 먼저 보내고, 느린 부분은 나중에 채워줍니다.
+```tsx
+
+ {/* 즉시 전송 */}
+ }>
+ {/* DB 조회 끝나면 전송, 그 전엔 Skeleton 표시 */}
+
+
+```
+
+
+ Suspense는 "아직 준비 안 됐으면 대신 이걸 보여줘"를 선언하는
+ React 컴포넌트입니다. Skeleton은 콘텐츠가 들어올 자리를 미리 잡아두는 빈 뼈대
+ UI입니다.
+
+
+
+ PPR (Partial Prerendering) — 컴포넌트별로 정적/동적 분리
+
+
+
+ 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}
+ ))}
+
+ );
+}
+
+// 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)
+
+
+ 가장 큰 콘텐츠가 화면에 나타나는 시간. 메인 이미지, 큰 텍스트
+ 블록이 해당합니다.
+
+
+```
+페이지 요청
+ │ 0.5s 헤더, 네비게이션 표시
+ │ 1.2s 메인 이미지 표시 ← 이게 LCP
+ │ 1.8s 나머지 콘텐츠
+
+좋음: ≤ 2.5s / 개선 필요: 2.5~4s / 나쁨: > 4s
+```
+
+- SSG나 SSR은 LCP를 빠르게 만들고, CSR은 느리게 만드는 주요 원인입니다.
+
+INP (Interaction to Next Paint)
+
+
+ 사용자가 클릭·입력한 뒤 화면이 반응하기까지의 시간.
+
+
+```
+사용자가 버튼 클릭
+ │ JS 이벤트 핸들러 실행
+ │ 상태 업데이트
+ │ DOM 변경 + 화면 다시 그림 ← 여기까지가 INP
+
+좋음: ≤ 200ms / 나쁨: > 500ms
```
+- Hydration이 무거우면 JS 메인 스레드가 막혀서 INP가 나빠집니다.
+- Server Component를 늘리면 Hydration 대상이 줄어 INP가 개선됩니다.
+
+CLS (Cumulative Layout Shift)
+
- React는 기본적으로 문자열을 escape해주기 때문에 많은 XSS를 막아줍니다. 하지만
- `dangerouslySetInnerHTML`처럼 HTML을 직접 넣는 기능을 쓰거나, 외부
- markdown/html을 렌더링할 때는 조심해야 합니다.
+ 페이지 로딩 중 레이아웃이 얼마나 흔들리는가. 광고가 갑자기
+ 끼어들어서 읽던 텍스트가 밀리는 경험이 대표적입니다.
+```
+좋음: ≤ 0.1 / 나쁨: > 0.25
+```
+
+- 흔한 원인: 이미지 크기 미지정, 폰트 로딩 후 텍스트 크기 변화
+- 해결: `width`/`height` 명시, Skeleton으로 공간 미리 확보
+
+| 지표 | 측정하는 것 | 좋음 기준 | 주요 영향 요소 |
+| :--- | :----------------- | :-------- | :--------------------------------------- |
+| LCP | 콘텐츠 표시 속도 | ≤ 2.5s | 렌더링 방식(SSG/SSR vs CSR), 이미지 크기 |
+| INP | 인터랙션 반응 속도 | ≤ 200ms | JS 번들 크기, Hydration 무게 |
+| CLS | 레이아웃 안정성 | ≤ 0.1 | 이미지 크기, Skeleton, 폰트 |
+
---
-
- 6. CSRF — 사용자가 원하지 않은 요청이 전송되는 문제
-
+8\. 퍼포먼스 측정하는 방법
- CSRF는 사용자가 로그인된 상태를 이용해, 사용자가 의도하지 않은 요청을 보내게
- 만드는 공격입니다. 쿠키 기반 인증에서는 브라우저가 쿠키를 자동으로 붙여 보내기
- 때문에 특히 신경 써야 합니다.
+ Web Vitals는 실제로 어떻게 측정할까요? Chrome DevTools에 이미 다 있습니다.
+Lighthouse
+
- SameSite Cookie, CSRF Token, Origin/Referer 검증 같은 대응이 있습니다. 오늘
- 전부 외울 필요는 없습니다. 핵심은 "브라우저가 자동으로 인증 정보를 붙여 보내는
- 구조에서는 이런 공격이 가능하다"는 감각입니다.
+ Chrome DevTools → Lighthouse 탭 → "Analyze page load" 버튼.
+ LCP, INP, CLS를 포함한 퍼포먼스 점수와 개선 제안을 한번에 볼 수 있습니다.
----
+- Mode: Navigation — 페이지 첫 로딩 시 측정 (가장 일반적)
+- Device: Mobile로 체크하면 더 엄격하게 측정됨
-7. API 키를 프론트에 넣으면 안 되는 이유
+Performance 탭
- 프론트엔드 번들에 들어간 값은 결국 사용자에게 전달됩니다. `.env`에 넣었다고
- 안전한 것이 아닙니다. 빌드 시점에 클라이언트 코드로 들어간 API 키는
- 브라우저에서 확인될 수 있습니다.
+ Chrome DevTools → Performance 탭 → 녹화 버튼 후 페이지 조작.
+ 타임라인에서 어느 시점에 뭐가 실행됐는지, 어디서 병목이 생겼는지 볼 수
+ 있습니다.
+Network 탭으로 렌더링 방식 확인하기
+
- 외부 서비스 키나 숨겨야 할 토큰은 서버, BFF, 서버리스 함수 같은 신뢰할 수 있는
- 계층에서 다뤄야 합니다.
+ Network 탭에서 첫 HTML 응답을 열어보면 렌더링 방식을 가늠할 수 있습니다.
+- HTML 응답에 내용이 가득 → SSR 또는 SSG
+- HTML 응답에 빈 div만 → CSR
+
+실제로 해보기
+
+1. 크롬에서 아무 사이트나 열고 DevTools → Lighthouse 실행
+2. LCP / INP / CLS 점수 확인
+3. "Opportunities" 섹션에서 개선 제안 읽기
+4. Network 탭으로 첫 HTML 응답 확인 — 내용이 있는지 없는지
+
---
-8. 다음 주 안내
+9\. 정리 — 렌더링 선택의 기준
- 다음 대면에서는 팀 협업을 다룹니다. Git, PR, 컨벤션, 리뷰 문화처럼 혼자 공부할
- 때는 잘 와닿지 않지만 팀 프로젝트에서 바로 필요한 것들을 봅니다.
+ 어떤 렌더링 방식을 쓸지는 "이 페이지의 데이터가 얼마나 자주 바뀌는가"와
+ "인터랙션이 중요한가"로 판단합니다.
+
+- 데이터가 거의 안 바뀐다 → SSG
+- 실시간 데이터가 필요하다 → SSR
+- 가끔 바뀌는 데이터 → ISR
+- 페이지 일부만 실시간 → PPR + Streaming
+- 클릭·입력이 필요한 컴포넌트만 → Client Component
+
+> 렌더링 방식을 잘 선택하는 것 자체가 프론트엔드 성능 최적화의 절반입니다.
+
+---
+
+10\. 다음 주 안내 — 10주차 "팀 협업"
+
+
+ 다음 주에는 팀 협업을 다룹니다. Git, PR, 컨벤션, 리뷰 문화처럼 혼자 공부할
+ 때는 잘 와닿지 않지만, 팀 프로젝트에서 바로 필요한 것들입니다.
+
+
+관련 포스팅
+
+- 이번 주 학습 자료(week9)
+-
+ vinext는 왜 빠를까? — SSR, Vite, Edge, Web Vitals까지 (참고)
+