Haeminway haeminway
English
기술 노트로
5 분 분량

시트가 텍스트를 날짜로 바꾼다: '2026-06'이 완료를 영영 막을 때

구글 시트는 '2026-06' 같은 문자열을 조용히 Date로 강제 변환한다. 저장·읽기·비교가 각자 월을 다르게 계산하면 완료가 영원히 거부된다. 무엇이 강제 변환되는지, 셀을 텍스트로 고정하고 읽을 때 방어하는 법, 배치 쓰기와 수식 인젝션까지.

시트는 DB가 아니다. 텍스트를 마음대로 타입으로 바꾼다. 셀에 "2026-06"을 넣으면 시트가 그걸 문자열이 아니라 Date로 강제 변환한다. 저장·출력·완료가 각자 “몇 월”을 다르게 계산하면, 어제 날짜를 넣는 순간 세 값이 어긋나 완료가 영원히 거부된다.

텍스트 '2026-06'이 시트에서 Date로 강제 변환되고, save·output·done 세 경로가 서로 다른 값을 읽어 어긋난다

왜 중요한가

이건 에러를 안 낸다. 저장은 되고 화면도 멀쩡한데 완료만 안 눌린다. 원인이 코드가 아니라 시트의 타입 변환이라, 로그를 아무리 봐도 안 보인다. 그리고 이 함정은 날짜 하나가 아니다.

무엇이 강제 변환되나

시트에 들어가는 순간 원래 타입이 아니게 되는 대표 케이스:

넣은 값시트가 바꾸는 것증상
2026-06, 3/4, 1-2Date월/일 계산이 어긋남
010-1234-5678숫자(앞자리 0 소실)전화번호 깨짐
1234567890123456지수 표기 1.23E+15주문번호·바코드 손상
=, +, -, @ 로 시작수식으로 해석오류·수식 인젝션
TRUE / FALSE불리언문자열 비교 실패

즉 “사용자가 텍스트로 넣은 것”이 “읽을 때 다른 타입”으로 나온다. 이걸 모른 채 함수마다 각자 타입을 가정하면 조용히 어긋난다.

잘못된 방식: 세 경로가 각자 월을 계산한다

AI가 짠 코드의 전형이다. 함수 하나하나는 “합리적”인데, 셀 타입 가정이 제각각이다.

// 저장 경로: 문자열이라고 가정
const savedMonth = String(row[COL.month]);              // Date면 "Mon Jun 01 2026..."가 된다

// 출력 경로: Date라고 가정
const outMonth = row[COL.month].getMonth() + 1;         // 문자열이면 getMonth is not a function

// 완료 경로: 문자열 슬라이스라고 가정
const doneMonth = row[COL.month].slice(0, 7);           // "2026-06" 또는 폭발

// → 셋이 서로 다른 값을 내고, 완료 비교(savedMonth === doneMonth)가 영영 실패

한 셀을 세 곳이 다르게 해석하는 순간, 어제 날짜 하나로 완료가 막힌다.

정답: 쓸 땐 텍스트로 고정, 읽을 땐 한 곳에서 정규화

셀 서식을 먼저 텍스트(@)로 걸고 값을 넣는다. 읽을 땐 Date로 변해 있어도 시트 타임존 기준으로 다시 YYYY-MM으로 정규화한다. 모든 읽기 지점이 이 함수 하나만 통과하게 한다.

const MONTH_RE = /^\d{4}-\d{2}$/;

// 쓰기: 서식을 '텍스트'로 먼저 건 뒤 값을 넣는다 → 시트가 Date로 못 바꾼다
function writeMonthCell_(range, month) {          // month = "2026-06"
  range.setNumberFormat("@").setValue(month);
}

// 읽기: 이미 Date여도 시트 타임존 기준으로 "YYYY-MM"으로 정규화 (단일 진실 소스)
function readMonthCell_(value) {
  if (Object.prototype.toString.call(value) === "[object Date]") {
    const tz = SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone();
    return Utilities.formatDate(value, tz, "yyyy-MM"); // Date → 문자열
  }
  const text = String(value).replace(/^'/, "").trim();
  if (!MONTH_RE.test(text)) throw new Error("BAD_MONTH_CELL: " + value);
  return text;
}

대량으로 쓸 땐 셀마다 setNumberFormat을 부르지 마라 — API 호출이 폭증한다. 열 전체 서식을 한 번에 걸고 setValues로 한 번에 쓴다.

// 대량 쓰기: 서식·값을 각각 '한 번의 호출'로 (셀 루프 금지 → 배치)
const col = sheet.getRange(2, COL.month, months.length, 1);
col.setNumberFormat("@");                          // 열 전체를 텍스트 서식으로
col.setValues(months.map((m) => [m]));             // 값도 한 번에

사용자 입력을 셀에 쓸 땐 수식 인젝션도 막는다. =, +, -, @ 로 시작하면 시트가 수식으로 실행한다.

// 수식 인젝션 방어: 위험 프리픽스면 앞에 ' 를 붙여 강제로 텍스트화
function sheetSafeCell_(v) {
  const s = String(v);
  return /^[=+\-@]/.test(s) ? "'" + s : s;
}

놓치기 쉬운 것

  • 정규화는 한 곳에서. 저장·출력·완료가 각자 월을 계산하면 어긋난다. 셀을 읽는 모든 지점을 readMonthCell_ 하나로 통과시켜라.
  • 서식을 값보다 먼저. setValue 다음에 setNumberFormat("@")은 이미 늦다. 서식을 먼저 걸어야 변환을 막는다.
  • 타임존. Date를 getMonth()로 읽으면 스크립트 타임존과 시트 타임존이 다를 때 월 경계에서 하루 밀린다. getSpreadsheetTimeZone() 기준으로 포맷하라.
  • 체크섬 drift. Date 셀을 체크섬·비교에 넣을 땐 toISOString() 등으로 표현을 고정하지 않으면, 값이 안 바뀌어도 체크섬이 흔들린다.
  • 배치. 서식·값은 getValues/setValues로 한 번에. 셀 루프는 배치 호출에서 다룬 그 함정이다.

깊이: 넣기 전에 “시트가 이걸 뭘로 볼까”를 물어라

값을 쓰는 모든 셀에 대해 시트의 해석을 한 번씩 의심하라. 특히 서빙 소스로 쓰는 탭은 수식·IMPORTRANGE·필터뷰를 두지 마라 — 코드 롤백으로 수식 손상은 못 되돌린다. 핵심 한 줄: 시트에 넣는 텍스트는 서식을 텍스트로 고정하고, 읽을 땐 타입을 방어하라.

자주 묻는 질문

구글 시트에 '2026-06'을 넣었는데 날짜로 바뀌는 이유는?
시트는 날짜처럼 보이는 문자열을 입력 즉시 Date로 자동 강제 변환합니다. 막으려면 셀 서식을 텍스트(@)로 먼저 설정한 뒤 값을 넣거나, 값 앞에 작은따옴표(')를 붙이면 됩니다.
Apps Script에서 셀 값이 문자열인지 Date인지 어떻게 구분하나요?
Object.prototype.toString.call(value) === '[object Date]'로 판별합니다. typeof는 Date도 'object'로 나와 구분이 안 됩니다. Date면 시트 타임존 기준으로 다시 문자열로 정규화하세요.
왜 저장은 되는데 완료 처리만 안 되나요?
저장·출력·완료 경로가 각자 '몇 월인지'를 다르게 계산하면, 시트가 값을 Date로 바꾼 순간 세 값이 어긋나 비교가 실패합니다. 월을 읽는 지점을 정규화 함수 하나로 통일하세요.
시트에서 = 나 + 로 시작하는 사용자 입력은 위험한가요?
네. =, +, -, @ 로 시작하는 문자열은 시트가 수식으로 해석해 수식 인젝션(CSV injection) 위험이 있습니다. 사용자 입력을 셀에 쓸 땐 이 프리픽스를 이스케이프하거나 텍스트 서식으로 강제하세요.