From a4184118c0e8a1bc049118a456ba45799421240e Mon Sep 17 00:00:00 2001 From: seojing Date: Mon, 11 May 2026 14:41:15 +0900 Subject: [PATCH 1/2] =?UTF-8?q?docs(study):=207=EC=A3=BC=EC=B0=A8=20React?= =?UTF-8?q?=20=EC=9E=85=EB=AC=B8=20=EC=B6=94=EA=B0=80,=209=EC=A3=BC?= =?UTF-8?q?=EC=B0=A8=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=A7=84=ED=99=94?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - week7: 바닐라 JS 한계 → React 등장 이유 → 컴포넌트 → Virtual DOM 섹션 추가, Next.js 간략 소개로 마무리 - week9: 웹 보안 → CSR/SSR/SSG/ISR/PPR 렌더링 진화 + Web Vitals 측정으로 전면 개편 Co-Authored-By: Claude Sonnet 4.6 --- .../study/clab-26-1/in-person/week7.mdx | 709 +++++++++++++++--- .../study/clab-26-1/in-person/week9.mdx | 402 +++++++--- 2 files changed, 920 insertions(+), 191 deletions(-) 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..d7b246a 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 - + + + ); +} +``` + + + 버튼을 누르면 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..caa85f9 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,399 @@ --- -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. 클라이언트 검증은 UX용이지 보안의 최종선이 아니다 - +2\. CSR — 브라우저가 직접 그리는 방식 - 회원가입 폼에서 비밀번호 길이를 검사하거나, 숫자만 입력하게 막는 코드는 사용자 - 경험을 좋게 만듭니다. 하지만 이것만으로 보안이 지켜지는 것은 아닙니다. + React로 만든 앱을 아무 설정 없이 배포하면 CSR(Client-Side Rendering) + 방식으로 동작합니다. 서버는 빈 HTML과 JS 파일만 보내고, 브라우저가 JS를 + 실행해서 직접 화면을 만듭니다. -```js -if (password.length < 8) { - alert("비밀번호는 8자 이상이어야 합니다."); -} +``` +서버 → 빈 HTML 전송 → JS 다운로드 → JS 실행 → DOM 생성 → 화면 표시 +``` + +```html + + + +
+ + + ``` - 사용자는 브라우저 개발자 도구나 직접 만든 요청으로 이 코드를 우회할 수 - 있습니다. 그래서 중요한 검증은 반드시 서버에서도 해야 합니다. 프론트엔드 - 검증은 사용자를 도와주는 장치이고, 서버 검증은 시스템을 지키는 장치입니다. + 사용자는 JS가 다운로드되고 실행되기 전까지 빈 화면을 봅니다. JS가 클수록, + 인터넷이 느릴수록, 화면이 늦게 뜹니다. +- 장점: 첫 로딩 후 페이지 이동이 빠르고, 서버 부하가 적음 +- 단점: 첫 화면이 늦게 뜸, 검색 엔진이 내용을 읽기 어려움(SEO) + --- - - 3. 실제 사례 — AI로 만든 웹 게임을 QA하다가 생긴 일 - +3\. SSR — 서버가 먼저 그려주는 방식 - 이전에 친구가 AI로 웹 게임을 만들어 온 적이 있습니다. 겉으로 보기에는 잘 - 동작했습니다. 버튼을 누르면 게임이 진행되고, 점수가 오르고, 랭킹도 보였습니다. - 그런데 브라우저 개발자 도구를 열어보니 내부 함수와 상태가 너무 쉽게 노출되어 - 있었습니다. + SSR(Server-Side Rendering)은 서버가 HTML을 미리 완성해서 + 보내줍니다. 사용자는 JS가 로드되기 전에 이미 화면 내용을 볼 수 있습니다. -```js -// 게임 내부 함수가 전역에 노출되어 있었다 -applyLaser(anyRegion, anyRegion); // 1회 호출에 점수 +38 ``` +서버에서 HTML 완성 → 완성된 HTML 전송 → 화면 즉시 표시 → JS 다운로드 → Hydration +``` + +| 지표 | CSR | SSR | +| :--------------------------- | :---------------- | :-------------------- | +| FCP (첫 콘텐츠가 보이는 시점) | 느림 (JS 실행 후) | 빠름 (HTML 도착 즉시) | +| SEO | 불리 (빈 HTML) | 유리 (완성된 HTML) | +| 서버 부하 | 없음 | 매 요청마다 생성 | + + + SSR이 "빠르다"는 건 정확히는{" "} + 사용자가 화면을 처음 보는 시점이 빠르다는 뜻입니다. 클릭이나 + 입력 같은 인터랙션이 되려면 Hydration이 끝나야 합니다. + + +--- + +4\. Hydration — 정적 HTML에 생명 불어넣기 - 원래라면 레이저 아이템을 실제로 가지고 있는지, 사용 가능한 상황인지 서버나 - 신뢰할 수 있는 로직에서 검증해야 합니다. 하지만 이 게임은 콘솔에서 함수를 - 호출하면 점수가 올라갔습니다. AI는 "돌아가는 게임"은 만들어줬지만, "나쁜 - 사용자가 어떻게 조작할 수 있는가"까지 먼저 챙겨주지는 않았습니다. + 서버가 보낸 HTML은 그림처럼 보이기만 합니다. 클릭해도 반응이 없습니다.{" "} + Hydration은 이 정적 HTML에 JS 이벤트 핸들러를 붙여서 실제로 + 동작하게 만드는 과정입니다. +``` +정적 HTML (보이지만 클릭해도 반응 없음) + ↓ JS 다운로드 + Hydration +동적 HTML (클릭, 입력 등 가능) +``` + - 이 사례는 AI 시대에 더 중요합니다. AI가 만든 앱은 빠르게 그럴듯해집니다. - 하지만 잘 돌아가는 것처럼 보인다고 안전한 것은 아닙니다. + Hydration이 진행되는 동안 사용자는 화면은 보이지만 인터랙션이 안 되는 어색한 + 구간을 경험할 수 있습니다. 이 구간을 줄이는 것이 현대 렌더링 최적화의 핵심 중 + 하나입니다. --- -4. 토큰 저장 — localStorage vs Cookie +5\. Next.js 렌더링 전략의 진화 + + + Next.js는 SSR만 하는 게 아닙니다. 페이지마다 다른 렌더링 전략을 선택할 수 + 있고, 그 전략들이 이렇게 진화해왔습니다. + + +``` +CSR → SSR → SSG → ISR → Streaming SSR → PPR +``` + +SSG (Static Site Generation) — 빌드 시 미리 만들기 + + + 서버가 요청마다 HTML을 만드는 SSR과 달리, SSG는{" "} + 빌드할 때 HTML을 미리 만들어둡니다. 요청이 오면 서버가 만들 + 필요 없이 미리 만든 파일을 바로 보냅니다. + + +``` +[SSR] 요청 → 서버가 HTML 생성 → 전송 (매번) +[SSG] 빌드 → HTML 생성 → CDN에 올림 → 요청 → 바로 전송 (빠름) +``` + +- 장점: 가장 빠름, 서버 부하 없음 +- 단점: 데이터가 바뀌면 전체를 다시 빌드해야 함 + +ISR (Incremental Static Regeneration) — 필요할 때만 갱신 + + + SSG의 단점을 해결합니다. 일정 시간마다 그 페이지만 조용히 + 재생성합니다. 1000페이지 중 요청이 온 1페이지만 갱신되는 식입니다. + + +```tsx +// Next.js에서 ISR 설정 +export const revalidate = 60; // 60초마다 재생성 +``` + +``` +0~60초: 캐시 그대로 반환 (SSG처럼 빠름) +60초 후: 요청이 오면 기존 캐시 반환 + 백그라운드에서 새 HTML 생성 + → 다음 요청부터 새 HTML 서빙 +``` + +Streaming SSR — 준비된 것부터 조각씩 보내기 - 로그인 기능을 만들면 토큰을 어디에 저장할지 고민하게 됩니다. 대표적으로 - localStorage와 cookie가 있습니다. 둘 다 장단점이 있습니다. + 일반 SSR은 페이지 전체가 준비될 때까지 기다렸다가 한번에 보냅니다. DB 조회가 + 느린 경우 사용자는 그만큼 기다려야 합니다. Streaming SSR은{" "} + 준비된 부분부터 먼저 보내고, 느린 부분은 나중에 채워줍니다. -- localStorage: 사용하기 쉽지만 JavaScript로 접근 가능해서 - XSS에 취약합니다. -- httpOnly Cookie: JavaScript에서 직접 읽을 수 없어 XSS에 - 상대적으로 강하지만, CSRF 대응과 SameSite 설정을 함께 봐야 합니다. +```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급 | 컴포넌트별 | 상품 상세, 대시보드 | + --- - - 5. XSS — 남의 스크립트가 내 사이트에서 실행되는 문제 - +6\. 서버 컴포넌트 vs 클라이언트 컴포넌트 - XSS는 공격자가 넣은 스크립트가 우리 사이트에서 실행되는 문제입니다. 예를 들어 - 게시글 내용에 스크립트를 넣었는데, 다른 사용자가 그 게시글을 볼 때 실행된다면 - 큰 문제가 됩니다. + Next.js App Router에서는 컴포넌트가 어디서 실행되는지를 직접 + 선택할 수 있습니다. 이것이 RSC(React Server Components)입니다. -```html - +```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 ; +} ``` - React는 기본적으로 문자열을 escape해주기 때문에 많은 XSS를 막아줍니다. 하지만 - `dangerouslySetInnerHTML`처럼 HTML을 직접 넣는 기능을 쓰거나, 외부 - markdown/html을 렌더링할 때는 조심해야 합니다. + 판단 기준은 하나입니다. 이 컴포넌트가 클릭, 입력, 상태 변화를 다루는가?{" "} + 그렇지 않다면 Server Component로 두는 게 낫습니다. JS 번들에 포함되지 않으니 + 번들 크기가 줄고, Hydration 대상도 줄어 성능이 좋아집니다. +| 구분 | Server Component | Client Component | +| :--------------- | :----------------- | :------------------- | +| 실행 위치 | 서버 | 브라우저 | +| useState, onClick | ❌ 불가 | ✅ 가능 | +| DB 직접 접근 | ✅ 가능 | ❌ 불가 | +| JS 번들 포함 | ❌ 포함 안 됨 | ✅ 포함됨 | + --- - - 6. CSRF — 사용자가 원하지 않은 요청이 전송되는 문제 - +7\. Web Vitals — "빠르다"를 숫자로 말하기 + + + Google이 정한 사용자 체감 성능의 3가지 핵심 지표입니다. 검색 순위(SEO)에도 + 실제로 영향을 줍니다. + + +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) - CSRF는 사용자가 로그인된 상태를 이용해, 사용자가 의도하지 않은 요청을 보내게 - 만드는 공격입니다. 쿠키 기반 인증에서는 브라우저가 쿠키를 자동으로 붙여 보내기 - 때문에 특히 신경 써야 합니다. + 사용자가 클릭·입력한 뒤 화면이 반응하기까지의 시간. +``` +사용자가 버튼 클릭 + │ JS 이벤트 핸들러 실행 + │ 상태 업데이트 + │ DOM 변경 + 화면 다시 그림 ← 여기까지가 INP + +좋음: ≤ 200ms / 나쁨: > 500ms +``` + +- Hydration이 무거우면 JS 메인 스레드가 막혀서 INP가 나빠집니다. +- Server Component를 늘리면 Hydration 대상이 줄어 INP가 개선됩니다. + +CLS (Cumulative Layout Shift) + - SameSite Cookie, CSRF Token, Origin/Referer 검증 같은 대응이 있습니다. 오늘 - 전부 외울 필요는 없습니다. 핵심은 "브라우저가 자동으로 인증 정보를 붙여 보내는 - 구조에서는 이런 공격이 가능하다"는 감각입니다. + 페이지 로딩 중 레이아웃이 얼마나 흔들리는가. 광고가 갑자기 + 끼어들어서 읽던 텍스트가 밀리는 경험이 대표적입니다. +``` +좋음: ≤ 0.1 / 나쁨: > 0.25 +``` + +- 흔한 원인: 이미지 크기 미지정, 폰트 로딩 후 텍스트 크기 변화 +- 해결: `width`/`height` 명시, Skeleton으로 공간 미리 확보 + +| 지표 | 측정하는 것 | 좋음 기준 | 주요 영향 요소 | +| :--- | :----------------- | :-------- | :-------------------------------- | +| LCP | 콘텐츠 표시 속도 | ≤ 2.5s | 렌더링 방식(SSG/SSR vs CSR), 이미지 크기 | +| INP | 인터랙션 반응 속도 | ≤ 200ms | JS 번들 크기, Hydration 무게 | +| CLS | 레이아웃 안정성 | ≤ 0.1 | 이미지 크기, Skeleton, 폰트 | + --- -7. API 키를 프론트에 넣으면 안 되는 이유 +8\. 퍼포먼스 측정하는 방법 + + + Web Vitals는 실제로 어떻게 측정할까요? Chrome DevTools에 이미 다 있습니다. + + +Lighthouse + + + Chrome DevTools → Lighthouse 탭 → "Analyze page load" 버튼. + LCP, INP, CLS를 포함한 퍼포먼스 점수와 개선 제안을 한번에 볼 수 있습니다. + + +- Mode: Navigation — 페이지 첫 로딩 시 측정 (가장 일반적) +- Device: Mobile로 체크하면 더 엄격하게 측정됨 + +Performance 탭 + + + Chrome DevTools → Performance 탭 → 녹화 버튼 후 페이지 조작. + 타임라인에서 어느 시점에 뭐가 실행됐는지, 어디서 병목이 생겼는지 볼 수 + 있습니다. + + +Network 탭으로 렌더링 방식 확인하기 - 프론트엔드 번들에 들어간 값은 결국 사용자에게 전달됩니다. `.env`에 넣었다고 - 안전한 것이 아닙니다. 빌드 시점에 클라이언트 코드로 들어간 API 키는 - 브라우저에서 확인될 수 있습니다. + Network 탭에서 첫 HTML 응답을 열어보면 렌더링 방식을 가늠할 수 있습니다. +- HTML 응답에 내용이 가득 → SSR 또는 SSG +- HTML 응답에 빈 div만 → CSR + +실제로 해보기 + +1. 크롬에서 아무 사이트나 열고 DevTools → Lighthouse 실행 +2. LCP / INP / CLS 점수 확인 +3. "Opportunities" 섹션에서 개선 제안 읽기 +4. Network 탭으로 첫 HTML 응답 확인 — 내용이 있는지 없는지 + +--- + +9\. 정리 — 렌더링 선택의 기준 + - 외부 서비스 키나 숨겨야 할 토큰은 서버, BFF, 서버리스 함수 같은 신뢰할 수 있는 - 계층에서 다뤄야 합니다. + 어떤 렌더링 방식을 쓸지는 "이 페이지의 데이터가 얼마나 자주 바뀌는가"와 + "인터랙션이 중요한가"로 판단합니다. +- 데이터가 거의 안 바뀐다 → SSG +- 실시간 데이터가 필요하다 → SSR +- 가끔 바뀌는 데이터 → ISR +- 페이지 일부만 실시간 → PPR + Streaming +- 클릭·입력이 필요한 컴포넌트만 → Client Component + +> 렌더링 방식을 잘 선택하는 것 자체가 프론트엔드 성능 최적화의 절반입니다. + --- -8. 다음 주 안내 +10\. 다음 주 안내 — 10주차 "팀 협업" - 다음 대면에서는 팀 협업을 다룹니다. Git, PR, 컨벤션, 리뷰 문화처럼 혼자 공부할 - 때는 잘 와닿지 않지만 팀 프로젝트에서 바로 필요한 것들을 봅니다. + 다음 주에는 팀 협업을 다룹니다. Git, PR, 컨벤션, 리뷰 문화처럼 혼자 공부할 + 때는 잘 와닿지 않지만, 팀 프로젝트에서 바로 필요한 것들입니다. + +관련 포스팅 + +- + 이번 주 학습 자료(week9) + +- + vinext는 왜 빠를까? — SSR, Vite, Edge, Web Vitals까지 (참고) + From eebd90d27aa0b5453ed0e8b4d6d3670c6d3086ff Mon Sep 17 00:00:00 2001 From: seojing Date: Mon, 11 May 2026 14:43:06 +0900 Subject: [PATCH 2/2] =?UTF-8?q?style:=20prettier=20=ED=8F=AC=EB=A7=B7=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../study/clab-26-1/in-person/week7.mdx | 48 ++++++------- .../study/clab-26-1/in-person/week9.mdx | 71 ++++++++++--------- 2 files changed, 63 insertions(+), 56 deletions(-) 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 d7b246a..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 @@ -66,8 +66,8 @@ badge.classList.toggle("hidden", n === 0); ``` - React는 선언형(declarative)입니다. 화면이 어떻게 보여야 하는지를 - 작성하면, 상태가 바뀔 때 React가 화면을 알아서 맞춥니다. + React는 선언형(declarative)입니다. 화면이 어떻게 보여야 + 하는지를 작성하면, 상태가 바뀔 때 React가 화면을 알아서 맞춥니다. ```tsx @@ -157,8 +157,8 @@ function StudyPage() { 컴포넌트는 상태(state)가 바뀌거나 props가 - 바뀌면 다시 실행됩니다. 다시 실행되면 새로운 화면을 반환하고, React가 그 결과를 - 실제 화면에 반영합니다. + 바뀌면 다시 실행됩니다. 다시 실행되면 새로운 화면을 반환하고, React가 그 + 결과를 실제 화면에 반영합니다. ```tsx @@ -176,16 +176,16 @@ function Counter() { 버튼을 누르면 setCount가 호출됩니다. 상태가 바뀌었으니 React는 - Counter를 다시 실행해서 새로운 화면을 만들고, <p> 안의 숫자만 - 바꿔줍니다. + Counter를 다시 실행해서 새로운 화면을 만들고, <p> 안의 + 숫자만 바꿔줍니다. Virtual DOM — 왜 빠른가 React는 실제 DOM을 바로 바꾸지 않습니다. 대신 메모리 안에{" "} - 가상 DOM(Virtual DOM)을 만들어서 이전 상태와 비교한 뒤, 달라진 - 부분만 실제 DOM에 반영합니다. + 가상 DOM(Virtual DOM)을 만들어서 이전 상태와 비교한 뒤, + 달라진 부분만 실제 DOM에 반영합니다. - 전체를 다시 그리지 않고, 변경된 부분만 업데이트합니다. @@ -563,19 +563,19 @@ apps/member/src } ``` -| 영역 | 왜 필요하나 | 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 | +| 영역 | 왜 필요하나 | 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 | 각 도구가 어떤 책임을 대신 맡고 있는지를 보는 것입니다. @@ -668,9 +668,9 @@ function StudyPage() { "서버에서 뭘 그리고, 클라이언트에서 뭘 그리냐"는 질문은 앞으로 점점 중요해지는 - 감각입니다. 자세한 내용은 나중에 웹 퍼포먼스를 다루는 편에서 제대로 볼 예정이고, - 지금은 "React를 더 편하게 쓰기 위한 틀" 정도로 이해해두면 - 충분합니다. + 감각입니다. 자세한 내용은 나중에 웹 퍼포먼스를 다루는 편에서 제대로 볼 + 예정이고, 지금은 "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 caa85f9..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 @@ -33,7 +33,8 @@ description: CSR부터 SSR, SSG, ISR, PPR까지 Next.js 렌더링 전략의 진 2\. CSR — 브라우저가 직접 그리는 방식 - React로 만든 앱을 아무 설정 없이 배포하면 CSR(Client-Side Rendering) + React로 만든 앱을 아무 설정 없이 배포하면{" "} + CSR(Client-Side Rendering) 방식으로 동작합니다. 서버는 빈 HTML과 JS 파일만 보내고, 브라우저가 JS를 실행해서 직접 화면을 만듭니다. @@ -73,11 +74,11 @@ description: CSR부터 SSR, SSG, ISR, PPR까지 Next.js 렌더링 전략의 진 서버에서 HTML 완성 → 완성된 HTML 전송 → 화면 즉시 표시 → JS 다운로드 → Hydration ``` -| 지표 | CSR | SSR | -| :--------------------------- | :---------------- | :-------------------- | +| 지표 | CSR | SSR | +| :---------------------------- | :---------------- | :-------------------- | | FCP (첫 콘텐츠가 보이는 시점) | 느림 (JS 실행 후) | 빠름 (HTML 도착 즉시) | -| SEO | 불리 (빈 HTML) | 유리 (완성된 HTML) | -| 서버 부하 | 없음 | 매 요청마다 생성 | +| SEO | 불리 (빈 HTML) | 유리 (완성된 HTML) | +| 서버 부하 | 없음 | 매 요청마다 생성 | SSR이 "빠르다"는 건 정확히는{" "} @@ -120,7 +121,9 @@ description: CSR부터 SSR, SSG, ISR, PPR까지 Next.js 렌더링 전략의 진 CSR → SSR → SSG → ISR → Streaming SSR → PPR ``` -SSG (Static Site Generation) — 빌드 시 미리 만들기 + + SSG (Static Site Generation) — 빌드 시 미리 만들기 + 서버가 요청마다 HTML을 만드는 SSR과 달리, SSG는{" "} @@ -136,11 +139,14 @@ CSR → SSR → SSG → ISR → Streaming SSR → PPR - 장점: 가장 빠름, 서버 부하 없음 - 단점: 데이터가 바뀌면 전체를 다시 빌드해야 함 -ISR (Incremental Static Regeneration) — 필요할 때만 갱신 + + ISR (Incremental Static Regeneration) — 필요할 때만 갱신 + - SSG의 단점을 해결합니다. 일정 시간마다 그 페이지만 조용히 - 재생성합니다. 1000페이지 중 요청이 온 1페이지만 갱신되는 식입니다. + SSG의 단점을 해결합니다.{" "} + 일정 시간마다 그 페이지만 조용히 재생성합니다. 1000페이지 중 + 요청이 온 1페이지만 갱신되는 식입니다. ```tsx @@ -177,7 +183,9 @@ export const revalidate = 60; // 60초마다 재생성 UI입니다. -PPR (Partial Prerendering) — 컴포넌트별로 정적/동적 분리 + + PPR (Partial Prerendering) — 컴포넌트별로 정적/동적 분리 + ISR은 페이지 전체를 정적 또는 동적으로 취급합니다. PPR은 한 페이지 안에서{" "} @@ -204,12 +212,12 @@ export default function ProductPage() { 전략 비교 -| | 속도 | 데이터 신선함 | 어울리는 콘텐츠 | -| :-- | :--------- | :------------- | :-------------------------- | +| | 속도 | 데이터 신선함 | 어울리는 콘텐츠 | +| :-- | :--------- | :------------- | :----------------------------- | | SSG | 가장 빠름 | 빌드 시점 고정 | 블로그, 문서, 변경 없는 페이지 | -| SSR | 느림 | 항상 최신 | 로그인 필요, 실시간 데이터 | -| ISR | SSG와 동일 | 약간의 지연 | 블로그, 상품 목록 | -| PPR | SSG급 | 컴포넌트별 | 상품 상세, 대시보드 | +| SSR | 느림 | 항상 최신 | 로그인 필요, 실시간 데이터 | +| ISR | SSG와 동일 | 약간의 지연 | 블로그, 상품 목록 | +| PPR | SSG급 | 컴포넌트별 | 상품 상세, 대시보드 | --- @@ -236,7 +244,7 @@ async function PostList() { // Client Component ('use client' 선언 필요) // 브라우저에서 실행됨. onClick, useState 사용 가능. -"use client"; +("use client"); function LikeButton() { const [liked, setLiked] = useState(false); return ; @@ -244,17 +252,18 @@ function LikeButton() { ``` - 판단 기준은 하나입니다. 이 컴포넌트가 클릭, 입력, 상태 변화를 다루는가?{" "} - 그렇지 않다면 Server Component로 두는 게 낫습니다. JS 번들에 포함되지 않으니 - 번들 크기가 줄고, Hydration 대상도 줄어 성능이 좋아집니다. + 판단 기준은 하나입니다.{" "} + 이 컴포넌트가 클릭, 입력, 상태 변화를 다루는가? 그렇지 않다면 + Server Component로 두는 게 낫습니다. JS 번들에 포함되지 않으니 번들 크기가 + 줄고, Hydration 대상도 줄어 성능이 좋아집니다. -| 구분 | Server Component | Client Component | -| :--------------- | :----------------- | :------------------- | -| 실행 위치 | 서버 | 브라우저 | -| useState, onClick | ❌ 불가 | ✅ 가능 | -| DB 직접 접근 | ✅ 가능 | ❌ 불가 | -| JS 번들 포함 | ❌ 포함 안 됨 | ✅ 포함됨 | +| 구분 | Server Component | Client Component | +| :---------------- | :--------------- | :--------------- | +| 실행 위치 | 서버 | 브라우저 | +| useState, onClick | ❌ 불가 | ✅ 가능 | +| DB 직접 접근 | ✅ 가능 | ❌ 불가 | +| JS 번들 포함 | ❌ 포함 안 됨 | ✅ 포함됨 | --- @@ -315,11 +324,11 @@ function LikeButton() { - 흔한 원인: 이미지 크기 미지정, 폰트 로딩 후 텍스트 크기 변화 - 해결: `width`/`height` 명시, Skeleton으로 공간 미리 확보 -| 지표 | 측정하는 것 | 좋음 기준 | 주요 영향 요소 | -| :--- | :----------------- | :-------- | :-------------------------------- | +| 지표 | 측정하는 것 | 좋음 기준 | 주요 영향 요소 | +| :--- | :----------------- | :-------- | :--------------------------------------- | | LCP | 콘텐츠 표시 속도 | ≤ 2.5s | 렌더링 방식(SSG/SSR vs CSR), 이미지 크기 | -| INP | 인터랙션 반응 속도 | ≤ 200ms | JS 번들 크기, Hydration 무게 | -| CLS | 레이아웃 안정성 | ≤ 0.1 | 이미지 크기, Skeleton, 폰트 | +| INP | 인터랙션 반응 속도 | ≤ 200ms | JS 번들 크기, Hydration 무게 | +| CLS | 레이아웃 안정성 | ≤ 0.1 | 이미지 크기, Skeleton, 폰트 | --- @@ -391,9 +400,7 @@ function LikeButton() { 관련 포스팅 -- - 이번 주 학습 자료(week9) - +- 이번 주 학습 자료(week9) - vinext는 왜 빠를까? — SSR, Vite, Edge, Web Vitals까지 (참고)