수요일

Kubernetes ImagePullBackOff 에러 해결법

🔍 검색 키워드: k8s ImagePullBackOff, 쿠버네티스 이미지 풀 실패, Docker 레지스트리 인증, 이미지 태그 오류, Pod 시작 실패

Kubernetes ImagePullBackOff 에러 해결법

증상

Kubernetes Pod를 배포했을 때 다음과 같은 상태에서 멈춘다:

$ kubectl get pods -n production
  NAME                    READY   STATUS             RESTARTS   AGE
  app-deployment-abc123   0/1     ImagePullBackOff   2          5m

상세 확인 시:

$ kubectl describe pod app-deployment-abc123 -n production
  Events:
    Type     Reason                 Age   Message
      ----     ------                 ----  -------
        Normal   Scheduled              5m    Successfully assigned production/app-deployment-abc123 to worker-node-1
          Normal   BackOff                4m    Back-off pulling image "myrepo/app:v1.0"
            Warning  Failed                 4m    Failed to pull image "myrepo/app:v1.0": rpc error: code = Unknown desc = Error response from daemon: unauthorized
              Warning  Failed                 3m    Back-off pulling image

또는 다음과 같은 메시지:

image not found
  image pull rate limit exceeded
  no such image
  invalid reference format

원인

  1. 이미지명 오류: 잘못된 저장소명, 태그 또는 레지스트리 주소
  2. 인증 실패: Private Docker 레지스트리 접근 권한 없음
  3. 네트워크 단절: 워커 노드에서 레지스트리 접근 불가
  4. 레지스트리 다운: Docker Hub, ECR 등 서비스 장애
  5. 이미지 미존재: 푸시되지 않은 이미지 태그
  6. 레이트 제한: Docker Hub 무료 계정 풀 한도 초과
  7. CPU/메모리 부족: 노드 리소스 부족으로 스케줄링 실패

해결 방법

방법 1: 이미지명 및 태그 확인

# 현재 Pod의 이미지 정보 확인
  kubectl get pod app-deployment-abc123 -n production -o yaml | grep image
  
  # 정확한 이미지명 확인 (레지스트리 포함)
  # 형식: [registry]/[repository]/[image]:[tag]
  # 정상 예: docker.io/myrepo/app:v1.0
  # 정상 예: gcr.io/my-project/app:latest
  # 정상 예: ecr.amazonaws.com/123456789.dkr.ecr.us-east-1.amazonaws.com/app:v1.0

Deployment 수정:

apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: app-deployment
      namespace: production
      spec:
        replicas: 3
          selector:
              matchLabels:
                    app: app
                      template:
                          metadata:
                                labels:
                                        app: app
                                            spec:
                                                  containers:
                                                        - name: app
                                                                image: docker.io/myrepo/app:v1.0  # 정확한 이미지명
                                                                        imagePullPolicy: IfNotPresent      # 또는 Always

방법 2: Private 레지스트리 인증 설정

# 1. Docker 자격증명으로 Secret 생성
  kubectl create secret docker-registry regcred \
    --docker-server=gcr.io \
      --docker-username=_json_key \
        --docker-password="$(cat ~/gcr-key.json)" \
          --docker-email=user@example.com \
            -n production
            
            # 2. 또는 기존 docker config 파일 사용
            kubectl create secret generic regcred \
              --from-file=.dockerconfigjson=$HOME/.docker/config.json \
                --type=kubernetes.io/dockercfg \
                  -n production

Deployment에서 사용:

apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: app-deployment
    spec:
      template:
          spec:
                imagePullSecrets:
                      - name: regcred  # 위에서 생성한 Secret 이름
                            containers:
                                  - name: app
                                          image: gcr.io/my-project/app:v1.0

방법 3: 워커 노드 네트워크 확인

# 워커 노드에서 직접 레지스트리 연결 확인
  kubectl debug node/worker-node-1 -it --image=ubuntu
  # Pod 내에서:
  apt-get update && apt-get install -y curl
  curl -I https://gcr.io
  curl -I https://docker.io
  
  # 또는 임시 Pod에서 테스트
  kubectl run test-curl --image=curlimages/curl -it --rm -- \
    curl -v https://gcr.io

방법 4: 로컬에서 이미지 빌드 및 푸시 확인

# 1. 로컬에서 이미지 빌드
  docker build -t myrepo/app:v1.0 .
  
  # 2. 레지스트리에 푸시
  docker push myrepo/app:v1.0
  
  # 3. 푸시된 이미지 확인
  # Docker Hub: https://hub.docker.com/r/myrepo/app
  # GCR: gcloud container images list
  # ECR: aws ecr describe-images --repository-name app
  
  # 4. 로컬에서 이미지 실행 가능 확인
  docker run --rm myrepo/app:v1.0 --version

방법 5: imagePullPolicy 조정

apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: app-deployment
    spec:
      template:
          spec:
                containers:
                      - name: app
                              image: myrepo/app:v1.0
                                      # imagePullPolicy 옵션:
                                              # Always: 매번 레지스트리에서 풀 (기본, 태그 latest 사용 시)
                                                      # IfNotPresent: 로컬에 없을 때만 풀
                                                              # Never: 로컬에서만 사용 (오프라인 환경)
                                                                      imagePullPolicy: Always

방법 6: 디버깅 및 재시도

# Pod 상세 로그 확인
  kubectl logs app-deployment-abc123 -n production --previous
  
  # 이벤트 확인 (시간 역순)
  kubectl get events -n production --sort-by='.lastTimestamp'
  
  # Pod 재생성 (자동 재시도)
  kubectl rollout restart deployment/app-deployment -n production
  
  # 캐시 클리어 후 재배포
  kubectl set image deployment/app-deployment \
    app=myrepo/app:v1.1 \
      -n production
      
      # 또는 현재 이미지로 강제 롤아웃
      kubectl rollout restart deployment/app-deployment -n production

정리표

원인 증상 해결법
이미지명 오류 image not found 정확한 이미지명 확인
인증 실패 unauthorized imagePullSecrets 설정
네트워크 단절 connection timeout 워커 노드 네트워크 확인
레지스트리 장애 service unavailable 레지스트리 상태 확인
태그 미존재 manifest not found docker push 재실행
레이트 제한 rate limit exceeded 인증된 계정으로 전환
노드 리소스 부족 OutOfmemory, OutOfDisk 노드 리소스 확인

: 배포 전에 로컬 환경에서 docker push까지 성공하는지 확인하고, 클러스터에는 imagePullPolicy: Always 설정으로 항상 최신 이미지를 가져오도록 설정하면 버전 관리가 쉬워집니다.

Redis WRONGTYPE 에러: 데이터 타입 불일치 해결

🔍 검색 키워드: Redis WRONGTYPE 에러, Redis 데이터 타입, Redis 명령어 호환성, INCR 문자열, Redis 키 재사용

Redis WRONGTYPE 에러: 데이터 타입 불일치 해결

Redis는 강타입 데이터베이스입니다. 같은 키에 다른 타입의 데이터를 저장하려고 하면 WRONGTYPE 에러가 발생합니다. 흔히 개발 중에 같은 키명을 여러 목적으로 재사용할 때 마주치는 실수입니다. 이 글에서는 에러 원인과 해결 방법을 실제 상황으로 설명하겠습니다.

1. 증상: WRONGTYPE 에러 메시지

ERR WRONGTYPE Operation against a key holding the wrong kind of value
  at Object.writeError (/app/node_modules/redis/lib/reply.js:71:15)
  at Socket.onreply (/app/node_modules/redis/lib/index.js:304:17)

또는 Redis CLI에서:

> set user:1 "{name: john}"
  OK
  > lpush user:1 "new_value"
  (error) WRONGTYPE Operation against a key holding the wrong kind of value

2. 원인 분석

Redis는 5가지 기본 데이터 타입을 가집니다:

  • String: set, get, incr
  • List: lpush, rpush, lrange
  • Set: sadd, scard, smembers
  • Hash: hset, hget, hgetall
  • Sorted Set: zadd, zscore, zrange

원인 1: 같은 키를 다른 타입으로 사용

// 기존 코드: user:1을 String으로 사용
  await redis.set('user:1', JSON.stringify({ name: 'john' }));
  
  // 신규 코드: 같은 키를 List로 변경
  await redis.lpush('user:1', 'new_value');
  // ❌ WRONGTYPE 에러!

원인 2: 개발 중 타입 변경

// 처음에는 값 저장 (String)
  redis.set('counter', '10');
  
  // 나중에 증가시키려고 시도 (INCR은 String 전용)
  redis.incr('counter');  // 기존 값이 JSON이면 에러
  
  // 또는 반대로
  redis.lpush('counter', 1);  // 이제 List
  redis.incr('counter');       // WRONGTYPE!

원인 3: TTL 만료 후 타입 재사용

redis.set('session:abc', 'token123', 'EX', 3600);  // String으로 저장
  
  // TTL이 지나 자동 삭제됨
  // 몇 시간 후...
  
  redis.hset('session:abc', 'user_id', '123');  // Hash로 저장 시도
  // 만약 키가 남아있으면 WRONGTYPE!

3. 해결 방법

방법 1: 기존 키 삭제 후 재생성

// 안전한 재설정
  await redis.del('user:1');
  await redis.lpush('user:1', 'value1', 'value2');

방법 2: 다른 키명 사용

// AS-IS: 충돌하는 키
  const key = 'user:1';
  
  // TO-BE: 타입 명시하는 키명
  const userKey = 'user:1:string';      // String용
  const userListKey = 'user:1:list';    // List용
  const userHashKey = 'user:1:hash';    // Hash용
  
  // 또는 버전 붙이기
  const userKeyV2 = 'user:1:v2';

방법 3: 타입 확인 후 처리

// Node.js Redis 클라이언트
  async function safeSet(key, value, type = 'string') {
    const existingType = await redis.type(key);
    
      if (existingType !== 'none' && existingType !== type) {
          console.warn(`키 '${key}'의 기존 타입: ${existingType}, 요청 타입: ${type}`);
              await redis.del(key);  // 기존 데이터 삭제
                }
                
                  if (type === 'string') {
                      await redis.set(key, value);
                        } else if (type === 'list') {
                            await redis.lpush(key, value);
                              } else if (type === 'hash') {
                                  await redis.hset(key, ...Object.entries(value).flat());
                                    }
                                    }
                                    
                                    // 사용
                                    await safeSet('user:1', { name: 'john' }, 'hash');

방법 4: Redis에서 타입 조회

# 기존 모든 키와 타입 확인
  $ redis-cli KEYS '*' | while read key; do echo -n "$key: "; redis-cli TYPE "$key"; done
  
  # 특정 패턴 확인
  $ redis-cli SCAN 0 MATCH "user:*" | xargs -I {} redis-cli TYPE {}

방법 5: 마이그레이션 스크립트

// 전체 키를 새 구조로 마이그레이션
  const redis = require('redis');
  const client = redis.createClient();
  
  async function migrateKeys() {
    const keys = await new Promise((resolve, reject) => {
        let allKeys = [];
            const stream = client.scanStream();
                stream.on('data', (keys) => allKeys.push(...keys));
                    stream.on('end', () => resolve(allKeys));
                        stream.on('error', reject);
                          });
                          
                            for (const key of keys) {
                                if (key.startsWith('user:')) {
                                      const type = await client.type(key);
                                      
                                            if (type === 'string') {
                                                    const value = await client.get(key);
                                                            await client.del(key);
                                                                    await client.hset(`${key}:v2`, 'data', value);
                                                                          }
                                                                                // 다른 타입도 처리...
                                                                                    }
                                                                                      }
                                                                                      
                                                                                        console.log('마이그레이션 완료');
                                                                                        }
                                                                                        
                                                                                        await migrateKeys();

4. 타입별 명령어 정리

명령어 대상 타입 예제
set / get String set key value
lpush / lrange List lpush list_key value
sadd / smembers Set sadd set_key member
hset / hget Hash hset hash_key field value
zadd / zrange Sorted Set zadd zset_key 1 member
incr String만 incr counter
lpop List만 lpop list_key

5. 예방 가이드

개발 단계:

  • 키 설계 시 타입 명시: {domain}:{id}:{type}
  • 예: user:123:string, session:456:hash, queue:789:list

테스트 단계:

  • Redis 모의 객체로 타입 검증
  • 개발/스테이징 Redis 정기 초기화

배포 단계:

  • 마이그레이션 스크립트로 기존 키 처리
  • TTL 만료 후 재사용 금지 정책 수립

WRONGTYPE 에러는 Redis를 처음 다룰 때 흔한 실수입니다. 핵심은 같은 키에는 하나의 타입만 유지하는 것. 타입을 바꿔야 한다면 새 키를 사용하거나, 기존 데이터를 명시적으로 삭제하세요. 이를 통해 안정적인 Redis 운영이 가능합니다.

React useEffect 무한루프 문제 해결하기

🔍 검색 키워드: React useEffect 무한루프, useEffect 의존성 배열, React hooks 성능 최적화, useEffect 콘솔 로그, 클로저 이슈

React useEffect 무한루프 문제 해결하기

useEffect는 강력하지만, 잘못 사용하면 무한 루프에 빠질 수 있습니다. 특히 의존성 배열이 없거나 잘못 설정되면 매번 렌더링 후에 effect가 실행되어 상태를 계속 변경하는 악순환에 빠집니다. 이 글에서는 증상부터 원인, 그리고 해결 방법까지 실제 코드로 설명하겠습니다.

1. 증상: 콘솔 로그가 무한정 출력된다

의존성 배열 없이 useEffect를 작성하면 다음과 같이 작동합니다.

useEffect(() => {
    console.log('effect 실행됨');
      setCount(count + 1);
      });

이 코드를 실행하면 콘솔에는:

  • effect 실행됨
  • effect 실행됨
  • effect 실행됨
  • (무한반복...)

이렇게 계속 출력되며, 브라우저가 느려지거나 먹통이 됩니다.

2. 원인 분석

의존성 배열이 없는 경우

useEffect(() => {
    setCount(count + 1); // 상태 변경
    }, []); // ❌ 의존성 배열 누락

의존성 배열을 생략하면 매번 렌더링 후마다 effect가 실행됩니다.

  1. 컴포넌트 렌더링
  2. useEffect 실행 → setCount 호출
  3. 상태 변경 → 컴포넌트 리렌더링
  4. useEffect 다시 실행
  5. 무한 반복...

의존성에 상태가 포함된 경우

useEffect(() => {
    setCount(count + 1);
    }, [count]); // ❌ count가 의존성에 포함됨

이 경우도 같은 문제가 발생합니다:

  1. count 변경 → effect 실행
  2. setCount(count + 1) → count 업데이트
  3. count 의존성이 변경됨 → effect 다시 실행
  4. 무한 루프...

3. 해결 방법

방법 1: 빈 의존성 배열 사용

마운트 시에만 한 번 실행하려면:

useEffect(() => {
    // API 호출, 리스너 등록 등
      fetchData();
      }, []); // ✅ 빈 배열: 마운트 시에만 실행

방법 2: 정확한 의존성 명시

실제 변경 감지할 항목만:

useEffect(() => {
    setDerivedValue(count * 2);
    }, [count]); // ✅ count 변경 시에만 effect 실행

방법 3: 상태 업데이트 함수 패턴

이전 상태에 기반해 업데이트:

useEffect(() => {
    setCount(prev => prev + 1);
    }, []); // ✅ 의존성 없이도 안전 (단, 무한루프 원하지 않으면 피하기)

방법 4: useCallback으로 함수 메모이제이션

함수가 의존성이 되는 경우:

const handleUpdate = useCallback(() => {
    setCount(count + 1);
    }, [count]);
    
    useEffect(() => {
      handleUpdate();
      }, [handleUpdate]); // 필요한 경우만 실행

방법 5: useRef로 초기 실행 방지

조건부 effect 실행:

const isFirstRender = useRef(true);
  
  useEffect(() => {
    if (isFirstRender.current) {
        isFirstRender.current = false;
            return;
              }
              
                setCount(count + 1); // 마운트 후에만 실행
                }, [count]);

4. 빠른 진단 가이드

증상 원인 해결책
무한 콘솔 로그 의존성 배열 누락 빈 배열 추가 []
특정 상태 변경 시 무한 루프 자신을 변경하는 상태가 의존성에 포함됨 의존성에서 제거하거나 함수 패턴 사용
마운트 후 특정 시점에만 실행 원함 조건 체크 없음 useRef 또는 상태 플래그로 조건 추가
객체/배열 의존성으로 매번 실행됨 새 객체/배열 매번 생성됨 useMemo 또는 상수로 변경

5. 실전 예제

API 호출 + cleanup 패턴 (무한루프 없음):

useEffect(() => {
    let isMounted = true;
    
      fetchUserData(userId).then(data => {
          if (isMounted) setUser(data);
            });
            
              return () => {
                  isMounted = false; // cleanup
                    };
                    }, [userId]); // userId 변경 시에만 새로 fetch

의존성 배열 체크

  • [] → 마운트 시에만 1회 실행
  • [dep1, dep2] → dep1 또는 dep2 변경 시 실행
  • 없음 → 매번 렌더링 후 실행 (⚠️ 위험)

useEffect의 무한루프는 React 개발에서 가장 흔한 실수입니다. 항상 의존성 배열을 명시하고, 그 안에는 실제 변경을 감지해야 할 항목만 넣으세요. 이 규칙만 지켜도 대부분의 성능 문제를 예방할 수 있습니다.