풀스택 개발자로서 기술 블로그를 운영하다 보면, 직접 만든 코드가 아닌 외부 템플릿이나 라이브러리에서 예상치 못한 문제와 마주할 때가 많습니다. 최근 제 블로그에서도 비슷한 경험을 했습니다. 검색 엔진 최적화(SEO)의 일환으로 Blogger 템플릿에서 제공하는 사이트맵 기능을 적용하던 중, 특정 조건에서 페이지가 깨지고 스크립트 전체가 멈춰버리는 치명적인 오류를 발견했습니다. 개발자 콘솔에 찍힌 범인은 바로 자바스크립트 개발자라면 누구나 한 번쯤 골머리를 앓아봤을 `TypeError: Cannot read properties of undefined`였습니다.
이 글은 단순한 문제 해결 기록이 아닙니다. 이 'undefined' 오류를 해결하는 과정에서 마주쳤던 기술적 함정들을 풀스택 개발자의 관점에서 깊이 있게 파헤쳐 보고자 합니다. 왜 단순한 if 조건문만으로는 부족했는지, 그리고 왜 try-catch 구문이 이런 종류의 런타임 오류에 대한 근본적인 해결책이 될 수 있는지를 실제 코드와 함께 상세히 분석합니다. 더 나아가 동기 및 비동기 환경에서 예외를 처리하는 올바른 방법론까지 다루어, 견고하고 안정적인 자바스크립트 코드를 작성하고자 하는 모든 개발자분들께 실질적인 도움이 되고자 합니다.
문제의 발단: 블로거 사이트맵에서 마주친 'undefined' 오류
모든 것은 블로거 사이트맵 페이지를 생성하면서 시작되었습니다. SEO의 기본은 검색 엔진이 내 사이트의 구조를 쉽게 파악할 수 있도록 XML 사이트맵을 제공하는 것이지만, 사용자 편의를 위해 HTML 형태의 사이트맵 페이지를 만드는 경우도 많습니다. 제가 사용하던 템플릿 역시 블로그의 모든 게시물 목록을 동적으로 불러와 예쁘게 보여주는 기능을 포함하고 있었습니다.
템플릿 가이드를 따라 기능을 활성화하고 페이지를 확인하는 순간, 기대와는 다른 화면이 저를 맞이했습니다. 일부 게시물 목록만 표시되다가 중간부터 내용이 잘려나간 것처럼 보였고, 레이아웃도 어딘가 어색하게 틀어져 있었습니다. 직감적으로 스크립트 오류를 의심하고 F12 키를 눌러 개발자 콘솔을 열었습니다. 아니나 다를까, 붉은색 오류 메시지가 선명하게 찍혀 있었습니다.
TypeError: Cannot read properties of undefined (reading 'url')
바로 그 유명한 자바스크립트 undefined 오류였습니다. 이 오류는 'undefined' 상태인 값에서 'url'이라는 속성을 읽으려고 시도했기 때문에 발생했다는 의미입니다. 자바스크립트에서 undefined는 변수는 선언되었지만 아직 값이 할당되지 않은 상태를 나타내는 원시 값(Primitive Value)입니다. 존재하지 않는 객체의 속성에 접근하려고 할 때도 이 오류가 발생합니다. 즉, 스크립트가 특정 게시물의 썸네일 이미지 URL을 가져오려고 했는데, 그 게시물에 썸네일 정보 자체가 존재하지 않아 오류가 발생하고, 그 순간 스크립트의 실행이 완전히 멈춰버린 것입니다.
자바스크립트는 기본적으로 인터프리터 언어이며, 단일 스레드(Single-threaded)로 동작합니다. 이것이 의미하는 바는, 코드의 한 줄에서 처리되지 않은 예외(Uncaught Exception)가 발생하면 해당 스크립트의 실행 흐름이 그 자리에서 즉시 중단된다는 것입니다. 이 때문에 제 사이트맵 페이지는 오류가 발생한 게시물 이후의 모든 목록을 화면에 그리지 못하고 멈춰버렸던 것입니다. 이는 단순히 기능 하나가 동작하지 않는 것을 넘어 전체 페이지의 동작을 마비시킬 수 있는 심각한 문제입니다.
근본 원인 분석: 썸네일 데이터 누락과 스크립트 실행 중단
오류 메시지를 단서 삼아 템플릿의 자바스크립트 코드를 파헤치기 시작했습니다. (안타깝게도 코드는 압축(minified)되어 있고 정렬도 엉망이라 가독성이 매우 떨어지는 상태였습니다.) 코드 분석 도구의 도움을 받아 문제의 코드를 찾아내니, Blogger API가 반환하는 JSON 데이터를 파싱하여 각 게시물의 제목, URL, 그리고 썸네일 이미지를 추출하는 로직이었습니다.
Blogger API는 게시물 목록을 JSON 형태로 제공하는데, 각 게시물 객체(entry)는 다음과 유사한 구조를 가집니다.
{
"entry": {
"title": { "$t": "게시물 제목" },
"link": [
{ "rel": "alternate", "href": "https://..." }
],
"media$thumbnail": {
"url": "https://.../thumbnail.jpg"
}
// ... 기타 데이터
}
}
문제가 된 코드는 바로 이 구조를 기반으로 썸네일 URL에 접근하는 부분이었습니다. 대략 아래와 같은 형태의 코드였죠.
// posts는 API로부터 받은 게시물 목록 배열
posts.forEach(function(entry) {
// ... (제목, 게시물 URL 등을 추출하는 코드)
// 바로 이 부분에서 오류 발생!
var thumbnailUrl = entry.media$thumbnail.url;
// 추출한 정보로 HTML 요소를 만들어 페이지에 추가
var newPostElement = document.createElement('div');
newPostElement.innerHTML = `<a href="..."><img src="${thumbnailUrl}">...</a>`;
document.getElementById('sitemap-container').appendChild(newPostElement);
});
이 코드는 모든 게시물(entry)에 media$thumbnail 객체와 그 안의 url 속성이 반드시 존재할 것이라고 '가정'하고 있습니다. 하지만 제가 작성한 게시물 중 일부는 테스트 목적이었거나 정보성 글이라 대표 이미지를 설정하지 않았습니다. 바로 이 기본 썸네일이 없는 게시물 차례가 되었을 때, entry.media$thumbnail은 undefined가 됩니다. 그리고 undefined에서 .url 속성을 찾으려고 하니, 자바스크립트 엔진은 "존재하지 않는 것의 속성을 읽을 수 없다"는 `TypeError`를 던지고 실행을 멈춰버린 것입니다.
자바스크립트에서 '값이 없음'을 나타내는 데에는 undefined와 null 두 가지가 있어 혼란을 주기도 합니다. 둘의 차이를 명확히 이해하는 것이 중요합니다.
undefined: 변수가 선언되었으나 아직 값이 할당되지 않았음을 의미합니다. 시스템이 암묵적으로 할당하는 값입니다. 함수에서 아무것도 반환하지 않거나, 존재하지 않는 객체 속성에 접근할 때도undefined가 반환됩니다.null: 개발자가 의도적으로 '값이 없음' 또는 '객체가 없음'을 명시하기 위해 사용하는 값입니다.typeof null이 `object`로 나오는 것은 자바스크립트 초기의 버그지만, 하위 호환성 때문에 수정되지 않고 있습니다.
이번 경우, 게시물 객체에 media$thumbnail 속성 자체가 존재하지 않았기 때문에 undefined가 반환된 것입니다.
첫 번째 시도와 실패: if 조건문만으로는 부족한 이유
문제의 원인을 파악했으니 해결은 간단해 보였습니다. 썸네일 객체가 존재하는지 확인하는 if 조건문을 추가하면 될 것이라고 생각했습니다. 이것은 대부분의 개발자가 가장 먼저 떠올리는 자연스러운 접근 방식입니다.
// 1차 시도: if 문으로 undefined 체크
posts.forEach(function(entry) {
var thumbnailUrl = '기본_이미지_URL'; // 기본값을 먼저 설정
if (entry.media$thumbnail !== undefined) {
thumbnailUrl = entry.media$thumbnail.url;
}
// ... 이후 로직은 동일
});
위와 같이 코드를 수정하고 다시 페이지를 확인했습니다. 하지만 여전히 동일한 오류가 발생했습니다. 순간 당황스러웠습니다. "분명히 undefined를 체크했는데 왜 안 되지?" 원인은 제가 객체 구조의 복잡성을 간과했기 때문입니다. 데이터는 여러 단계로 중첩될 수 있으며, 어느 단계에서든 `undefined`가 될 가능성이 있습니다. 예를 들어, 극단적인 경우지만 entry 객체 자체가 `undefined`일 수도 있고, entry.media$thumbnail은 존재하지만 그 안에 url 속성이 없을 수도 있습니다.
이러한 중첩된 객체의 속성에 안전하게 접근하기 위해서는 각 단계를 모두 확인해야 합니다. 이것을 '방어 코드(Defensive Code)'라고 합니다.
// 더 안전한 접근 방식 (하지만 코드가 길어진다)
var thumbnailUrl = '기본_이미지_URL';
if (entry && entry.media$thumbnail && entry.media$thumbnail.url) {
thumbnailUrl = entry.media$thumbnail.url;
}
이 코드는 논리 연산자 `&&`의 단축 평가(Short-circuit evaluation)를 이용한 것입니다. 첫 번째 조건 entry가 `false`로 평가되는 값(예: `undefined`, `null`)이면 뒤의 조건은 아예 평가하지 않고 넘어가므로 오류를 피할 수 있습니다. 이 방식은 효과적이지만, 객체의 깊이가 깊어질수록 코드가 길고 지저분해진다는 단점이 있습니다.
최신 자바스크립트(ES2020)에서는 이런 문제를 해결하기 위해 '옵셔널 체이닝(Optional Chaining)' 연산자 ?.가 도입되었습니다.
// 옵셔널 체이닝을 사용한 훨씬 간결한 코드
var thumbnailUrl = entry?.media$thumbnail?.url || '기본_이미지_URL';
?. 연산자는 왼쪽 피연산자가 `null` 또는 `undefined`이면 평가를 멈추고 `undefined`를 반환합니다. 덕분에 코드가 훨씬 간결하고 가독성이 좋아집니다. 하지만 제가 수정하던 Blogger 템플릿의 자바스크립트 환경은 구형 브라우저 호환성을 고려해야 해서 최신 문법을 마음대로 사용하기 어려웠습니다.
더 근본적인 문제는, 오류가 발생할 수 있는 지점이 썸네일 추출 외에도 여러 곳에 산재해 있을 수 있다는 점이었습니다. 모든 잠재적 오류 지점을 if문으로 도배하는 것은 비효율적이며 유지보수를 어렵게 만듭니다. 이때, 과거 자바(Java) 프로젝트에서 예외 처리를 위해 사용했던 강력한 도구가 떠올랐습니다.
결정적 해결책: try-catch를 활용한 안정적인 예외 처리
try-catch 구문은 자바스크립트에서도 다른 언어들과 마찬가지로 런타임에 발생하는 예외를 처리하기 위한 핵심적인 메커니즘입니다. if문이 특정 '조건'을 확인하여 코드 흐름을 분기하는 것이라면, try-catch는 코드 블록을 실행하다가 '예외(오류)'가 발생했을 때 프로그램이 중단되지 않고 대안적인 흐름을 탈 수 있도록 해줍니다. 이것이 바로 제가 찾던 해결책이었습니다.
try-catch문의 기본 구조와 동작 원리
try-catch는 `try`, `catch`, 그리고 선택적으로 `finally` 블록으로 구성됩니다.
try블록: 예외 발생 가능성이 있는 코드를 이 블록 안에 작성합니다. 자바스크립트 엔진은try블록 내부의 코드를 실행합니다.catch블록:try블록에서 예외가 발생하면, 그 즉시 실행이 중단되고 제어권이catch블록으로 넘어옵니다. 발생한 예외에 대한 정보는catch블록의 매개변수(보통error또는e로 명명)로 전달됩니다. 예외가 발생하지 않으면catch블록은 실행되지 않습니다.finally블록 (선택 사항):try블록의 실행이 정상적으로 끝나든,catch블록으로 넘어가든 상관없이 항상 마지막에 실행되는 코드 블록입니다. 주로 자원 해제(예: 파일 닫기, 네트워크 연결 종료)와 같이 반드시 실행되어야 하는 정리 코드를 넣습니다.
이 구조를 제 문제에 적용해 보았습니다. 썸네일 URL을 추출하는 코드를 포함한, 각 게시물을 처리하는 로직 전체를 try 블록으로 감쌌습니다.
posts.forEach(function(entry) {
try {
// 예외 발생 가능성이 있는 모든 코드를 try 블록에 넣는다.
var title = entry.title.$t;
var postUrl = '';
for (var i = 0; i < entry.link.length; i++) {
if (entry.link[i].rel == 'alternate') {
postUrl = entry.link[i].href;
break;
}
}
// 가장 위험한 부분!
var thumbnailUrl = entry.media$thumbnail.url;
// 성공적으로 모든 정보를 추출했다면 HTML 요소를 생성한다.
var newPostElement = document.createElement('div');
newPostElement.innerHTML = `<a href="${postUrl}"><img src="${thumbnailUrl}">${title}</a>`;
document.getElementById('sitemap-container').appendChild(newPostElement);
} catch (error) {
// try 블록에서 오류가 발생하면 이곳으로 온다.
// 일단은 콘솔에 오류를 기록하여 어떤 게시물에서 문제가 생겼는지 파악할 수 있게 한다.
console.warn("게시물 처리 중 오류 발생:", error);
console.warn("문제가 발생한 게시물 데이터:", entry);
// 여기서 멈추지 않고 다음 게시물 처리를 위해 루프를 계속 진행한다.
// (catch 블록에 아무것도 작성하지 않아도 루프는 계속된다)
}
});
결과는 성공적이었습니다! try-catch를 적용하자, 썸네일이 없는 게시물에서 오류가 발생하더라도 catch 블록이 이를 '잡아채고' 프로그램이 중단되는 것을 막아주었습니다. 덕분에 해당 게시물은 목록에 표시되지 않지만, 그 이후의 게시물들은 정상적으로 처리되어 사이트맵 전체가 끝까지 렌더링되었습니다. 드디어 페이지가 깨지지 않는 안정적인 상태가 된 것입니다.
try-catch 심화: 언제, 어떻게 사용해야 할까?
try-catch는 강력하지만, 남용하면 코드의 흐름을 파악하기 어렵게 만들거나 성능에 미세한 영향을 줄 수도 있습니다. 따라서 언제, 어떻게 사용하는 것이 가장 효과적인지 이해하는 것이 중요합니다. 풀스택 개발자로서 저는 try-catch를 다음과 같은 상황에 주로 사용합니다.
- 외부 데이터 처리: API 응답, 사용자 입력, 파일 읽기 등 내가 통제할 수 없는 외부 데이터를 다룰 때. 데이터의 형식이 예상과 다르거나 손상되었을 가능성에 항상 대비해야 합니다. 제 블로거 사이트맵 사례가 여기에 해당합니다.
- 네트워크 통신:
fetch나axios를 이용한 AJAX 요청은 실패할 수 있습니다. 서버 다운, 네트워크 단절, CORS 오류 등 다양한 예외 상황이 발생할 수 있습니다. - JSON 파싱:
JSON.parse()는 유효하지 않은 JSON 문자열을 만나면 `SyntaxError`를 던집니다. 서버로부터 받은 텍스트가 항상 유효한 JSON이라고 보장할 수 없으므로try-catch로 감싸는 것이 안전합니다. - 브라우저 API 접근:
localStorage,sessionStorage같은 웹 스토리지 API는 사용자가 개인정보 보호 모드에서 사용하거나 설정을 통해 비활성화한 경우 접근 시 보안 오류를 발생시킬 수 있습니다.
동기(Synchronous) 코드 vs. 비동기(Asynchronous) 코드에서의 예외 처리
여기서 매우 중요한 점은, 전통적인 try-catch 구문은 동기적으로 실행되는 코드의 예외만 잡을 수 있다는 것입니다. 비동기 코드에서는 다른 방식으로 예외를 처리해야 합니다.
| 방식 | 설명 | 예외 처리 방법 | 코드 예시 |
|---|---|---|---|
| 동기(Synchronous) | 코드가 순서대로 즉시 실행됨. `forEach` 루프, `JSON.parse` 등이 해당. | try-catch |
|
| 비동기(Callbacks) | 과거에 많이 사용되던 방식으로, 작업 완료 후 콜백 함수를 호출. setTimeout, 구형 라이브러리 등. |
콜백 함수의 첫 번째 인자로 에러를 전달하는 패턴 (Error-first callback) | |
| 비동기(Promises) | ES6에 도입된 비동기 처리 객체. fetch API가 Promise를 반환. |
.catch() 메서드 체이닝 |
|
| 비동기(Async/Await) | ES2017에 도입. Promise를 동기 코드처럼 보이게 만드는 문법적 설탕(Syntactic Sugar). | 동기 코드와 동일하게 try-catch 사용 가능! |
|
위 표에서 볼 수 있듯이, async/await 문법의 등장은 비동기 코드의 예외 처리 방식을 다시 try-catch로 통일시켜 주어 코드의 일관성과 가독성을 크게 높였습니다. 비동기 통신이 잦은 현대 웹 개발에서 async/await와 try-catch 조합은 거의 필수적인 패턴으로 자리 잡았습니다.
완성도 높이기: 기본 썸네일 적용과 UI/UX 개선
이제 블로거 사이트맵은 더 이상 오류로 멈추지 않게 되었습니다. 하지만 사용자 경험(UX) 관점에서 보면 아직 개선할 점이 남아있었습니다. 썸네일이 없는 게시물은 그냥 목록에서 누락되어 버리기 때문에, 전체적인 레이아웃이 들쑥날쑥해 보이고 사용자에게 완전한 목록을 제공하지 못하는 문제가 있었습니다.
이 문제를 해결하기 위해, catch 블록을 단순히 오류를 무시하는 공간이 아니라, 오류 발생 시 대안적인 동작을 수행하는 '복구'의 공간으로 활용하기로 했습니다. 즉, 썸네일 추출에 실패하면(오류가 발생하면) 미리 준비해 둔 기본 썸네일(Default Thumbnail) 이미지 URL을 사용하도록 로직을 수정했습니다.
const DEFAULT_THUMBNAIL_URL = 'https://.../default-image.png'; // 기본 이미지 주소
posts.forEach(function(entry) {
let title, postUrl, thumbnailUrl; // 변수 선언을 루프 시작 부분으로
try {
// 성공 시나리오: 모든 정보가 정상적으로 존재
title = entry.title.$t;
// ... postUrl 추출 로직 ...
thumbnailUrl = entry.media$thumbnail.url;
} catch (error) {
// 실패(복구) 시나리오: try 블록에서 오류 발생 시 실행
console.warn("썸네일 처리 중 예외 발생, 기본 이미지로 대체합니다.", entry.title.$t);
// 썸네일을 제외한 나머지 정보는 최대한 가져오려고 시도
title = entry.title?.$t || '제목 없음';
// ... postUrl 추출 로직 ... (이 부분도 안전하게)
// 핵심: 썸네일 변수에 기본 이미지 URL을 할당
thumbnailUrl = DEFAULT_THUMBNAIL_URL;
}
// try-catch 블록 이후, 추출된 정보(성공했든, 복구했든)를 사용하여 UI를 생성
// 단, title이나 postUrl 마저 없다면 아예 표시하지 않는 등의 추가 로직도 가능
if (title && postUrl) {
var newPostElement = document.createElement('div');
newPostElement.innerHTML = `<a href="${postUrl}"><img src="${thumbnailUrl}">${title}</a>`;
document.getElementById('sitemap-container').appendChild(newPostElement);
}
});
이처럼 catch 블록을 활용하여 오류가 발생했을 때의 대체 흐름을 정의함으로써, 프로그램의 안정성뿐만 아니라 사용자 경험의 질까지 높일 수 있습니다. 이제 제 사이트맵은 썸네일 유무와 상관없이 모든 게시물 목록을 빠짐없이, 그리고 일관된 디자인으로 보여주게 되었습니다. 이것이 바로 견고한 예외 처리가 가져다주는 힘입니다.
템플릿 코드 작업의 현실적인 어려움과 교훈
이번 Blogger 템플릿 커스터마이징 과정은 외부 코드를 다룰 때 겪게 되는 현실적인 어려움을 다시 한번 상기시켜 주었습니다. 특히 가독성이 떨어지는 코드, 즉 정렬이 되어 있지 않거나 변수명이 의미 없이 축약되어 있고, 주석 하나 없는 코드를 디버깅하는 것은 맨땅에 헤딩하는 것과 같습니다.
개발자에게 '읽기 좋은 코드'가 왜 중요한지를 온몸으로 체감한 경험이었습니다. 코드는 한 번 작성하고 끝나는 것이 아니라, 미래의 나 또는 동료 개발자가 계속해서 읽고, 수정하고, 개선해야 하는 살아있는 문서입니다. 코드 포매터(Prettier 등)를 사용하여 일관된 스타일을 유지하고, 의미 있는 변수명을 사용하며, 복잡한 로직에는 주석을 다는 습관은 장기적으로 엄청난 유지보수 비용을 절감해 줍니다.
장기적인 관점: 리팩토링과 성능 최적화
이번에는 급한 불을 끄는 데 집중했지만, 근본적으로는 템플릿의 자바스크립트 코드를 대대적으로 리팩토링할 필요성을 느꼈습니다. 거대한 단일 스크립트 파일을 기능별 모듈로 분리하고, 오래된 코딩 패턴을 현대적인 방식으로 개선하며, 불필요한 코드를 제거하는 작업이 필요합니다. 이는 단순히 코드 품질을 높이는 것을 넘어, 저자가 언급했던 블로그 로딩 속도 저하 문제 해결에도 직접적으로 기여할 수 있습니다.
예를 들어, 현재 사이트맵은 페이지가 로드될 때 모든 게시물 정보를 한 번에 불러오고 있지만, '무한 스크롤'이나 '더 보기' 버튼을 구현하여 사용자가 스크롤할 때마다 필요한 만큼의 데이터만 동적으로 불러오는 방식으로 최적화할 수 있습니다. 이는 초기 로딩 시간을 획기적으로 단축시켜 블로그 SEO에도 긍정적인 영향을 줄 것입니다.
결론: 오류는 성장의 기회다
블로그 사이트맵의 작은 오류에서 시작된 이번 여정은 자바스크립트 undefined 오류의 본질부터 시작해, 동기와 비동기를 아우르는 견고한 예외 처리 전략의 중요성까지 되짚어보는 소중한 기회가 되었습니다. 개발자에게 버그나 오류는 좌절의 대상이 아니라, 우리의 코드를 더 단단하게 만들고 우리 자신을 더 성장시키는 밑거름이 됩니다.
이번 경험을 통해 얻은 핵심적인 교훈들을 다시 한번 정리해 봅니다.
- `undefined`를 경계하라: 외부 데이터나 중첩된 객체에 접근할 때는 항상 값이 존재하지 않을 가능성을 염두에 두어야 합니다. 옵셔널 체이닝(
?.)은 훌륭한 방어 수단입니다.- 단순 `if`를 넘어서라: 예상 가능한 조건 분기는 `if`로 처리하되, 예기치 못한 런타임 오류로부터 전체 어플리케이션을 보호하기 위해서는
try-catch가 필수적입니다.- 오류를 '처리'하라: `catch` 블록을 비워두지 마십시오. 오류를 기록(Logging)하여 디버깅에 활용하고, 가능하다면 대체 데이터를 제공하거나 사용자에게 상황을 안내하는 등 적극적인 복구 로직을 구현하는 것이 좋은 사용자 경험을 만듭니다.
- 비동기 예외 처리를 이해하라: Promise의
.catch()와 `async/await`에서의try-catch사용법을 명확히 구분하고 활용할 줄 알아야 현대적인 웹 어플리케이션을 안정적으로 구축할 수 있습니다.- 코드는 항상 가독성 있게: 미래의 자신을 위해, 동료를 위해 항상 깨끗하고 읽기 쉬운 코드를 작성하는 습관을 들입시다.
혹시 저와 비슷한 자바스크립트 undefined 오류나 Blogger 템플릿 문제로 어려움을 겪고 계신 분이 있다면, 이 글이 문제 해결의 실마리가 되었기를 바랍니다. 더 좋은 해결 방법이나 관련된 경험이 있다면 언제든지 댓글로 공유해주시면 감사하겠습니다. 함께 배우고 성장하는 개발 문화가 최고의 자산이니까요.
Post a Comment