Search
🔫

Sentry로 TypeError 해결하기

분야
FE
ISSUE
주제
FE
심각도
높음🔥
제보자
jeekim
담당자
작성자
상태
처리 완료
작성일자
2025/02/10 08:48
공개여부
공개
글감
TypeScript

문제

발견

Sentry와 연동된 디스코드로 에러 메시지가 전송되어 에러 발생을 인지했습니다.
undefined is not an object (evaluating 'r.toString')
JavaScript
복사
해당 에러 메시지를 보면, undefinedtoString 메서드를 호출하려 할 때 에러가 발생했다는 걸 알 수 있습니다.
좀 더 자세한 내용을 파악하기 위해 Sentry - Issues - 해당 이슈 페이지로 이동 ∙∙∙
Stack Trace를 보면
MainPage-bb*.js 파일의 하이라이트 된 부분(7행)에서 발생한다는 걸 알 수 있습니다.
Vite는 프로젝트를 빌드 시 기본적으로 파일 이름에 해시를 추가합니다.
[파일명]-[hash].js
Plain Text
복사
따라서 빌드 전 파일은 MainPage.tsx임을 유추할 수 있습니다.
MainPage.tsx로 이동해서 toString 검색
코드끼리 대조해 봤을 때 rcurrentFloor인 것을 알 수 있습니다.

정의

/main에서 undefined가 된 currentFloortoString 메서드 호출하면 에러(TypeError) 발생

원인

currentFloorundefined 타입일 수 있는지 확인해 보니, 같은 파일(MainPage.tsx)에서 다음과 같이 선언되어 있습니다 :
const currentFloor = useRecoilValue<number>(currentFloorNumberState); // 현재 층 수
TypeScript
복사
currentFloor는 number 타입으로 지정되어 있습니다. 따라서 toString 호출하는 코드 작성 당시 에러 메시지가 안 띄워져 오류가 발생하지 않을 것이라고 생각했을 것입니다.
그리고 Recoil 전역변수여서 atom으로는 정의된 부분을 살펴보니,
export const currentFloorNumberState = atom<number>({ key: "CurrentFloor", default: undefined, // effects_UNSTABLE: [persistAtom], });
TypeScript
복사
초깃값이 undefined 로 돼 있었습니다.
atom은 number 타입으로 지정돼 있지만,,(타입 불일치)
파악 후,

궁금증

: 내비게이션 UI를 통해 /main 경로에 접속하면 바로 현재 층수에 대한 값이 전역변수에 저장되는데, 이 전역 변수의 값은 언제 undefined가 되는 거지?
궁금증을 해결하기 위해 Replays 섹션으로 가서 See Full Replay 버튼 누르면
하나의 리플레이에 대한 더 구체적인 디테일 알 수 있는데,
Breadcrumbs에서 이벤트 발생 과정을 살펴봅시다.
00:05에 새로고침 버튼 클릭 후
00:50에 에러와 Reload 이벤트(새로고침)가 발생
00:05~00:50 사이 어떤 일이 일어났는지 안 나와 있어서 앱 외에서 조작한 가능성이 있다고 판단했습니다. 아무것도 안 했는데 currentFloor가 사라질 일은 없으니까…
Sentry의 Session Replay 기록은 사용자가 웹 애플리케이션의 UI를 통해 수행하는 행동은 기록되지만, 그 외는 기록이 안 됨

추정

1.
로컬 스토리지에서(앱 외에서) currentFloor값 삭제 후 새로고침()
삭제 후 브라우저 페이지를 새로고침하면,
예상대로 에러가 발생합니다.
2.
코드 내에서 명시적으로 currentFloorundefined로 변경
코드에서 currentFloor 값이 변경되는 경우
setCurrentFloor 호출 :
LeftMainNav.container.tsx - 왼쪽 내비게이션에서 층 버튼 클릭 시
MapItem.tsx - 지도 아이템 클릭 시
HomePage.tsx - 시작하기 버튼 클릭 시
resetCurrentFloor 호출 :
ProfileCard.container.tsx, LeftMainNav.container.tsx - 로그아웃 버튼 클릭 시
확인 결과, 코드상 undefined로 설정하는 경우는 없습니다.
그럼,

결론

currentFloor값을 의도적으로 지워서 값이 없는 채로 웹 페이지를 새로고침하게되면 Recoil의 상태도 함께 초기화됨. 따라서 currentFloor는 초깃값인 undefined가 됨.
이후 currentFloor.toString() 실행할 때 에러가 발생하는 것.
Recoil은 상태를 메모리에서 관리합니다. 따라서 페이지를 새로 고침하면 메모리가 초기화되고, Recoil의 상태도 함께 초기화됩니다.

해결

초깃값 변경 고민

초깃값을 기존 undefined로 유지할 지 0으로 변경할지 고민했습니다.
undefined로 유지
변수가 초기화되었음을 명확히 나타낼 수 있음
그러나 number 타입 변수에 undefined를 할당하면 나중에 숫자 연산을 할 때 이 에러처럼 예상치 못한 오류 발생 가능
타입을 <number | undefined>로 지정하면 → 이 상태 사용하는 모든 코드에서 타입 체크 필요
0으로 변경()
코드 상 state가 undefined 되는 경우 방지
기존 코드 변경 최소화
0층은 존재하지 않으므로 초기값으로 사용 가능

적용

초깃값 0으로 변경 후 로컬스토리지에서 값 삭제하고 새로고침하면  /home으로 이동 (정상 작동)

리팩토링

currentFloorNumberState 변수명 통일

현재 currentFloorNumberState를 여러 파일에서 currentFloorNumber, currentFloor, floor 같이 각기 다르게 선언해서 사용 중입니다.
이렇게 같은 Recoil state 임에도 변수명이 일관되지 않으니까,
상태 파악의 어려움 : 같은 state인지 즉시 파악하기 어려워 불필요하게 시간을 소모했습니다.
코드 검색의 비효율성 : 코드 검색 시 원하는 결과를 한 번에 찾기 어려웠고, 일부 코드가 누락될 위험이 있었습니다.
이런 불편을 해소하기 위해 변수명을 일관성있게 조정하기로 하였습니다.
const currentFloor = useRecoilValue<number>(currentFloorNumberState);
TypeScript
복사
현재 코드에서 가장 많이 사용하고, 직관적인 의미를 가지고 있는 currentFloor로 통일하였고, 이를 통해 코드의 가독성과 유지보수성을 향상할 수 있었습니다.

useRecoilState vs useRecoilValue

const [currentFloor] = useRecoilState<number>(currentFloorNumberState); const currentFloor = useRecoilValue<number>(currentFloorNumberState);
TypeScript
복사
여러 파일에서 Recoil 상태를 읽기만 하는 경우에도 useRecoilState를 사용하고 있었습니다.
이런 경우 useRecoilState 대신 useRecoilValue를 사용하는 것은 미미하지만 유효한 차이가 있습니다.
useRecoilValue
상태 값만 반환
useRecoilState
상태 값을 반환할 뿐만 아니라 상태를 변경할 수 있는 setter 업데이트 함수가 생성되고 반환
쓰기 작업을 하지 않을 경우에도 불필요하게 setter 함수가 생성돼서 메모리에 로드되므로 메모리 낭비가 발생
컴포넌트가 리렌더링될 때마다 setter 함수가 함께 생성
코드에서 setter 함수를 직접적으로 변수에 할당하지 않아도, 컴포넌트가 언마운트될 때까지 메모리에 남아서 계속 사용될 가능성 있음
따라서 currentFloor를 선언할 때 값만 사용하는 경우 useRecoilState 대신 useRecoilValue로 변경했습니다.

참고