Skip to main content
← 블로그

하루는 항상 86,400초일까?

VauDium ·

Hermes 엔진 크래시인 줄 알았던 버그의 진짜 원인은 addDays 함수 한 줄이었습니다.

하루는 항상 86,400초일까?

크래시 리포트가 쏟아졌다

Sentry를 연동한 다음 날, 크래시 리포트가 여러 건 올라왔습니다. 전부 같은 패턴이었습니다.

EXC_BAD_ACCESS: KERN_INVALID_ADDRESS
hermes::vm::DictPropertyMap::findOrAdd
hermes::vm::JSObject::getComputedPrimitiveDescriptor

Hermes VM 내부에서 메모리 접근 위반. 네이티브 크래시라 JS 스택 트레이스도 없었습니다. “Hermes GC 버그인가?” 하고 넘길 수도 있었습니다.

며칠간의 삽질

처음에는 진짜 Hermes 엔진 버그라고 생각했습니다. 스택 트레이스가 전부 hermes::vm:: 내부를 가리키고 있었으니까요. Hermes GC가 메모리를 잘못 정리하는 것 아닌가? 데이터가 너무 많아서 메모리 압박이 생기는 건가?

weekTasksMap의 배열 참조를 안정화하고, FlatList의 windowSize를 줄이고, extraData를 버전 카운터로 바꾸고, 휴일 데이터 로딩을 InteractionManager로 지연시키고. 캘린더 화면의 메모리 사용량을 줄이기 위해 할 수 있는 건 다 해봤습니다.

그래도 크래시는 계속 올라왔습니다. 재현도 안 됐습니다. 한국 시간대에서는 절대 발생하지 않는 버그였으니까요.

Sentry를 연동하고 나서야 실마리가 잡혔습니다.

단서는 console error에 있었다

Sentry breadcrumb을 자세히 보니, 네이티브 크래시 직전에 JS 에러가 찍혀 있었습니다.

TypeError: Cannot read property 'startDate' of undefined
    at WeekCalendarDepiction
    at toWeekDepictions
    at CalendarScreen (useState)

캘린더 화면이 마운트될 때, 주간 달력 데이터를 초기화하는 과정에서 터지고 있었습니다. 이 JS 에러가 발생한 후 React Native가 에러를 네이티브로 전달하는 과정에서 Hermes 크래시로 이어진 것이었습니다.

null이 나올 수 없는 곳에서 null

문제의 코드:

this.weekIdInMonth = weekInterval
    .getIntersection(monthInterval)!
    .startDate.toISOString();

주간(week)과 월간(month) 기간의 교차 구간을 구하는 건데, !로 “절대 null 아니야”라고 단언하고 있었습니다. 실제로 주간의 시작일이 속한 월의 interval을 가져오니까, 논리적으로는 반드시 겹쳐야 합니다.

그런데 null이 나왔습니다.

addDays의 함정

원인을 추적해보니 addDays 함수에 있었습니다.

// Before
export const addDays = (d, days) =>
    new Date(d.getTime() + days * 86400000);

하루를 86,400,000 밀리초로 계산하고 있었습니다. 대부분의 경우 맞습니다. 하지만 서머타임(DST) 전환일에는 하루가 23시간이거나 25시간입니다.

3월 마지막 일요일에 시계를 한 시간 앞으로 돌리는 유럽 타임존에서:

  • 자정(00:00)에서 하루를 더하면 → 다음 날 01:00이 됩니다 (23시간짜리 하루)
  • 자정에서 하루를 빼면 → 전날 23:00이 됩니다

전날 23:00은 날짜가 하나 밀립니다. 이 밀린 날짜로 월간 interval을 구하면 다른 달이 나오고, 주간과 월간이 겹치지 않게 되고, getIntersection이 null을 반환하고, 크래시.

수정은 한 줄

// After
export const addDays = (d, days) => {
    const result = new Date(d);
    result.setDate(result.getDate() + days);
    return result;
};

setDate는 날짜 단위로 연산합니다. DST 전환과 무관하게 항상 정확한 날짜가 나옵니다.

돌아보면

  • Hermes 크래시처럼 보이는 것이 JS 버그일 수 있다
  • new Date(d.getTime() + ms) 패턴은 DST에 취약하다
  • “논리적으로 null일 수 없다”는 확신은 위험하다. 환경이 논리를 깨뜨릴 수 있다
  • Sentry의 breadcrumb이 없었으면 Hermes 버그로 치부하고 넘어갔을 것이다

하루는 항상 86,400초가 아닙니다.