에러 메시지와 스택 트레이스 분석을 통해 문제 해결하기

Dev Log

자바스크립트로 카드 게임 로직을 구현하던 중 TypeError: Cannot read properties of undefined라는 에러를 만났다.

 

코드가 복잡하고 짐작가는 부분이 단번에 떠오르지 않아 막막했는데, 에러 메시지와 스택 트레이스를 차근차근 분석해가며 원인을 찾아낸 과정을 기록해본다.

 

에러 발생

게임 로직을 테스트하던 중 특정 입력에서 다음과 같은 에러가 발생했다.

TypeError: Cannot read properties of undefined (reading 'array')
    at getLastElement (03.debug.js:45:19)
    at submitCard (03.debug.js:85:7)
    at processPlayerAction (03.debug.js:103:3)
    at executeTurn (03.debug.js:118:5)
    at play (03.debug.js:182:5)

과정의 이해를 위해 게임의 구조를 간단히 설명하자면,

플레이어들이 턴마다 카드를 제출해서 4개의 숫자 배열 중 하나에 추가하는 게임으로, 조건에 따라 배열에 추가할 수 없으면 벌점을 받고 해당 배열이 비워지는 규칙이 있다.

 

에러 메시지 해석하기

TypeError: Cannot read properties of undefined (reading 'array')
  • undefined인 객체의 array 속성에 접근하려고 함
  • 즉, 어떤 변수나 함수 반환값이 예상과 달리 undefined라는 것

스택 트레이스 읽기

스택 트레이스를 보면 함수들이 어떤 순서로 호출되었는지 알 수 있다. 아래에서 위로 올라가면서 읽으면 된다.

    at getLastElement (03.debug.js:45:19)
    at submitCard (03.debug.js:85:7)
    at processPlayerAction (03.debug.js:103:3)
    at executeTurn (03.debug.js:118:5)
    at play (03.debug.js:182:5)
  • play() → executeTurn() → processPlayerAction() → submitCard() → getLastElement()
  • 가장 위의 getLastElement()에서 에러 발생

 

에러 지점 추적하기

가장 위에 있는 getLastElement()에서 에러가 발생했으니 이 함수를 먼저 살펴봤다.

function getLastElement(arrayObj) {
  return arrayObj.array[arrayObj.array.length - 1]; // 에러 발생 지점
}

function submitCard(selectedCard, selectedPlayer, bestArray) {
  if (getLastElement(bestArray) > selectedCard) { // bestArray가 undefined
    bestArray.array.push(selectedCard);
  }
  // ...
}

getLastElement() 함수는 객체를 받아서 그 객체 안의 array 속성에 접근하는 함수이다. 여기서 arrayObj.array에 접근하려는데 arrayObj 자체가 undefined라는 것이다.

 

getLastElement()가 사용된 submitCard()를 보면, 전달받은 bestArray 매개변수가 undefined라는 뜻이 된다.

 

undefined의 출처 찾기

그러면 이제 bestArray가 어디서 왔는지를 찾아봐야 한다.

function processPlayerAction(selectedCardInfo) {
  let selectedCard = selectedCardInfo.card;
  let selectedPlayer = selectedCardInfo.player;

  let activeArrays = getActiveArrays(); // 비어있지 않은 배열들만 선별
  let bestArray = findBestArray(activeArrays, selectedCard); // 최적 배열 찾기

  submitCard(selectedCard, selectedPlayer, bestArray);
}

findBestArray() 함수에서 최적의 배열을 찾아서 반환하는데, 이 함수가 undefined를 반환했다는 거다.

 

findBestArray() 함수를 살펴보자.

function findBestArray(activeArrays, selectedCard) {
  // activeArrays를 순회하면서 최적 배열 찾기
  activeArrays.forEach((item) => {
    let lastElement = getLastElement(item);
    item.diff = Math.abs(lastElement - selectedCard);
  });

  let minDiff = Math.min(...activeArrays.map((item) => item.diff));
  let bestArrays = activeArrays.filter((item) => item.diff === minDiff);

  if (bestArrays.length === 1) {
    return bestArrays[0];
  } else {
    let maxLastElement = Math.max(
      ...bestArrays.map((item) => getLastElement(item))
    );
    return bestArrays.find((item) => getLastElement(item) === maxLastElement);
  }
}

함수의 마지막 부분을 보면, bestArrays.find()를 사용해 조건에 맞는 배열을 찾아 반환하고 있다.

 

한 가지 생각해 볼 것은, find() 메서드는 조건에 맞는 요소가 없으면 undefined를 반환한다는 것이다.

그렇다면 bestArrays 배열 자체가 비어있거나 조건에 맞는 요소가 없다는 것인데 후자의 경우는 불가능하다.

왜냐하면 bestArrays에 요소가 하나라도 있다면 Math.max()로 maxLastElement를 찾게 되고, 그 값은 반드시 bestArrays 안의 어떤 요소에서 나온 값이기 때문이다.

따라서 그 값과 일치하는 요소를 find()로 찾으면 반드시 찾을 수 있어야 한다.

 

결국 bestArrays 자체가 빈 배열일 경우에 발생하는 문제라는 것인데, bestArrays는 activeArrays.filter()의 결과이므로 activeArrays가 빈 배열일 것이라는 추론이 가능하다.

 

근본 원인 발견

activeArrays는 왜 빈 배열이 되었을까?

function getActiveArrays() {
  const arrays = [
    { name: "arr1", array: arr1 },
    { name: "arr2", array: arr2 },
    { name: "arr3", array: arr3 },
    { name: "arr4", array: arr4 },
  ];

  return arrays.filter((item) => !isEmpty(item.array)); // 비어있지 않은 배열만 반환
}

이 함수는 게임에서 사용하는 4개의 배열 중에서 비어있지 않은 것들만 골라내는 역할을 한다. 그런데 만약 4개의 배열이 모두 비어있다면 최종적으로 빈 배열을 반환하게 되는 것이다.

 

즉, 게임 진행 중에 모든 배열이 비워져 있는데 게임이 종료되지 않고 유효한 배열을 찾아 게임을 진행하려고 해서 생긴 문제인 것이다.

 

코드 개선

문제의 원인 파악

기존 코드를 다시 살펴보니 해결 방안을 찾을 수 있었다.

function play(param0) {
  // ...
  while (true) {
    // 턴 시작 전에만 종료 조건 체크
    if (areAllEmpty(arr1, arr2, arr3, arr4)) {
      return demerits;
    }

    executeTurn(); // 단순히 턴만 실행
  }
}

function executeTurn() {
  let playersCards = getPlayersFirstCard();
  playersCards.sort((a, b) => a.card - b.card);

  while (!isEmpty(playersCards)) {
    // 턴 진행 중에는 종료 조건 체크 안함
    let selectedCardInfo = playersCards[0];
    processPlayerAction(selectedCardInfo); // 모든 배열이 비워진 상태에서 호출
    playersCards.shift();
  }

  removePlayersFirstCard();
}

원래는 play() 함수의 while 루프에서만 게임 종료 조건을 체크했었는데, 이 경우 턴 시작 전에만 확인을 하게 된다.

턴이 진행되는 중에도 모든 배열이 비워질 수 있는데, 이 부분을 간과한 것이다.

 

그래서 executeTurn() 내부에서도 각 카드를 처리하기 전마다 배열 상태를 체크하도록 변경했다.

function play(param0) {
  // ...
  while (true) {
    // 턴 시작 전 체크 (기존과 동일)
    if (areAllEmpty(arr1, arr2, arr3, arr4)) {
      return demerits;
    }

    // 턴 실행 결과를 확인하도록 변경
    if (!executeTurn()) {
      return demerits; // 턴 중간에 게임이 종료되면 바로 반환
    }
  }
}

function executeTurn() {
  let playersCards = getPlayersFirstCard();
  playersCards.sort((a, b) => a.card - b.card);

  while (!isEmpty(playersCards)) {
    // 각 플레이어 차례 전에 종료 조건 체크 추가
    if (areAllEmpty(arr1, arr2, arr3, arr4)) {
      return false; // 게임 종료 신호
    }

    let selectedCardInfo = playersCards[0];
    processPlayerAction(selectedCardInfo);
    playersCards.shift();
  }

  removePlayersFirstCard();
  return true; // 정상 완료
}

처음에는 단순한 undefined 에러라고 생각했는데, 결국 게임 설계에서 놓친 예외 처리가 원인이었다.

 

평소 프레임워크나 라이브러리 관련 에러는 검색으로 해결했고, 오타나 직관적인 에러는 직접 생각해서 처리할 수 있었지만, 이렇게 복잡한 구조에서 짐작하기 어려운 에러를 에러 메시지와 스택 트레이스를 통해 체계적으로 디버깅한 건 처음이었다.

 

비로소 에러 메시지를 보는 방식을 제대로 깨우치게 된 좋은 경험이었다.

'Dev Log' 카테고리의 다른 글

new Array(length).map()으로 배열 초기화시 콜백이 실행되지 않는 문제  (0) 2025.11.13
Next.js Server Actions를 활용한 캐시 재검증 문제 해결  (0) 2025.11.09
[PeaNutter] Vercel 배포 시 하위 라우터 404 오류 해결  (0) 2025.11.08
[PeaNutter] TypeScript에서 버튼 클릭 이벤트 타입 에러 해결  (0) 2025.11.08
[PearNutter] 소셜미디어 앱 마이그레이션 계획  (0) 2025.11.07
'Dev Log' 카테고리의 다른 글
  • new Array(length).map()으로 배열 초기화시 콜백이 실행되지 않는 문제
  • Next.js Server Actions를 활용한 캐시 재검증 문제 해결
  • [PeaNutter] Vercel 배포 시 하위 라우터 404 오류 해결
  • [PeaNutter] TypeScript에서 버튼 클릭 이벤트 타입 에러 해결
고견
고견
개발 자국 남기기
  • 고견
    개발자국
    고견
  • 전체
    오늘
    어제
    • 분류 전체보기 (157)
      • Frontend (29)
        • Next.js (16)
        • JavaScript (7)
      • CS (19)
        • 자료구조 (9)
        • 알고리즘 (5)
        • 운영체제 (4)
        • 네트워크 (1)
      • TIL (93)
      • Dev Log (16)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    ai 감성 일기장
    클래스
    배열
    자료구조
    javascript
    C
    CS
    인터페이스
    타입 좁히기
    Pages Router
    제네릭
    cs50
    memory
    알고리즘
    generic
    App Router
    바닐라 자바스크립트
    Next.js
    트러블 슈팅
    algorithm
    react
    문자열
    useState
    Trouble Shooting
    앱 라우터
    함수 타입
    페이지 라우터
    Spa
    typescript
    emotion diary
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
고견
에러 메시지와 스택 트레이스 분석을 통해 문제 해결하기
상단으로

티스토리툴바