
SSR 환경의 반응형 깜빡임 현상 해결하기

🧶 문제 상황
우리는 Next.js app router
를 기반으로, 블로그를 SSR로 제공하고 있다.
그런데 이전부터 너무 신경 쓰였던 것이.. 모바일에서 블로그에 접근을 하게 되면 아래처럼 레이아웃이 한차례 깨지고 스타일이 적용되는 깜빡임 현상이 발생한다.


이 현상은 사용자에게 매번 피로감을 주기 때문에 사용자 경험에 좋지 않다고 생각했고, 무엇보다 나 스스로 불편함을 느껴서 해결해보고자 했다.
🤔 이런 현상이 왜 일어날까?
가장 먼저 생각나는 것은 SSR에서 styled-component
적용 시점에 의한 깜빡임 현상이다. 그러나 Next.js 공식문서에 나와있는 것처럼 처음부터 잘 처리해줬기 때문에, 원인은 아니었다.
그리고 모바일에서만 이 현상이 나타나는 것을 봐서 반응형과 관련이 있다고 생각을 했다.
우리는 useMediaQuery
을 사용해 반응형을 구현하고 있었다.
커스텀 훅 useCheckMobile
을 만들어, 각 컴포넌트에서 isMobile
값을 받아 조건적으로 스타일링을 구현해주도록 했다.
useMediaQuery는
window.matchMedia
즉 브라우저 API를 사용하기 때문에 SSR에서는 아무 효과를 줄 수 없다는 것을 미리 알고 가자!
useCheckMobile.ts
'use client';
import { useEffect, useState } from 'react';
import { useMediaQuery } from 'react-responsive';
const useCheckMobile = () => {
const [isMobile, setIsMobile] = useState(false); // default: false
const mobile = useMediaQuery({
query: '(max-width:768px)',
});
useEffect(() => {
if (mobile) {
setIsMobile(mobile);
} else {
setIsMobile(false);
}
}, [mobile]);
return isMobile;
};
export default useCheckMobile;
BlogImg.ts (블로그 대문 이미지 코드를 대표로 가져왔다.)
'use client';
import Image from 'next/image';
import styled from 'styled-components';
import useCheckMobile from '@/hooks/useCheckMobile';
const BlogImg = () => {
const isMobile = useCheckMobile(); // 디바이스 값 반환 받기
return (
<BlogImgContainer>
{/* 이렇게 조건 분기해서 사용 */}
<BlogImgWrapper className={isMobile ? 'mobile' : ''}>
{/* ... 생략 */}
</BlogImgWrapper>
</BlogImgContainer>
);
};
// 스타일 컴포넌트 css 생략
⭐ 여기서 중요한 점!
위에서 언급했던 것처럼 styled-component가 서버 사이드에서 입혀지도록 처리를 해놨기 때문에, 서버 사이드에서 한차례 스타일링이 입혀진 HTML을 클라이언트 사이드로 보내준다.
우리의 구현 방식에 따르면,
서버 사이드에서 상태값
isMobile
이false
로 initialize 된다. 그리고 그에 맞게 데스크탑 스타일링이 입혀진 HTML이 클라이언트 사이드로 전달된다.최초로 우리는 서버사이드에서 내려준 HTML을 보게 되고,
이후 클라이언트 사이드에서
useMediaQuery
에 의해 HTML에 다시 모바일 스타일링이 입혀지게 되면서 깜빡임 현상이 일어나는 것이었다.
✨ 해결 방법
위 원인을 파악하고 이를 해결하기 위해서는 서버 사이드에서 디바이스를 미리 판단해야 한다고 생각했다.

그래서 이렇게 request header에 디바이스 정보를 가지고 있는 user-agent
값을 이용하기로 했고, middleware을 다음과 같이 작성해 보았다.
middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export const middleware = (request: NextRequest) => {
// ...
// User-Agent를 사용하여 디바이스 모바일 여부 결정const userAgent = request.headers.get('user-agent');const isMobile = userAgent ? /mobile/i.test(userAgent) : false;
// ...
const response = NextResponse.next();
// response header에 x-is-mobile값 세팅
response.headers.set('x-is-mobile', isMobile.toString());
return response;
}
이렇게 request에서 user-agent 값을 받아와 정규표현식을 통해 모바일 값인지 판단한 후, response 헤더에 x-is-mobile
값으로 담아 주었다.
헤더에 잘 실리는지 확인을 해보니
모바일에서 접근했을 때:

데스크탑에서 접근했을 때:

아주 잘 들어온다!
그럼 이 값을 각 컴포넌트에서 어떻게 사용하느냐!
import { headers } from 'next/headers';
const headerList = headers();
const isDeviceMobile = headerList.get('x-is-mobile') === 'true';
이렇게 next/headers
의 headers
를 이용하여 받아올 수 있었다.
그 다음 중요한 점은 서버 사이드에서 디바이스를 판단했으니 isMobile의 초기값으로 판단한 값을 넣어주는 것이다.
useCheckMobile.ts
'use client';
import { useEffect, useState } from 'react';
import { useMediaQuery } from 'react-responsive';
const useCheckMobile = (initialValue: boolean) => {
// user-agent로 판단한 isMobile 값을 initialValue로 설정const [isMobile, setIsMobile] = useState(initialValue);
const mobile = useMediaQuery({
query: '(max-width:768px)',
});
useEffect(() => {
if (mobile) {
setIsMobile(mobile);
} else {
setIsMobile(false);
}
}, [mobile]);
return isMobile;
};
export default useCheckMobile;
BlogImg.ts
'use client';
import Image from 'next/image';
import styled from 'styled-components';
import useCheckMobile from '@/hooks/useCheckMobile';
const BlogImg = () => {
const headerList = headers();
const isDeviceMobile = headerList.get('x-is-mobile') === 'true';
const isMobile = useCheckMobile(isDeviceMobile); // 디바이스 값 반환 받기
return (
//블로그 대문 이미지가 있는 경우에만 블로그 소개글이 같이 나타남
<BlogImgContainer>
{/* 이렇게 조건 분기해서 사용 */}
<BlogImgWrapper className={isMobile ? 'mobile' : ''}>
// ... 생략
</BlogImgWrapper>
</BlogImgContainer>
);
};
// 스타일 컴포넌트 css 생략
이렇게 해주면 서버 사이드에서 처음부터 디바이스를 판단한 값이 적용된 isMobile값을 기반으로 HTML을 만들기 때문에, 모바일이면 모바일에 맞는 스타일링을 입혀 클라이언트 사이드로 보낼 수 있게 된다.
그 결과..! dev 환경에서 실행했을 때, 새로고침을 하거나 접속했을 때 레이아웃이 깨지지 않는다 !!!! 😆


잘 되는걸 확인하고 배포를 했다.
⚡ 그.런.데
배포본을 확인해보니 여전히 레이아웃이 깨졌다. 절망감과 함께 response header 에 있는 X-Is-Mobile
을 확인해보니, 모바일로 접속했음에도 불구하고 false
값이 나오고 있었다.
어디서부터 잘못된 것인지 확인하기 위해 response header에 userAgent값도 추가해보았다.
middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export const middleware = (request: NextRequest) => {
// ...
const userAgent = request.headers.get('user-agent');
const isMobile = userAgent ? /mobile/i.test(userAgent) : false;
// ...
let response = NextResponse.next();
response.headers.set('user-agent', userAgent.toString()); // 여기 추가!
response.headers.set('x-is-mobile', isMobile.toString());
return response;
}
그랬더니 확인한 결과..!
원래 나와야 하는 값은 이건데

User-Agent: Amazon CloudFront
가 실려오고 있었다.
우리는 AWS CloudFront를 사용하고 있기 때문에, request header 가 cloudfront를 거치는 과정에서 그대로 실려오지 못하고, Amazon CloudFront
로 변경되어 오고 있던 것이다.
그래서 AWS CloudFront에서 user-agent 값이 그대로 실려오도록 설정을 바꿔주어야 했다.
✅ 해결 방법
AWS CloudFront 배포 인스턴스에서 기존에 쓰고 있던 캐시 정책을 찾아 다음과 같이 헤더에 User-Agent를 추가해줬다.

그 결과! 배포본에도 잘 적용이 되었다.
사용자 경험을 향상시키는 경험을 하는 것은 언제나 뿌듯하다 😊


