최신 자바스크립트 기능 살펴보기
New Set Methods (ES2025)
ES2025에서는 Set 인스턴스에서 직접 집합 연산을 할 수 있는 메서드들이 추가됐습니다. 합집합, 교집합, 차집합뿐만 아니라 부분집합·상위집합 여부, 서로소 여부까지 메서드 한 번으로 확인할 수 있죠.
기존에는 두 컬렉션의 중복 없는 합집합/교집합을 만들려면 배열을 Set으로 변환하거나 filter와 includes를 조합해야 했습니다.
const a = [1, 2, 3, 3]
const b = [3, 4, 5, 5]
// 합집합
const union = [...new Set([...a, ...b])]
// 교집합
const intersection = a.filter((value) => b.includes(value))
이제 배열을 Set으로 감싸면 메서드 호출만으로 같은 결과를 얻습니다. 인자로는 Set뿐 아니라 모든 iterable을 넘길 수 있습니다.
const setA = new Set([1, 2, 3])
const setB = new Set([3, 4, 5])
setA.union(setB) // Set {1, 2, 3, 4, 5}
setA.intersection(setB) // Set {3}
setA.difference(setB) // Set {1, 2}
setA.symmetricDifference(setB) // Set {1, 2, 4, 5}
또한 집합 간 관계를 확인하는 메서드도 추가됐습니다.
const small = new Set([1, 2])
const large = new Set([1, 2, 3, 4])
small.isSubsetOf(large) // true
large.isSupersetOf(small) // true
small.isDisjointFrom(large) // false
모든 메서드는 기존 Set을 변경하지 않고 새로운 Set을 반환합니다. union과 intersection 요 두마리는 정말 꿀 메소드군요.
Promise.try (ES2025)
어떤 함수가 동기 에러를 던질지, 비동기 에러를 던질지 알 수 없을 때 에러 처리가 복잡했습니다. 동기 에러는 try...catch로, 비동기 에러는 catch로 따로 잡아야 했으니까요.
function loadUser(userId) {
if (!userId) {
throw new Error('userId가 필요합니다')
}
return fetch(`/api/users/${userId}`).then((res) => res.json())
}
// 기존 방식: 동기/비동기 에러를 모두 대응하려면...
try {
loadUser(userId)
.then((user) => console.log(user))
.catch((err) => console.error('비동기 에러:', err))
} catch (err) {
console.error('동기 에러:', err)
}
Promise.try는 전달받은 함수를 실행한 뒤, 그 결과를 자동으로 Promise로 감싸 줍니다. 함수 실행 중에 나온 동기 에러도 reject로 바꿔주므로 .then()/.catch()/.finally()로 체이닝해서 일관되게 처리할 수 있습니다.
// user는 Promise 입니다
const user = Promise.try(() => loadUser(userId))
.then((user) => {
console.log('불러오기 성공:', user)
return user
})
.catch((err) => {
console.error('에러:', err)
return null
})
.finally(() => {
console.log('로딩 종료')
})
Promise.try는 함수 실행 직후 발생하는 동기 예외도 reject로 만들어 주기 때문에, 함수의 에러 처리 방식이 동기인지 비동기인지와 상관없이 일관되게 다뤄질 수 있습니다.
Import Attribute (ES2025)
ES 모듈에서 JSON 파일을 직접 가져오는 표준 방법이 생겼습니다. 기존에는 JSON을 fetch로 받아서 JSON.parse를 해야 했거나, 번들러가 알아서 처리해 주는 방식에 의존해야 했죠.
// 기존 방식
const res = await fetch('/config.json')
const config = await res.json()
ES2025에서는 with 키워드로 가져오는 모듈의 속성을 명시할 수 있습니다. JSON 파일을 가져올 때는 type: 'json'을 지정해 주면 됩니다.
import config from './config.json' with { type: 'json' }
console.log(config.apiUrl)
동적 가져오기에서는 import()의 두 번째 인자로 전달합니다.
const config = await import('./config.json', {
with: { type: 'json' },
})
console.log(config.default.apiUrl)
with를 사용하면 브라우저와 런타임이 해당 모듈을 어떻게 해석해야 할지 명확히 알 수 있어, 보안과 예측 가능성이 높아집니다.
Error.isError (ES2026)
Error.isError는 값이 진짜 Error 객체인지 확인합니다. instanceof Error와 비슷해 보이지만, 훨씬 안전합니다.
instanceof Error는 같은 페이지의 Error 클래스로 만든 인스턴스만 인정합니다. iframe, Web Worker, 서버에서 넘어온 Error는 같은 클래스가 아니라서 false가 될 수 있습니다.
Error.isError는 Error 객체 본질을 확인합니다.
Error.isError(new Error('fail')) // true
Error.isError(new TypeError('fail')) // true
Error.isError({ message: 'fail' }) // false
Error.isError('fail') // false
실무에서는 catch로 잡은 값이 시스템 에러인지, 비즈니스 로직에서 의도적으로 던진 값인지 구분할 때 씁니다. 시스템 에러는 로깅이나 Sentry로, 비즈니스 에러는 사용자 메시지로 처리합니다.
1. TypeScript에서 catch (e: unknown) 좁힐 때
TS 4.4부터 catch 변수가 unknown 타입이라 직접 .message를 쓸 수 없습니다.
try {
await fetchUser()
} catch (e) {
console.error(e.message) // ❌ TS 에러
}
Error.isError로 타입을 좁히면 안전하게 사용할 수 있습니다.
try {
await fetchUser()
} catch (e) {
if (Error.isError(e)) {
console.error(e.message) // ✅
} else {
console.error('알 수 없는 에러:', e)
}
}
2. Zod 검증 에러
Zod는 ZodError라는 커스텀 Error를 던집니다.
import { z } from 'zod'
const schema = z.object({ email: z.string().email() })
try {
schema.parse({ email: 'invalid' })
} catch (e) {
if (Error.isError(e)) {
console.log('Error 객체임') // true
}
// ZodError 전용 처리
if (e instanceof z.ZodError) {
console.log(e.errors[0].message)
}
}
Error.isError(e)는 true지만, 세부적인 종류까지 알고 싶으면 instanceof z.ZodError를 추가로 씁니다.
3. Axios 인터셉터
Axios는 AxiosError를 던지지만, 코드 흐름상 다른 값이 들어올 수도 있습니다.
api.interceptors.response.use(
(res) => res,
(error) => {
if (!Error.isError(error)) {
// Error가 아닌 값은 감싸서 통일
return Promise.reject(new Error(String(error)))
}
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
logout()
}
return Promise.reject(error)
}
return Promise.reject(error)
}
)
4. Promise.allSettled + AggregateError
여러 API를 동시에 부르고 실패한 것만 모을 때 유용합니다.
const results = await Promise.allSettled([fetch('/api/a'), fetch('/api/b'), fetch('/api/c')])
const errors = results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.map((r) => r.reason)
.filter(Error.isError) // 진짜 Error만 필터링
if (errors.length > 0) {
throw new AggregateError(errors, '일부 요청 실패')
}
AggregateError 안에도 Error 객체만 깔끔하게 담고 싶을 때 씁니다.
5. 커스텀 에러 클래스
서비스별로 에러를 구분하고 싶을 때 커스텀 Error 클래스를 만듭니다.
class PaymentError extends Error {
constructor(
message: string,
public code: string
) {
super(message)
this.name = 'PaymentError'
}
}
try {
throw new PaymentError('카드 거절', 'CARD_DECLINED')
} catch (e) {
console.log(Error.isError(e)) // true
console.log(e instanceof PaymentError) // true
console.log(e instanceof Error) // true
}
Error.isError는 커스텀 Error 클래스도 true로 잡습니다.
6. WebSocket 연결 실패
const ws = new WebSocket('wss://api.example.com')
ws.onerror = (event) => {
if (Error.isError(event.error)) {
console.error('WS 에러:', event.error.message)
} else {
console.error('WS 연결 실패')
}
}
Error.isError는 어떤 컨텍스트에서 만들어졌든 실제 Error 객체만 true로 반환합니다. 시스템 에러는 추적 도구로 보내고, 비즈니스 에러는 사용자 메시지로 처리하는 기준점으로 쓰면 됩니다.