
Codefug Blog
react-responsive를 사용하지 못하는 상황, dotenv 사용시 문제 발생
어리석게도 마음 한켠에 있던 생각이었다. 물론 직접 구현하는 것이 좋다는 이야기를 듣고 인피니티 스크롤을 interceptor observer API로 직접 구현해보는 등의 작업은 하긴 했지만 마음에 와닿지는 않았었다.
하지만 nextjs를 학습하면서 정말 필요한 일이 생겨버린다.
여느때와 같이 js를 이용해서 반응형으로 데이터를 불러오려고 했던 나.
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.
nextjs를 사용하면 무조건 만나는 문제인 hydration error를 만나버렸다.
SSR방식에서는 유저 요청시 서버에서 HTML을 인터랙션 부분을 제외해서 한번 주고 이후에 JS 번들을 넘겨서 인터랙션 부분을 붙인다. 이 붙이는 부분은 물을 뿌린다고 해서 hydration 이라고 한다.
다시 말해 위의 에러는 "처음 서버에서 준 HTML이랑 다음에 받은 UI가 다르다는 것이다." 라는 거다.
이는 react-responsive 내부에서 size를 확인하기 위해 window 관련 API를 사용했고 Node.js 환경에서 돌아가는 서버 부분은 이를 이해하지 못한 채 처음 HTML을 넘기고 이후 JS 번들은 이를 이해해서 다르게 반응했기에 벌여진 일이었다.
역경이 있어도 프로젝트를 해내야 했기에 성능 최적화까지 겸한 responsive를 만들기로 했다.
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 방식에 대해서 깊게 공부하게 된 좋은 기회가 되었다. 좀 더 다양한 방식으로 확장할 수 있는 개발자가 되어야겠다.
다른 진영은 잘 모르지만 js 진영에서는 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에서는 환경변수를 분리해놨다.
NEXTPUBLIC~ 로 env에 넣어놓으면 빌드 타임에 js bundle에 환경 변수를 넣어놓음으로써 브라우저에서 접근할 수 있게 된다.
(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 ), 라우트 핸들러 등 프론트엔드 서버 단에서 사용되는 함수들은 해당 환경 변수에 접근할 수 있다.
개인 학습 중에 겪은 경험들입니다. 부족한 부분이 있다면 댓글 적어주세요.
감사합니다.