7월, 2026의 게시물 표시

Next.js Image hostname is not configured 에러 원인과 해결법

이미지
3초 요약: 이렇게 고치세요 원인: next/image 로 외부 호스트의 이미지를 불러올 때 그 호스트를 next.config.js 에 등록해두지 않아서 발생 해결: next.config.js 의 images.remotePatterns 에 해당 호스트를 추가 주의: 이미지 호스팅 호스트가 새로 생길 때마다 설정 파일을 고치고 재배포해야 함 — 사용자 업로드처럼 호스트가 자주 바뀌면 와일드카드 패턴을 고려할 것 images: { remotePatterns: [ { protocol: 'https', hostname: 'cdn.example-cms.com' }, ], } ↓ 왜 이런 에러가 나는지 원리 보기 외부 CMS나 스토리지에서 받아온 이미지 URL을 next/image 에 그대로 넣었더니 화면 대신 빨간 에러 화면이 뜬 적 있으실 겁니다. "Invalid src prop ... hostname ... is not configured under images in your next.config.js"라는 메시지인데, 이미지 URL 자체는 멀쩡한데도 나는 에러라 처음 보면 당황스럽습니다. next/image가 외부 호스트를 화이트리스트로 관리하는 이유 일반 <img> 태그는 브라우저가 어떤 URL이든 그냥 요청해서 그리면 끝입니다. 반면 next/image 는 요청이 들어올 때마다 Next.js 서버(또는 배포 플랫폼의 이미지 최적화 API)가 원본 이미지를 가져와 크기를 줄이고 포맷을 변환한 뒤 캐싱해서 내려주는 구조입니다. 이때 서버가 아무 URL이나 제한 없이 대신 가져오게 허용하면, 그 최적화 엔드포인트가 임의의 외부 서버에 요청을 대신 보내주는 통로로 악용될 수 있습니다. 그래서 Next.js는 next.conf...

Next.js 15 params should be awaited 에러 원인과 해결 가이드

이미지
3초 요약: 이렇게 고치세요 원인: Next.js 15부터 params · searchParams 가 Promise로 바뀌었는데, 예전처럼 params.id 를 동기적으로 바로 읽어서 발생 해결: 서버 컴포넌트·Route Handler· generateMetadata → const { id } = await params; / 클라이언트 컴포넌트 → const { id } = use(params); 주의: 공식 codemod가 대부분 자동 변환해주지만, params를 조건부로 읽거나 다른 함수에 그대로 넘기는 패턴은 놓칠 수 있어 직접 확인 필요 export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; } ↓ 왜 이런 에러가 나는지 원리 보기 Next.js 14 프로젝트를 15로 올리자마자 params.id 를 쓰던 코드에서 "params should be awaited" 같은 경고나 타입 에러를 마주친 적 있으실 겁니다. 원인은 Next.js 15부터 동적 라우트의 params 와 searchParams 가 더 이상 일반 객체가 아니라 Promise로 바뀌었기 때문인데, 코드 몇 줄만 고치면 되는 문제지만 프로젝트 전체에 흩어져 있으면 손이 꽤 갑니다. params가 갑자기 Promise가 된 이유 Next.js 14까지는 page.tsx 가 받는 params · searchParams 가 렌더링 시점에 이미 값이 채워진 평범한 객체였습니다. Next.js 15는 이 값들을 포함해 cookies() , headers() , draftMode() 처럼 요청이 와야만 알 수 있는 값들을 전부 비동기 API로 통일...

TypeScript: Unknown vs Any 타입 차이점과 안전한 타입 가드(Type Guard)

이미지
3초 요약: 이렇게 고치세요 원인: any 는 타입 검사 자체를 포기하는 선언이라, 잘못된 속성 접근이 컴파일 시점엔 통과했다가 런타임에 터짐 해결: 외부 API 응답처럼 타입을 확신할 수 없는 데이터는 unknown 으로 받고, data is T 형태의 타입 가드로 검증한 뒤에만 사용 주의: 타입 가드 내부에서 as Record<string, unknown> 같은 임시 캐스팅으로 속성을 하나씩 검사하는 절차를 생략하면 unknown 을 쓰는 의미가 없어짐 function isUserProfile(data: unknown): data is UserProfile { ... } ↓ 왜 any가 위험한지 원리 보기 실무에서 대규모 TypeScript 프로젝트를 마이그레이션하거나, 외부 API 또는 레거시 라이브러리를 통합하다 보면 필연적으로 "타입을 확신할 수 없는 데이터"를 다루게 됩니다. 이때 많은 개발자들이 급한 마음에 가장 손쉬운 탈출구인 any 를 사용하곤 합니다. 하지만 이는 TypeScript의 가장 큰 무기인 타입 안정성을 스스로 내려놓는 행위이자, 미래의 나에게 런타임 에러라는 시한폭탄을 던지는 것과 같습니다. any 가 왜 위험한지, TypeScript 3.0에서 도입된 unknown 이 이를 어떻게 막아주는지, 그리고 실무에서 바로 써먹을 수 있는 사용자 정의 타입 가드(User-Defined Type Guards) 작성 패턴을 정리합니다. any의 위험성과 unknown의 등장 프로덕션 환경에서 발생한 실제 에러 로그를 분석해 보면, 꽤 많은 비율이 TypeError: Cannot read properties of undefined 에서 시작합니다. 그리고 그 이면에는 높은 확률로 any 타입이 자리 잡고 있죠. any: 타입...

Next.js: CSS Modules의 클래스명 조합(clsx, classnames) 최적화 적용기

이미지
3초 요약: 이렇게 고치세요 원인: 템플릿 리터럴로 조건부 클래스를 조합하면 삼항 연산자가 중첩되고, 조건이 꺼졌을 때 빈 문자열이나 false 문자열이 그대로 클래스에 섞여 들어감 해결: clsx (또는 classnames )에 객체 문법 {[styles.클래스]: 조건} 을 넘겨 조건이 참일 때만 클래스가 병합되게 함 주의: CSS Module 클래스명은 리터럴 문자열이 아니라 반드시 대괄호로 계산된 속성(computed property) 형태로 넘겨야 함 clsx(styles.button, { [styles.active]: isActive }) ↓ 왜 이런 문제가 생기는지 원리 보기 Next.js 프로젝트에서 컴포넌트 상태에 따라 CSS Modules 클래스를 조건부로 붙이다 보면, 템플릿 리터럴 안에 삼항 연산자가 두세 개만 겹쳐도 코드가 금방 지저분해집니다. 예전에 외주로 맡았던 어드민 대시보드 프로젝트에서 처음엔 그냥 && 단축 패턴으로 조건을 하나씩 욱여넣는 식으로 땜질하다가, 조건이 꺼졌을 때 클래스에 false 라는 문자열이 그대로 찍혀서 스타일이 깨진 걸 보고 나서야 clsx 객체 문법으로 정리하는 패턴을 쓰게 됐습니다. 그 과정에서 정리한 실전 코드와, 처음엔 놓치기 쉬운 함정 몇 가지를 공유합니다. 템플릿 리터럴(Template Literals) 방식의 한계와 고통 상태에 따라 스타일을 다르게 주어야 하는 흔한 예시인 '버튼(Button)' 컴포넌트를 예로 들어보겠습니다. CSS Modules를 연동한 뒤, 템플릿 리터럴로 다중 조건부 스타일을 조합하면 보통 아래와 같이 구현하게 됩니다. // 템플릿 리터럴을 사용한 기존의 조건부 스타일 조합 <button className={`${styles.button} ${isPrimary ? styles.primary ...

TypeScript: 'Type instantiation is excessively deep' 재귀 타입 제한 해제

이미지
3초 요약: 이렇게 고치세요 원인: 재귀적으로 자기 자신을 참조하는 제네릭 타입이 TypeScript 컴파일러의 타입 인스턴스화 깊이 제한을 넘어감 해결: 급할 때 → 재귀 호출을 조건부 타입의 마지막 반환값으로 바로 두는 꼬리 재귀 형태로 재작성 / 근본적으로 → 중첩 구조 자체를 얕게 리팩토링 주의: // @ts-ignore 나 any 로 눌러 끄면 그 타입이 지켜주던 자동완성·오타 검증이 통째로 사라짐 // Before: 매 단계 결과를 감싸면서 내려가 인스턴스화가 누적됨 type Keys<T> = T extends object ? { [K in keyof T]: `${K & string}.${Keys<T[K]>}` }[keyof T] : ""; // After: 누산값을 파라미터로 넘기는 꼬리 재귀 형태 type Keys<T, P extends string = ""> = T extends object ? { [K in keyof T]: Keys<T[K], `${P}${K & string}.`> }[keyof T] : P; ↓ 왜 이런 에러가 나는지 원리 보기 React 컴포넌트의 props나 폼 스키마처럼 중첩된 객체 구조를 다루다 보면, 그 구조의 모든 키 경로를 문자열 리터럴 타입으로 뽑아내거나 DeepPartial<T> 같은 유틸리티 타입을 재귀적으로 만들어야 하는 순간이 옵니다. 그런데 어느 순간 에디터가 멈추는 것처럼 느려지다가 Type instantiation is excessively deep and possibly infinite. 라는 빨간 줄이 뜨면, 코드는 분명 멀쩡해 보이는데 뭘 고쳐야 할지 감이 안 잡힙니다. 결론부터 말하면 이건 런타임 버그가 아니라 TypeScript 컴파...

React 18: useDeferredValue를 통한 입력창 무겁고 버벅이는 타자 현상 교정

이미지
3초 요약: 이렇게 고치세요 원인: 입력값이 바뀔 때마다 무거운 리스트 렌더링이 같은 우선순위로 동기 처리되면서 메인 스레드를 붙잡음 해결: 입력 자체는 즉시 반영하고 무거운 렌더링만 뒤로 미루고 싶을 때 → useDeferredValue / 하위 트리가 실제로 무거우면 React.memo 도 같이 적용 주의: 렌더링 자체를 빠르게 만들어주는 훅이 아니라 우선순위만 미루는 것 — 계산량이 줄지 않으면 체감 효과가 약할 수 있음 const deferredText = useDeferredValue(text); <HeavyList query={deferredText} /> ↓ 왜 이런 문제가 생기는지 원리 보기 대량의 리스트를 필터링하는 검색창에 타자를 치면 입력창 자체가 버벅이면서 글자가 밀리는 경우가 있습니다. 원인은 입력값이 바뀔 때마다 무거운 리스트 렌더링이 같은 우선순위로 동기 처리되면서 브라우저 메인 스레드를 잠깐 붙잡기 때문인데, React 18의 useDeferredValue 는 이 렌더링의 우선순위만 뒤로 미뤄서 입력 반응성을 지켜주는 훅입니다. 입력창 타자 밀림 현상이 발생하는 구조적 원인 일반적인 React 앱에서 입력창의 상태( state )와 화면에 뿌려지는 리스트 데이터가 동일한 입력 흐름에 묶여 있을 때, 유저가 키보드를 입력하면 다음과 같은 현상이 순차적으로 일어납니다. 사용자가 텍스트 입력 (키 입력 이벤트 발생) onChange 핸들러가 트리거되며 상태 변경 ( setState ) [동기적 리렌더링] 입력값을 가공해야 하는 하위의 무거운 리스트 컴포넌트가 같은 우선순위로 곧바로 다시 렌더링을 시작 이 과정에서 브라우저의 싱글 스레드(Main Thread)가 렌더링 계산에 묶여버림 브라우저가 다음 키 입력을 즉각 화면에...