[7팀 황준태] Chapter 4-1 성능최적화: SSR, SSG, Infra#27
Open
jthw1005 wants to merge 10 commits intohanghae-plus:mainfrom
Open
[7팀 황준태] Chapter 4-1 성능최적화: SSR, SSG, Infra#27jthw1005 wants to merge 10 commits intohanghae-plus:mainfrom
jthw1005 wants to merge 10 commits intohanghae-plus:mainfrom
Conversation
|
수료하고 여유로울때 다시보니까 또 한 번 감탄 + 학습하게 된다 ㅎㅎ 좋은 PR 땡큐 준태햄~ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
과제 체크포인트
배포 링크
vanilla
react
기본과제 (Vanilla SSR & SSG)
Express SSR 서버
<!--app-html-->,<!--app-head-->)서버 사이드 렌더링
클라이언트 Hydration
window.__INITIAL_DATA__스크립트 주입Static Site Generation
심화과제 (React SSR & SSG)
React SSR
renderToString서버 렌더링React Hydration
Static Site Generation
자유롭게 회고하기
SSR이나 SSG, ISR 등의 개념만 어렴풋이 알고 있었지 직접 구현해보는건 처음이라 재밌을거 같았지만 어디서부터 시작해야할지 전혀 감이 잡히지 않았다.
직접 구현할 수 없다면 제대로 알고 있는게 아닌거 같아서 기초부터 다시 하나하나 공부하면서 과제를 진행했고, 그 과정에서 정말 많이 배운 것 같다.
1. CSR & SSR & SSG 구조 비교
CSR
sequenceDiagram participant Browser participant Server participant API Browser->>Server: HTML 요청 Server-->>Browser: 빈 HTML + JS 번들 Browser->>Browser: JS 실행, React 마운트 Browser->>API: 데이터 요청 API-->>Browser: JSON 응답 Browser->>Browser: 화면 렌더링SSR
sequenceDiagram participant Browser participant Server participant API Browser->>Server: HTML 요청 Server->>API: 데이터 미리 가져오기 API-->>Server: JSON 응답 Server->>Server: HTML 문자열 생성 Server-->>Browser: 완성된 HTML + 초기 데이터 Browser->>Browser: Hydration (이벤트 연결)SSG
sequenceDiagram participant Browser participant CDN participant Build as 빌드 타임 Note over Build,CDN: 배포 전 (빌드 시점) Build->>Build: 데이터 가져오기 + HTML 생성 Build->>CDN: 정적 파일 업로드 Note over Browser,CDN: 사용자 접속 Browser->>CDN: HTML 요청 CDN-->>Browser: 미리 생성된 HTML + 초기 데이터 Browser->>Browser: Hydration (이벤트 연결)위 다이어그램은 CSR, SSR, SSG 등을 어렴풋이 알고만 있을 때 이해했던 각 렌더링 과정이다.
이를 직접 구현하려고 보니 아래와 같은 질문들이 쏟아졌다.
1. HTTP 응답은 항상 바이트 스트림(문자열)이다.
2. JS 번들이 여러 개인지는 코드 스플리팅을 어떻게 설정했는지에 따라 다름.
보통 공통적으로 모든 페이지에서 사용하는 js 파일이 있고, 각 html 페이지마다 사용하는 js파일이 따로 있다.
3. fetching된 데이터를 기존 HTML에 어떻게 추가하지?
React 기준 renderToString 메서드를 이용해서 HTML을 만들고 미리 삽입해둔 문자열()과 교체해준다.
4. fetching 해야할 데이터를 어떻게 알아내지?
1. Route에서 수동으로 매핑
2. 컴포넌트에 메타 데이터로 추가
3. Next.js의 처리 방식
파일 기반 라우팅으로 자동 매핑됨.
Next.js 서버 내부 동작
-> Remix는 다르게 처리됨. 즉, 정해진 방식이 없이 자유롭게 구현할 수 있음.
5.
window.__INITIAL_DATA__vswindow.__NEXT_DATA__차이?이건 단순히 변수명의 차이 일뿐, 어떤 이름을 써도 상관이 없다.
(처음 하다 보니 이런 것도 괜히 궁금함...)
6. Vite 미들웨어 모드
개발 환경에서 Vite를
middlewareMode로 써야 한다는 게 이해가 안 됐다. 왜 그냥 Vite 개발 서버를 쓰면 안 되는 거지?Vite 개발 서버는 기본적으로
index.html을 그대로 던져준다. 근데 SSR은 우리가 HTML을 직접 조작해서 보내야 하잖아. 그래서 Vite의 기능(HMR, 모듈 해석 등)만 빌려 쓰고, 실제 요청 처리는 Express가 해야 하는 것.개발/프로덕션 분기
처음엔 그냥 똑같이 하면 되지 않나 싶었는데, 생각해보니 당연함. 개발 중에는 코드 바꿀 때마다 반영되어야 하고, 프로덕션에서는 빌드된 최적화 코드를 써야지.
7. 서버 라우터 구현
클라이언트 라우터는
popstate이벤트 듣고,window.location읽고 하면 되는데 서버에는window가 없음;ServerRouter
=> 서버 라우터는 진짜 단순하게 만들면 됨. 이벤트 리스너, 구독 같은 거 필요 없고, 그냥 URL 받아서 매칭되는 라우트 찾아주면 끝.
(클라이언트 라우터가 복잡했던 건 상태 변화를 추적해야 하기 때문.)
8. 서버 Store 격리
근데 서버에서는:
SSR에서는 요청마다 격리된 상태가 핵심. A 사용자의 요청이 B 사용자에게 영향 주면 안 되니까.
5. main-server.js - SSR 관련 로직이 있는 곳
flowchart TD A[URL 요청] --> B[URL 파싱] B --> C{라우트 매칭} C -->|홈| E[상품 목록 + 카테고리 조회] C -->|상세| F[상품 상세 + 관련 상품 조회] C -->|404| G[에러 페이지] E --> H[Store에 데이터 세팅] F --> H G --> H H --> I[HTML 문자열 생성] I --> J[head 메타 태그 생성] J --> K[initialData 반환]6. Hydration
flowchart LR A[서버 HTML] --> B{클라이언트에서} B --> C[HTML 파싱해서 화면 표시] B --> D[JS 로드 및 실행] D --> E[__INITIAL_DATA__ 읽기] E --> F[Store 상태 복원] F --> G[React hydrate] G --> H[이벤트 핸들러 연결]window.__INITIAL_DATA__주입클라이언트에서 복원
main.tsx에서 분기 처리
Hydration mismatch 에러가 왜 나는지 드디어 이해함. 서버에서 렌더링한 HTML과 클라이언트에서 첫 렌더링할 때의 HTML이 다르면 React가 경고를 뱉는 거였다. 그래서 같은 데이터로 같은 결과가 나와야 함.
7. SSG 구현은 어떻게 할까
SSG는 그냥 SSR을 빌드 타임에 돌리는 것. 진짜 이게 다였다.
서버에서 요청 올 때마다 렌더링하는 게 아니라, 미리 다 만들어놓고 정적 파일로 서빙하니까 CDN에 올려도 되고, 서버 부하도 없다는 장점이 있음.
8. React SSR - TypeScript + Universal Router
React SSR은 Vanilla보다 조금 더 복잡했다.
renderToString쓰면 되는 건 알았는데, 문제는 라우터.서버용 React Router
renderToString 사용
Lesson Learned 총정리
1. SSR은 결국 문자열 조작이다
처음에 SSR을 너무 복잡하게 생각했다. "서버에서 React를 돌린다"는 게 대체 무슨 말인지, 가상 DOM이 서버에서 어떻게 동작하는지 막연하게 어렵게만 느껴졌음.
근데 막상 구현해보니까 핵심은 간단했다:
이게 다임. React 컴포넌트를 실행해서 HTML 문자열을 뽑아내는 것. DOM API 호출 없이 순수하게 문자열만 만들어냄. 그래서 서버에서 돌릴 수 있는 거였다.
오히려 복잡한 건 주변 인프라(빌드 설정, 데이터 페칭, Hydration)지, SSR 자체는 문자열 생성이 핵심.
2. 요청 간 격리가 생명이다
이건 실수하면 진짜 큰일 나는 부분이다. 처음에 별 생각 없이 클라이언트 Store를 서버에서도 그대로 썼다가 문제를 깨달음.
문제 상황:
A 유저 요청 처리 중에 B 유저 요청이 들어오면? Store에 B 유저 데이터가 덮어씌워지고, A 유저는 B 유저 데이터가 담긴 HTML을 받게 됨. 보안 이슈이자 버그.
해결:
Node.js는 싱글 스레드지만 비동기로 여러 요청을 동시에 처리함. 그래서 전역 상태 쓰면 요청 간 데이터가 섞일 수 있다. 이건 클라이언트 개발할 때는 고려 안 해도 되는 부분이라 처음엔 생각도 못 했다.
3. Hydration은 "이어받기"다
서버에서 HTML 만드는 건 쉬운데, 클라이언트가 그걸 받아서 React 앱으로 이어받는게 어려움.
Hydration이 하는 일:
왜 어렵지?
서버에서 렌더링한 HTML과 클라이언트에서 첫 렌더링하려는 HTML이 완전히 동일해야 함. 한 글자라도 다르면 React가 경고를 뱉고, 심하면 전체를 다시 렌더링함
이번 과제에서
window.__INITIAL_DATA__로 서버 데이터를 클라이언트에 전달한 이유도 이거다. 같은 데이터로 렌더링해야 같은 결과가 나오니까.실제로 category 필드의 순서가 달랐어서 테스트 계속 실패했었는데, 디버깅하느라 시간 은근히 뺏김.
4. "Universal" 코드 작성법
서버와 클라이언트 양쪽에서 돌아가는 코드를 작성하는 건 생각보다 신경 쓸 게 많았다.
window 타입의 존재 유무로 환경을 판단한다.
서버에서 안 되는 것들:
조건부 실행 패턴:
라우터도 분기:
5. 빌드 과정에 대한 정리
처음에 package.json에 있는 빌드 스크립트 이해하는데만 꽤나 시간이 걸렸다;
클라이언트 빌드 (브라우저용):
main.tsx서버 빌드 (Node.js용):
main-server.tsx왜 다를까?
react-dom/client와react-dom/server는 완전히 다른 코드다. 하나는 DOM 조작하고, 하나는 문자열 생성함. 그래서 빌드도 따로 해야 하는 것.6. SSG는 "미리 돌려놓은 SSR"
SSG는 생각보다? 간단했다.
그래서 SSG의 장점:
단점:
7. 데이터 페칭 타이밍
CSR에서는 컴포넌트 마운트 후
useEffect에서 데이터 가져왔다. SSR에서는 렌더링 전에 데이터를 다 가져와야 함.이게 Next.js의
getServerSideProps나getStaticProps가 하는 일이다. 페이지 렌더링 전에 데이터를 미리 가져오는 것. 이번 과제 하면서 프레임워크가 왜 저런 API를 제공하는지 이해됨.8. 에러 처리가 더 중요해진다
CSR에서 에러 나면 그 컴포넌트만 안 보이거나, Error Boundary가 잡아줌. SSR에서 에러 나면?
서버가 죽으면 모든 사용자가 영향 받음. 그래서 SSR 코드는 에러 처리를 더 꼼꼼하게 해야 함.
특히 데이터 페칭 실패 시:
9. 디버깅,,,
CSR은 브라우저 개발자 도구에서 확인할 수 있다. 서버는,
curl찍어봐야 함디버깅 팁:
nodemon..
마무리
SSR이 왜 "은탄환"이 아닌지 이제 알겠다. 성능 개선, SEO 이점이 있지만, 복잡도가 확실히 올라감. 모든 프로젝트에 SSR이 필요한 건 아니다.
실제로 회사에서 SSR을 많이들 쓰고 있는데, 한 번 더 생각해보는 계기가 되었다.
SSR이 필요한 경우:
+이번 과제 하면서 "아 그래서 Next.js가 이렇게 해주는 거구나" 싶은 순간이 많았다. 프레임워크가 추상화해주는게 얼마나 많은지 직접 해보니까 체감되었음..
추후 학습 예정
리뷰 받고 싶은 내용
서버에서 Store를 매 요청마다 생성하는 방식이 맞을까요? - 요청이 많아지면 메모리 부담이 클 것 같은데, 다른 방식이 있는지 궁금합니다. 요청 보낸 직후 해제한다거나 하면 될까요?
Hydration 불일치 방지를 위한 더 좋은 패턴? - 현재는 조심해서 코드 짜는 수밖에 없는데, 구조적으로 방지하는 방법이 있는지.
ISR 전략을 취하는 경우 정해진 주기마다 스크립트 파일을 돌리는건가요? on-demand 방식이라면 stale 같은 상태를 페이지 별로 관리해야하는건가요?
window.__INITIAL_DATA__와 같은 방식으로 데이터를 내려주게 되면 보안상의 문제가 있을거 같은데, 민감한 데이터나 토큰 같은 정보들은 어떻게 내려줘야할까요?