안녕하세요! 프론트엔드 개발의 즐거움과 치열한 삽질의 기록을 공유하는 웹개발 블로거입니다.
지난 1편에서는 제가 개인 사이트(Code-Bricks)에 준비 중인 '깡통전세 및 전세사기 위험도 분석기'의 기획 배경과 Next.js 아키텍처 설계에 대해 이야기해 보았습니다. 멋지게 뼈대를 잡고 "이제 계산 로직만 쓱쓱 짜면 금방 끝나겠지?"라고 생각하며 콧노래를 불렀습니다.
하지만 프론트엔드 생태계는 늘 그렇듯 제게 쉽게 길을 내어주지 않았습니다. 바로 "사용자가 억 단위의 숫자를 입력할 때 콤마(,)를 찍어주는 아주 단순해 보이는 기능"에서 엄청난 벽을 만났기 때문입니다.
오늘은 프론트엔드 개발자들이 폼(Form)을 다룰 때 가장 많이 겪는 고충인 '숫자 포맷팅'과, Next.js 환경에서 이 녀석을 잘못 건드렸을 때 마주하게 되는 무시무시한 붉은색 에러, 'Hydration Mismatch Error'를 어떻게 피하고 해결했는지 그 치열한 트러블슈팅 과정을 공유해 보겠습니다.
1. "300000000" 이거 3억이야, 3천만 원이야?
부동산이나 금융 관련 서비스를 만들 때 가장 중요한 UX(사용자 경험) 중 하나는 바로 '숫자의 가독성'입니다. 전세금이나 매매가는 기본이 억 단위입니다. 입력창에 300000000이라고 치면, 0이 몇 개인지 눈이 빠져라 세어봐야 합니다. 실수로 0을 하나 덜 치면 3억이 3천만 원이 되어버리죠.
그래서 저는 입력과 동시에 300,000,000처럼 천 단위마다 콤마를 찍어주기로 결심했습니다. 자바스크립트에는 이를 위한 아주 훌륭한 내장 함수가 있습니다. 바로 toLocaleString()이죠.
저는 호기롭게 코드를 작성했습니다. (아래는 실패한 초창기 코드입니다.)
❌ 실패한 초창기 코드 (에러 유발 주의!)
const [price, setPrice] = useState(0);
return (
<input
type="text"
value={price.toLocaleString('ko-KR')}
onChange={(e) => setPrice(Number(e.target.value.replace(/,/g, '')))}
/>
);
코드를 저장하고 브라우저를 띄웠습니다. 입력창에 콤마가 아주 예쁘게 찍히더군요! "역시 난 천재야"라고 자화자찬하며 새로고침(F5)을 누른 순간... 제 모니터에 등골을 서늘하게 만드는 무시무시한 빨간색 에러 창이 떴습니다.

2. Next.js의 붉은 악마: Hydration Mismatch Error
콘솔창에는 이런 에러 메시지가 떡하니 적혀 있었습니다. Error: Text content does not match server-rendered HTML.
이게 도대체 무슨 소리일까요? 이 에러를 이해하려면 Next.js의 핵심 렌더링 방식인 SSR(Server-Side Rendering)과 Hydration(수화)의 개념을 알아야 합니다.
Next.js는 브라우저(클라이언트)가 화면을 그리기 전에, 먼저 Node.js가 돌아가는 '서버'에서 HTML을 한 번 미리 그려서(Pre-rendering) 내려보냅니다. 그리고 브라우저가 이 뼈대(HTML)를 받으면, 그 위에 자바스크립트 이벤트(클릭, 체인지 등)를 입히는 과정을 거치는데 이를 'Hydration(메마른 HTML에 생명수를 붓는다)'이라고 부릅니다.
문제는 서버에서 그려온 HTML 요소와, 브라우저가 첫 렌더링을 할 때 기대하는 DOM 요소가 단 1픽셀이라도, 띄어쓰기 하나라도 다르면 React는 패닉에 빠지며 이 에러를 뱉어냅니다.
toLocaleString() 함수가 바로 이 비극의 원인이었습니다. Node.js 서버 환경의 언어 설정(Locale)과, 제 브라우저의 언어 설정이 달랐던 것입니다. 서버에서는 단순히 300000000으로 렌더링해서 내려보냈는데, 브라우저에서 Hydration을 하려고 보니 300,000,000으로 콤마가 찍혀 있는 불일치(Mismatch)가 발생해 버린 것이죠.

3. 해결 과정: 완벽한 상태(State) 분리와 정규식의 활용
이 에러를 피하기 위해 useEffect를 써서 컴포넌트가 마운트된 이후(클라이언트 환경이 보장된 이후)에만 포맷팅을 하도록 꼼수를 부릴 수도 있습니다. 하지만 폼(Form) 요소에 그렇게 처리하면 깜빡임(Flickering) 현상이 생겨 UX가 매우 지저분해집니다.
그래서 저는 발상을 바꿨습니다. "입력창에는 콤마를 강제로 우겨넣지 말고 숫자만 받자. 대신, 그 아래에 한국인이 가장 읽기 편한 '억/만' 단위의 한글 포맷터를 실시간으로 띄워주자!"
실제 계산에 쓰일 숫자 데이터를 오염시키지 않으면서, 사용자에게는 완벽한 가독성을 제공하는 두 마리 토끼를 잡는 전략이었습니다. 제가 수정한 실제 코드는 다음과 같습니다.
✅ 정규식을 이용한 숫자 강제 추출 핸들러
const handlePriceChange = (setter: React.Dispatch<React.SetStateAction<string>>) => (e: React.ChangeEvent<HTMLInputElement>) => {
// 사용자가 '3억'이라고 한글을 치거나 콤마를 넣어도, 오직 숫자(0-9)만 남기고 다 날려버립니다.
const value = e.target.value.replace(/[^0-9]/g, '');
setter(value);
};
이렇게 정규식(/[^0-9]/g)을 사용하면 사용자가 실수로 영어, 한글, 특수문자를 쳐도 오직 숫자 문자열만 State에 안전하게 저장됩니다. 서버와 클라이언트 간의 불일치도 절대 일어날 수 없죠.
그리고 이 안전한 숫자 문자열을 받아서, 화면 아래에 '억/만' 단위로 예쁘게 변환해 주는 커스텀 포맷터 함수를 만들었습니다.
✅ 한국인 맞춤형 금액 변환기 (10000 -> 1억)
const formatKoreanCurrency = (amount: number) => {
if (amount === undefined || amount === null || isNaN(amount)) return '0원';
if (amount === 0) return '0원';
// 만원 단위로 입력받는다고 가정 (입력값이 10000이면 실제로는 1억)
const eok = Math.floor(amount / 10000);
const man = Math.floor(amount % 10000);
let result = '';
if (eok > 0) result += `${eok}억 `;
if (man > 0) result += `${man.toLocaleString()}만 `; // 여기서 안전하게 사용!
result += '원';
return result.trim();
};

이 함수를 Input 태그 바로 아래에 배치했습니다. 사용자가 입력창에 30000 (단위: 만원) 이라고 치면, 그 아래 파란색 글씨로 "변환: 3억 원" 이라고 실시간으로 뜨게 만들었습니다. 결과적으로 Hydration 에러는 완벽하게 사라졌고, 사용자는 자기가 입력한 금액이 3억인지 3천만 원인지 한눈에 정확히 파악할 수 있는 최고의 UX가 완성되었습니다!
프론트엔드 개발을 하다 보면, 이처럼 별거 아니라고 생각했던 '숫자 다루기'에서 생각보다 엄청난 시간을 쏟게 됩니다. 특히 SSR을 지원하는 Next.js 환경에서는 브라우저와 서버의 환경 차이를 항상 머릿속에 염두에 두고 코딩해야 한다는 뼈저린 교훈을 얻은 값진 트러블슈팅이었습니다.
자, 이제 입력 폼도 튼튼하게 만들었고 에러도 잡았습니다. 이제 본격적으로 이 계산기의 진짜 존재 이유인 '부동산 비즈니스 로직'을 심어줄 차례입니다.
다음 [메이킹로그 3편]에서는 단순한 사칙연산을 넘어, "아파트/오피스텔/빌라의 경매 낙찰가율 스트레스 테스트 로직"과 "HUG 보증보험 90% 가입 기준 판별 로직"을 코드로 어떻게 우아하게 풀어냈는지 그 과정을 상세히 다뤄보겠습니다. 다음 글도 많은 기대 부탁드립니다!
[깡통전세 위험도 분석기 #03] 전세사기 막아주는 '깡통전세 분석기' 기획부터 아키텍처 설계까지
안녕하세요! 프론트엔드 개발의 쏠쏠한 재미와 치열한 삽질의 기록을 전해드리는 웹개발 블로거입니다.지난 2편에서는 폼(Form)에서 억 단위 숫자를 입력받을 때 콤마를 찍다가 Next.js의 무시무시
code-bricks.tistory.com