diff --git a/keyword/chapter10/referential-equality.md b/keyword/chapter10/referential-equality.md new file mode 100644 index 0000000..c24aa10 --- /dev/null +++ b/keyword/chapter10/referential-equality.md @@ -0,0 +1,314 @@ +# Referential Equality (참조 동일성) + +## Referential Equality는 무엇인가요? + +간단히 말하면, **참조 동일성**은 “두 변수가 **완전히 같은 객체를 가리키고 있는가**?”를 보는 개념이다. + +- “같은 값이냐?”가 아니라 “같은 **메모리 주소(같은 객체 인스턴스)**를 가리키냐?”를 묻는 것 +- **“두 객체가 진짜로 같은 녀석이냐?”** 를 확인할 때 쓰는 기준 + +--- + +## 1. 자바스크립트에서 값 vs 참조 + +### 1-1. 값(primitive) 타입 + +- `number`, `string`, `boolean`, `null`, `undefined`, `bigint`, `symbol` +- 변수 안에 **값 자체**가 들어 있다. + +```js +let a = 10; +let b = 10; + +console.log(a === b); // true (값 비교) +``` + +### 1-2. 참조(object) 타입 + +- `object`, `array`, `function`, `Date`, `Map` 등 +- 변수 안에는 “객체가 저장된 메모리 위치(참조)”가 들어 있다. + +```js +const obj1 = { x: 1 }; +const obj2 = { x: 1 }; +const obj3 = obj1; + +console.log(obj1 === obj2); // false (서로 다른 객체) +console.log(obj1 === obj3); // true (같은 객체를 가리킴) +``` + +- `obj1`과 `obj2`는 모양은 같지만 **서로 다른 객체** +- `obj1`과 `obj3`는 **정확히 같은 객체**를 공유 → 참조 동일 + +--- + +## 2. `==`, `===`, `Object.is` + +### `==` (느슨한 동등) + +- 타입을 **암묵적으로 변환**하면서 비교 +- 예측하기 어려운 경우가 많아 **실무에서는 거의 사용하지 않는 것이 좋다** + +```js +0 == false; // true +"" == false; // true +null == undefined; // true +``` + +### `===` (엄격한 동등) + +- 타입 변환 없이 **그대로 비교** +- primitive → “값 동일성” +- 객체/배열/함수 → “참조 동일성” + +```js +// primitive +1 === 1; // true +"hi" === "hi"; // true + +// object +const a = { x: 1 }; +const b = { x: 1 }; +const c = a; + +a === b; // false +a === c; // true +``` + +### `Object.is` + +- 기본적으로 `===`와 비슷하지만 `NaN`, `+0` vs `-0` 같은 특수 케이스에서만 다름 +- **객체에 대해서는 `===`와 동일하게 참조 동일성**을 비교 + +```js +NaN === NaN; // false +Object.is(NaN, NaN); // true + ++0 === -0; // true +Object.is(+0, -0); // false +``` + +--- + +## 3. 객체 / 배열 / 함수 예제 + +### 객체 + +```js +const user1 = { name: "kim" }; +const user2 = { name: "kim" }; +const user3 = user1; + +console.log(user1 === user2); // false +console.log(user1 === user3); // true +``` + +### 배열 + +```js +const arr1 = [1, 2, 3]; +const arr2 = [1, 2, 3]; +const arr3 = arr1; + +console.log(arr1 === arr2); // false +console.log(arr1 === arr3); // true +``` + +“배열 **내용**이 같은지”를 보려면 직접 비교 로직이 필요하다. + +```js +const isArrayEqual = (a, b) => + a.length === b.length && a.every((v, i) => v === b[i]); + +console.log(isArrayEqual(arr1, arr2)); // true (값 동일성) +``` + +### 함수 + +```js +function foo() {} +const bar = function () {}; +const baz = foo; + +console.log(foo === bar); // false (다른 함수 객체) +console.log(foo === baz); // true (같은 함수 객체) +``` + +함수도 **객체**이므로 참조 단위로 비교된다. + +--- + +## 4. 참조 동일성이 중요한 이유 + +### 4-1. 가변 객체 공유로 인한 사이드 이펙트 + +```js +const state = { count: 0 }; + +function increment(s) { + s.count += 1; + return s; +} + +const a = state; +const b = increment(state); + +console.log(a === b); // true +console.log(state.count); // 1 +``` + +`state`, `a`, `b`가 **같은 객체를 공유**하므로 한 곳에서 수정하면 전부 같이 바뀐다. + +### 4-2. 불변 객체 + 참조 동일성 + +```js +const state = { count: 0 }; + +function incrementImmutable(s) { + return { ...s, count: s.count + 1 }; +} + +const a = state; +const b = incrementImmutable(state); + +console.log(a === b); // false (새 객체) +console.log(a.count); // 0 +console.log(b.count); // 1 +``` + +원본은 그대로 두고 **새 객체를 반환**하면, “참조가 바뀌었는지”만으로 변경 여부를 감지할 수 있다. + +리액트에서 `useMemo`, `useCallback`, `React.memo`, Redux 상태 비교 등이 **“참조가 바뀌었냐?”** 를 기준으로 최적화하는 이유가 여기에 있다. + +--- + +## 5. 자주 하는 실수 + +### “객체 내용이 같은데 왜 false죠?” + +```js +const a = { x: 1 }; +const b = { x: 1 }; + +console.log(a === b); // false +``` + +`===`는 **내용**이 아니라 **같은 객체인지**를 묻는다. 내용 비교는 deepEqual 로직이 필요하다. + +### 배열도 마찬가지 + +```js +[1, 2, 3] === [1, 2, 3]; // false +``` + +매번 새로 만들어지는 배열은 **다른 객체**다. + +### 직접 수정하면 참조 비교로는 변경을 알 수 없음 + +```js +const state = { x: 1 }; + +function mutate(s) { + s.x = 2; +} + +mutate(state); +console.log(state === state); // 항상 true +``` + +참조 비교로 변경을 감지하려면 **불변 패턴(새 객체 생성)**을 써야 한다. + +--- + +## 리액트 렌더링 최적화와 Referential Equality의 관계 + +### 리렌더링이 일어나는 조건 + +리액트는 기본적으로 **상태(state)나 props가 바뀌면** 컴포넌트를 다시 렌더링한다. + +문제는 props가 “겉보기 값”은 같아도 **참조가 매번 새로 만들어지면** 리액트 입장에서는 “바뀌었다”고 판단한다는 점이다. + +```tsx +function Parent() { + const [count, setCount] = useState(0); + + // 렌더마다 새 함수 객체 생성 → 참조가 매번 바뀜 + const handleClick = () => console.log('click'); + + return ; +} +``` + +`count`만 바뀌어도 `handleClick`은 매 렌더마다 새 함수이므로, `Child`는 `onClick` props가 바뀌었다고 인식한다. + +### React.memo와의 관계 + +`React.memo`는 **props를 얕은 비교(shallow compare)** 한다. 즉 `===`로 각 prop을 비교한다. + +```tsx +const Child = memo(function Child({ onClick }: { onClick: () => void }) { + return ; +}); +``` + +- `onClick`의 **참조가 같으면** → 리렌더링 스킵 +- `onClick`의 **참조가 다르면** → 다시 렌더링 + +객체·배열·함수 props는 내용이 같아도 참조가 바뀌면 memo 효과가 사라진다. + +### useCallback / useMemo와의 관계 + +| 훅 | 메모이제이션 대상 | 목적 | +|---|---|---| +| `useCallback` | 함수 | 함수 참조를 deps가 같을 때 유지 | +| `useMemo` | 계산 결과(값) | 값/객체/배열 참조를 deps가 같을 때 유지 | + +둘 다 **“참조 동일성을 유지해서, memo·useEffect·Context 소비자의 불필요한 재실행을 줄이는 것”** 이 핵심이다. + +### 실습 코드에서의 예시 (LpsPage) + +무한 스크롤 LP 목록에서 카드가 수십~수백 개일 때, 부모(`LpsPage`)의 `sort` 상태가 바뀌면 모든 `LpCard`가 다시 그려질 수 있다. + +```tsx +const LpCard = memo(function LpCard({ lp, onNavigate }: LpCardProps) { + return ( +
onNavigate(lp.id)}> + {/* ... */} +
+ ); +}); + +function LpsPage() { + const navigate = useNavigate(); + + // navigate가 바뀌지 않는 한 같은 함수 참조 유지 + const handleCardClick = useCallback( + (id: number) => navigate(`/lp/${id}`), + [navigate], + ); + + return ( + <> + {lps.map((lp) => ( + + ))} + + ); +} +``` + +- `memo` → `lp`, `onNavigate` 참조가 같으면 `LpCard` 리렌더 스킵 +- `useCallback` → `onNavigate` 참조를 안정적으로 유지 +- `onClick={() => navigate(...)}` 처럼 **렌더마다 새 함수를 넘기면 memo가 무력화**된다 + +### 정리 + +| 개념 | 역할 | +|---|---| +| Referential Equality | `===`로 “같은 객체/함수인가” 판단 | +| React.memo | props 참조가 같으면 리렌더 스킵 | +| useCallback | 함수 참조 유지 | +| useMemo | 계산 결과 참조 유지 | +| 불변 업데이트 | 상태 변경 시 새 객체/배열 생성 → 변경 감지 가능 | + +**참조 동일성은 리액트 최적화의 기반 개념**이다. memo·useCallback·useMemo는 모두 “참조가 불필요하게 바뀌지 않게” 돕는 도구이고, 남용하면 오히려 메모리·비교 비용만 늘어날 수 있으므로 **실제로 리렌더 비용이 큰 구간**에 쓰는 것이 좋다. diff --git a/keyword/chapter10/use-callback-and-memo.md b/keyword/chapter10/use-callback-and-memo.md new file mode 100644 index 0000000..6941fef --- /dev/null +++ b/keyword/chapter10/use-callback-and-memo.md @@ -0,0 +1,309 @@ +# useCallback과 memo + +> 참고: [useCallback 공식 문서](https://react.dev/reference/react/useCallback) · [memo 공식 문서](https://react.dev/reference/react/memo) + +--- + +## useCallback + +### useCallback이 무엇인지? + +`useCallback`은 **함수를 메모이제이션**하는 React Hook이다. + +```tsx +const cachedFn = useCallback(fn, dependencies); +``` + +- **메모이제이션**: `dependencies` 배열의 값이 이전 렌더와 **모두 같으면** 이전에 만들어 둔 **같은 함수 참조**를 재사용한다. +- **하나라도 바뀌면** 새 함수를 만들어 반환한다. + +렌더마다 `const fn = () => {}` 를 작성하면 **매번 새 함수 객체**가 생기지만, `useCallback`으로 감싸면 deps가 같을 때 **참조가 유지**된다. + +### 왜 useCallback을 사용하는지? + +#### 1. 불필요한 리렌더링 방지 + +`React.memo`로 감싼 자식에게 콜백을 props로 넘길 때, 부모가 리렌더되어도 **함수 참조가 같으면** 자식은 리렌더를 건너뛸 수 있다. + +#### 2. useEffect / useMemo 등의 deps 안정화 + +effect나 다른 훅의 dependency array에 함수를 넣을 때, 참조가 매번 바뀌면 **effect가 불필요하게 재실행**된다. `useCallback`으로 참조를 고정하면 이를 방지할 수 있다. + +#### 3. Context value에 함수를 넣을 때 + +Context Provider의 value에 함수를 포함하면, 참조가 바뀔 때마다 **모든 consumer가 리렌더**된다. `useCallback`으로 login/logout 같은 함수를 고정하는 패턴이 흔하다. + +#### 이득 vs 오버헤드 + +| 이득 | 오버헤드 | +|---|---| +| memo된 자식의 불필요한 리렌더 감소 | deps 비교 비용 | +| effect 재실행 감소 | 클로저 유지로 인한 메모리 | +| 대규모 리스트·무거운 자식에서 효과적 | 단순 컴포넌트에서는 체감 거의 없음 | + +React 공식 문서도 **“모든 함수에 무조건 useCallback을 쓰지 말 것”** 을 권장한다. Profiler로 실제 병목을 확인한 뒤 적용하는 것이 좋다. + +### useCallback 기본 사용법 + +```tsx +import { useCallback, useState } from 'react'; + +function SearchPage() { + const [query, setQuery] = useState(''); + + const handleSearch = useCallback(() => { + console.log('검색:', query); + }, [query]); // query가 바뀔 때만 새 함수 생성 + + return ; +} +``` + +#### deps 배열 규칙 + +- 콜백 **안에서 사용하는** props, state, context, 다른 함수를 deps에 넣는다. +- eslint-plugin-react-hooks의 `exhaustive-deps` 규칙을 따르는 것이 안전하다. +- deps가 `[]`이면 **마운트 시점의 값만** 캡처한다 → stale closure 주의. + +#### 의존성 변경 시 동작 + +```tsx +const [count, setCount] = useState(0); + +const increment = useCallback(() => { + setCount(count + 1); // count를 deps에 넣어야 최신값 사용 +}, [count]); +``` + +`count`가 0 → 1로 바뀌면 `increment` 함수 참조도 새로 만들어진다. + +### useCallback에서 중요한 개념 + +#### 참조 동일성 + +```tsx +const fn1 = useCallback(() => {}, []); +const fn2 = useCallback(() => {}, []); + +console.log(fn1 === fn2); // false (서로 다른 useCallback 호출) +``` + +같은 렌더 안에서 `useCallback`을 두 번 호출하면 **각각 다른 함수**다. 하나의 변수에 담아 재사용해야 한다. + +#### stale closure (낡은 값 캡처) + +```tsx +function Counter() { + const [count, setCount] = useState(0); + + const log = useCallback(() => { + console.log(count); // deps에 count가 없으면 항상 0 + }, []); // ❌ stale closure + + return ; +} +``` + +**해결 방법** + +1. deps에 `count` 추가 +2. functional update 사용: `setCount(c => c + 1)` → count를 deps에서 제거 가능 +3. `useRef`로 최신값 보관 + +```tsx +const increment = useCallback(() => { + setCount((c) => c + 1); // count를 deps에 넣지 않아도 됨 +}, []); +``` + +### useCallback 콜백 메모이제이션 예시 + +#### useCallback 없이/comparison + +```tsx +function Parent() { + const [sort, setSort] = useState('desc'); + + return ( + <> + + {items.map((item) => ( + // ❌ 렌더마다 새 함수 → memo 무력화 + handleClick(item.id)} /> + ))} + + ); +} +``` + +#### useCallback + memo + +```tsx +const Item = memo(function Item({ + item, + onSelect, +}: { + item: Item; + onSelect: (id: number) => void; +}) { + return
onSelect(item.id)}>{item.name}
; +}); + +function Parent() { + const handleSelect = useCallback((id: number) => { + navigate(`/item/${id}`); + }, [navigate]); + + return items.map((item) => ( + + )); +} +``` + +- `handleSelect` 참조가 안정적 → `Item`은 `item`이 바뀌지 않으면 리렌더 스킵 +- `onSelect(item.id)`처럼 **자식 내부에서 id를 넘기는 패턴**이 `(id) => () => navigate(...)` 팩토리보다 memo와 잘 맞는다 + +### 이벤트 핸들러 / 비동기 로직 예시 + +#### API 호출 핸들러 + +```tsx +const handleSubmit = useCallback(async () => { + await createLp({ title, content }); +}, [title, content]); +``` + +#### useEffect dependency + +```tsx +const fetchData = useCallback(async () => { + const res = await getLps({ order: sort }); + setData(res); +}, [sort]); + +useEffect(() => { + fetchData(); +}, [fetchData]); // fetchData 참조가 sort 변경 시에만 바뀜 +``` + +#### 디바운스와 함께 + +```tsx +const debouncedSearch = useMemo( + () => debounce((q: string) => searchApi(q), 300), + [], +); + +const handleChange = useCallback( + (e: React.ChangeEvent) => { + debouncedSearch(e.target.value); + }, + [debouncedSearch], +); +``` + +--- + +## memo + +### memo가 무엇인지? + +`memo`는 **컴포넌트를 메모이제이션**하는 고차(Higher-Order) API다. + +```tsx +const MemoizedComponent = memo(Component, arePropsEqual?); +``` + +- 부모가 리렌더되어도 **props가 이전과 같으면** 해당 컴포넌트의 리렌더를 **건너뛴다**. +- props 비교는 기본적으로 **얕은 비교(shallow compare)** — 각 prop에 `Object.is` (거의 `===`) 적용. + +### 왜 memo를 사용하는지? + +- **렌더 비용이 큰** 자식(복잡한 UI, 긴 리스트 아이템)의 불필요한 재렌더 방지 +- 부모 state 변경(정렬, 페이지네이션 등)과 **무관한 자식**을 보호 +- 무한 스크롤 리스트처럼 **동일 아이템이 많이 반복**되는 UI에서 효과적 + +### memo 기본 사용법 + +```tsx +import { memo } from 'react'; + +type LpCardProps = { + lp: LpDto; + onNavigate: (id: number) => void; +}; + +const LpCard = memo(function LpCard({ lp, onNavigate }: LpCardProps) { + return ( +
onNavigate(lp.id)}> + {lp.thumbnail ? ( + {lp.title} + ) : null} +

{lp.title}

+
+ ); +}); + +export default LpCard; +``` + +#### 커스텀 비교 함수 (선택) + +```tsx +const LpCard = memo( + LpCardComponent, + (prev, next) => prev.lp.id === next.lp.id && prev.lp.title === next.lp.title, +); +``` + +기본 shallow compare로 충분한 경우가 많고, 커스텀 compare는 **실제로 필요할 때만** 사용한다. + +### memo를 언제 쓰면 좋은지 / 안 좋은지 + +#### 쓰면 좋은 경우 + +- 리스트 아이템 컴포넌트 (수십~수백 개) +- 차트, 지도, 에디터 등 **렌더 비용이 큰** UI +- props가 자주 같고, 부모만 자주 리렌더되는 구조 +- `useCallback` / `useMemo`와 **함께** props 참조를 안정화한 경우 + +#### 과하거나 효과 없는 경우 + +- props가 **매 렌더마다 바뀌는** 경우 (새 객체·새 함수를 계속 넘김) +- 컴포넌트가 **매우 가벼운** 경우 (비교 비용 > 렌더 비용) +- 거의 모든 props가 매번 변경되는 경우 +- **memo만 쓰고 useCallback은 안 쓰는** 경우 (함수 props 때문에 memo 무효) + +#### React 공식 입장 + +> “memo는 성능 문제가 **측정·확인된 후**에 추가하세요.” + +Profiler(React DevTools)로 “왜 이 컴포넌트가 자주 리렌더되지?”를 먼저 확인하는 것이 좋다. + +--- + +## useCallback + memo 함께 쓰는 패턴 정리 + +``` +부모 리렌더 + ↓ +useCallback → onNavigate 참조 유지 + ↓ +memo(LpCard) → lp, onNavigate 같으면 스킵 + ↓ +불필요한 카드 N개 리렌더 방지 +``` + +| 없을 때 | 있을 때 | +|---|---| +| 부모 state 변경 → 모든 자식 리렌더 | 변경 없는 props의 자식은 스킵 | +| 함수 props 매번 새 참조 | 함수 참조 안정 | +| 긴 리스트에서 체감 지연 | 스크롤·정렬 시 UX 개선 | + +--- + +## 🍠 실습 1. 기록하기 + +- **깃허브 주소**: _(본인 레포 URL 작성)_ +- **실습 내용**: `LpsPage`의 `LpCard`에 `memo` + `useCallback` 적용, React DevTools Profiler로 리렌더 횟수 비교 +- **실행 영상**: _(Profiler Before/After 캡처 또는 영상 링크 작성)_ diff --git a/keyword/chapter10/use-memo.md b/keyword/chapter10/use-memo.md new file mode 100644 index 0000000..bb5cc8b --- /dev/null +++ b/keyword/chapter10/use-memo.md @@ -0,0 +1,238 @@ +# useMemo + +> 참고: [useMemo 공식 문서](https://react.dev/reference/react/useMemo) + +--- + +## useMemo가 무엇인지? + +`useMemo`는 **계산 결과(값)를 메모이제이션**하는 React Hook이다. + +```tsx +const cachedValue = useMemo(calculateValue, dependencies); +``` + +- `calculateValue` 함수를 실행한 **반환값**을 캐시한다. +- `dependencies`가 이전 렌더와 **모두 같으면** 이전 결과를 **그대로 재사용**한다. +- 하나라도 바뀌면 함수를 다시 실행하고 새 결과를 반환한다. + +`useCallback`이 **함수 참조**를, `useMemo`는 **값(객체·배열·숫자·문자열 등) 참조**를 유지한다고 보면 된다. + +```tsx +// useCallback — 함수 자체를 캐시 +const fn = useCallback(() => doSomething(a, b), [a, b]); + +// useMemo — 함수 실행 결과를 캐시 +const value = useMemo(() => doSomething(a, b), [a, b]); +``` + +--- + +## 왜 useMemo를 사용하는지? + +### 1. 비용 큰 계산 스킵 + +필터링·정렬·집계처럼 **연산이 무거운** 로직을 매 렌더마다 다시 실행하지 않게 한다. + +```tsx +const filteredLps = useMemo( + () => lps.filter((lp) => lp.title.includes(query)), + [lps, query], +); +``` + +### 2. 참조 안정화 (memo / useEffect와 연계) + +객체·배열을 매 렌더마다 새로 만들면, 참조가 바뀌어 **memo된 자식이나 useEffect가 불필요하게 재실행**된다. + +```tsx +const contextValue = useMemo( + () => ({ user, login, logout }), + [user, login, logout], +); + +return {children}; +``` + +### 3. 파생 상태(derived state) 표현 + +원본 state에서 **계산으로만 얻을 수 있는 값**을 명시적으로 분리한다. + +```tsx +const canSubmit = useMemo( + () => loginSchema.safeParse(values).success, + [values], +); +``` + +### 이득 vs 오버헤드 + +| 이득 | 오버헤드 | +|---|---| +| 무거운 계산 1회만 실행 | deps 비교 + 캐시 저장 | +| 객체/배열 참조 유지 | 가벼운 계산에는 오히려 손해 | +| Context value 안정화 | 남용 시 코드 복잡도 증가 | + +React 공식 문서: **“useMemo는 성능 최적화 도구이지만, 모든 계산에 쓸 필요는 없다.”** + +--- + +## useMemo 기본 사용법 + +```tsx +import { useMemo, useState } from 'react'; + +function LpsPage() { + const [sort, setSort] = useState<'asc' | 'desc'>('desc'); + + const { data } = useInfiniteQuery({ /* ... */ }); + + // pages 배열을 flatMap할 때마다 새 배열이 생기므로, data/sort 기준으로 메모 + const lps = useMemo( + () => data?.pages.flatMap((page) => page?.data ?? []) ?? [], + [data], + ); + + return (/* ... */); +} +``` + +### deps 배열 규칙 + +- `calculateValue` **안에서 읽는** reactive value(state, props, context, 다른 hook 결과)를 deps에 넣는다. +- `useCallback`과 동일하게 `exhaustive-deps` 규칙을 따른다. +- deps가 비어 있으면 **마운트 시 1회만** 계산하고 이후 항상 같은 결과를 반환한다. + +### 의존성 변경 시 + +```tsx +const sorted = useMemo( + () => [...items].sort((a, b) => a.date - b.date), + [items, sortOrder], +); +``` + +`items`나 `sortOrder`가 바뀔 때만 정렬을 다시 수행한다. + +--- + +## useMemo에서 중요한 개념 + +### 1. 참조 동일성 + +```tsx +const options = useMemo(() => ({ staleTime: 60_000 }), []); +// options 참조는 렌더마다 같음 → useQuery options prop 안정 + +const options = { staleTime: 60_000 }; +// 매 렌더 새 객체 → 참조 매번 변경 +``` + +### 2. useCallback과의 관계 + +```tsx +// 아래 두 코드는 거의 동일한 효과 +const fn = useCallback(() => greet(a, b), [a, b]); +const fn = useMemo(() => () => greet(a, b), [a, b]); +``` + +함수를 메모할 때는 **`useCallback`이 더 의도가 명확**하다. + +### 3. useMemo ≠ “값을 기억해 두는 저장소” + +`useMemo`는 **렌더 간** 결과를 재사용하는 것이지, 영구 저장소가 아니다. 컴포넌트가 언마운트되면 캐시도 사라진다. + +영구 저장이 필요하면 `useRef`, 외부 store(zustand, Redux), 서버 캐시(TanStack Query) 등을 사용한다. + +### 4. React Compiler (참고) + +React 19+ / React Compiler 환경에서는 컴파일러가 자동으로 메모이제이션을 적용할 수 있다. 그래도 **deps·참조 동일성 개념**은 이해해 두는 것이 중요하다. + +--- + +## useMemo 실전 예시 + +### 1. 무한 스크롤 flatMap + +TanStack Query `useInfiniteQuery`의 `data.pages`를 flatMap하면 **매 렌더 새 배열**이 생긴다. + +```tsx +const lps = useMemo( + () => data?.pages.flatMap((page) => page?.data ?? []) ?? [], + [data], +); +``` + +`data` 참조가 같으면 `lps`도 같은 배열 참조를 유지한다. + +### 2. Context value 메모이제이션 + +```tsx +const value = useMemo( + () => ({ + loggedIn, + userName, + login, + logout, + updateUserName, + }), + [loggedIn, userName, login, logout, updateUserName], +); + +return {children}; +``` + +`value` 객체 참조가 안정적이면 Context consumer의 **불필요한 리렌더**를 줄일 수 있다. + +### 3. 폼 유효성(canSubmit) 파생 + +```tsx +const canSubmit = useMemo( + () => signupSchema.safeParse(values).success, + [values], +); +``` + +매 입력마다 schema parse를 실행하지만, `values`가 같으면 결과 참조·불리언을 재사용한다. + +### 4. 필터 + 정렬 파이프라인 + +```tsx +const visibleTodos = useMemo(() => { + return todos + .filter((t) => (showDone ? true : !t.done)) + .sort((a, b) => a.createdAt - b.createdAt); +}, [todos, showDone]); +``` + +`todos`나 `showDone`이 바뀔 때만 filter/sort를 다시 실행한다. + +### 5. useMemo를 쓰지 않아도 되는 경우 + +```tsx +// ❌ 과한 useMemo — 단순 문자열 연결 +const label = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]); + +// ✅ 그냥 계산해도 충분 +const label = `${firstName} ${lastName}`; +``` + +--- + +## useCallback vs useMemo vs memo 한눈에 + +| API | 메모 대상 | 주요 용도 | +|---|---|---| +| `useCallback` | 함수 | 콜백 참조 유지, effect deps 안정화 | +| `useMemo` | 값(계산 결과) | 무거운 계산, 객체/배열 참조 유지 | +| `memo` | 컴포넌트 | props 같을 때 리렌더 스킵 | + +세 가지는 **참조 동일성**이라는 같은 기반 위에서 동작한다. + +--- + +## 🍠 실습 2. 기록하기 + +- **깃허브 주소**: _(본인 레포 URL 작성)_ +- **실습 내용**: `AuthContext`의 `value`에 `useMemo` 적용, 또는 `LpsPage`의 `lps` flatMap에 `useMemo` 적용 후 Profiler 비교 +- **실행 영상**: _(Before/After 캡처 또는 영상 링크 작성)_ diff --git a/keyword/chapter10/vercel-cicd.md b/keyword/chapter10/vercel-cicd.md new file mode 100644 index 0000000..fe4272b --- /dev/null +++ b/keyword/chapter10/vercel-cicd.md @@ -0,0 +1,251 @@ +# Vercel을 활용한 배포와 CI/CD + +> 참고: [Vercel Documentation](https://vercel.com/docs) · [GitHub Actions documentation](https://docs.github.com/en/actions) + +--- + +## 배포(Deployment)는 무엇인가요? + +**배포(Deployment)** 는 개발자가 만든 코드를 **실제 사용자**들이 사용할 수 있도록 인터넷에 공개하는 과정이다. localhost에서만 돌아가던 웹사이트를, 누구나 URL로 접속할 수 있는 서버 환경으로 옮기는 작업이다. + +### 특징 + +| 단계 | 설명 | +|---|---| +| **빌드(Build)** | React·TypeScript 등을 브라우저가 실행 가능한 HTML/CSS/JS로 변환 | +| **호스팅(Hosting)** | 빌드 결과물을 24시간 켜져 있는 서버에 올려 요청에 응답 | +| **접근성** | 고유 도메인(예: `my-app.vercel.app`)으로 어디서든 접속 가능 | + +### 과거 방식의 단점 + +- 서버 직접 구매·리눅스 설치·네트워크 설정 필요 +- 코드 수정마다 수동으로 파일 업로드 → 실수·누락 빈번 +- HTTPS, CDN, 캐시 등을 직접 구성해야 함 + +--- + +## Vercel은 무엇인가요? + +**Vercel**은 프론트엔드 개발자가 **복잡한 서버 설정 없이** 웹사이트를 빠르게 배포할 수 있게 해주는 플랫폼이다. Next.js를 만든 팀에서 개발했기 때문에 React·Vite·Next.js 프로젝트와 궁합이 좋다. + +### 특징 + +- **GitHub 연동**: Push만 하면 Vercel이 자동으로 빌드·배포 +- **자동 HTTPS**: SSL 인증서 자동 발급·갱신 +- **Preview Deployment**: PR/브랜치마다 미리보기 URL 생성 → 리뷰·QA에 유용 +- **글로벌 CDN**: Edge Network로 전 세계 사용자에게 빠른 응답 +- **Zero Config**: Vite/React 등 프레임워크 자동 감지 (대부분 설정 없이 동작) + +### Vite + React 프로젝트 배포 흐름 + +``` +1. GitHub에 코드 Push +2. Vercel이 저장소 연결 감지 +3. npm install → npm run build 실행 +4. dist/ 폴더를 CDN에 배포 +5. https://프로젝트명.vercel.app 에서 접속 가능 +``` + +### Vercel 대시보드에서 하는 일 + +1. [vercel.com](https://vercel.com) 가입 (GitHub 계정 연동) +2. **Add New Project** → GitHub 레포 선택 +3. Framework Preset: **Vite** (자동 감지) +4. Build Command: `npm run build` / Output Directory: `dist` +5. Deploy 클릭 + +환경 변수(`VITE_API_URL` 등)는 **Project Settings → Environment Variables** 에서 설정한다. + +--- + +## CI / CD는 무엇인가요? + +**CI/CD**는 개발부터 배포까지의 과정을 **자동화**하여, 더 자주·더 안전하게 코드를 배포할 수 있게 하는 방법론이다. + +### CI (Continuous Integration — 지속적 통합) + +- 여러 개발자의 코드를 **자주, 정기적으로 main/develop에 Merge** +- Merge/Push 시 **자동으로 테스트·린트·빌드** 실행 +- “문지기” 역할 — 버그·빌드 실패를 배포 전에 차단 + +``` +개발자 Push → GitHub Actions 실행 → lint / test / build → 성공 시 Merge 허용 +``` + +### CD (Continuous Deployment / Delivery — 지속적 배포·전달) + +- CI를 통과한 코드를 **자동으로 배포 환경에 반영** +- Delivery: 배포 **가능 상태**까지 자동화 (수동 배포 버튼은 사람이 누름) +- Deployment: **프로덕션까지 자동** 반영 + +``` +CI 통과 → Vercel Preview(브랜치) 또는 Production(main) 배포 +``` + +### Vercel만 쓸 때 vs GitHub Actions + Vercel + +| | Vercel 기본 연동 | GitHub Actions 추가 | +|---|---|---| +| 빌드·배포 | Push 시 자동 | workflow에서 제어 가능 | +| 테스트 | 별도 설정 없음 | CI 단계에서 test/lint 실행 | +| Preview | PR마다 자동 생성 | 동일 | +| 커스터마이징 | 제한적 | `.github/workflows/*.yml`로 자유롭게 구성 | + +소규모 프론트엔드 프로젝트는 **Vercel Git 연동만으로도 충분**하고, 팀 프로젝트·테스트 필수 환경에서는 **GitHub Actions CI + Vercel CD** 조합이 일반적이다. + +--- + +## 배포 vs Vercel vs CI/CD 한눈에 비교 + +| 항목 | 배포 (Deployment) | Vercel | CI / CD | +|---|---|---|---| +| **핵심 개념** | 서비스 공개 | 배포 플랫폼 | 자동화 프로세스 | +| **역할** | 로컬 → 세상 밖으로 | 배포를 쉽게 해주는 도구 | 개발~배포 파이프라인 | +| **비유** | 가게 오픈 | 인테리어·설비 갖춘 매장 임대 | 청소·요리·서빙 자동화 | +| **난이도** | (과거) 매우 높음 | 매우 낮음 | 설정에 따라 다름 | +| **주요 작업** | 파일 업로드, 서버 실행 | GitHub 레포 연결 | 테스트, workflow(.yml) 설정 | + +--- + +## Vercel과 CI/CD가 권장되는 이유 + +현대 프론트엔드 개발의 핵심은 **생산성**과 **안정성**이다. + +> “개발자는 기능 구현에 집중하고, 빌드·테스트·배포는 도구가 처리한다.” + +- **Vercel**: 인프라 구축 시간 절약, HTTPS·CDN·Preview 자동 제공 +- **CI/CD**: 사람이 놓치기 쉬운 lint/test/build를 Push마다 자동 검증 +- **Preview URL**: PR 리뷰어가 실제 동작을 바로 확인 → 버그 조기 발견 + +### 언제 쓰면 좋은가 + +- 포트폴리오·미션 결과물을 **URL로 제출**해야 할 때 +- 코드 수정마다 **수동 빌드·업로드**가 번거로울 때 +- 팀 프로젝트에서 **Merge 전 테스트**를 강제하고 싶을 때 +- 실무와 비슷한 **개발 → 검증 → 배포** 흐름을 경험하고 싶을 때 + +### 과할 수 있는 경우 + +- 로컬 전용 도구·CLI (배포 불필요) +- 순수 알고리즘·문법 학습 단계 (인프라 불필요) + +--- + +## GitHub Actions CI 예시 (Vite + React) + +`.github/workflows/ci.yml`: + +```yaml +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + - run: npm run lint + - run: npm run build +``` + +- `npm ci`: lockfile 기준으로 정확한 의존성 설치 (CI에서 `npm install`보다 권장) +- `npm run lint`, `npm run build`: 린트·TypeScript·Vite 빌드 성공 여부 확인 +- main 브랜치 Push 후 Vercel이 자동으로 Production 배포 (Git 연동 시) + +--- + +## 환경 변수 주의사항 (Vite) + +```env +# .env.local (로컬 전용, gitignore 대상) +VITE_API_BASE_URL=https://api.example.com +``` + +- Vite는 **`VITE_` 접두사**가 붙은 변수만 클라이언트에 노출 +- Vercel 대시보드에서도 동일한 key로 Environment Variables 등록 +- API Secret·Private Key는 **절대 `VITE_`로 노출하지 않음** (클라이언트 번들에 포함됨) + +--- + +## 배포 후 확인 체크리스트 + +- [ ] Production URL 접속 시 화면 정상 렌더 +- [ ] API 호출(CORS, base URL) 정상 동작 +- [ ] 환경 변수 누락 없음 (Vercel Settings 확인) +- [ ] SPA 라우팅: 새로고침 시 404 없음 (Vercel은 Vite/React SPA 기본 rewrites 자동 처리) +- [ ] Preview URL(PRs)에서도 동일하게 동작 + +--- + +## 추가로 학습한 내용 + +### Preview Deployment의 실무 활용 + +PR을 올리면 Vercel이 **브랜치별 Preview URL**을 자동 생성한다. 코드 리뷰어가 “로컬에서 안 돌려봐도” 실제 배포 환경과 동일한 URL에서 UI·API 연동을 확인할 수 있어, **“내 컴퓨터에선 됐는데…”** 류의 환경 차이 문제를 줄여준다. + +### Production vs Preview vs Development + +| 환경 | 트리거 | 용도 | +|---|---|---| +| Production | `main` Push (또는 Merge) | 실제 서비스 | +| Preview | PR·feature 브랜치 Push | 리뷰·QA·데모 | +| Development | `vercel dev` 로컬 | 로컬에서 Vercel 환경 시뮬레이션 | + +Vercel 대시보드에서 환경별로 **다른 API URL**을 넣을 수 있다 (예: Preview → staging API, Production → production API). + +### SPA 404 이슈 + +React Router 등 클라이언트 라우팅 SPA는 `/lp/123` 같은 경로로 **직접 접속·새로고침** 시 서버가 해당 파일을 찾지 못해 404가 날 수 있다. Vercel은 `vercel.json` 또는 프레임워크 preset으로 **모든 경로를 `index.html`로 rewrite** 해 SPA fallback을 처리한다. + +```json +{ + "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] +} +``` + +Vite + React preset 사용 시 대부분 자동 적용되지만, 커스텀 설정 시 확인이 필요하다. + +### CI/CD와 프론트엔드 테스트 + +실무에서는 CI 단계에서 아래를 자주 함께 돌린다. + +- `npm run lint` — ESLint +- `npm run build` — TypeScript + Vite 빌드 성공 +- (선택) `npm test` — Vitest/Jest 단위 테스트 + +**빌드가 CI에서 통과해야 Vercel 배포도 안전**하다는 점이 CI/CD의 핵심이다. 로컬에서만 되고 CI에서 터지는 타입 에러·import 오류를 배포 전에 잡을 수 있다. + +### Vercel vs Netlify vs GitHub Pages (간단 비교) + +| | Vercel | Netlify | GitHub Pages | +|---|---|---|---| +| React/Vite | 매우 좋음 | 좋음 | 가능 (설정 필요) | +| Preview URL | PR마다 자동 | PR마다 자동 | 없음 | +| HTTPS | 자동 | 자동 | 자동 | +| 무료 tier | Hobby (개인·소규모) | Free tier | Public repo 무료 | +| 적합 | React SPA·Next.js | SPA·Jamstack | 정적 사이트·문서 | + +포트폴리오·미션 SPA 배포에는 **Vercel 또는 Netlify**가 GitHub Pages보다 설정 부담이 적고 Preview까지 지원해 실무에 가깝다. + +--- + +## 🍠 실습 기록하기 + +- **깃허브 주소**: _(본인 레포 URL 작성)_ +- **배포 URL**: _(예: https://my-app.vercel.app)_ +- **실습 내용**: GitHub Push → Vercel 자동 배포, (선택) GitHub Actions CI workflow 추가 +- **확인 사항**: 환경 변수, SPA 라우팅, Preview URL 동작 +- **실행 영상 / 스크린샷**: _(Vercel 대시보드·배포된 사이트 캡처)_