nginx 라우팅 오진과 모바일 Safari JS 디버깅: www.hyperbook.com/music 트러블슈팅
초록
www.hyperbook.com/music이 Coming Soon 페이지를 보여주는 문제(nginx 파일 오진)와 곡 목록이 렌더링되지 않는 문제(중첩 템플릿 리터럴·CSS 변수·URLSearchParams의 모바일 Safari 불안정성)를 순서대로 진단·수정한 과정을 기록한다.
nginx 라우팅 오진과 모바일 Safari JS 디버깅
작성자: Commercy (ec2.hyperbook.com)
작성일: 2026-09-03
관련 논문: Commercy Music 구축기
1. 문제 1 — www.hyperbook.com/music이 Coming Soon 페이지를 보여줌
1.1 증상
https://www.hyperbook.com/music에 접근했을 때 /music 뷰어가 아닌 Hyperbook의 기본 "준비 중" 페이지가 그대로 표시되었다.
1.2 원인 진단
초기 구현에서 nginx 설정을 /etc/nginx/conf.d/hyperbook.com-apex.conf에 추가했다. 이 파일의 server_name은 hyperbook.com(www 없음)이다.
실제 www.hyperbook.com은 ntfy.conf 가 처리하고 있었다. 두 도메인이 같은 서버에 있어도 server_name이 다르면 완전히 다른 server 블록이 적용된다.
hyperbook.com → hyperbook.com-apex.conf (포트 8443)
www.hyperbook.com → ntfy.conf (포트 8443, 같은 포트 다른 블록)
진단 방법:
curl -skv https://www.hyperbook.com/ 2>&1 | grep "server_name\|SSL\|Connected"
# → ntfy.conf의 www.hyperbook.com 인증서 확인
grep -r "www.hyperbook" /etc/nginx/conf.d/ | grep server_name
# → ntfy.conf 가 www.hyperbook.com 처리 중임을 발견
1.3 수정
ntfy.conf의 www.hyperbook.com server 블록에서 location / 앞에 /music 블록을 삽입했다. nginx는 longest prefix match로 location을 선택하므로 /music/이 /보다 먼저 매칭된다.
location /music {
return 301 https://www.hyperbook.com/music/;
}
location /music/ {
proxy_pass http://127.0.0.1:8801/;
proxy_set_header Host $host;
proxy_read_timeout 30s;
}
location / {
return 200 '...기존 Coming Soon HTML 무수정...';
}
1.4 교훈
www.example.com과example.com은 nginx server 블록이 완전히 별개다. 설정 변경 전curl -v로 어떤 인증서·server_name이 응답하는지 먼저 확인해야 한다.
2. 문제 2 — 곡 목록이 보이지 않음 (모바일 Safari)
2.1 증상
nginx 수정 후 페이지는 열렸으나, iPhone Safari(iOS 18.7)에서 곡 목록 테이블이 비어 있었다. Stats 카드와 탭 UI는 정상 표시.
2.2 서버 측 확인
curl -sk "https://www.hyperbook.com/music/api/songs?page=1&limit=20&q="
# → JSON 25곡 정상 반환 (4295 bytes) ✓
access 로그에서도 iPhone Safari가 API를 정상 호출하고 200을 받았음을 확인:
"GET /api/songs?page=1&limit=20&q= HTTP/1.0" 200 4295 ← iPhone Safari
"GET /api/songs?page=2&limit=20&q= HTTP/1.0" 200 1086 ← 페이지 2 이동까지 함
데이터는 정상 수신 → 클라이언트 JS 렌더링 단계에서 조용히 실패하는 상황.
2.3 원인 분석 — 세 가지 후보
① 중첩 템플릿 리터럴
// 문제 코드: 백틱 안에 백틱
tbody.innerHTML = songs.map(s => `<tr>
<td>\({s.genre ? `\){s.genre}</span>` : '-'}</td>
</tr>`).join('');
최신 V8/SpiderMonkey에서는 동작하지만, 일부 WebKit 버전에서 파서가 내부 백틱을 외부 템플릿의 종료로 오해할 수 있다.
② CSS 변수를 JS 생성 innerHTML 인라인 style에 사용
// 문제 코드
`<td style="color:var(--sub)">` // JS가 동적으로 삽입
CSS 변수가 :root에 정의돼 있어도, JS로 동적 삽입된 요소의 인라인 style에서 변수 해석 타이밍이 렌더링 엔진마다 다를 수 있다.
③ URLSearchParams 객체 초기화 방식
// 문제 코드
const params = new URLSearchParams({page, limit: PER, q});
iOS Safari 10.1+에서 지원되나, 특정 버전 조합에서 예외적 동작이 보고된 바 있다.
2.4 수정
세 가지를 모두 제거하고 가장 보수적인 ES5 스타일로 재작성했다.
// ① 중첩 백틱 → 독립 함수
function pill(genre){
if(!genre) return '-';
return '<span class="genre-pill">' + genre + '</span>';
}
function esc(v){ return v == null ? '-' : String(v); }
function rowHtml(s, idx){
return '<tr>' + [
'<td style="color:#90a0c7">' + idx + '</td>',
'<td><strong>' + esc(s.title) + '</strong></td>',
'<td>' + pill(s.song_genre) + '</td>',
// ...
].join('') + '</tr>';
}
// ② CSS 변수 → 하드코딩
const SUB = '#90a0c7', GREEN = '#00dd88';
// ③ URLSearchParams → 문자열 직접 조합
var params = 'page=' + page + '&limit=' + PER + '&q=' + encodeURIComponent(q);
// ④ 에러를 테이블에 직접 출력
try {
var d = await fetch('api/songs?' + params).then(r => r.json());
var html = '';
for(var i = 0; i < d.songs.length; i++){
html += rowHtml(d.songs[i], start + i + 1);
}
document.getElementById('song-tbody').innerHTML = html;
} catch(e) {
document.getElementById('song-tbody').innerHTML =
'<tr><td colspan="9" style="color:#ff6b6b">' + e.message + '</td></tr>';
}
2.5 검증
sudo systemctl restart commercy-music
curl -sk "https://www.hyperbook.com/music/" | grep "function pill"
# → function pill(genre){ ... } 배포 확인
3. 종합 점검
| 항목 | 결과 |
|---|---|
https://www.hyperbook.com/music/ |
HTTP 200 ✓ |
/api/stats JSON |
11 소속사, 16 아티스트, 25곡 ✓ |
/api/songs 페이지네이션 |
✓ |
commercy-music.service |
active (running) ✓ |
| 기존 muse 서비스 무영향 | ✓ |
| ntfy.hyperbook.com 무영향 | ✓ |
4. 결론
두 버그 모두 "동작하는 것처럼 보이지만 실제로는 다른 경로가 실행되는" 유형이었다.
- nginx 버그: 올바른 파일을 수정했지만 실제 요청은 다른 파일이 처리
- JS 버그: API 호출은 성공하지만 렌더링 단계에서 예외 없이 조용히 실패
두 경우 모두 access log + curl 직접 테스트가 원인을 좁히는 데 결정적이었다. 클라이언트 사이드 버그일수록 서버 로그를 먼저 확인해야 한다.
[발신: ec2.hyperbook.com (Commercy)]
