자바스크립트에서 JSON.parse 에러 처리 방법 정리

개발/javascript&typescript1 2025. 12. 24. 07:53

API 응답이나 로컬스토리지 값을 다루다 보면 JSON.parse()는 거의 매번 등장합니다.

문제는 JSON.parse()가 실패할 때 "조용히 실패"가 아니라, 바로 예외(throw)를 던져서 코드 흐름을 끊어버린다는 점입니다.

그래서 "정상 입력만 들어온다"를 가정하기보다, "깨진 JSON이 언제든 들어올 수 있다"를 전제로 방어적으로 처리하는 편이 안전합니다.

 

아래는 현업에서 자주 쓰는 JSON.parse 에러 처리 패턴들을 상황별로 정리한 글입니다.

 


 

1) JSON.parse는 어떤 경우에 에러가 날까?

JSON.parse(str)는 문자열이 "정확한 JSON 문법"을 만족하지 않으면 SyntaxError를 던집니다. 대표적으로는 아래 케이스들이 많습니다.

 

  • 빈 문자열 "" 또는 공백만 있는 문자열
  • JSON이 아닌 텍스트 "hello"
  • 따옴표 규칙 위반: {name:"kim"} 처럼 키/문자열에 쌍따옴표가 없음
  • trailing comma: {"a":1,} 처럼 마지막 콤마가 남아있음
  • undefined, NaN, Infinity 같은 JSON에 없는 값이 섞인 문자열
  • 서버/로그에서 잘린 응답(중간에 끊김)
  • HTML이 섞여 들어옴(프록시/에러 페이지 등): "<html>..."

 

이런 상황은 생각보다 자주 발생하니, 파싱은 "항상 실패할 수 있다"로 접근하는 게 좋습니다.

 


 

2) 가장 기본: try/catch로 감싸기

JSON.parse의 정석은 결국 try/catch입니다. 단, catch에서 무조건 삼키지 말고 "원인 추적이 가능하게" 처리하는 게 핵심입니다.

function safeJsonParse(text) {
  try {
    return { ok: true, value: JSON.parse(text) };
  } catch (e) {
    return { ok: false, error: e };
  }
}

// 사용 예시
const res = safeJsonParse(localStorage.getItem("profile"));
if (!res.ok) {
  console.warn("JSON 파싱 실패:", res.error);
  // fallback 처리
} else {
  console.log(res.value);
}

포인트는 "성공/실패를 명확히 분리"하는 겁니다. 그냥 null만 반환하면, 진짜 값이 null인 경우와 실패한 경우가 섞여버립니다.


 

3) 실무형 패턴: 기본값(fallback) 제공하기

대부분의 경우 파싱에 실패하면 "기본값으로 대체"하고 계속 진행하는 게 사용자 경험상 낫습니다.

function parseOrDefault(text, defaultValue) {
  if (typeof text !== "string" || text.trim() === "") return defaultValue;

  try {
    return JSON.parse(text);
  } catch {
    return defaultValue;
  }
}

const settings = parseOrDefault(localStorage.getItem("settings"), {
  theme: "light",
  lang: "ko",
});

여기서 중요한 점 하나:

 

  • text.trim()===""을 먼저 체크하면, "빈 값" 때문에 불필요하게 예외를 던지는 비용과 로그 오염을 줄일 수 있습니다.

 


 

4) "JSON인데 원하는 형태가 아닌" 경우까지 처리하기 (파싱 성공 후 검증)

 

JSON.parse가 성공했다고 해서 데이터가 "정상"이라는 보장은 없습니다. 예를 들어 서버가 {}를 내려주거나, 배열이어야 하는데 객체가 내려오는 경우가 많습니다.

 

그래서 실무에서는 파싱 다음 단계로 "형태 검증"을 자주 합니다.

function isObject(v) {
  return v !== null && typeof v === "object" && !Array.isArray(v);
}

function parseProfile(text) {
  const obj = parseOrDefault(text, null);
  if (!isObject(obj)) return null;

  // 필요한 키 검증
  if (typeof obj.name !== "string") return null;
  if (typeof obj.age !== "number") return null;

  return obj;
}

정리하면:

 

  • 1차: JSON 문법 검증(try/catch)
  • 2차: 스키마/형태 검증(타입, 필수 키 등)

 

5) 로깅/모니터링 관점: "어디서 실패했는지" 남기기

파싱 에러는 재현이 어려운 경우가 많아서, 운영에서는 로그에 최소한의 단서를 남겨두는 게 유용합니다.

 

추천 패턴은 아래 정도입니다.

 

  • 에러 메시지
  • 입력 문자열 길이
  • 일부 앞부분 스니펫(민감정보 제외)
function parseWithLog(text, context = "unknown") {
  try {
    return JSON.parse(text);
  } catch (e) {
    const snippet = typeof text === "string" ? text.slice(0, 200) : String(text);
    console.warn(`[${context}] JSON.parse 실패:`, e?.message, {
      length: typeof text === "string" ? text.length : null,
      snippet,
    });
    return null;
  }
}

주의할 점:

 

  • 개인정보/토큰이 들어올 수 있는 문자열은 그대로 로그에 남기면 안 됩니다.
  • 스니펫을 남기더라도 마스킹 룰을 정해두는 편이 안전합니다.

 


 

6) API 응답 처리에서 자주 쓰는 패턴

 

6.1 서버가 "가끔 JSON이 아닌 문자열"을 주는 경우

예: 에러 상황에 HTML을 반환하거나, plain text를 반환하는 API

async function fetchJsonSafe(url) {
  const resp = await fetch(url);
  const text = await resp.text();

  const data = parseOrDefault(text, null);
  if (data === null) {
    // 여기서 status, url 등을 같이 기록해두면 추적이 쉬움
    console.warn("응답 JSON 파싱 실패", { url, status: resp.status });
  }
  return data;
}

resp.json()을 바로 쓰면 내부에서 파싱 예외가 나면서 흐름이 끊길 수 있어서,

운영 안정성을 원하면 text()로 받은 다음 직접 파싱하는 방식도 꽤 많이 씁니다.

 

6.2 "status가 200인데 내용이 비정상" 같은 케이스

이럴 때는 파싱 후 스키마 검증이 중요합니다.

 


 

7) 흔한 실수들

7.1 JSON.parse(null) 은 어떻게 될까?

 

  • JSON.parse(null)은 인자로 null이 들어가면 내부적으로 문자열 "null"로 처리되지 않습니다.
  • 실제로는 JSON.parse에 null을 넘기면 타입이 맞지 않아 예외가 날 수 있습니다(환경에 따라 메시지는 다르지만 실패로 보는 게 안전).
  • 그래서 typeof text === "string" 체크를 먼저 하는 패턴이 유용합니다.

 

 

7.2 "이미 객체인데 또 parse 하는" 실수

API 라이브러리/axios 설정 등에 따라 이미 객체로 들어오는 경우가 있습니다.

function ensureObject(input) {
  if (typeof input === "string") return parseOrDefault(input, null);
  if (input !== null && typeof input === "object") return input;
  return null;
}

 

7.3 try/catch 안에서 너무 많은 일을 하는 경우

try 블록에는 "예외가 날 수 있는 최소 범위(= JSON.parse 한 줄)"만 넣는 편이 디버깅이 쉽습니다.

 


8) 실전 템플릿: 운영 친화형 safeParse 함수 하나로 통일하기

프로젝트에서 파싱을 여기저기 흩어놓으면, 에러 처리 품질이 들쑥날쑥해집니다. 아래처럼 유틸 하나로 통일해두면 유지보수가 쉬워집니다.

export function safeParseJson(text, {
  defaultValue = null,
  context = "json",
  validate = null, // (value) => boolean
  onError = null,  // (error, meta) => void
} = {}) {
  if (typeof text !== "string" || text.trim() === "") return defaultValue;

  try {
    const value = JSON.parse(text);

    if (typeof validate === "function" && !validate(value)) {
      return defaultValue;
    }

    return value;
  } catch (error) {
    if (typeof onError === "function") {
      onError(error, { context, length: text.length });
    }
    return defaultValue;
  }
}

 

사용 예:

const profile = safeParseJson(localStorage.getItem("profile"), {
  defaultValue: null,
  context: "localStorage.profile",
  validate: (v) => v && typeof v === "object" && typeof v.name === "string",
  onError: (e, meta) => console.warn("파싱 실패", e.message, meta),
});

 


 

JSON.parse()는 실패 시 예외를 던지기 때문에, 운영 환경에서 안정성을 확보하려면 반드시 방어 로직이 필요합니다.

기본은 try/catch지만, 실무에서는 보통 여기서 한 단계 더 나아가서 (1) 빈값/타입 체크로 불필요한 예외를 줄이고, (2) 파싱 성공 후 데이터 형태를 검증하고, (3) 로그에는 추적 가능한 최소 정보를 남기는 방식으로 설계합니다.

반응형
admin