ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 로컬 개발 환경에서 가상호스트(Virtual Host)를 써야 하는 이유와 설정 방법 🙏
    웹 개발 2026. 7. 26. 18:24

    localhost:3000 으로만 개발하고 있다면, 이 글을 읽고 나서 생각이 바뀔 수도 있어요.

    1. 왜 가상호스트가 필요한가요?

    개발 초반에는 localhost:3000, 127.0.0.1:8080 같은 포트 기반 방식으로 충분하게 느껴져요. 그런데 프로젝트가 조금만 복잡해지면 생각지 못한 곳에서 문제가 생기기 시작해요.

    포트 기반 방식의 한계

    • 쿠키 domain 분리가 안 돼요. localhost는 서브도메인을 지원하지 않아서, domain=.myapp.local 같은 방식으로 쿠키를 공유할 수가 없어요.
    • CORS / SameSite 정책이 달라져요. 브라우저가 localhost를 특별하게 취급하는 경우가 있어서, 운영 환경에서 터지는 CORS 문제를 로컬에서 재현하기 어렵습니다.
    • OAuth redirect_uri 문제. 인증 서버에 포트 번호가 포함된 URI를 등록해야 하는 번거로움이 생겨요.
    • 포트 충돌 관리가 피곤해요. 프로젝트가 여러 개면 포트 번호를 외우고 관리해야 하죠.

    가상호스트 방식의 이점

    • api.myapp.local, admin.myapp.local 처럼 운영 환경과 동일한 도메인 구조를 로컬에서 그대로 재현할 수 있어요.
    • 쿠키 domain=.myapp.local 설정으로 서브도메인 간 쿠키 공유가 가능해져요.
    • Nginx upstream, 리버스 프록시 같은 인프라 설정을 로컬에서 직접 테스트할 수 있어요.
    • 프로젝트마다 도메인으로 구분하니 포트 충돌 걱정도 없어요.

    💡 핵심 원칙 — 로컬 개발 환경이 운영 환경과 구조적으로 다를수록, 배포 직후 터지는 버그가 늘어납니다. 가상호스트는 그 구조적 간극을 좁히는 가장 저비용 방법이에요.


    2. 어떻게 동작하나요? — DNS 해석 흐름

    브라우저가 myapp.local을 요청할 때 내부적으로 이런 순서로 처리돼요.

    브라우저 요청
        → /etc/hosts 조회 (DNS 서버보다 먼저!)
        → 127.0.0.1 매핑 발견
        → 로컬 웹서버 도달
        → Host 헤더로 ServerName 매칭
        → DocumentRoot 응답

    운영체제는 DNS 서버에 쿼리를 보내기 전에 /etc/hosts 파일을 먼저 확인해요. 여기서 도메인-IP 매핑을 찾으면 외부 DNS 쿼리 자체가 발생하지 않습니다.

    웹서버는 HTTP 요청의 Host 헤더를 읽어서 어떤 가상호스트 블록을 사용할지 결정해요. 이게 바로 하나의 IP로 여러 도메인을 처리할 수 있는 원리예요.


    3. 1단계: /etc/hosts 파일 등록

    macOS / Linux

    sudo nano /etc/hosts

    아래 내용을 추가해 주세요.

    # 로컬 개발 프로젝트
    127.0.0.1   myapp.local
    127.0.0.1   api.myapp.local
    127.0.0.1   admin.myapp.local

    저장 후 DNS 캐시를 초기화해야 해요.

    # macOS Monterey 이상
    sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
    
    # Linux (systemd-resolved)
    sudo systemctl restart systemd-resolved

    Windows

    메모장을 관리자 권한으로 실행한 다음 아래 파일을 열어주세요.

    C:\Windows\System32\drivers\etc\hosts

    내용을 추가하고 저장한 뒤, cmd에서 DNS 캐시를 초기화해요.

    ipconfig /flushdns

    4. 2단계: 웹서버 가상호스트 설정

    Apache (macOS / Linux)

    먼저 httpd.conf에서 가상호스트 관련 설정이 활성화되어 있는지 확인해 주세요.

    # 아래 두 줄의 주석(#)이 제거되어 있어야 해요
    LoadModule vhost_alias_module libexec/apache2/mod_vhost_alias.so
    Include /private/etc/apache2/extra/httpd-vhosts.conf

    그다음 httpd-vhosts.conf 파일을 편집해요.

    # 기본 호스트 — 매칭 실패 시 fallback
    <VirtualHost *:80>
        ServerName    localhost
        DocumentRoot  /var/www/html
    </VirtualHost>
    
    # 프론트엔드 앱
    <VirtualHost *:80>
        ServerName    myapp.local
        DocumentRoot  /Users/you/projects/myapp/public
    
        <Directory "/Users/you/projects/myapp/public">
            Options       Indexes FollowSymLinks
            AllowOverride All
            Require       all granted
        </Directory>
    
        ErrorLog  /var/log/apache2/myapp-error.log
        CustomLog /var/log/apache2/myapp-access.log combined
    </VirtualHost>
    
    # API 서버 — Node/Python 등 백엔드 리버스 프록시
    <VirtualHost *:80>
        ServerName  api.myapp.local
    
        ProxyPreserveHost On
        ProxyPass        / http://127.0.0.1:3000/
        ProxyPassReverse / http://127.0.0.1:3000/
    
        ErrorLog  /var/log/apache2/api-error.log
        CustomLog /var/log/apache2/api-access.log combined
    </VirtualHost>

    설정을 검증하고 재시작해 주세요.

    apachectl configtest        # Syntax OK 확인
    sudo apachectl restart
    
    # Homebrew Apache를 쓰고 있다면
    brew services restart httpd

    ⚠️ 리버스 프록시를 쓰려면 httpd.conf에서 mod_proxy, mod_proxy_http 모듈 주석을 해제해야 해요.


    Nginx (macOS / Linux)

    /etc/nginx/sites-available/ 경로에 설정 파일을 만들어요.

    # 정적 파일 서빙
    server {
        listen      80;
        server_name myapp.local;
        root        /Users/you/projects/myapp/public;
        index       index.html;
    
        location / {
            try_files $uri $uri/ /index.html;
            # SPA가 아니라면 =404 로 변경해요
        }
    
        access_log /var/log/nginx/myapp-access.log;
        error_log  /var/log/nginx/myapp-error.log;
    }
    
    # 리버스 프록시 — Node/FastAPI 등
    server {
        listen      80;
        server_name api.myapp.local;
    
        location / {
            proxy_pass         http://127.0.0.1:3000;
            proxy_http_version 1.1;
            proxy_set_header   Upgrade $http_upgrade;
            proxy_set_header   Connection 'upgrade';
            proxy_set_header   Host $host;
            proxy_cache_bypass $http_upgrade;
        }
    }

    symlink를 연결하고 재시작해요.

    # sites-available → sites-enabled 심링크 생성
    sudo ln -s /etc/nginx/sites-available/myapp.local \
               /etc/nginx/sites-enabled/
    
    sudo nginx -t            # 설정 검증
    sudo nginx -s reload     # 재시작
    
    # Homebrew Nginx를 쓰고 있다면
    brew services restart nginx

    💡 macOS Homebrew Nginx 경로

    • Apple Silicon: /opt/homebrew/etc/nginx/
    • Intel Mac: /usr/local/etc/nginx/

    servers/ 폴더에 conf 파일을 직접 넣으면 자동으로 include돼요.


    5. Windows 환경 (XAMPP / IIS)

    XAMPP (Apache)

    C:\xampp\apache\conf\extra\httpd-vhosts.conf 파일을 열어 아래 내용을 추가해요.

    <VirtualHost *:80>
        ServerName    myapp.local
        DocumentRoot  C:/xampp/htdocs/myapp/public
    
        <Directory "C:/xampp/htdocs/myapp/public">
            Options       Indexes FollowSymLinks
            AllowOverride All
            Require       all granted
        </Directory>
    </VirtualHost>

    httpd.conf에서 Include conf/extra/httpd-vhosts.conf 줄의 주석이 해제되어 있는지 먼저 확인하고, XAMPP Control Panel에서 Apache를 재시작해 주세요.

    IIS

    IIS 관리자에서 사이트 추가 시 호스트 이름 필드에 myapp.local을 입력하면 GUI만으로 설정이 끝나요. 별도 conf 파일 편집이 필요 없어서 비교적 간단해요.

    ⚠️ IIS 바인딩 주의 — 호스트 이름을 지정하면 IP 직접 접근이 차단돼요. 기본 사이트와 포트가 겹치면 충돌하니, 기본 사이트를 중지하거나 포트를 분리하는 게 좋아요.


    6. 고급 팁 — 쿠키, CORS, 도메인 네이밍

    쿠키 domain 공유

    로그인 토큰을 myapp.localapi.myapp.local이 함께 공유하려면, 쿠키 발급 시 domain=.myapp.local (앞에 점 포함)으로 설정해야 해요.

    localhost는 dot-prefix를 허용하지 않기 때문에 서브도메인 간 쿠키 공유가 구조적으로 불가능해요. 이게 가상호스트를 써야 하는 가장 대표적인 이유 중 하나예요.

    CORS 헤더 처리 (Nginx)

    add_header Access-Control-Allow-Origin      "http://myapp.local" always;
    add_header Access-Control-Allow-Methods     "GET, POST, PUT, DELETE, OPTIONS" always;
    add_header Access-Control-Allow-Headers     "Authorization, Content-Type" always;
    add_header Access-Control-Allow-Credentials "true" always;
    
    location / {
        if ($request_method = OPTIONS) {
            return 204;
        }
        proxy_pass http://127.0.0.1:3000;
    }

    도메인 네이밍 전략

    TLD 특징
    .local 가장 일반적으로 쓰여요. mDNS와 충돌 가능성이 있지만 실무에서 가장 널리 사용돼요.
    .test IANA가 테스트용으로 예약한 TLD예요 (RFC 2606). DNS 충돌이 없어서 안전해요.
    .localhost 최신 브라우저에서 HTTPS-only 처리를 면제받는 경우가 있어요.

    🚫 .dev TLD는 피하세요!
    Google이 실제 TLD로 등록해 두었고, 브라우저가 HSTS preload를 적용해요.
    http://myapp.dev 접속이 강제로 HTTPS 리다이렉트되어 로컬 HTTP 개발이 불가능해져요.


    7. 한 걸음 더 — mkcert로 로컬 HTTPS

    Service Worker, Geolocation, WebRTC, Clipboard API 등은 HTTPS 환경에서만 동작해요. mkcert를 쓰면 인증서 경고 없는 로컬 HTTPS를 5분 안에 구성할 수 있어요.

    # 설치 (macOS Homebrew)
    brew install mkcert nss   # nss는 Firefox 지원을 위해 필요해요
    mkcert -install           # 로컬 CA를 시스템 키체인에 등록
    
    # 인증서 발급
    mkcert myapp.local api.myapp.local
    
    # 결과: myapp.local+1.pem, myapp.local+1-key.pem 생성

    Nginx에 SSL을 적용해요.

    server {
        listen      443 ssl;
        server_name myapp.local;
    
        ssl_certificate     /path/to/myapp.local+1.pem;
        ssl_certificate_key /path/to/myapp.local+1-key.pem;
    
        root  /Users/you/projects/myapp/public;
        index index.html;
    }
    
    # HTTP → HTTPS 리다이렉트
    server {
        listen      80;
        server_name myapp.local;
        return      301 https://$host$request_uri;
    }

    마무리

    가상호스트는 단순한 편의 기능이 아니라, 로컬과 운영 환경 사이의 구조적 격차를 줄이는 중요한 선택이에요.
    설정 순서 요약:

    1. /etc/hosts (또는 Windows hosts 파일)에 도메인-IP 매핑 추가
    2. 웹서버(Apache 또는 Nginx)에 VirtualHost / server block 설정
    3. (선택) mkcert로 로컬 HTTPS 인증서 적용

    한 번만 해두면 이후 프로젝트마다 server block 하나 복사로 끝나요.
    처음 설정이 조금 번거롭게 느껴질 수 있지만, 운영 환경과 동일한 구조로 개발할 수 있다는 장점은 충분히 그 수고를 보상해 준답니다. 😊
     
    📌 공식 문서


    📌 심화 참고


    📌 실용 가이드

Designed by Tistory.