[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,
  },
}));
  • 깊은 구조면 직접 복사 필요

Categories:

Updated:

Leave a comment