diff --git "a/week-07/\352\271\200\353\257\274\355\230\201.md" "b/week-07/\352\271\200\353\257\274\355\230\201.md" new file mode 100644 index 0000000..9872801 --- /dev/null +++ "b/week-07/\352\271\200\353\257\274\355\230\201.md" @@ -0,0 +1,748 @@ +# ESLint를 활용한 정적 코드 분석 + +## 정적 코드 분석이란? + +정적 코드 분석은 코드를 실제로 실행하지 않고, 문제의 소지가 있는 코드를 사전에 수정하는 것을 의미한다. + +예를 들어 다음과 같은 코드가 있다고 해보자. + +```ts +function getUserName(user) { + return user.name; +} +``` + +이 코드는 `user`가 정상적으로 들어온다면 문제 없이 동작한다. 하지만 `user`가 `undefined`라면 런타임에서 에러가 발생한다. + +```ts +// Cannot read properties of undefined +``` + +모든 문제를 정적 분석으로 잡을 수 있는 것은 아니지만, 코드 실행 전에 발견할 수 있는 문제들도 많다. + +예를 들어 다음과 같은 문제는 ESLint 같은 정적 분석 도구로 미리 확인할 수 있다. + +```txt +- 사용하지 않는 변수 +- 선언되지 않은 변수 +- 잘못된 import +- useEffect 의존성 배열 누락 +``` + +즉, ESLint는 코드가 실행되기 전에 문제 가능성이 있는 부분을 미리 알려주는 안전장치라고 볼 수 있다. + +--- + +## eslint-plugin과 eslint-config + +`eslint-plugin` 이라는 접두사로 시작하는 플러그인은 여러 규칙(rule)을 제공하는 패키지이고, + +`eslint-config`는 이러한 `eslint-plugin` 묶어둔 패키지이다. + +예를 들어 React 프로젝트에서는 `eslint-plugin-react`, `eslint-plugin-react-hooks` 등을 사용해 React와 Hooks 관련 규칙을 검사할 수 있다. + +--- + +## 나만의 ESLint 규칙 만들기 + +ESLint의 장점은 이미 만들어진 규칙만 사용하는 것이 아니라, 프로젝트에 맞는 규칙을 직접 만들 수도 있다는 점이다. + +예를 들어 React 17부터는 새로운 JSX Transform이 도입되어 JSX를 사용하기 위해 매번 `import React from 'react';` 을 선언할 필요가 없다. + +기존 코드베이스에 이 구문이 많이 남아 있다면, ESLint의 `no-restricted-imports` 규칙을 활용해 기본 import를 제한할 수 있다. + +```js +export default [ + { + rules: { + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: 'react', + importNames: ['default'], + message: + 'React 17 이후 JSX 사용을 위한 기본 React import는 필요하지 않습니다.', + }, + ], + }, + ], + }, + }, +]; +``` + +--- + +## ESLint와 Prettier의 차이 + +| 구분 | Prettier | ESLint | +| -------- | ---------------------- | ---------------------------- | +| 핵심 역할 | 코드 스타일 및 포맷 정리 | 코드 품질 및 위험 요소 점검 | +| 주요 목적 | 코드 형태의 일관성 유지 | 잠재적인 버그와 잘못된 패턴 방지 | +| 관리 대상 | 줄바꿈, 들여쓰기, 따옴표, 세미콜론 등 | 사용하지 않는 변수, 잘못된 조건문, 전역 변수 등 | +| 적용 방식 | 저장 시 또는 커밋 전 자동 포맷 적용 | 규칙 위반 시 경고 또는 에러 표시 | +| 팀 운영 관점 | 스타일 논쟁 최소화 | 팀의 코드 기준을 규칙으로 고정 | +| 대체 가능 여부 | ESLint로 대체 불가 | Prettier로 대체 불가 | + +--- + + +# React Testing Library + +React Testing Library란 리액트 공식 문서에서도 사용을 권장하는 UI 컴포넌트 테스트 라이브러리이다. + +컴포넌트의 내부 상태나 구현 방식이 아닌, **사용자가 화면에서 실제로 상호작용하는 방식**을 그대로 테스트하도록 설계된 것이 가장 큰 특징이다. + +예를 들어 다음과 같은 컴포넌트가 있다고 해보자. + +```tsx +function LoginForm() { + const [email, setEmail] = useState(''); + + return ( + <> + setEmail(e.target.value)} + /> + + + ); +} +``` + +이 컴포넌트를 테스트한다고 했을 때 이런 생각을 하기 쉽다. + +```txt +- state 값이 변경되었는가? +- setEmail 함수가 호출되었는가? +``` + +하지만 사용자는 이런 것을 전혀 알지 못한다. + +
+ +사용자는 + +'입력 창이 보인다', '이메일을 입력한다', '로그인 버튼이 활성화 된다.' 등 + +실제로 보이는 것에 의존한다. + +즉, 테스트도 **사용자가 경험하는 흐름**을 기준으로 작성해야 한다는 것이 React Testing Library의 핵심 철학이다. + +--- + +## React Testing Library 예시 + +예를 들어 로그인 폼이 있다고 해보자. + +```tsx +export function LoginForm() { + const [email, setEmail] = useState(''); + + return ( +
+ + + setEmail(e.target.value)} + /> + + +
+ ); +} +``` + +이 컴포넌트에서 사용자가 실제로 경험하는 흐름은 다음과 같다. + +```txt +1. 로그인 버튼은 처음에 비활성화 상태다. +2. 이메일을 입력한다. +3. 로그인 버튼이 활성화된다. +``` + +테스트도 그대로 작성한다. + +```tsx +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +test('이메일을 입력하면 로그인 버튼이 활성화된다.', async () => { + const user = userEvent.setup(); + + render(); + + const input = screen.getByLabelText('이메일'); + const button = screen.getByRole('button', { + name: '로그인', + }); + + expect(button).toBeDisabled(); + + await user.type(input, 'test@example.com'); + + expect(button).toBeEnabled(); +}); + +``` + +
+ +작성된 테스트 코드를 보면, 앞서 정리한 사용자의 행동 흐름이 그대로 녹아있다는 것을 알 수 있다. + +* ``: 사용자가 화면에서 '이메일'이라는 라벨을 보고 입력창을 찾는 과정을 뜻한다. +* ``: 사용자가 실제로 키보드를 두드려 이메일을 입력하는 행위를 시뮬레이션한다. +* ``: 이메일이 입력된 후, 사용자 눈에 버튼이 활성화되어 보이는지 검증한다. + +테스트 어디에서도 `email`이라는 state가 잘 바뀌었는지, `setEmail` 함수가 실행되었는지는 확인하지 않는다. + +오직 **사용자에게 무엇이 보이고, 사용자가 어떤 상호작용을 하는지**에만 집중할 뿐이다. + +--- + +
+ +# 핵심 웹 지표란? + +핵심 웹 지표(Core Web Vitals)는 구글에서 만든 지표로, 웹페이지의 사용자 경험을 측정하기 위해 제시한 성능 지표다. + +핵심 웹 지표로 다음 세 가지를 다룬다. + +* LCP: Largest Contentful Paint (최대 콘텐츠풀 페인트) +* FID: First Input Delay (최초 입력 지연) +* CLS: Cumulative Layout Shift (누적 레이아웃 이동) + +다만 현재 기준에서는 `FID`가 `INP`로 대체되었다. + +따라서 `FID`를 먼저 이해하되, 실제 서비스 성능을 점검할 때는 `INP`까지 함께 확인하는 것이 좋다. + +--- + +# LCP (최대 콘텐츠풀 페인트) + +## LCP란? + +`LCP(Largest Contentful Paint)`는 페이지가 처음 로드되기 시작한 시점부터, 뷰포트 안에서 가장 큰 요소가 화면에 렌더링되기까지 걸리는 시간을 의미한다. + +여기서 뷰포트는 사용자가 현재 보고 있는 화면 영역을 말한다. + +LCP의 측정 대상이 될 수 있는 요소는 다음과 같다. + +* `` +* `` 내부의 `` +* `poster` 속성을 사용하는 `` 태그는 HTML 파싱 과정에서 브라우저의 프리로드 스캐너(preload scanner)에 의해 빠르게 발견된다. + +프리로드 스캐너는 HTML 파싱을 차단하지 않고 이미지, 폰트 등 우선적으로 로딩하면 좋은 리소스를 먼저 찾아 병렬로 다운로드하는 기능이다. + +따라서 HTML 파싱이 완료되지 않았더라도 ``는 빠르게 요청될 수 있으며, `` 역시 동일한 방식으로 동작한다. + +반면 SVG 내부에 포함된 이미지 리소스는 상황이 다르다. + +```html + + + +``` + +이 경우 SVG 내부 이미지가 모두 로드되어야 최대 콘텐츠가 완성된 것으로 판단될 수 있다. 따라서 LCP 측면에서는 일반적인 `` 태그보다 불리할 수 있다. + +결론적으로 화면의 주요 이미지라면 단순한 배경 이미지나 SVG 내부 이미지보다 `` 또는 ``를 사용하는 편이 유리하다. + +--- + +### 5. LCP 리소스는 가능하면 동일 출처에서 직접 호스팅하기 + +LCP 후보가 되는 이미지나 비디오 같은 리소스는 가능하면 현재 서비스와 동일한 출처(origin)에서 직접 제공하는 것이 좋다. + +예를 들어 다음과 같은 경우를 생각해볼 수 있다. + +```html + +``` + +브라우저는 해당 리소스를 가져오기 위해 다음 과정을 추가로 수행해야 한다. + +```text +DNS 조회 +→ TCP 연결 +→ TLS 핸드셰이크 +→ 리소스 요청 +→ 응답 수신 +``` + +이미 연결이 맺어져 있는 동일 출처의 리소스보다 네트워크 비용이 더 발생할 수 있다. + +특히 LCP 이미지처럼 사용자 경험에 직접 영향을 주는 리소스라면 외부 출처에서 가져오기보다 서비스에서 직접 호스팅하거나, 최소한 사전에 연결을 준비할 수 있도록 최적화하는 것이 좋다. + +--- + +### 6. LCP 요소에 불필요한 애니메이션 적용하지 않기 + +사용자가 가장 먼저 봐야 하는 콘텐츠에 `fadeIn`, `slideIn` 같은 애니메이션을 적용하면 시각적으로는 자연스러워 보일 수 있다. + +하지만 브라우저는 애니메이션이 완료되어 실제로 콘텐츠가 표시되는 시점을 기준으로 LCP를 측정할 수 있다. + +```css +.hero { + opacity: 0; + animation: fadeIn 1s ease forwards; +} +``` + +위와 같은 경우 사용자는 콘텐츠가 존재하더라도 애니메이션이 끝날 때까지 주요 콘텐츠를 제대로 인식하지 못할 수 있다. + +따라서 LCP 후보가 되는 영역에는 다음을 고려하는 것이 좋다. + +* 불필요한 진입 애니메이션 제거 +* 애니메이션 시간을 최소화 +* 핵심 콘텐츠는 즉시 표시 + +사용자에게 가장 중요한 콘텐츠는 가능한 한 즉시 보여주는 것이 좋다. + +--- + +### 7. video 요소에는 poster를 제공하기 + +`poster`는 사용자가 비디오를 재생하거나 탐색하기 전까지 화면에 표시되는 대표 이미지다. + +```html + +``` + +`poster` 이미지는 ``와 마찬가지로 프리로드 스캐너에 의해 빠르게 발견되어 요청될 수 있다. + +반대로 `poster`가 없는 경우 브라우저는 비디오 자체를 로드한 뒤 첫 번째 프레임을 추출해 화면에 표시해야 한다. 이 과정은 이미지 하나를 불러오는 것보다 비용이 크기 때문에 LCP에 악영향을 줄 수 있다. + +따라서 비디오가 LCP 후보가 될 수 있는 영역이라면 반드시 `poster`를 제공하는 것이 좋다. + +--- + +### 8. 클라이언트에서 LCP 영역을 늦게 만들지 않기 + +다음과 같이 `useEffect` 이후 API 응답을 받아 주요 콘텐츠를 보여주는 구조라면 LCP가 늦어질 수 있다. + +```tsx +function MainContent() { + const [show, setShow] = useState(false); + + useEffect(() => { + async function load() { + const result = await fetch("/api/main"); + + if (result.ok) { + setShow(true); + } + } + + load(); + }, []); + + if (!show) { + return null; + } + + return
메인 콘텐츠
; +} +``` + +이 구조에서는 다음 과정이 모두 끝나야 주요 콘텐츠가 나타난다. + +```text +HTML 로드 +→ JavaScript 다운로드 +→ React 실행 +→ useEffect 실행 +→ API 요청 +→ 응답 수신 +→ 상태 변경 +→ 렌더링 +``` + +LCP 영역은 가능하면 서버에서 미리 렌더링되거나, 초기 HTML에 포함되는 편이 좋다. + +--- + +# FID (최초 입력 지연) + +## FID란? + +`FID(First Input Delay)`는 사용자가 페이지와 처음 상호작용한 시점부터, 브라우저가 해당 이벤트 처리를 시작할 수 있을 때까지 걸리는 시간을 의미한다. + +예를 들어 사용자가 버튼을 클릭했는데 브라우저의 메인 스레드가 바쁘다면, 클릭 이벤트 처리가 바로 시작되지 못한다. +이때 발생하는 지연 시간이 FID다. + +FID는 이벤트 핸들러가 실행되는 데 걸리는 전체 시간이 아니라, **이벤트 처리를 시작하기 전까지의 지연 시간**을 측정한다. + +만약 이벤트 핸들러의 실행 시간을 측정하고 싶다면 Event Timing API를 사용하는 것이 좋다. + +--- + +## FID가 나빠지는 이유 + +FID가 나빠지는 가장 큰 이유는 브라우저의 메인 스레드가 바쁘기 때문이다. + +브라우저의 메인 스레드는 JavaScript 실행, 렌더링, 스타일 계산, 레이아웃 계산 등 많은 작업을 처리한다. + +특히 JavaScript는 기본적으로 싱글 스레드로 동작하기 때문에, 무거운 JavaScript 작업이 실행 중이라면 사용자의 클릭이나 입력 이벤트를 즉시 처리하지 못한다. + +예를 들어 다음과 같은 상황에서 FID가 나빠질 수 있다. + +* 초기 로딩 시 JavaScript 번들이 너무 큼 +* 사용하지 않는 코드가 많이 포함됨 +* 무거운 연산이 메인 스레드를 오래 점유함 +* 타사 스크립트가 초기 로딩 중 실행됨 +* 폴리필이나 라이브러리가 과하게 포함됨 + +--- + +## FID 기준 + +책에서 다루는 FID 기준은 다음과 같다. + +```text +100ms 이하 → 좋음 +100ms ~ 300ms → 개선 필요 +300ms 초과 → 나쁨 +``` + +사용자는 아주 짧은 지연에도 민감하게 반응한다. +따라서 클릭, 입력, 탭 같은 상호작용은 최대한 빠르게 처리될 수 있어야 한다. + +--- + +## FID 개선 방법 + +### 1. Long Task 분리하기 + +메인 스레드를 오래 점유하는 작업을 Long Task라고 한다. + +긴 작업 하나가 메인 스레드를 계속 차지하면, 그동안 사용자의 입력을 처리할 수 없다. +따라서 무거운 작업은 작은 단위로 나누거나, 초기 로딩 이후로 미루는 것이 좋다. + +--- + +### 2. JavaScript 코드 줄이기 + +초기 JavaScript 번들이 클수록 다운로드, 파싱, 실행에 시간이 오래 걸린다. + +React 애플리케이션에서는 다음과 같은 방법을 고려할 수 있다. + +* 코드 스플리팅 +* `React.lazy` +* `Suspense` +* Next.js의 `dynamic import` +* 사용하지 않는 라이브러리 제거 +* 무거운 기능은 필요한 시점에 로드 + +초기 화면에 필요하지 않은 코드는 처음부터 불러오지 않는 것이 좋다. + +--- + +### 3. 타사 스크립트 실행 지연하기 + +Google Analytics, 광고 스크립트, 채팅 위젯, Firebase 등 타사 스크립트는 성능에 영향을 줄 수 있다. + +이런 스크립트가 초기 로딩에 반드시 필요하지 않다면 `async`, `defer`를 사용해 실행 시점을 늦추는 것이 좋다. + +```html + +``` + +--- + +# INP + +## 현재는 FID보다 INP가 중요하다 + +책에서는 FID를 핵심 웹 지표로 설명하지만, 현재 Core Web Vitals에서는 FID 대신 `INP(Interaction to Next Paint)`가 사용된다. + +FID는 사용자의 첫 번째 입력만 측정한다. +하지만 실제 서비스에서는 첫 번째 클릭뿐만 아니라, 페이지를 사용하는 전체 과정에서의 반응성이 중요하다. + +INP는 사용자가 페이지에 머무는 동안 발생한 클릭, 탭, 키보드 입력 등의 상호작용을 관찰하고, 그중 느린 상호작용을 기준으로 페이지의 전반적인 반응성을 평가한다. + +즉, FID가 '첫인상'에 가까운 지표라면, INP는 '페이지를 사용하는 전체 과정에서의 반응성'에 더 가깝다. + +--- + +## INP 기준 + +현재 기준에서 INP는 다음과 같이 판단한다. + +```text +200ms 이하 → 좋음 +200ms ~ 500ms → 개선 필요 +500ms 초과 → 나쁨 +``` + +INP를 개선하려면 결국 사용자의 상호작용에 대해 브라우저가 빠르게 다음 화면을 그릴 수 있어야 한다. + +이를 위해서는 다음을 고려해야 한다. + +* 이벤트 핸들러 내부 작업 줄이기 +* 불필요한 리렌더링 줄이기 +* 무거운 계산은 메모이제이션 또는 Web Worker 고려 +* DOM 크기 줄이기 +* 복잡한 CSS 선택자나 레이아웃 계산 줄이기 +* 상호작용 직후 실행되는 동기 작업 최소화 + +--- + +# CLS (누적 레이아웃 이동) + +## CLS란? + +`CLS(Cumulative Layout Shift)`는 페이지의 생명주기 동안 발생하는 예상치 못한 레이아웃 이동의 누적 점수를 의미한다. + +사용자가 버튼을 누르려고 하는 순간 갑자기 광고 배너가 나타나 버튼 위치가 밀린다면, 사용자는 원하지 않는 요소를 클릭할 수 있다. + +이런 경험은 사용자를 매우 불편하게 만든다. + +CLS는 이런 시각적 불안정성을 측정한다. + +--- + +## CLS가 발생하는 예시 + +다음과 같이 렌더링 이후 비동기 요청 결과에 따라 배너를 추가하는 경우를 생각해볼 수 있다. + +```tsx +function Banner() { + const [show, setShow] = useState(false); + + useEffect(() => { + async function fetchBanner() { + const result = await fetch("/api/banner"); + + if (result.ok) { + setShow(true); + } + } + + fetchBanner(); + }, []); + + if (!show) { + return null; + } + + return
이벤트 진행 중!
; +} +``` + +처음에는 배너가 없다가, API 응답 후 배너가 갑자기 생긴다. +이때 기존 콘텐츠가 아래로 밀리면 레이아웃 이동이 발생한다. + +사용자 입장에서는 보고 있던 콘텐츠 위치가 갑자기 바뀌기 때문에 불안정한 화면으로 느껴진다. + +--- + +## CLS 기준 + +CLS는 다음 기준으로 판단한다. + +```text +0.1 이하 → 좋음 +0.1 ~ 0.25 → 개선 필요 +0.25 초과 → 나쁨 +``` + +CLS는 낮을수록 좋다. + +--- + +## CLS 개선 방법 + +### 1. 이미지 크기 미리 지정하기 + +이미지의 `width`, `height`를 지정하지 않으면, 이미지가 로딩되기 전까지 브라우저가 해당 이미지의 공간을 정확히 알 수 없다. + +이미지가 뒤늦게 로드되면서 주변 콘텐츠가 밀리면 CLS가 발생한다. + +```html +썸네일 +``` + +반응형 이미지라면 `aspect-ratio`를 사용하는 것도 도움이 된다. + +```css +.image-box { + aspect-ratio: 4 / 3; +} +``` + +--- + +### 2. 동적 콘텐츠가 들어올 공간 미리 확보하기 + +광고, 배너, 추천 영역, 알림 영역처럼 나중에 삽입될 수 있는 요소는 미리 공간을 확보해두는 것이 좋다. + +```tsx +function BannerSkeleton() { + return
; +} +``` + +스켈레톤 UI를 사용하면 사용자는 로딩 중임을 인식할 수 있고, 브라우저는 미리 공간을 확보할 수 있다. + +--- + +### 3. 폰트 로딩 최적화하기 + +웹 폰트도 레이아웃 이동의 원인이 될 수 있다. + +폰트 로딩 과정에서 다음과 같은 현상이 발생할 수 있다. + +* FOUT: 기본 폰트가 먼저 보이다가 웹 폰트로 교체되는 현상 +* FOIT: 폰트가 로딩되기 전까지 텍스트가 보이지 않는 현상 + +폰트가 바뀌면서 글자의 크기나 줄바꿈이 달라지면 레이아웃 이동이 발생할 수 있다. + +이를 줄이기 위해 다음 방법을 사용할 수 있다. + +* 중요한 폰트는 `preload`로 먼저 로드 +* `font-display: optional` 또는 `swap` 사용 +* 기본 폰트와 웹 폰트의 크기 차이를 줄이기 + +```css +@font-face { + font-family: "MyFont"; + src: url("/fonts/my-font.woff2") format("woff2"); + font-display: optional; +} +``` + +--- + +### 4. useEffect 이후 화면 구조가 바뀌는 작업 줄이기 + +`useEffect`는 렌더링이 끝난 이후 실행된다. +따라서 `useEffect` 안에서 화면의 높이나 위치를 바꾸는 작업이 많아지면 CLS가 나빠질 수 있다. + +렌더링 이후 갑자기 요소를 추가하기보다는 다음과 같은 방식이 좋다. + +* 서버에서 미리 필요한 데이터를 내려준다. +* 스켈레톤 UI로 공간을 확보한다. +* 초기 화면 영역에는 갑작스러운 삽입을 피한다. +* 사용자 액션 이후 발생하는 변화로 만든다. +