Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/hooks/useBreakpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useSyncExternalStore } from 'react';

import { Breakpoint, breakpoints } from '@/styles/mediaQuery';

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 = [

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.

맞습니다!

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));
}
Comment on lines +5 to +28

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));
}


export function useBreakpoint() {
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으로 지정했어요!

);

return {
breakpoint: bp,
isMobile: bp === 'mobile',
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을 동일하게 디자인하는 경우가 많아서 그렇게 뒀습니다!

};
}
35 changes: 15 additions & 20 deletions src/styles/mediaQuery.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
const bp = {
mobile: 480,
tablet: 1024,
};

const mq = (label: keyof typeof bp) => {
const bpArray = Object.keys(bp).map((key) => [
key,
bp[key as keyof typeof bp],
]);

const [result] = bpArray.reduce((acc, [name, size]) => {
if (label === name) return [...acc, `@media (max-width: ${size}px)`];
return acc;
}, []);

return result;
};

export default mq;
export const breakpoints = {
mobile: 0,
tablet: 768,
desktop: 1024,
desktopLarge: 1260,
} as const;

export const media = {
mobile: `@media (max-width: ${breakpoints.tablet - 1}px)`,
tablet: `@media (min-width: ${breakpoints.tablet}px) and (max-width: ${breakpoints.desktop - 1}px)`,
desktop: `@media (min-width: ${breakpoints.desktop}px) and (max-width: ${breakpoints.desktopLarge - 1}px)`,
desktopLarge: `@media (min-width: ${breakpoints.desktopLarge}px)`,
} as const;

export type Breakpoint = keyof typeof media;
Loading