현재 작업하고 있는 프로젝트는 모바일 뷰와 데스크탑 뷰가 많이 달라 모바일 웹을 따로 만들어야할까 고민될 정도이다. 하지만, 절대적으로 프론트엔드 수가 부족해서 최대한 반응형으로 작업하기로 하였다.
현재 합의된 서비스 요구사항 중 모바일과 데스크탑에서 서로 너무 상이한 동작을 하는 경우가 있다. 처음 시도할 때에는 도저히 어떻게 해야할 지 감이 오지 않아 혹시 이건 공상과학인가...?했지만 오늘! 드디어 해냈다.
화면 크기 별로 이벤트 핸들러를 적용해보기에 앞서 화면 크기 별로 어떻게 css 설정을 적용하고 있는지를 살펴보도록 하겠다.
일반적으로 반응형 페이지를 만들 때 media query의 break point를 사용하여 어느 정도 화면에서 어떻게 보일지를 결정할 수 있다.
css에서 기본적으로 제공되는 media query를 사용해보자.
프로젝트 초반에는 styled-components를 많이 사용하여 다음과 같이 작성했었다.
const 컴포넌트 = styled.div`
@media screen and (max-width 또는 min-width: 원하는 breakpoint) {
width: 75px;
}
`
그리고 종종 해당 media query를 CSS Inline으로 넣어야하는 상황이 생기기도 했는데, 그럴 때에는 아래와 같이 사용하였다.
<컴포넌트
style={{
'@media (max-width 또는 min-width: 원하는 breakpoint)': {
width: '75px',
},
...
}}
>
여기서 문제는, breakpoint로 사용하는 화면 크기들은 보통 정해져있고, 한 번 어떤 breakpoint를 모바일 기기를 위해서 사용했다면 이후 다른 코드에서도 동일한 breakpoint를 사용하는 것이 좋다. 따라서, 매번 하드 코딩하기보다는 theme으로 breakpoint와 media query를 빼고 themeProvider를 통해 전역에 전달하는 것이 필요하다.
위를 직접 만들 수도 있지만, MUI에서 제공 중이라 가져다 사용하고 있다. 덕분에 위의 동일한 코드를 다음과 같이 작성할 수 있다.
<Box
sx={{
// MUI에서 sm 크기로 정의된 화면 사이즈일 때 width를 75px로 지정하는 코드이다.
// 만약 MUI에서 제공하는 breakpoint를 변경하고 싶다면 관련 MUI theme을 변경하면 된다.
sm: {
width: '75px',
}
}}
>
이러면 장점이
1. 화면 크기마다 다르게 적용되어야 하는 속성이 있다면 편리하게 사용할 수 있다.
'&.MuiGrid-item': {
m: {
xs: '80px 40px 60px 0',
sm: '120px 120px 20px 0',
},
boxSizing: 'border-box',
},
}
위는 실제 오늘 작성한 코드인데, &.MuiGrid-item
라는 클래스의 margin을 화면 크기에 따라 쉽게 지정할 수 있었다.
2. 레이아웃에 적용이 쉽다.
Masonry
나 Grid
와 같이 레이아웃을 쉽게 해주는 MUI 컴포넌트들을 적극 활용하고 있는데, 이 때 화면 사이즈가 모바일 사이즈라면 n 개 열이 보이게와 같은 설정을 쉽게 할 수 있다.
<Masonry
columns={{ xs: 1, sm: 2, md: 3, lg: 4, xl: 5 }}
>
단점으로 MUI에서 제공중인 컴포넌트에만 사용할 수 있다. 하지만, 원하는 media query를 적용시킨 MUI 컴포넌트로 컴포넌트를 감싸는 방법으로 꼼수를 부려 해결할 수 있다.
<Box
sx={{
transform: {
xs: 'scale(2)',
sm: 'scale(1)',
},
}}
>
// transform 설정을 image에 직접 적용하는 대신 상위 Box 컴포넌트에 적용
<img src={section3Image2} alt="section3Image2" style={{
width: '100%',
}} />
</Box>
이제 CSS가 아닌 이벤트 핸들러에 media query를 적용해보자. media query 자체가 CSS에서 제공하는 것인데 어떻게 onClick 이벤트에 적용할 수 있을지 고민이 많았는데, 구글링으로도 찾지 못한 해답을 chatGPT가 알려주었다.
function MyComponent() {
const [isSmallScreen, setIsSmallScreen] = useState(false);
// 미디어 쿼리를 사용하여 디바이스 크기를 확인하고 상태를 업데이트합니다.
useEffect(() => {
const mediaQuery = window.matchMedia('(max-width: 600px)');
const handleResize = () => {
setIsSmallScreen(mediaQuery.matches);
};
// 초기 실행
handleResize();
// 미디어 쿼리 변경 사항을 감지하고 상태를 업데이트합니다.
mediaQuery.addListener(handleResize);
// 컴포넌트가 언마운트될 때 이벤트 리스너를 제거합니다.
return () => {
mediaQuery.removeListener(handleResize);
};
}, []);
// 작은 화면과 큰 화면에 따라 다른 onClick 이벤트 핸들러를 할당합니다.
const handleClick = () => {
if (isSmallScreen) {
// 작은 화면에서 클릭할 때의 동작
console.log('작은 화면에서 클릭했습니다.');
} else {
// 큰 화면에서 클릭할 때의 동작
console.log('큰 화면에서 클릭했습니다.');
}
};
return (
<Box
onClick={handleClick}
sx={{
width: 200,
height: 200,
backgroundColor: 'lightblue',
cursor: 'pointer',
}}
>
클릭하세요
</Box>
);
}
위는 chat GPT씨의 답변 코드였다. 불가능하다고 생각했던 일을 할 수 있게 되어 기뻤던 것도 잠시, 코드가 너무 복잡해보여서 좀 줄여보고 싶었다.
코드를 살펴보면
- matchMedia를 통해 화면 크기 확인
- 작은 스크린인지에 대한 상태 변경
- useEffect를 통해 미디어 쿼리 변경 감지 및 listener 탈부착
- 이벤트 핸들러에는 작은 스크린인지에 대한 상태에 따라 서로 다른 동작하도록 설정
으로 이루어져 있다.
이 중 media query를 사용해 device 크기를 확인하고 알려주는 1~3 부분의 코드를 개선해보았다.
const isSMDevice = useMediaQuery(theme.breakpoints.down('sm'));
const handleClick = () => {
if (isSMDevice) {
// 작은 화면에서의 동작
} else {
// 큰 화면에서의 동작
}
}
MUI에서 useMediaQuery
라는 hook을 제공중이라 사용해보았더니 코드가 훨씬 간결해졌다.
This React hook listens for matches to a CSS media query. It allows the rendering of components based on whether the query matches or not.
: 이 리액트 hook은 CSS 미디어 쿼리와 일치하는 것을 듣습니다. 쿼리의 일치 여부에 따라 구성 요소를 렌더링할 수 있습니다.
-MUI 공식 문서, 파파고 번역
useMediaQuery는 query를 media query를 파라미터로 받아 현재 화면 사이즈가 이와 매치되는지를 리턴해준다. 즉, useMediaQuery hook의 결과인 isSMDevice 자체가 이전 코드에서의 isSmallScreen
과 동일한 역할을 하게 된다는 의미이다. 더불어, MUI에서 제공하는 코드이다보니, MUI의 breakpoint를 그대로 사용할 수 있었다. 만약, 직접 media query를 지정하고 싶다면,
// 이 부분을
const isSMDevice = useMediaQuery(theme.breakpoints.down('sm'));
// 이렇게 바꾸면 된다.
const isSMDevice = useMediaQuery('(max-width:600px)');
(이날부로 MUI는 나에게 화개장터가 되었다...)