Skip to content

[Feat] update breakpoint + add useBreakpoint - #275

Merged
constantly-dev merged 3 commits into
devfrom
feat/#274_update-breakpoint
Jun 12, 2026
Merged

[Feat] update breakpoint + add useBreakpoint#275
constantly-dev merged 3 commits into
devfrom
feat/#274_update-breakpoint

Conversation

@constantly-dev

Copy link
Copy Markdown
Contributor

✨ 구현 기능 명세

  • src/styles/mediaQuery.ts — breakpoints (mobile/tablet/desktop/desktopLarge), media (범위 기반 미디어 쿼리 문자열) 정의
  • src/hooks/useBreakpoint.ts — 현재 breakpoint를 반환하는 커스텀 훅 구현

✅ PR Point

1️⃣ PR Point: useBreakpoint 구현 방식 선택 근거

먼저 해당 훅을 구현하기위해 라이브러리 코드와 기존 makers 팀 (crew) 코드를 비교하고 가장 적합한 코드를 찾으려고 했어요.

  1. 기존 crew 방식 (useDisplay)
const mobile = useMediaQuery({ query: '(max-width: 430px)' }); // react-responsive
  useIsomorphicLayoutEffect(() => {
    setIsMobile(mobile);
}, [mobile]);

크루는 breakpoint 하나당 useMediaQuery + useState + useIsomorphicLayoutEffect 세트가 반복되는 구조에요. 그리고 next.js를 사용하기에 SSR hydration mismatch를 막기 위해 useIsomorphicLayoutEffect를 사용하는 방식을 채택했어요.


  1. Chakra UI 방식 (useState + useEffect) 코드 링크
const [value, setValue] = useState(() => {
  if (!ssr) return { media, matches: window.matchMedia(query).matches }
  return { media: query, matches: !!fallback[index] }
})

useEffect(() => {
  const mql = queries.map((query) => win.matchMedia(query))
  const handler = (evt) => setValue(...)
  const cleanups = mql.map((v) => listen(v, handler))
  return () => cleanups.forEach((fn) => fn())
}, [getWin]) 

Chakra UI는 useState 초기값으로 SSR/CSR을 분기하고, useEffect에서 matchMedia.addEventListener로 변화를 구독하는 구조에요.


  1. MUI 방식 (useSyncExternalStore) 코드 링크
function useMediaQueryNew(...) {
  const [getSnapshot, subscribe] = React.useMemo(() => {
    // matchMedia 구독 등록/해제
  }, [...])

  return maybeReactUseSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
}

MUI는 React 18 대응으로 명시적으로 useSyncExternalStore로 전환한 것을 확인할 수 있었어요. PR 제목이 "Ensure no tearing in React 18"이더라구요!
PR 링크

Note

useSyncExternalStore와 tearing이란?

  • tearing: Concurrent Mode(동시성 모드)에서는 렌더링이 중단되고 재개될 수 있다. 그 사이에 외부 스토어가 변경되면 같은 화면에서 컴포넌트마다 서로 다른 데이터를 보여주는 불일치 현상이 발생할 수 있다. 이를 Tearing이라고 한다.
    useState + useEffect 방식은 이를 막을 수 없음. 외부 상태 변화를 effect에서 뒤늦게 반영하기 때문.

  • useSyncExternalStore: React가 렌더링 도중에도 외부 스토어의 스냅샷을 동기적으로 확인하여 일관성을 보장. 세 번째 인자 getServerSnapshot으로 SSR 기본값도 명시적으로 선언 가능해 useIsomorphicLayoutEffect 트릭이 필요 없음.

즉 여러 라이브러리와 기존 팀의 코드를 살펴보면 useEffect/useLayoutEffect + useState의 조합이거나 MUI와 같이 useSyncExternalStore를 쓰는 것을 볼 수 있었어요.

media query 변화는 유저의 브라우저 창 조절 시에만 발생하기 때문에 tearing 실현 가능성 자체는 낮지만 아래와 같은 이유로 MUI와 같은 useSyncExternalStore 방식을 채택했어요.

  1. React 18 공식 권장 — 브라우저 API 등 외부 상태 구독에 React 팀이 명시적으로 권장하는 훅 공식 문서
  2. MUI 채택 확인 — 같은 목적(breakpoint 감지)으로 MUI가 실제 마이그레이션 완료
  3. boilerplate 감소 — useState + useIsomorphicLayoutEffect + react-responsive 조합 대신 단일 훅으로 SSR·구독·스냅샷을 통합
  4. 외부 의존성 제거 — react-responsive(라이브러리 > 크루가 사용) 없이 브라우저 표준 window.matchMedia만으로 구현
  5. 이벤트 효율 — resize 이벤트 대신 matchMedia.addEventListener('change') 사용으로 breakpoint 경계를 넘을 때만 콜백 발생

이러한 이유로 useSyncExternalStore를 사용한 방식으로 구현했어요.


2️⃣ PR Point: useBreakpoint 동작

이렇게 구현된 useBreakpoint의 동작을 오약하자면

  1. subscribe가 마운트 시 3개 matchMedia에 리스너를 등록하고, 768·1024·1260px 경계를 넘을 때만 React에 알림.
  2. React는 알림을 받으면 getBreakpoint를 호출해 matchMedia.matches로 현재 breakpoint를 판단.
  3. SSR에서는 window 없으니 세 번째 인자 'desktop'을 기본값으로 반환.

요렇게 돼요! 저도 이 로직은 처음 구현해봐서,,, 정확히 이게 괜찮은지 잘 모르겠습니다,,,
리뷰와 코멘트 너무 환영입니다!!!!!!!!!!!!!!

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

릴리즈 노트

  • New Features

    • 반응형 디자인을 위한 뷰포트 감지 기능을 추가했습니다. 현재 화면 크기에 따라 자동으로 모바일, 태블릿, 데스크톱 상태를 감지합니다.
  • Refactor

    • 미디어 쿼리 구조를 개선하여 명시적인 브레이크포인트 정의로 일관성 있는 반응형 지원을 강화했습니다.

Walkthrough

미디어 쿼리 기반 반응형 디자인을 지원하기 위해 src/styles/mediaQuery.ts에서 명시적인 브레이크포인트 상수를 정의하고, 새로운 useBreakpoint 훅을 src/hooks/useBreakpoint.ts에 추가했다. 훅은 useSyncExternalStore를 통해 뷰포트 변화를 감지하고 현재 브레이크포인트와 파생 불리언 상태를 반환한다.

Changes

반응형 브레이크포인트 관리

Layer / File(s) Summary
Media query constants and types
src/styles/mediaQuery.ts
breakpoints 상수에 mobile, tablet, desktop, desktopLarge 픽셀 값을 정의하고, media 상수에 각 범위의 @media 쿼리 문자열을 사전 계산해 저장했다. Breakpoint 타입을 media의 키로부터 파생시켰다.
useBreakpoint hook with reactive state synchronization
src/hooks/useBreakpoint.ts
getBreakpointwindow.matchMedia를 통해 현재 뷰포트의 브레이크포인트를 검사하고, subscribe로 미디어 쿼리 변화를 감지한다. useSyncExternalStore로 상태를 동기화하며, 서버 렌더링 시에는 기본값 'desktop'을 제공한다. 반환 객체에 breakpointisMobile, isTablet, isDesktop, isDesktopLarge, isMobileOrTablet 파생 플래그를 포함시켰다.

Sequence Diagram

sequenceDiagram
  participant Component
  participant useBreakpoint
  participant getBreakpoint
  participant MediaQueryList
  participant useSyncExternalStore

  Component->>useBreakpoint: 훅 호출
  useBreakpoint->>getBreakpoint: 현재 뷰포트 너비 확인
  getBreakpoint->>MediaQueryList: window.matchMedia로 각 범위 검사
  MediaQueryList-->>getBreakpoint: 일치하는 breakpoint 반환
  useBreakpoint->>useSyncExternalStore: subscribe 함수 등록
  MediaQueryList->>useSyncExternalStore: change 이벤트 발생 시 콜백
  useSyncExternalStore-->>Component: breakpoint와 파생 상태 반환
  Component->>Component: 렌더링
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • sopt-operation-frontend#274: mediaQuery.ts의 브레이크포인트 정의와 타입 내보내기를 직접 다루므로 이 PR의 변경사항과 밀접하게 관련되어 있다.
  • sopt.org-frontend#550: 프로젝트의 브레이크포인트 로직을 다루므로, 새로운 useBreakpoint 훅과 mediaQuery 내보내기 리팩토링과 관련이 있다.

Poem

🐰 미디어 쿼리 상수는 반짝반짝,
useBreakpoint 훅이 뷰포트를 재깍재깍,
창 크기 변할 때마다 새로고침,
반응형 매직으로 픽셀을 싹둑싹둑! 📱💻

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 PR의 주요 변경사항인 breakpoint 업데이트와 useBreakpoint 훅 추가를 명확하게 요약하고 있습니다.
Description check ✅ Passed 설명은 구현 기능, PR Point, 동작 원리 등을 상세하게 설명하며 변경사항과 직접적으로 관련이 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#274_update-breakpoint

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: one or more packages not found in the registry.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new useBreakpoint hook using useSyncExternalStore to track responsive layout changes, and refactors the media query configuration to export explicit breakpoint values. The reviewer suggested optimizing the hook by caching window.matchMedia instances to prevent redundant object creation and simplifying the subscription logic to only listen to boundary changes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +5 to +28
function getBreakpoint(): Breakpoint {
if (window.matchMedia(`(min-width: ${breakpoints.desktopLarge}px)`).matches)
return 'desktopLarge';
if (window.matchMedia(`(min-width: ${breakpoints.desktop}px)`).matches)
return 'desktop';
if (window.matchMedia(`(min-width: ${breakpoints.tablet}px)`).matches)
return 'tablet';
return 'mobile';
}

function subscribe(callback: () => void) {
const mqls = [
window.matchMedia(`(max-width: ${breakpoints.tablet - 1}px)`),
window.matchMedia(
`(min-width: ${breakpoints.tablet}px) and (max-width: ${breakpoints.desktop - 1}px)`,
),
window.matchMedia(
`(min-width: ${breakpoints.desktop}px) and (max-width: ${breakpoints.desktopLarge - 1}px)`,
),
window.matchMedia(`(min-width: ${breakpoints.desktopLarge}px)`),
];
mqls.forEach((m) => m.addEventListener('change', callback));
return () => mqls.forEach((m) => m.removeEventListener('change', callback));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

💡 성능 개선 및 코드 단순화 제안

현재 구현된 useBreakpoint 훅의 성능 최적화와 코드 안정성을 위해 아래 두 가지 개선 사항을 제안합니다.

  1. window.matchMedia 호출 최소화 및 캐싱 (성능 개선)

    • getBreakpoint 함수는 스냅샷을 확인할 때마다(컴포넌트 렌더링 및 상태 변경 시 자주 호출됨) window.matchMedia를 매번 새로 생성하고 문자열을 파싱합니다.
    • window.matchMedia가 반환하는 MediaQueryList 객체는 실시간(live) 객체이므로, 한 번만 생성해 두고 .matches 값만 조회하는 방식으로 성능을 개선할 수 있습니다.
    • 또한, SSR 환경이나 테스트 환경(Jest 등)에서 window가 정의되지 않았을 때 발생할 수 있는 ReferenceError를 방지하기 위해 안전한 방어 코드가 필요합니다.
  2. 미디어 쿼리 리스너 단순화 (복잡도 감소)

    • subscribe 함수에서는 4개의 범위 기반 미디어 쿼리(max-widthand 조건 포함)를 등록하고 있습니다.
    • 실제 브라우저 창 크기가 변할 때 breakpoint가 바뀌는 시점은 오직 **경계값(768px, 1024px, 1260px)**을 넘어설 때뿐입니다.
    • 따라서 getBreakpoint에서 사용하는 동일한 3개의 min-width 쿼리만 구독(subscribe)해도 완전히 동일하게 동작하며, 불필요한 미디어 쿼리 객체 생성을 줄이고 코드를 단순하게 만들 수 있습니다.
const getMqls = () => {
  if (typeof window === 'undefined') return [];
  return [
    { key: 'desktopLarge' as const, mql: window.matchMedia(`(min-width: ${breakpoints.desktopLarge}px)`) },
    { key: 'desktop' as const, mql: window.matchMedia(`(min-width: ${breakpoints.desktop}px)`) },
    { key: 'tablet' as const, mql: window.matchMedia(`(min-width: ${breakpoints.tablet}px)`) },
  ];
};

let cachedMqls: ReturnType<typeof getMqls> | null = null;

function getCachedMqls() {
  if (!cachedMqls) {
    cachedMqls = getMqls();
  }
  return cachedMqls;
}

function getBreakpoint(): Breakpoint {
  const mqls = getCachedMqls();
  for (const { key, mql } of mqls) {
    if (mql.matches) return key;
  }
  return 'mobile';
}

function subscribe(callback: () => void) {
  const mqls = getCachedMqls();
  mqls.forEach(({ mql }) => mql.addEventListener('change', callback));
  return () => mqls.forEach(({ mql }) => mql.removeEventListener('change', callback));
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/hooks/useBreakpoint.ts (2)

15-27: ⚡ Quick win

경계값만 구독하면 중복 알림을 줄일 수 있습니다.

Line 17-24처럼 구간별 MediaQueryList를 모두 구독하면 1024px 같은 경계 통과 시 이전 구간과 새 구간이 함께 바뀌어서 callback이 두 번 호출됩니다. getBreakpoint()가 최종 상태를 계산하고 있으니 768/1024/1260 경계 세 개만 구독해도 동작은 같고 알림은 한 번으로 줄일 수 있습니다.

변경 예시
 function subscribe(callback: () => void) {
   const mqls = [
-    window.matchMedia(`(max-width: ${breakpoints.tablet - 1}px)`),
-    window.matchMedia(
-      `(min-width: ${breakpoints.tablet}px) and (max-width: ${breakpoints.desktop - 1}px)`,
-    ),
-    window.matchMedia(
-      `(min-width: ${breakpoints.desktop}px) and (max-width: ${breakpoints.desktopLarge - 1}px)`,
-    ),
-    window.matchMedia(`(min-width: ${breakpoints.desktopLarge}px)`),
+    window.matchMedia(`(min-width: ${breakpoints.tablet}px)`),
+    window.matchMedia(`(min-width: ${breakpoints.desktop}px)`),
+    window.matchMedia(`(min-width: ${breakpoints.desktopLarge}px)`),
   ];
   mqls.forEach((m) => m.addEventListener('change', callback));
   return () => mqls.forEach((m) => m.removeEventListener('change', callback));
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useBreakpoint.ts` around lines 15 - 27, The current subscribe
function adds listeners for every range which causes duplicate callbacks at
boundary transitions; instead subscribe only to the three breakpoint boundaries
used by getBreakpoint(): listen to MediaQueryList for breakpoints.tablet,
breakpoints.desktop and breakpoints.desktopLarge (e.g. matchMedia using those
threshold queries) in the subscribe function, keep the
addEventListener('change', callback) and return cleanup that removes those three
listeners, so getBreakpoint still computes the final state but callback fires
only once per boundary crossing.

30-35: ⚡ Quick win

SSR 기본값을 고정 desktop으로 두면 모바일 첫 페인트가 틀릴 수 있습니다.

Line 34의 서버 스냅샷이 항상 desktop이라서, 이 훅으로 마크업을 분기하는 컴포넌트는 모바일 SSR에서도 먼저 데스크톱 상태로 그려진 뒤 hydration 후 다시 바뀝니다. 기본값을 인자로 받게 열어 두거나 상위에서 서버 추정값을 주입할 수 있게 해두는 편이 안전합니다.

변경 예시
-export function useBreakpoint() {
+export function useBreakpoint(defaultBreakpoint: Breakpoint = 'desktop') {
   const bp = useSyncExternalStore(
     subscribe,
     getBreakpoint,
-    () => 'desktop' as Breakpoint,
+    () => defaultBreakpoint,
   );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useBreakpoint.ts` around lines 30 - 35, The server snapshot for
useSyncExternalStore in useBreakpoint is hardcoded to 'desktop', causing mobile
SSR to render wrong initial markup; change useBreakpoint to accept an optional
serverSnapshot/defaultBreakpoint parameter (or an injected server-estimated
breakpoint) and pass that into the third argument of useSyncExternalStore
instead of the fixed 'desktop', referencing the existing useBreakpoint function
and its use of subscribe and getBreakpoint so callers or the server can supply
an appropriate initial breakpoint.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/hooks/useBreakpoint.ts`:
- Around line 15-27: The current subscribe function adds listeners for every
range which causes duplicate callbacks at boundary transitions; instead
subscribe only to the three breakpoint boundaries used by getBreakpoint():
listen to MediaQueryList for breakpoints.tablet, breakpoints.desktop and
breakpoints.desktopLarge (e.g. matchMedia using those threshold queries) in the
subscribe function, keep the addEventListener('change', callback) and return
cleanup that removes those three listeners, so getBreakpoint still computes the
final state but callback fires only once per boundary crossing.
- Around line 30-35: The server snapshot for useSyncExternalStore in
useBreakpoint is hardcoded to 'desktop', causing mobile SSR to render wrong
initial markup; change useBreakpoint to accept an optional
serverSnapshot/defaultBreakpoint parameter (or an injected server-estimated
breakpoint) and pass that into the third argument of useSyncExternalStore
instead of the fixed 'desktop', referencing the existing useBreakpoint function
and its use of subscribe and getBreakpoint so callers or the server can supply
an appropriate initial breakpoint.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 989536a4-a680-4bc3-9538-666783dffc27

📥 Commits

Reviewing files that changed from the base of the PR and between e7a6106 and bcb3e52.

📒 Files selected for processing (2)
  • src/hooks/useBreakpoint.ts
  • src/styles/mediaQuery.ts

@jeonghoon11 jeonghoon11 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다!! 많은 레퍼런스 찾아보고 구현하신 티가 나네요 굿굿!!

}

function subscribe(callback: () => void) {
const mqls = [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mqls이 어떤 약자인가욥??

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 MediaQueryLists인것 같네용

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다!

const bp = useSyncExternalStore(
subscribe,
getBreakpoint,
() => 'desktop' as Breakpoint,

@jeonghoon11 jeonghoon11 Jun 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSR에서는 window 없으니 세 번째 인자 'desktop'을 기본값으로 반환.

Admin 서비스여서 데탑 사용 비중이 높아서 SSR fallback을 desktop으로 지정하신건가용??

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다! default 값을 지정해야하는데 어드민이라 desktop으로 지정했어요!

isTablet: bp === 'tablet',
isDesktop: bp === 'desktop',
isDesktopLarge: bp === 'desktopLarge',
isMobileOrTablet: bp === 'mobile' || bp === 'tablet',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isMobileOrTablet도 useBreakpoint의 return값에 넣은 이유가 궁금해요!
isTabletOrDesktop, isDesktopOrDesktopLarge처럼 조합 조건이 늘어날 수 있지 않을까 싶은데 공통 도메인 의미가 있는 조건이 아니라면 사용처에서 breakpoint 기준으로 판단하는건 어떤가 싶은데 진혁님의 생각은 어떤가요??

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 breakpoint.ts에서 두개 합친 것이 없긴해서 일관성 측면에서는 빼는게 맞긴하겠네요!
처음에는 디자인에서 breakpoint를 둘 때 tablet과 mobile을 동일하게 디자인하는 경우가 많아서 그렇게 뒀습니다!

@wuzoo wuzoo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR 디스크립션 잘 읽었습니다, 고생하셨어요!

사실 useState + useEffect가 저는 가장 리액트스러운 구현이라고는 생각해요.

  1. 일단 tearing이 발생할일은 절대 없을거같아요. useState + useEffect가 내재되어있는 Provider가 있고, 해당 프로바이더 하위로 media query 상태를 넘겨준다면 말이에요.
  2. css를 sync store로 담기에는 부적절하다고 느껴지긴 해요. 리액트가 아닌 외부 스토어와의 구독을 위해 사용하는 훅인데, css in js는 사실 리액트 구현체의 일부긴 하니까요.

mui와 같은 좋은 레퍼런스에서 채택하고 있기도 하고, 사실 성능 면에서 그렇게 큰 차이는 없다고 느껴져서 그대로 반영하셔도 이견은 없습니다~

@constantly-dev

Copy link
Copy Markdown
Contributor Author

PR 디스크립션 잘 읽었습니다, 고생하셨어요!

사실 useState + useEffect가 저는 가장 리액트스러운 구현이라고는 생각해요.

  1. 일단 tearing이 발생할일은 절대 없을거같아요. useState + useEffect가 내재되어있는 Provider가 있고, 해당 프로바이더 하위로 media query 상태를 넘겨준다면 말이에요.
  2. css를 sync store로 담기에는 부적절하다고 느껴지긴 해요. 리액트가 아닌 외부 스토어와의 구독을 위해 사용하는 훅인데, css in js는 사실 리액트 구현체의 일부긴 하니까요.

mui와 같은 좋은 레퍼런스에서 채택하고 있기도 하고, 사실 성능 면에서 그렇게 큰 차이는 없다고 느껴져서 그대로 반영하셔도 이견은 없습니다~

css라고 생각하니 말씀해주신 것처럼 useState+useEffect가 나을 것 같기도 하네요!
큰 문제가 없으니 일단 머지하고, return 하는 interface는 동일하게 유지하고 내부 로직을 변경할 수 있으니 조금 더 찾아보고, 말씀해주신 부분 고민해보겠습니다!

@constantly-dev
constantly-dev merged commit 06afce1 into dev Jun 12, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants