구현하고자 하는 것
공통 컴포넌트를 정의하고 사용하다보면 브레이크 포인트마다 다른 사이즈를 사용하는 상황이 많습니다.
예를 들어 버튼 컴포넌트에 large, medium, small 등의 사이즈를 정의 하더라도 어떤 페이지에서는 데스크탑인 경우 large, 태블릿인 경우 medium, 모바일인 경우 small 사이즈를 사용해야합니다. 또 어떤 페이지에서는 데스크탑인 경우 medium, 태블릿인 경우 small, 모바일인 경우 xsmall 사이즈를 사용해야할 수도 있습니다.
그렇다고 사용할 때마다 해당 컴포넌트에서 사이즈에 대한 미디어쿼리 스타일을 계속 덮어쓰기 한다면 그것은 매우 번거로운 일입니다.
그래서 제가 구현하고자 하는 것은 단일 사이즈를 지정할 수도 있고
<Button size="large">버튼</Button>
다중 사이즈를 지정할 수도 있는 컴포넌트를 구현하려고 합니다. 브레이크 포인트마다 차례로 [wide, desktop, tablet, mobile]
형태로 다중 사이즈를 지정하는 것이죠.
<Button size={['large', 'medium', 'small', 'xsmall']}>버튼</Button>
또한 모든 사이즈를 입력하지 않는다면 작성하지 않은 나머지 두 항목은 자동으로 마지막에 작성한 값이 지정됩니다. 즉 ["large", "medium", "medium", "medium"]
으로 작성한 것과 같습니다.
<Button size={['large', 'medium']}>버튼</Button>
추가로 기본 사이즈는 large, medium, small, xsmall 네 가지 지만, 어떤 컴포넌트에서는 large, medium, small 세가지 사이즈만 정의해야할 수도 있습니다. 이런 케이스까지 고려해서 구현해보려고 합니다.
1. useResponsive 커스텀 훅
먼저 useResponsive.ts 파일을 생성하고 컴포넌트 사이즈, 브레이크 포인트에 따른 디바이스, 브레이크 포인트를 정의합니다.
export type SizeType = 'large' | 'medium' | 'small' | 'xsmall';
export type ResponsiveSizeType = 'wide' | 'desktop' | 'tablet' | 'mobile';
export const breakpoints = [1440, 1024, 768];
ResiponsiveSizeType
: 디바이스 사이즈SizeType
: 컴포넌트 사이즈breakponts
: 반응형 브레이크 포인트- wide: 1440px 이상
- desktop: 1024px ~ 1439px
- tablet: 768px ~ 1023px
- mobile: 767px 이하
다음은 브라우저 width 값에 따라서 현재 브레이크 포인트를 반환하는 calcBreakpoint 함수를 생성합니다.
const calcBreakpoint = (width: number) => {
if (width >= breakpoints[0]) return 'wide';
if (width >= breakpoints[1]) return 'desktop';
if (width >= breakpoints[2]) return 'tablet';
return 'mobile';
};
이제 본격적으로 훅을 작성해줄거예요. useResponsive 훅은 현재 브레이크 포인트의 디바이스를 리턴합니다.
export const useResponsive = () => {
const [currentBreakpoint, setCurrentBreakpoint] = useState<ResponsiveSizeType>(
calcBreakpoint(window.innerWidth)
);
// ...
return currentBreakpoint;
};
디바이스 사이즈를 갱신하기 위해 상태값 currentBreakpoint
값을 정의하고 기본 디바이스를 할당합니다.
이 상태값은 브라우저가 리사이즈 될 때마다 갱신되어야하니, useEffect hook에 리사이즈 이벤트를 작성합니다.
useEffect(() => {
const handleSize = () => {
setCurrentBreakpoint(calcBreakpoint(window.innerWidth));
};
window.addEventListener('resize', handleSize);
return () => window.removeEventListener('resize', handleSize);
}, []);
컴포넌트를 벗어나고도 이벤트가 실행되지 않도록 이벤트 리스너까지 깔끔하게 제거해주세요.
전체코드
import { useEffect, useState } from 'react';
export type ResponsiveSizeType = 'wide' | 'desktop' | 'tablet' | 'mobile';
export type SizeType = 'large' | 'medium' | 'small' | 'xsmall';
export const breakpoints = [1440, 1024, 768];
const calcBreakpoint = (width: number) => {
if (width >= breakpoints[0]) return 'wide';
if (width >= breakpoints[1]) return 'desktop';
if (width >= breakpoints[2]) return 'tablet';
return 'mobile';
};
const useResponsive = () => {
const [currentBreakpoint, setCurrentBreakpoint] = useState<ResponsiveSizeType>('wide');
useEffect(() => {
const handleSize = () => {
setCurrentBreakpoint(calcBreakpoint(window.innerWidth));
};
window.addEventListener('resize', handleSize);
return () => window.removeEventListener('resize', handleSize);
}, []);
return currentBreakpoint;
};
export default useResponsive;
2. getResponsiveSize 함수
이 함수는 주어진 컴포넌트크기(size
)와 화면의 브레이크 포인트(breakpoint
)에 따라 적절한 크기를 반환하는 유틸리티 함수입니다.
// breakpoint에 해당하는 size 반환
export const getResponsiveSize = (
size: SizeType | SizeType[], breakpoint: ResponsiveSizeType
) = {
};
매개변수
size: SizeType | SizeType[]
- size는 단일 값 또는 배열로 전달될 수 있습니다. (large, medium, small, xsmall)
- 배열로 주어지면 각 브레이크 포인트에 맞는 크기를 의미합니다.
breakpoint: ResponsiveSizeType
- 반응형 레이아웃의 브레이크포인트를 나타냅니다. (wide, desktop, tablet, mobile)
먼저 컴포넌트에서 기본적인 허용 사이즈를 설정합니다.
예를 들어 다음과 같이 설정했다면 이 함수를 사용하는 버튼 컴포넌트의 사이즈는 large, medium, small, xsmall 로 네 가지 사이즈로 구성됩니다.
export const getResponsiveSize = (
size: SizeType | SizeType[], breakpoint: ResponsiveSizeType
) = {
const allowSizes = ['large', 'medium', 'small', 'xsmall'] as SizeType[];
};
다음은 getResponsiveSize
함수 내부에 normalizeSize
함수를 생성합니다. 이 내부 함수는 size 값을 표준화하는 역할을 합니다. 즉 주어진 size 값이 단일 값이라면 모든 breakpoint를 동일한 사이즈로 초기화 합니다.
normalizeSize('large'); // output: ["large", "large", "large", "large"]
다중 값이라면 빈 breakpoint 에는 마지막의 값으로 초기화합니다.
normalizeSize(['large', 'small']); // output: ["large", "small", "small", "small"]
normalizeSize(['large', 'medium', 'small', 'xsmall']); // output: ["large", "medium", "small", "xsmall"]
따라서 normalizeSize 함수 안에서는 주어진 값이 다중 사이즈인지, 단일 사이즈인지 판단하고, 허용 사이즈에 존재하는 사이즈인지 유효성을 체크합니다. 끝으로 표준화된 사이즈를 반환합니다.
export const getResponsiveSize = (
size: SizeType | SizeType[], breakpoint: ResponsiveSizeType
) = {
const allowSizes = ['large', 'medium', 'small', 'xsmall'] as SizeType[];
const normalizeSize = (size: SizeType | SizeType[]) {
// 다중 사이즈
if(Array.isArray(size)) {
// 주어진 모든 사이즈가 허용 사이즈에 포함되어있는지 유효성 체크
if (!size.every((s) => allowSizes.includes(s))) {
throw new Error(`Invalid size in allowed sizes: ${size}`);
}
// breakpoint를 모두 채우지 않았을 경우 마지막 요소로 채움
return [...size, ...Array(4 - size.length).fill(size[size.length - 1])];
}
// 단일 사이즈
else {
// 모든 breakpoint를 동일한 사이즈로 초기화
if(!allowedSizes.includes(size)) {
throw new Error(`Invalid size in allowed sizes: ${size}`);
}
return [size, size, size, size];
}
}
const normalizedSize = normalizeSize(size);
};
마지막으로 브레이크 포인트에 따라 인덱스를 매핑합니다.
export const getResponsiveSize = (
size: SizeType | SizeType[], breakpoint: ResponsiveSizeType
) = {
const allowSizes = ['large', 'medium', 'small', 'xsmall'] as SizeType[];
const normalizeSize = (size: SizeType | SizeType[]) {
//
}
const normalizedSize = normalizeSize(size);
// breakpoint에 적절한 사이즈 반환
const indexMap: Record<ResponsiveSizeType, number> = {
wide: 0,
desktop: 1,
tablet: 2,
mobile: 3,
};
return normalizedSize[indexMap[breakpoint]];
};
indexMap
은 브레이크 포인트(wide
,desktop
,tablet
,mobile
)와normalizedSize
배열의 인덱스를 매핑합니다.- 예를 들어,
breakpoint
가'tablet'
일 경우 인덱스2
에 해당하는 값을 반환합니다.
전체 코드
import { ResponsiveSizeType, SizeType } from '@/hooks/useResponsive';
export const getResponsiveSize = (
size: SizeType | SizeType[],
breakpoint: ResponsiveSizeType
) => {
const allowedSizes = ['large', 'medium', 'small', 'xsmall'] as SizeType[];
const normalizeSize = (size: SizeType | SizeType[]): SizeType | SizeType[] => {
if (Array.isArray(size)) {
if (!size.every((s) => allowedSizes.includes(s))) {
throw new Error(`Invalid size in allowed sizes: ${size}`);
}
return [...size, ...Array(4 - size.length).fill(size[size.length - 1])];
} else {
if (!allowedSizes.includes(size)) {
throw new Error(`Invalid size in allowed sizes: ${size}`);
}
return [size, size, size, size];
}
};
const normalizedSize = normalizeSize(size);
const indexMap: Record<ResponsiveSizeType, number> = {
wide: 0,
desktop: 1,
tablet: 2,
mobile: 3,
};
return normalizedSize[indexMap[breakpoint]];
};
3. 특정 사이즈 제한하기
어떤 컴포넌트에서는 medium, small 사이즈만 필요한 컴포넌트가 있을 수 있습니다. getResponsieveSize 함수를 일부 수정하여 options 값을 통해 허용 사이즈를 변경할 수 있도록 합니다.
import { ResponsiveSizeType, SizeType } from '@/hooks/useResponsive';
export const getResponsiveSize = (
size: SizeType | SizeType[],
breakpoint: ResponsiveSizeType,
options?: { allowedSizes?: SizeType[] } // 허용가능한 사이즈 목록 옵션
) => {
const allowedSizes =
options?.allowedSizes || (['large', 'medium', 'small', 'xsmall'] as SizeType[]);
const normalizeSize = (size: SizeType | SizeType[]): SizeType | SizeType[] => {
if (Array.isArray(size)) {
if (!size.every((s) => allowedSizes.includes(s))) {
throw new Error(`Invalid size in allowed sizes: ${size}`);
}
return [...size, ...Array(4 - size.length).fill(size[size.length - 1])];
} else {
if (!allowedSizes.includes(size)) {
throw new Error(`Invalid size in allowed sizes: ${size}`);
}
return [size, size, size, size];
}
};
const normalizedSize = normalizeSize(size);
const indexMap: Record<ResponsiveSizeType, number> = {
wide: 0,
desktop: 1,
tablet: 2,
mobile: 3,
};
return normalizedSize[indexMap[breakpoint]];
};
4. 사용 예제
import useResponsive, { SizeType } from '@/hooks/useResponsive';
import { getResponsiveSize } from '@/utils/getResponsivSize';
import styles from './Button.module.scss';
interface ButtonProps {
size?: SizeType | SizeType[];
}
const Button = ({ size = 'medium' }: ButtonProps) => {
const currentBreakpoint = useResponsive();
const buttonSize = getResponsiveSize(size, currentBreakpoint);
return (
<button className={`${styles.button} ${styles[buttonSize]}`}>{buttonSize}</button>
);
};
export default Button;
// 특정 컴포넌트에서 허용 사이즈를 변경할 경우
export type ButtonSizeType = 'medium' | 'small';
const currentBreakpoint = useResponsive();
const buttonSize = getResponsiveSize(size, currentBreakpoint, {
allowedSizes: ['medium', 'small'], // Button 전용 제한 사이즈
}) as ButtonSizeType;
이제 반응형 페이지에서 컴포넌트 사이즈를 Props를 통해 훨씬 손쉽게 변경할 수 있게 되었습니다.
끝!