금요일

tRPC 타입 에러: Input Validation 실패와 해결방법

🔍 검색 키워드: tRPC 타입 에러, tRPC input validation 실패, tRPC ZodError, tRPC 클라이언트 타입 추론, tRPC 런타임 검증

tRPC 타입 에러: Input Validation 실패와 해결방법

증상

tRPC 클라이언트에서 정확하게 타입을 맞춰 데이터를 전송했는데도 서버에서 타입 검증 에러가 발생합니다.

Error: [UNPROCESSABLE_CONTENT]: Input validation failed
    ├─ expected number, received string (code: invalid_type)
      └─ at "age"

또는 클라이언트에서 데이터를 보낼 때 타입스크립트 컴파일 에러:

Argument of type '{ name: string; age: string }' is not assignable
  to parameter of type '{ name: string; age: number }'

원인

tRPC는 타입스크립트의 타입 검사런타임 검증(보통 Zod 스키마)을 별도로 수행합니다. 세 가지 주요 원인이 있습니다:

  1. Zod 스키마와 TS 타입이 불일치
    서버에서 z.number()로 정의했지만, 클라이언트 데이터는 문자열로 전달
  2. 타입 강제(coerce)가 없음
    API 요청에서 쿼리 파라미터나 폼 데이터는 항상 문자열이지만, 스키마에서 변환하지 않음
  3. 선택적 필드와 기본값 처리 오류
    .optional() 또는 .default() 누락으로 undefined/null 검증 실패

해결방법

해결책 1: Zod 스키마에서 타입 강제(Coerce)

import { z } from 'zod';
  import { router, publicProcedure } from '@trpc/server';
  
  const userSchema = z.object({
    name: z.string().min(1, "이름은 필수입니다"),
      age: z.coerce.number().min(0, "나이는 0 이상이어야 합니다"),
        email: z.string().email().optional(),
        });
        
        export const appRouter = router({
          createUser: publicProcedure
              .input(userSchema)
                  .mutation(async ({ input }) => {
                        // input.age는 이제 자동으로 number 타입 보장
                              return { success: true, age: input.age };
                                  }),
                                  });

해결책 2: 쿼리 파라미터 검증 시 .default() 사용

export const appRouter = router({
    listUsers: publicProcedure
        .input(
              z.object({
                      limit: z.coerce.number().default(10).min(1).max(100),
                              skip: z.coerce.number().default(0).min(0),
                                      sortBy: z.enum(['name', 'age', 'createdAt']).default('createdAt'),
                                            })
                                                )
                                                    .query(async ({ input }) => {
                                                          // input.limit, skip, sortBy 모두 안전하게 기본값 적용됨
                                                                return { data: [], total: 0 };
                                                                    }),
                                                                    });

해결책 3: 클라이언트에서 타입 안전성 확보

// 클라이언트
  const trpc = createTRPCReact();
  
  export function UserForm() {
    const createUser = trpc.createUser.useMutation();
    
      const handleSubmit = (e: React.FormEvent) => {
          e.preventDefault();
              const formData = new FormData(e.currentTarget);
              
                  // 명시적 타입 변환
                      const payload = {
                            name: formData.get('name') as string,
                                  age: parseInt(formData.get('age') as string), // 문자열 → 숫자
                                        email: (formData.get('email') as string) || undefined,
                                            };
                                            
                                                // 이제 tRPC이 타입 검증 & 런타임 검증 수행
                                                    createUser.mutate(payload);
                                                      };
                                                      
                                                        return (
                                                            
); }

정리표

문제 상황 원인 해결방법
쿼리 파라미터가 문자열인데 number 검증 실패 Zod에서 강제 변환 안 함 z.coerce.number() 사용
폼 데이터 전송 시 "필드 없음" 에러 선택적 필드에 .optional() 미적용 스키마에 .optional() 또는 .default() 추가
TS 컴파일은 통과했는데 런타임 에러 타입과 Zod 스키마 불일치 스키마에서 satisfies z.ZodType<YourType> 검증
API 응답 타입이 예상과 다름 뮤테이션 반환값 타입 정의 누락 .mutation() 뒤에 .output(schema) 체인 추가

tRPC는 타입스크립트 안전성과 런타임 검증을 결합한 강력한 도구입니다. Zod 스키마를 "소스 오브 트루스"로 생각하고, 클라이언트 타입은 자동으로 유추되도록 하면 대부분의 타입 에러를 사전에 방지할 수 있습니다.

AWS Lambda Task Timed Out 에러 해결 — 타임아웃 원인 6가지와 실무 대응법

🔍 검색 키워드: AWS Lambda timeout 해결, Lambda Task timed out after 해결, Lambda 타임아웃 에러, Lambda 함수 느림 원인, AWS Lambda 3초 제한 해결, Lambda cold start 해결

Lambda를 운영하다 보면 CloudWatch 로그에서 이런 메시지를 맞닥뜨리게 된다.

REPORT RequestId: abc123  Duration: 3000.21 ms  Billed Duration: 3000 ms
ERROR	Task timed out after 3.00 seconds

함수는 실행됐는데 응답 없이 죽어버렸다. 처음엔 당황���럽지만 원인은 거의 정해져 있다.


증상

  • CloudWatch 로그에 Task timed out after X.XX seconds 출력
  • Lambda 함수가 응답을 반환하지 않고 강제 종료됨
  • 간헐적으로만 발생하거나, 특정 요청에서 반드시 발생
  • API Gateway 연동 시 502 Bad Gateway 또는 504 Gateway Timeout 함께 발생

원인 1: 기본 타임아웃(3초)이 너무 짧다

Lambda 함수의 기본 타임아웃은 3초다. 이게 문제다. DB 쿼리 한 번, 외부 API 호출 한 번만 해도 금방 넘어간다.

확인 방법:

aws lambda get-function-configuration --function-name my-function \
  --query 'Timeout'
# 출력: 3

해결: 콘솔에서 [구성] → [일반 구성] → [편집] → 타임아웃 값을 늘린다.
CLI로 변경하려면:

aws lambda update-function-configuration \
  --function-name my-function \
  --timeout 30
⚠️ 주의: 타임아웃 증가는 최후의 수단이다. 먼저 왜 느린지를 찾아야 한다.

원인 2: DB 연결이 매 요청마다 새로 맺어진다

가장 많이 보는 패턴이다. Lambda 핸들러 쳸안에서 DB 커넥션을 생성하면 요청마다 TCP 핸드셰이크 + 인증 과정을 반복한다. RDS 기준으로 연결 하나 맺는 데 수백 ms가 날아간다.

나쁜 패턴:

def handler(event, context):
    conn = psycopg2.connect(host=..., dbname=..., user=..., password=...)  # 매번 생성
    cursor = conn.cursor()
    cursor.execute("SELECT ...")
    return cursor.fetchall()

좋은 패턴:

import psycopg2

# 핸들러 밖 — 컨테이너가 살아있는 동안 재사용
conn = psycopg2.connect(host=..., dbname=..., user=..., password=...)

def handler(event, context):
    cursor = conn.cursor()
    cursor.execute("SELECT ...")
    return cursor.fetchall()

Lambda 컨테이너가 재사용되는 Warm 상태에서는 전역 변수가 유지된다. 커넥션을 핸들러 밖에 두면 같은 컨테이너 인스턴스에서 재사용된다.

RDS를 쓰고 있다면 RDS Proxy 도입을 검토한다. 커넥션 풀링을 프록시가 대신 처리해줘서 Lambda와 RDS 사이의 커넥션 폭발 문제도 같이 해결된다.


원인 3: 외부 API 호출 병목 접근하연동 해에서 없다

// 이 코드는 외부 API가 응답 안 하면 Lambda 타임아웃까지 기다린다
const response = await axios.get('https://some-api.example.com/data');

외부 서비스가 느리거나 다운된 경우, 설정된 타임아웃까지 Lambda가 하염없이 대기한다.

수정 (JavaScript):

const response = await axios.get('https://some-api.example.com/data', {
  timeout: 5000,  // 5초 안에 응답 없으면 에러
});

수정 (Python):

import requests

response = requests.get(
    'https://some-api.example.com/data',
    timeout=(3.0, 10.0)  # (connect timeout, read timeout)
)

원인 4: 순차 API 호출을 직렬로 처리화고 있다

// 이러면 A + B + C 호출 시간이 전부 합산된다
const userInfo = await fetchUser(userId);
const orderInfo = await fetchOrder(userId);
const reviewInfo = await fetchReview(userId);

병렬 처리로 개선:

const [userInfo, orderInfo, reviewInfo] = await Promise.all([
  fetchUser(userId),
  fetchOrder(userId),
  fetchReview(userId),
]);

독립적인 API 호출이라면 Promise.all로 묶으면 가장 느린 요청 시간 하나로 줄어든다.


원인 5: Cold Start + VPC 설정

Lambda를 VPC 안에 넣으면 Cold Start 시간이 크게 늘어난다. CloudWatch Logs에서 Init Duration이 표시되면 Cold Start다.

REPORT RequestId: abc123  Duration: 850.00 ms  Billed Duration: 851 ms
       Init Duration: 612.38 ms

대응 방법:

  • VPC가 필요 없다면 빼는 게 제일 낫다
  • 꼭 필요하다면 Provisioned Concurrency 사용 (미리 컨테이너를 띄워둠)

원인 6: 메모리 부족 → CPU 부족

Lambda에서 메모리와 CPU는 연동된다. 메모리를 늘리면 CPU도 더 할당된다. CloudWatch Metrics에서 Max Memory Used를 확인한다.

aws lambda update-function-configuration \
  --function-name my-function \
  --memory-size 1024

진단 체크리스트

확인 항목 방법
타임아웃 설정값 확인AWS 콘솔 → 함수 → 일반 구성
Cold Start 여부CloudWatch Logs에서 Init Duration 검색
VPC 설정 여부함수 구성 → VPC 섹션
메모리 사용량CloudWatch → Max Memory Used
외부 호출 병목AWS X-Ray 트레이싱 활성화 후 확인
DB 커넥션 위치코드에서 커넥션이 핸들러 안에 있는지 확인

X-Ray로 병목 찾기

CloudWatch Logs만으로는 어디서 느린지 파악이 어렵다. AWS X-Ray를 활성화하면 함수 내 각 구간의 소요 시간을 추적할 수 있다.

from aws_xray_sdk.core import xray_recorder, patch_all
patch_all()  # boto3, requests, psycopg2 등 자동 추적

def handler(event, context):
    with xray_recorder.in_subsegment('db-query'):
        result = query_database()
    return result

정리

Lambda 타임아웃 에러는 대부분 이 순서로 접근하면 해결된다.

  1. CloudWatch Logs에서 Init Duration 유무로 Cold Start 여부 확인
  2. X-Ray 트레이싱 켜고 어느 구간에서 시간이 먹히는지 파악
  3. DB 커넥션이 핸들러 안에 있으면 밖으로 빼기
  4. 외부 API 호출에 명시적 timeout 설정
  5. 순차 호출을 Promise.all / asyncio.gather로 병렬화
  6. 그래도 안 되면 메모리 늘리거나 타임아웃 값 상향

타임아웃 값 늘리는 게 제일 빠른 임시방편이긴 하지만, 근본 원인을 안 잡으면 요금만 늘어난다. 특히 RDS 커넥션 관리와 외부 API timeout 설정은 Lambda 쓰면서 기본 중의 기본이다.


관련 글: Kubernetes OOMKilled 에러 해결 — exit code 137 원인과 메모리 설정

MongoDB ECONNREFUSED ::1:27017 에러 해결 — 연결 거부의 원인과 진단법

🔍 검색 키워드: MongoDB connection refused 해결, MongoDB ECONNREFUSED 27017, MongoDB connect ECONNREFUSED ::1:27017, MongoDB 연결 거부 에러, MongoDB IPv6 IPv4 연결 에러, MongoServerError connection refused, Node.js MongoDB 연결 안 됨

이 에러가 뜨는 상황

Node.js 앱을 로컬에서 실행하는데 이런 에러가 나온다.

MongoServerSelectionError: connect ECONNREFUSED ::1:27017
    at Topology.selectServer (/node_modules/mongoose/...)
    at ...

또는 Python 쪽이라면:

pymongo.errors.ServerSelectionTimeoutError: localhost:27017: [Errno 111] Connection refused

분명히 mongod를 실행했다고 생각하는데 연결이 안 된다. mongodb://localhost:27017로 접속 시도하는데 왜 ::1(IPv6)로 가는지도 모르겠다. 이게 왜 생기는지, 어떻게 잡는지 순서대로 정리한다.

핵심 원인 세 가지

1. MongoDB 서비스가 실제로 안 돌고 있음

가장 기본적인 원인인데 의외로 많다. mongod 명령어를 쳤는데 포그라운드로 떠서 터미널 닫을 때 같이 죽는 경우, 또는 서비스 등록 없이 수동 실행했다가 시스템 재시작 후 안 뜨는 경우다.

# Linux/macOS — MongoDB 상태 확인
sudo systemctl status mongod    # Linux (systemd)
brew services list | grep mongodb  # macOS (Homebrew)

# 안 떠있으면 시작
sudo systemctl start mongod     # Linux
brew services start mongodb-community  # macOS
# 실제로 27017 포트 열려있는지 확인
netstat -tlnp | grep 27017  # Linux
lsof -i :27017              # macOS

포트 리스닝이 없으면 MongoDB가 안 뜬 것이다.

2. localhost가 ::1(IPv6)로 해석되는 문제

Node.js v17부터 DNS 리졸버가 IPv6를 우선한다. localhost를 DNS로 조회하면 ::1(IPv6)를 먼저 반환하는데, MongoDB가 IPv4(127.0.0.1)에서만 리스닝 중이면 연결이 거부된다.

에러 메시지의 ::1:27017이 바로 이 경우다.

빠른 확인: MongoDB가 IPv4로 리스닝하는지 확인

netstat -tlnp | grep 27017
# 결과 예시:
# tcp  0  0  127.0.0.1:27017  0.0.0.0:*  LISTEN  (IPv4만 리스닝)
# tcp6 0  0  :::27017         :::*       LISTEN  (IPv6도 리스닝)

3. bindIp 설정이 제한되어 있음

/etc/mongod.confbindIp127.0.0.1로만 설정돼 있으면 IPv6(::1)로 오는 연결은 거부된다.

상황별 해결 방법

방법 1: 연결 문자열에서 localhost 대신 IP 직접 지정

가장 빠른 임시 해결책이다. localhost 대신 127.0.0.1을 명시한다.

// 변경 전
mongoose.connect('mongodb://localhost:27017/mydb');

// 변경 후
mongoose.connect('mongodb://127.0.0.1:27017/mydb');
# Python pymongo
from pymongo import MongoClient
# 변경 전
client = MongoClient('localhost', 27017)
# 변경 후
client = MongoClient('127.0.0.1', 27017)

localhost 대신 127.0.0.1을 쓰면 DNS 조회 없이 바로 IPv4로 연결한다. 많은 경우데 이것만으로 해결된다.

방법 2: MongoDB bindIp에 IPv6 추가

근본 해결을 원하면 MongoDB 설정에서 IPv6도 리스닝하도록 한다.

# /etc/mongod.conf
net:
  port: 27017
  bindIp: 127.0.0.1,::1  # IPv4, IPv6 모두 리스닝

설정 변경 후 재시작:

sudo systemctl restart mongod

방법 3: Docker로 MongoDB 실행 중인 경우

Docker로 MongoDB를 띄웠는데 포트 매핑을 빠뜨린 경우다.

# 잘못된 예 — 포트 매핑 없음
docker run -d --name mongo mongo:7

# 올바른 예 — 27017 포트 매핑
docker run -d --name mongo -p 27017:27017 mongo:7

Docker Compose라면:

services:
  mongo:
    image: mongo:7
    ports:
      - "27017:27017"  # 이 줄이 없으면 호스트에서 접속 불가
    volumes:
      - mongo_data:/data/db

상황별 체크리스트

체크 항목명령어기대 결과
MongoDB 프로세스 실행 중ps aux | grep mongodmongod 프로세스 보임
27017 포트 리스닝lsof -i :27017mongod 프로세스가 27017 점유
IPv4 리스닝 확인netstat -tlnp | grep 27017127.0.0.1:27017 또는 0.0.0.0:27017
Docker 포트 매핑docker ps0.0.0.0:27017->27017/tcp 확인
연결 테스트 (IPv4)nc -zv 127.0.0.1 27017Connection succeeded
연결 테스트 (IPv6)nc -zv ::1 27017succeeded/failed 여부 확인

MongoDB 로그로 원인 확인

# 로그 실시간 확인
sudo tail -f /var/log/mongodb/mongod.log

# 최근 에러만
sudo grep -i error /var/log/mongodb/mongod.log | tail -20

로그에서 bind failed 또는 exception in initAndListen 같은 메시지가 보이면 포트 충돌이나 권한 문제다.

# 27017 포트 점유 프로세스 확인 (MongoDB 아닌 다른 프로세스일 수도 있음)
sudo lsof -i :27017

MongoServerError: Authentication Failed도 같이 나온다면

연결은 됐는데 인증에서 막히는 경우는 별개 이슈다. ECONNREFUSED(연결 거부)와 헷갈리기 쉬운데, 연결 거부는 TCP 자체가 안 되는 것이고 인증 실패는 연결 후 자격증명 검증 단계다.

인증 실패 에러:

MongoServerError: Authentication failed.

이 경우는 username/password 오류이거나 authSource 설정 문제다.

// authSource 명시
mongoose.connect('mongodb://user:pass@127.0.0.1:27017/mydb?authSource=admin');

마무리

ECONNREFUSED ::1:27017 에러는 90%가 두 가지 원인 중 하나다:

  1. MongoDB가 실제로 안 떠있음
  2. localhost가 IPv6로 해석되는데 MongoDB는 IPv4만 리스닝

연결 문자열의 localhost127.0.0.1로 바꾸는 것만으로도 대부분 해결된다. Docker를 쓴다면 포트 매핑(-p 27017:27017)을 빠뜨리지 않았는지 확인하자.

DB 연결 에러 관련해서 MySQL 연결 에러는 MySQL connection error 해결, Redis 연결 에러는 Redis connection refused 해결 글을 참고하면 된다.

작성일: 2026-06-25