Jay's <Devlog />Jay's Devlog
HomeBlogAbout
Login

© 2026 Jay's <Devlog />. All Rights Reserved.

GitHubLinkedIn
Gravatar
  1. Home
  2. Blog
  3. 홈 서버
  4. [Next.js + WordPress] 홈서버 배포기: ETIMEDOUT 해결부터 성능 최적화까지
INFRASTRUCTURE
홈 서버Next JS개인서버 21 min read

[Next.js + WordPress] 홈서버 배포기: ETIMEDOUT 해결부터 성능 최적화까지

jay

jay

2026년 1월 21일 08:00

개인 홈서버(MiniPC)에 Next.js(Frontend)와 WordPress(Headless CMS)를 Docker로 구축하여 기술 블로그를 운영하고 있다. 로컬 개발 환경에서는 완벽하게 작동하던 기능들이 실제 배포 환경으로 넘어가자마자 간헐적인 404 에러, 리스트 미노출, 그리고 처참한 Lighthouse 성능 점수 등 다양한 문제들을 쏟아내기 시작했다.

이 글은 홈서버라는 특수한 환경에서 발생한 네트워크 이슈(ETIMEDOUT)부터 프론트엔드 렌더링 최적화까지, 서비스를 안정화하고 고도화한 과정을 기록한 트러블슈팅 로그다.

목차

  • 1. 문제 정의: 불안정한 서비스와 낮은 성능
  • 2. 네트워크 & 데이터 안정성 확보 (Backend/Infra)
    • 2-1. Docker Hairpinning 문제와 ETIMEDOUT
    • 2-2. API 호출 재시도(Retry) 로직 구현
    • 2-3. 한글 슬러그 인코딩 문제 (404 Error)
  • 3. 프론트엔드 성능 최적화 (Frontend)
    • 3-1. 이미지 최적화 (next/image + sharp)
    • 3-2. 번들 사이즈 다이어트 (highlight.js)
    • 3-3. Forced Reflow 방지
  • 4. CI/CD 파이프라인 가속 (Jenkins)
  • 5. 결론 및 성과

1. 문제 정의: 불안정한 서비스와 낮은 성능

배포 직후 마주한 서비스의 상태는 다음과 같았다.

  1. 네트워크 타임아웃 (ETIMEDOUT): 간헐적으로 글 목록이 아예 뜨지 않거나, 서버 로그에 fetch failed 에러가 빈번하게 찍혔다.
  2. 404 Not Found: 분명 존재하는 글임에도, 특히 한글 슬러그가 포함된 URL로 접속하면 404 페이지가 반환되었다.
  3. 낮은 퍼포먼스: 모바일 환경에서 LCP(Largest Contentful Paint)가 매우 느렸고, 메인 스레드 부하가 심했다.
  4. 느린 배포 속도: Jenkins 빌드 시간이 너무 오래 걸렸다.

2. 네트워크 & 데이터 안정성 확보 (Backend/Infra)

2-1. Docker Hairpinning 문제와 ETIMEDOUT

가장 치명적인 문제는 fetch failed: ETIMEDOUT 에러였다. Frontend 컨테이너가 데이터를 가져오기 위해 API 서버(https://jay-gemini.com/wp-json/...)를 호출할 때 발생했다.

원인 분석: NAT Loopback (Hairpinning) Docker 컨테이너 내부에서 자신의 공인 도메인(jay-gemini.com)을 호출하면, 요청이 공유기(NAT) 밖으로 나갔다가 다시 들어와야 한다.

하지만 많은 가정용 공유기가 이 NAT Loopback(Hairpinning) 기능을 제대로 지원하지 않거나, 방화벽 정책에 의해 패킷이 유실되는 경우가 많다. 내 홈서버 환경 또한 이 문제로 인해 컨테이너가 자기 자신을 찾지 못해 타임아웃이 발생하고 있었다.

해결 방안: extra_hosts 설정 Frontend 컨테이너가 도메인을 찾을 때 외부 DNS를 거치지 않고, 호스트 머신(MiniPC)의 내부망으로 바로 접속하도록 docker-compose 설정을 변경했다.

# docker-compose.webapp.yml
services:
  frontend:
    image: jmangel3404/frontend-app:latest
    restart: always
    # 핵심 설정: 도메인을 호스트 게이트웨이 IP로 매핑
    extra_hosts:
      - "jay-gemini.com:host-gateway"
    ports:
      - "5173:3000"

이 설정 한 줄로 컨테이너는 외부망을 타지 않고 다이렉트로 호스트를 찾아가게 되었고, 타임아웃 문제는 완벽하게 해결되었다.

2-2. API 호출 재시도(Retry) 로직 구현

네트워크가 안정화되었음에도, 일시적인 서버 부하로 API가 실패할 경우 화면에 아무것도 뜨지 않는 문제가 남았다. 특히 Next.js의 ISR(Incremental Static Regeneration) 과정에서 에러가 나면 빈 페이지가 캐싱되어버리는 치명적인 이슈가 있었다.

해결 방안 fetch 래퍼 함수에 Retry 로직을 추가하여 네트워크 회복탄력성을 높였다.

// src/services/wordpressService.ts
async function fetchWordPressData<T>(endpoint: string, options?: RequestInit, retries = 3, delay = 500): Promise<T> {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(`${API_BASE_URL}${endpoint}`, { ...options });
      // ... (JSON 파싱 및 에러 처리)
      return data as T;
    } catch (error) {
      if (i === retries - 1) throw error; // 마지막 시도 실패 시 에러 throw
      await new Promise(resolve => setTimeout(resolve, delay)); // 대기 후 재시도
    }
  }
  // ...
}

2-3. 한글 슬러그 인코딩 문제 (404 Error)

/blog/쿠버네티스-기초와 같이 한글 제목이 포함된 포스트에 접속하면 404가 떴다. 브라우저는 URL을 인코딩해서 보내지만, WordPress API나 Next.js 서버 사이드 간의 인코딩/디코딩 불일치로 인해 해당 글을 찾지 못하는 것이 원인이었다.

해결 방안 encodeURIComponent를 명시적으로 사용하여 API 호출 URL을 안전하게 처리했다.

// src/services/wordpressService.ts
export const getWordPressPostBySlug = async (slug: string) => {      
   // 슬러그를 명시적으로 인코딩하여 API에 전달
   const encodedSlug = encodeURIComponent(slug);
   const [wpPosts, ...] = await Promise.all([
     fetchWordPressData<Post[]>(`/posts?slug=${encodedSlug}&_embed`),
     // ...
   ]);
   // ...
};

3. 프론트엔드 성능 최적화 (Frontend)

안정성을 확보한 후, Lighthouse 점수를 깎아먹는 주범들을 하나씩 제거하며 사용자 경험(UX)을 개선했다.

3-1. 이미지 최적화 (next/image + sharp)

기존 <img> 태그를 사용하던 부분을 next/image로 교체하고, 고성능 이미지 처리 라이브러리인 sharp를 도입했다.

  • Before: 원본 이미지를 그대로 로드 (느린 속도, 과도한 데이터 사용)
  • After: WebP 포맷 자동 변환, 디바이스 크기에 맞는 리사이징, LCP 요소 우선순위 조정
// src/components/PostCover.tsx
import Image from "next/image";

// ...
<Image
  src={imageUrl}
  alt={alt}
  fill
  priority={priority} // LCP 요소는 우선 로드하여 체감 속도 향상
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  className="object-cover"
/>

3-2. 번들 사이즈 다이어트 (highlight.js)

코드 하이라이팅을 위해 사용한 highlight.js가 너무 무거웠다. 분석해 보니 사용하지 않는 수많은 언어 팩까지 모두 불러오고 있었다.

해결 방안 Tree Shaking이 가능하도록 자주 사용하는 언어만 선별적으로 등록(register)하는 방식으로 변경하여 JS 번들 크기를 획기적으로 줄였다.

// src/components/BlogPostDetail.tsx
import hljs from "highlight.js/lib/core";
import javascript from "highlight.js/lib/languages/javascript";
import python from "highlight.js/lib/languages/python";

// 헬퍼 함수로 안전하게 등록
const registerLanguage = (name: string, language: any) => {
  const langFn = typeof language === 'function' ? language : language.default;
  if (typeof langFn === 'function') {
    hljs.registerLanguage(name, langFn);
  }
};

registerLanguage("javascript", javascript);
// ... 필요한 언어만 등록

3-3. Forced Reflow 방지

Lighthouse에서 지속적으로 “Forced reflow” 경고가 발생했다. 마우스 인터랙션이 있는 3D 카드 컴포넌트(ProfileCard3D)에서 mousemove 이벤트가 발생할 때마다 getBoundingClientRect()를 호출하고 있었기 때문이다. 이는 브라우저가 레이아웃을 강제로 다시 계산하게 만들어 메인 스레드에 큰 부하를 준다.

해결 방안 값비싼 연산인 getBoundingClientRect()는 mouseenter 혹은 초기화 시점에 한 번만 수행하여 useRef에 캐싱하고, 애니메이션 루프에서는 캐싱된 값을 재사용하도록 수정했다.

// src/components/ProfileCard3D.tsx
const handleMouseEnter = () => {
  // 마우스 진입 시 한 번만 계산하여 캐싱
  if (cardRef.current) {
    cardRect.current = cardRef.current.getBoundingClientRect();
  }
};

const handleMouseMove = (e: MouseEvent) => {
  // 캐싱된 cardRect.current 값을 사용하여 Reflow 방지
  if (!cardRect.current) return;
  // ... transform 연산 수행
};

4. CI/CD 파이프라인 가속 (Jenkins)

Frontend, Backend, ML(ZenML) 빌드가 순차적으로 실행되다 보니 전체 배포 시간이 너무 길어졌다.

해결 방안 Jenkins의 parallel 문법을 사용하여 빌드를 병렬로 처리하고, Docker의 --cache-from 옵션을 활용해 이전 빌드 레이어를 재사용하도록 파이프라인을 개선했다.

// Jenkinsfile
stage('Build & Push Parallel') {
    parallel {
        stage('Build Backend') { /* ... */ }
        stage('Build Frontend') {
            steps {
                script {
                    // 이전 이미지 Pull -> Cache 활용 -> Build
                    sh "docker pull ${imageName}:latest || true"
                    sh "docker build --cache-from ${imageName}:latest -t ..."
                }
            }
        }
        stage('Build ZenML') { /* ... */ }
    }
}

5. 결론 및 성과

이번 최적화 작업을 통해 얻은 성과는 다음과 같다.

  1. 안정성 확보: extra_hosts 설정과 재시도 로직 도입으로 ETIMEDOUT과 404 에러가 사라졌다. 이제 배포 후 모니터링을 하지 않아도 될 만큼 견고해졌다.
  2. 성능 향상: 이미지 최적화와 Forced Reflow 제거를 통해 모바일 환경에서도 빠릿빠릿한 반응 속도를 보여주며 Lighthouse 점수도 녹색(Good) 구간에 진입했다.
  3. 효율성 증대: Jenkins 병렬 빌드로 배포 시간을 단축하여 개발 생산성을 높였다.

홈서버 환경은 상용 클라우드와 달리 네트워크 설정(NAT, DNS)부터 하드웨어 리소스 관리까지 직접 챙겨야 할 부분이 많다. 하지만 그 덕분에 Host Gateway의 개념이나 Reflow가 발생하는 원인 등 깊이 있는 기술적 고민을 할 수 있었던 값진 경험이었다.

#DevOps#Docker#Jenkins#Lighthouse#Next.js#Optimization#Self-hosted#Troubleshooting#TypeScript#WebVitals#WordPress#홈 서버

You might also like

[Jenkins] Docker로 띄운 Jenkins, 데이터 그대로 Kubernetes로 이관하기

#Helm#Jenkins#k3s

Kubernetes K3s 환경에서 PostgreSQL HA 부하 테스트: pgbench로 검증하는 성능과 오토스케일링(HPA)

#Autoscaling#Helm#Home Lab

[Home Server] 우분투 24.04 LTS 보안 강화 가이드: UFW 설정과 3중 방어 체계 분석

#Cloudflare#Docker#Fail2Ban

Comments (0)

Leave a Reply

Or login to comment with your account.

No comments yet. Be the first to share your thoughts!