[250528] TIL
Today I Learned (2025-05-28)
돌핀 사용자한테 받은 피드백 반영 진행중
-
[기능 제안] 장소 리스트 - 장소 리스트와 장소 핀 정보 UI 중복
- https://github.com/100-hours-a-week/7-team-ddb-wiki/issues/64
-
[기능 제안] 검색 완료 페이지 -> 상세페이지 -> 뒤로가기 클릭 시, 검색 완료 페이지때 상태 저장
- https://github.com/100-hours-a-week/7-team-ddb-wiki/issues/95
- 전역상태관리를 이용해서 위 내용을 반영중이다.
- 아래는 진행하면서 공부한 내용이다.
상태란 ?
- 현재 상황을 나타내는 데이터
전역 상태 라이브러리 도입
왜 전역 라이브러리를 도입해야할까?
- 상세 페이지에 다녀온 뒤에도 검색 페이지의 UI 상태 유지
- 유저가 리스트를 보고 있던 중간 지점을 기억하고 돌아왔을 때, UX에 큰 기여
- 코드 구조 정리 (복잡한 props drilling 이나 useEffect 로직 줄일 수 있음)
-
[[개발/라이브러리/zustand.md zustand 도입]]
기본 전역 상태의 생명 주기는?
- 기본적으로 페이지가 바뀌거나 새로고침하면 데이터가 사라짐
- 전역 상태는 대부분 메모리(RAM)에만 저장되고, 페이지를 벗어나면 브라우저가 메모리를 초기화하기 때문
구분 | zustand 같은 클라이언트 전역 상태와의 관계 |
---|---|
SSR (getServerSideProps) | ❌ 직접적 관련 없음 (렌더링 시점 다름) |
서버 컴포넌트 (RSC) | ❌ zustand 접근 불가 (서버에서 실행됨) |
클라이언트 컴포넌트 | ✅ zustand 상태 접근/유지 가능 |
브라우저 새로고침 | ❌ zustand 초기화됨 (메모리 초기화) |
페이지 이동 (SPA) | ✅ zustand 상태 유지됨 |
SSR + zustand 같이 쓰기
export async function getServerSideProps() {
const data = await fetch("...");
return { props: { initialData: data } };
}
// 클라이언트 컴포넌트
const Page = ({ initialData }) => {
const setData = useDataStore((s) => s.setData);
useEffect(() => {
setData(initialData);
}, []);
return <Component />;
};
어떤 정보를 전역 상태로 관리할까?
type BottomSheetType = 'none' | 'list' | 'pin'
interface BottomSheetState {
opened: BottomSheetType //
scrollY: number
lastPlaceId: string | null
setOpened: (type: BottomSheetType) => void
setScrollY: (y: number) => void
setLastPlaceId: (id: string | null) => void
}
export const useBottomSheetStore = create<BottomSheetState>((set) => ({
opened: 'none',
scrollY: 0,
lastPlaceId: null,
setOpened: (type) => set({ opened: type }),
setScrollY: (y) => set({ scrollY: y }),
setLastPlaceId: (id) => set({ lastPlaceId: id }),
}))
바텀시트 열기 로직
// 리스트 바텀시트 열기
const { setOpened, setScrollY } = useBottomSheetStore();
const openListSheet = () => {
setScrollY(window.scrollY);
setOpened("list");
};
// 핀 바텀시트 열기 (마커 클릭 시)
const openPinSheet = (placeId: string) => {
setScrollY(window.scrollY);
setLastPlaceId(placeId);
setOpened("pin");
};
검색 완료 페이지 진입 시
// pages/search-result.tsx (예시)
const { opened, scrollY, lastPlaceId } = useBottomSheetStore();
useEffect(() => {
// 페이지 진입 시 복원
window.scrollTo(0, scrollY);
if (opened === "list") {
openListSheet();
} else if (opened === "pin" && lastPlaceId) {
openPinSheet(lastPlaceId);
}
}, []);
동시 열림 방지
{
opened === "list" && <ListBottomSheet />;
}
{
opened === "pin" && lastPlaceId && <PinBottomSheet placeId={lastPlaceId} />;
}
얕은 비교(shallow equality) 와 깊은 비교(deep equality)
구분 | 얕은 비교 (=== , shallowEqual ) |
깊은 비교 (deepEqual, 재귀적) |
---|---|---|
비교 방식 | 참조값 또는 최상위 값만 비교 | 모든 중첩된 값까지 전부 비교 |
성능 | 빠름 ⚡ | 느림 🐢 (재귀적으로 모든 값 비교해야 함) |
예시 | === , shallowEqual(a, b) |
JSON.stringify(a) === JSON.stringify(b) 또는 라이브러리 사용 |
주 사용처 | 리렌더 최적화, 상태관리, 변경 감지 등 | 테스트, 복잡한 동기화 등 |
타입 | 예시 코드 | 결과 | 설명 |
---|---|---|---|
기본값 | 1 === 1 , 'a' === 'a' , true === true |
true |
값 자체가 같음 |
배열 | [1,2] === [1,2] |
false |
다른 참조 |
객체 | {a:1} === {a:1} |
false |
다른 참조 |
함수 | () => {} === () => {} |
false |
다른 참조 |
동일 참조 | const a = [1]; const b = a; a === b |
true |
같은 메모리 주소 |
NaN | NaN === NaN |
false |
JS에서만 특이함 |
Object.is | Object.is(NaN, NaN) |
true |
이건 특별히 NaN도 같다고 봐줌 |
얕은 비교
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2 };
obj1 === obj2; // false (참조 다름)
// shallowEqual 함수
function shallowEqual(a, b) {
if (a === b) return true;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every((key) => a[key] === b[key]);
}
shallowEqual(obj1, obj2); // true (얕게 같음)
깊은 비교
const deepEqual = (a, b) => {
return JSON.stringify(a) === JSON.stringify(b);
};
deepEqual({ a: { x: 1 } }, { a: { x: 1 } }); // true (깊은 비교 성공)
타입 | 얕은 비교 (===) | 얕은 비교 (shallowEqual ) |
깊은 비교 |
---|---|---|---|
원시값 | ✅ 같음 | ✅ 같음 | ✅ 같음 |
객체/배열 | ❌ 참조 다르면 다름 | ✅ 최상위 키 값 비교 | ✅ 구조 전체 비교 |
함수 | ❌ 참조 다르면 다름 | ❌ 비교 못함 | ❌ 일반적으로 불가능 |
성능 | 빠름 | 빠름 | 느림 |
얕은 복사(shallow copy) vs 깊은 복사(deep copy)
구분 | 얕은 복사 (Shallow Copy) | 깊은 복사 (Deep Copy) |
---|---|---|
정의 | 1단계까지만 복사. 내부 참조값은 공유함 | 내부에 있는 모든 값/객체도 재귀적으로 복사 |
메모리 공유 | 내부 객체나 배열은 같은 참조값 사용 | 내부 객체/배열까지 새로운 참조값 생성 |
속도 | 빠름 ⚡ | 느림 🐢 (재귀, 순회가 필요) |
사용 예 | 상태 일부만 바꿀 때, 빠르게 복사 | 상태 완전 분리, 원본 영향 없게 하고 싶을 때 |
const original = {
name: "Alice",
tags: ["dev", "frontend"],
};
얕은 복사
const shallow = { ...original };
shallow.name = "Bob";
shallow.tags.push("zustand");
console.log(original.tags); // ['dev', 'frontend', 'zustand'] ← 내부 배열 공유됨
...
전개 연산자나Object.assign()
은 얕은 복사tags
배열은 여전히 같은 참조값 → 수정 시 원본에도 영향
깊은 복사
const deep = JSON.parse(JSON.stringify(original));
deep.name = "Charlie";
deep.tags.push("react");
console.log(original.tags); // ['dev', 'frontend', 'zustand']
console.log(deep.tags); // ['dev', 'frontend', 'zustand', 'react']
복사 방법 | 복사 깊이 | 특징/제약 |
---|---|---|
{ ...obj } , Array.slice() |
얕은 복사 | 간단하지만 내부 객체/배열은 참조 공유됨 |
JSON.parse(JSON.stringify(obj)) |
깊은 복사 | 순환참조, Date , Map , Set , 함수 포함 안 됨 ❌ |
structuredClone(obj) |
깊은 복사 ✅ | 브라우저 내장, 최신 환경에서 안정적 (함수는 복사 안 됨) |
lodash.cloneDeep(obj) |
깊은 복사 ✅ | 모든 타입 지원, 순환 참조도 복사 가능 |
useState 쓸 때 복사 관련해서 꼭 알아야 할 주의점들
const [user, setUser] = useState({ name: "Alice", age: 20 });
user.age = 21;
setUser(user); // ❌ 이건 상태 안 바뀜
- user 에는 참조값. 직접 바꾸면 React는 값이 안바뀌었다고 판단함
setUser((prev) => ({ ...prev, age: 21 })); // ✅ 얕은 복사 + 변경
- 얕은 복사이기 때문에 참조 값이 바뀌어서 리렌더링 발생
setObj((prev) => ({
...prev,
nested: {
...prev.nested,
count: prev.nested.count + 1,
},
}));
- 깊은 구조면 직접 복사 필요
Leave a comment