
Codefug Blog

react-responsive를 사용하지 못하는 상황, dotenv 사용시 문제 발생
라이브러리가 개발자의 편의를 위해 만들어졌다고 해서 내부 동작을 이해하지 않아도 되는 것은 아닙니다.
인피니티 스크롤을 Intersection Observer API로 직접 구현해보는 등의 작업을 했지만 중요성이 와닿지 않았습니다.
Next.js를 학습하면서 라이브러리 내부 이해가 필수임을 깨달았습니다.
JavaScript를 이용해 반응형으로 데이터를 불러오려고 했습니다.
npm install react-responsive --save
react-responsive를 활용해서 사용자 지정 훅을 만들었습니다.
{isMobile, isTablet, isDesktop} 형식으로 분리하여 사용하기 쉽게 만들려고 했습니다.
오류가 발생했습니다.
Error: Hydration failed because the initial UI does not match what was rendered on the server.
Next.js에서 자주 만나는 hydration 에러를 마주했습니다.
SSR 방식에서는 사용자 요청 시 서버에서 HTML을 먼저 전송합니다.
인터랙션 부분은 제외되고 이후 JavaScript 번들로 인터랙션을 추가합니다.
이 과정을 물을 뿌린다는 의미로 hydration이라고 합니다.
에러는 "서버에서 생성한 HTML과 클라이언트에서 생성한 UI가 다르다"는 의미입니다.
react-responsive 내부에서 size를 확인하기 위해 window 관련 API를 사용했습니다.
Node.js 환경의 서버는 window API를 이해하지 못해 초기 HTML을 생성합니다.
이후 JavaScript 번들은 window API를 이해하여 다른 UI를 생성했습니다.
성능 최적화까지 고려한 반응형 커스텀 훅을 직접 만들기로 했습니다.
import { useEffect, useState } from "react";
import { debounce } from "./debounce";
export const useScreenDetector = () => {
const [screen, setScreen] = useState({
isMobile: false,
isTablet: false,
isDesktop: false,
});
const handleWindowSizeChange = () => {
if (window.innerWidth <= 744)
setScreen((prev) => ({
isMobile: true,
isTablet: false,
isDesktop: false,
}));
else if (window.innerWidth > 1200)
setScreen((prev) => ({
isMobile: false,
isTablet: false,
isDesktop: true,
}));
else
setScreen((prev) => ({
isMobile: false,
isTablet: true,
isDesktop: false,
}));
};
useEffect(() => {
window.addEventListener("resize", handleWindowSizeChange);
return () => {
window.removeEventListener("resize", handleWindowSizeChange);
};
}, []);
return screen;
};
객체 방식으로 리턴받는 것을 유지하고 싶었습니다.
객체 자체를 state로 만들어 innerWidth를 검사하는 방식으로 구현했습니다.
export function debounce(func: () => void, wait: number) {
let timeoutId: ReturnType<typeof setTimeout>;
return () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(func, wait);
};
}
디바운스를 적용하여 resize 이벤트의 과부하를 줄였습니다.
import { useEffect, useState } from "react";
import { debounce } from "./debounce";
export const useScreenDetector = () => {
const [screen, setScreen] = useState({
isMobile: false,
isTablet: false,
isDesktop: false,
});
const handleWindowSizeChange = () => {
if (window.innerWidth <= 744)
setScreen((prev) => ({
isMobile: true,
isTablet: false,
isDesktop: false,
}));
else if (window.innerWidth > 1200)
setScreen((prev) => ({
isMobile: false,
isTablet: false,
isDesktop: true,
}));
else
setScreen((prev) => ({
isMobile: false,
isTablet: true,
isDesktop: false,
}));
};
useEffect(() => {
window.addEventListener("resize", debounce(handleWindowSizeChange, 100));
return () => {
window.removeEventListener(
"resize",
debounce(handleWindowSizeChange, 100),
);
};
}, []);
return screen;
};
라이브러리를 맹신하면 안 된다는 것을 경험으로 배웠습니다.
SSR 방식을 깊이 공부하는 좋은 기회가 되었습니다.
다양한 방식으로 문제를 해결할 수 있는 개발자가 되어야겠습니다.
JavaScript 진영에서는 dotenv 라이브러리로 환경 변수를 저장하는 경우가 많습니다.
dotenv는 Node.js 환경에서 사용하는 환경 변수입니다.
문제는 브라우저에서는 사용할 수 없다는 점입니다.
예를 들면
export async function getCommentWithId(id: string, cursor: number | null) {
try {
const res = await fetch(
`${process.env.BASE_URL}/articles/${id}/comments?limit=10${cursor !== null ? `&&cursor=${cursor}` : ""}`,
);
const data: Comments = await res.json();
return data;
} catch (error) {
throw new Error();
}
}
위 코드는 SSR 시에는 동작하지만 렌더링 이후 브라우저에서 CSR할 때 문제가 발생할 수 있습니다.
Next.js에서는 환경 변수를 분리했습니다.
NEXT_PUBLIC_으로 시작하는 환경 변수를 사용하면 빌드 타임에 JavaScript 번들에 포함됩니다.
브라우저에서 접근할 수 있게 됩니다.
(async () => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/articles/${router.query.id}/comments?limit=10${cursor !== null ? `&&cursor=${cursor}` : ""}`,
);
const comments: Comments = await response.json();
setCommentList(comments.list);
setCursor(comments.nextCursor);
})();
2025-04-21 추가NEXT_PUBLIC으로 지정하면 빌드 파일 내부에 해당 변수가 포함됩니다.
중요한 환경 변수는 NEXT_PUBLIC으로 사용하면 안 됩니다.
프론트엔드 서버의 환경 변수를 사용하면 해결할 수 있습니다.
process.env.~는 서버 환경 변수입니다.
서버 컴포넌트, 서버 사이드 렌더링(getServerSideProps), 라우트 핸들러 등 서버 단에서 사용되는 함수들은 해당 환경 변수에 접근할 수 있습니다.
부족한 부분이 있다면 댓글로 알려주세요. 감사합니다.