목요일

React useEffect 무한루프 문제 해결

🔍 검색 키워드: React useEffect 무한루프, useEffect 의존성 배열, useEffect 성능 최적화, React 렌더링 무한 반복

React useEffect 무한루프 문제 해결

증상

import { useEffect, useState } from 'react';

export default function DataFetcher() { const [data, setData] = useState([]);

useEffect(() => { fetch('/api/data') .then(res => res.json()) .then(data => setData(data)); }); // ⚠️ 의존성 배열 없음 return <div>{data.length} items</div>; }

결과: 컴포넌트가 무한히 렌더링되고 API 요청이 계속 발생하며 브라우저 성능이 급격히 저하됩니다.

원인

React 18 Strict Mode에서 개발 환경 시 useEffectꊔ 의도적으로 2회 실행되지만, 가장 흔한 무한루프 원인은 의존성 배열이 생략되거나 빈 배열이 아닌 경우입니다.

- 의존성 배열 없음: 렌더링될 때마다 effect가 실행 → state 업데이트 → 다시 렌더링 → 무한 루프 - 객체/배열을 의존성으로 사용: 매 렌더링마다 새 참조 생성 → effect 재실행 → 무한 루프

// ❌ 나쁜 예: effect 내부에서 생성한 객체를 의존성으로 사용
useEffect(() => {
  const config = { timeout: 5000 };
  setData(config);
}, [config]); // config는 매번 새로 생성됨

// ❌ 나쁜 예: 함수를 의존성으로 사용 const fetchData = () => fetch('/api'); useEffect(() => { fetchData(); }, [fetchData]); // fetchData는 매번 새로 생성됨

해결방법

1. 의존성 배열 추가 (기본 해결)

import { useEffect, useState } from 'react';

export default function DataFetcher() { const [data, setData] = useState([]);

useEffect(() => { fetch('/api/data') .then(res => res.json()) .then(data => setData(data)); }, []); // ✅ 마운트 시에만 실행 return <div>{data.length} items</div>; }

2. 객체/배열 의존성 안정화

// ❌ useEffect 내부에서 객체 생성
useEffect(() => {
  const options = { headers: { 'Authorization': token } };
  fetchWithOptions(options);
}, []); // 작동하지만 options는 매번 재생성

// ✅ useMemo로 안정화 const options = useMemo(() => ({ headers: { 'Authorization': token } }), [token] );

useEffect(() => { fetchWithOptions(options); }, [options]);

3. 함수 의존성 안정화 (useCallback)

// ❌ fetchData 매번 재생성
const fetchData = () => {
  return fetch('/api/data').then(r => r.json());
};

useEffect(() => { fetchData(); }, [fetchData]);

// ✅ useCallback으로 메모이제이션 const fetchData = useCallback(() => { return fetch('/api/data').then(r => r.json()); }, []);

useEffect(() => { fetchData(); }, [fetchData]);

4. 실전 예제: API 데이터 페칭

import { useEffect, useState, useCallback } from 'react';

export default function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);

useEffect(() => { let isMounted = true; // 언마운트 후 state 업데이트 방지

const fetchUser = async () => { try { setLoading(true); const response = await fetch(/api/users/${userId}); const data = await response.json(); if (isMounted) { setUser(data); setError(null); } } catch (err) { if (isMounted) { setError(err.message); setUser(null); } } finally { if (isMounted) { setLoading(false); } } };

fetchUser();

return () => { isMounted = false; // 정리 함수: 언마운트 시 플래그 설정 }; }, [userId]); // userId 변경 시에만 재실행

if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error}</p>; return <div>{user?.name}</div>; }

정리표

상황 원인 해결책
의존성 배열 없음 매 렌더링마다 effect 재실행 [] 추가하거나 올바른 의존성 지정
객체를 의존성으로 사용 참조 변경으로 매번 재실행 useMemo로 메모이제이션
함수를 의존성으로 사용 새 함수 참조로 무한 루프 useCallback으로 메모이제이션
state setter 직접 의존성 불필요한 재실행 의존성에서 제거 (setter는 안정함)
외부 변수를 직접 사용 ESLint 경고 무시 시 버그 모든 의존성을 배열에 포함
핵심: useEffect7�va 의존성 배열 시 따르면 대부분의 무한루프를 방지할 수 있습니다.

금요일

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 원인과 메모리 설정