TRENDSHIFT #6 딥다이브 · 2026-06-04

cpaczek/skylight 딥다이브
— 머리 위를 지나는 비행기를 천장에 실시간으로 쏘는 장치

방에 누웠는데 위로 비행기 소리가 들립니다. 저건 어디서 와서 어디로 갈까요? Skylight2만 원짜리 라디오(RTL-SDR)로 머리 위 비행기의 신호를 직접 받아, 그 비행기를 천장에 쏜 프로젝터 위로 실시간으로 미끄러지게 그려 줍니다. 항공사·기종·목적지 라벨이 붙고, 배경에는 해·달·별자리·국제우주정거장(ISS)까지 진짜 위치로 떠 있죠. 마치 지붕을 X-ray로 투시하는 느낌입니다. (저장소: cpaczek/skylight · TypeScript · Express + React + Vite · ⭐ 빠르게 상승 중 · MIT 라이선스)
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어/시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

한 문장으로: "값싼 라디오로 머리 위 비행기 신호를 받아, 천장에 별·달과 함께 실시간으로 띄우는 셀프호스팅 앱."

비행기는 자기 위치·고도·속도·편명을 1초에 한 번꼴로 무선으로 떠들고 다닙니다. 이걸 ADS-B라고 부르는데, 누구나 받을 수 있는 공개 신호입니다. Skylight는 그 신호를 2만~3만 원짜리 USB 라디오(RTL-SDR)로 받아 해독한 뒤, 머리 위를 지나는 비행기를 천장을 향해 쏜 프로젝터 화면에 그립니다. 실제로 소리가 들리는 그 제트기가, 같은 순간 천장을 가로질러 미끄러지는 거죠.

배경은 완전한 검정입니다. 프로젝터의 사각형 테두리가 어둠 속으로 사라지고, 비행기와 별만 빛나도록 설계했습니다. 그리고 비행기 뒤에는 그 시각·그 위치의 진짜 하늘이 그려집니다 — 해, 위상까지 표현된 달, 밝은 별과 별자리 선, 그리고 TLE(궤도 데이터)로 계산한 인공위성과 ISS. 모든 설정은 누워서 휴대폰으로 조절합니다.

영웅 비유

"지붕을 투명하게 만드는 X-ray 안경 — 단, 라디오로 듣고 천장에 그려서"

병원의 X-ray는 살을 투과해 뼈를 비춥니다. Skylight는 지붕을 투과해 비행기를 비추는 X-ray입니다. 다만 빛이 아니라 전파(ADS-B)로 비행기를 "듣고", 그 결과를 천장이라는 스크린에 그려 넣죠. 누워서 위를 보면, 실제로 그 자리를 날고 있는 비행기가 라벨을 단 채 천장을 가로지릅니다. 게다가 이 X-ray는 비행기 너머의 진짜 별과 달, 우주정거장까지 함께 비춰 줍니다. 천장이 곧 밤하늘 창문이 되는 셈입니다.

용어
ADS-B (Automatic Dependent Surveillance–Broadcast)
비행기가 GPS로 잰 자기 위치·고도·속도·편명을 1090MHz 전파로 1초에 한 번씩 스스로 방송하는 시스템. 관제소가 레이더로 "찾는" 게 아니라, 비행기가 알아서 "나 여기 있어요"라고 외칩니다. 암호화가 없어 누구나 안테나만 있으면 받을 수 있어, 전 세계 항공기 추적(FlightRadar24 등)의 토대가 됩니다. Skylight는 이 외침을 받아 천장에 옮겨 그립니다.
용어
RTL-SDR (소프트웨어 정의 라디오)
원래 TV 수신용으로 나온 값싼 USB 동글을, 펌웨어를 바꿔 넓은 주파수 대역의 라디오 수신기로 쓰는 장치. "SDR"은 라디오의 핵심 처리를 하드웨어 회로가 아니라 소프트웨어로 한다는 뜻. 2~3만 원짜리 동글 하나로 항공기 신호(1090MHz)부터 위성·라디오까지 받을 수 있어, 취미 무선·항공 추적의 표준 입문 장비입니다.

2왜 주목받는가

"멋진 결과물 + 따라 만들 수 있는 친절함 + 깨끗한 코드"의 삼박자

비행기 추적 프로젝트나 ADS-B 디코더는 예전부터 많았습니다. Skylight가 트렌딩에 오른 건 "기능"이 아니라 "경험과 완성도" 때문입니다. 천장에 별과 함께 비행기가 흐르는 데모 영상 한 편이 SNS에서 강한 인상을 남겼고, README가 "라디오 없이도 지금 당장 켜 볼 수 있게" 설계돼 진입장벽을 거의 0으로 낮췄습니다.

차별점 1 · 라디오가 없어도 바로 체험

하드웨어를 사기 전에 코드로 먼저 논다

보통 이런 하드웨어 프로젝트는 "장비부터 사세요"로 시작해 좌절을 줍니다. Skylight는 반대입니다. DATA_SOURCE=api pnpm dev 한 줄이면 무료 공개 API(airplanes.live)에서 실제 비행기 데이터를 받아 노트북 브라우저에서 그대로 돌아갑니다. 코드 변경은 0줄. 마음에 들면 그때 라디오를 사서 DATA_SOURCE=radio로 바꾸면 됩니다.

차별점 2 · "비행기 추적"을 넘어선 감성

데이터 시각화가 아니라 하나의 '오브제'

숫자 테이블이나 지도 위 점이 아니라, 누워서 바라보는 천장 위 오브제로 설계했습니다. 순검정 배경, 고도별 색, 혜성 같은 꼬리(trail), 기종별로 모양이 다른 글리프(광동체는 크게, 헬기는 로터가 돌고, 프로펠러기는 프롭이 돈다), 그리고 비행기가 향하는 도시·현지 시각·남은 거리를 보여 주는 "다른 곳으로의 창". 기술 데모를 감성 제품으로 끌어올린 점이 사람들을 매료시켰습니다.

차별점 3 · 가전제품처럼 완결된 셀프호스팅

라즈베리파이에 올리면 '켜면 켜지는' 기기

SD카드 헤드리스 세팅 → 드라이버·디코더·앱 설치 → 부팅 시 전체화면 키오스크까지, pi-setup/ 스크립트로 자동화돼 있습니다. 전원만 넣으면 곧장 천장에 하늘이 뜨는 '어플라이언스(가전)'가 됩니다. 게다가 제작자가 크라우드펀딩 키트(skylightceiling.com)를 예고해, "코드 → 제품"의 서사까지 갖췄습니다.

비유로 정리

비슷한 프로젝트가 "엔진과 부품을 줄 테니 차를 조립하라"라면, Skylight는 "시승부터 해 보고(API 모드), 마음에 들면 키트로 완성차를 만들라(라디오+파이)"는 흐름입니다. 그 매끄러운 온보딩과 결과물의 아름다움이 트렌딩의 진짜 이유입니다.

3기술 스택 전체 지도

한 가지 언어(TypeScript)로 라디오부터 천장까지 — pnpm 모노레포의 4개 패키지 구조

Skylight의 가장 깔끔한 점은 처음부터 끝까지 TypeScript 하나로 묶여 있다는 것입니다. 라디오 신호를 받는 서버도, 천장에 그림을 그리는 화면도, 둘이 공유하는 수학 공식도 전부 같은 언어죠. 그리고 이걸 pnpm 워크스페이스라는 한 저장소 안 4개 패키지(shared·server·web·tracker)로 나눠 둡니다.

용어
모노레포 (Monorepo) · pnpm 워크스페이스
여러 개의 작은 프로젝트(패키지)를 하나의 저장소 안에 폴더로 나눠 담는 방식. Skylight는 shared(공유 코드)·server(서버)·web(화면)·tracker(하늘 카메라 서브시스템)으로 나뉘어 있습니다. pnpm은 패키지들이 서로를 @shared처럼 import할 수 있게 연결해 주고, 라이브러리를 한 번만 받아 디스크를 아낍니다. "한 집 안에 부엌·거실·서재·촬영실을 나눠 두되, 복도로 다 연결돼 있는" 구조입니다.

① 백엔드 — server/ (Node · Express · ws)

비행기 데이터를 모으고, 다듬고, 뿌리는 심장부입니다. 핵심 부품을 뜯어보면:

부품역할
Express가벼운 HTTP 서버. /api/config·/api/aircraft·/api/tle 같은 REST 창구를 열고, 빌드된 화면(정적 파일)도 같이 서빙
ws (WebSocket)화면·휴대폰과 양방향 실시간 통로. 비행기 위치를 1초마다 밀어내고, 휴대폰이 보낸 설정 변경을 받아 모두에게 다시 방송
Poller데이터 소스(라디오 또는 API)를 POLL_MS(기본 1초)마다 긁어와 정규화·보강하는 수집기 클래스 (datasource.ts)
enrich/편명→항공사, 기종코드→기종명 표(airlines.json·types.json) + adsbdb API로 출발/도착 공항을 채우는 보강 모듈
TleStoreCelestrak에서 위성 궤도(TLE)를 받아 메모리+디스크에 캐시, 하루 단위 갱신 (tle.ts)
ConfigStore설정을 config.json 파일에 저장하는 단일 진실원. 변경 시 구독자(WebSocket)에게 알림

② 프론트엔드 — web/ (Vite · React · Canvas)

5개의 진입점이 한 빌드 안에 들어 있습니다. 천장에 비치는 화면, 손에 든 리모컨, 카메라 포인팅 파이프라인 디버그 UI(tracker.html), 실시간 피드+레이더 대시보드(tv.html), 그리고 스트림 뷰어(stream.html)입니다.

부품역할
Vite개발 서버 + 번들러. index.html(디스플레이)·control.html(리모컨)·tracker.html(카메라 디버그)·tv.html(TV 대시보드)·stream.html(스트림) 5개 진입점을 빌드 (vite.config.ts)
ReactUI 골격. 단, 비행기 그림 자체는 React가 아니라 <canvas>에 직접 그림(성능)
renderer.ts약 37KB짜리 핵심 렌더러. 비행기 글리프·꼬리·범위 링·라벨을 매 프레임 캔버스에 그리는 엔진
celestial.ts + stars.tsastronomy-engine으로 해·달·별의 실제 방위/고도를 계산해 화면 좌표로 변환
satellite.js서버가 준 TLE로 위성·ISS의 현재 위치를 브라우저에서 계산
connection.ts자동 재접속 WebSocket 클라이언트. 끊기면 1.5초마다 다시 연결 시도
Control.tsx휴대폰용 설정 패널(회전·미러·테마·필터·하늘 토글). 바꾸는 즉시 WebSocket으로 반영

③ 공유 코어 — shared/ (순수 TypeScript)

서버와 화면이 똑같이 쓰는, DOM도 상태도 없는 순수 코드입니다. 타입 정의(aircraft.ts·messages.ts), 설정 스키마(config.ts), 그리고 좌표 변환 수학(geo.ts)이 들어 있죠. 서버와 클라이언트가 같은 공식을 쓰니 "위치 계산이 두 곳에서 어긋나는" 버그가 원천 차단됩니다.

④ 선택 사항: 하늘 카메라 서브시스템 — tracker/ (PTZ 카메라 자동 조준)

tracker는 4번째 패키지로, ADS-B 위치 데이터를 이용해 PTZ 카메라를 비행기 방향으로 자동 조준해 촬영하는 선택 사항 서브시스템입니다. 핵심 구성을 보면:

부품역할
VISCA-over-IPPTZ 카메라를 네트워크로 제어 (팬·틸트·줌)
비전 탐지기고전 blob 탐지 + YOLOX-Nano ONNX 모델로 화면 안 비행기 인식
RTSP 스트림 처리카메라 영상을 실시간으로 받아 탐지 파이프라인에 공급
자동 캘리브레이션ADS-B 예측 위치와 실제 화면 위치를 맞춰 마운트 오프셋 자동 보정
시뮬레이터 모드카메라 없이도 파이프라인 전체를 테스트 가능

카메라 디버그 UI는 web/tracker.html에서, 실시간 피드+레이더 대시보드는 web/tv.html에서 확인할 수 있습니다. 라즈베리파이에는 pi-setup/skylight-tracker.service로 별도 systemd 서비스로 등록됩니다.

⑤ 인프라/배포 — pi-setup/ + scripts/ + Docker

라즈베리파이 OS 위 셸 스크립트 배포 외에 Docker(compose.yaml)도 공식 지원됩니다 — docker compose up -d --build 한 줄로 라디오 없이 서버+디스플레이를 컨테이너로 구동할 수 있습니다. 파이 셸 세트는 SD카드 헤드리스 프로비저닝(provision-sd.sh), 드라이버+디코더+앱 설치(install-on-pi.sh), 크롬 키오스크 자동시작(setup-kiosk.sh), systemd 서비스 등록(skylight-server.service), 그리고 개발 PC에서 파이로 코드를 밀어 넣는 deploy-to-pi.sh(rsync 기반)까지 한 세트입니다.

용어
dump1090-fa
RTL-SDR가 받은 1090MHz 생전파를 비행기별 위치·고도 JSON으로 해독해 주는 표준 디코더 프로그램(FlightAware 판). Skylight는 이게 만들어 주는 aircraft.json(8080 포트)을 1초마다 읽기만 합니다. 즉 Skylight는 라디오 신호를 직접 해독하지 않고, 해독은 dump1090에 맡기고 자기는 "그리기"에 집중합니다. 역할 분담의 좋은 예입니다.

4아키텍처 심화 분석

전파 → 해독 → 보강 → WebSocket → 천장. 그리고 60fps를 만드는 '시간 트릭'

전체 데이터 흐름은 왼쪽(하늘)에서 오른쪽(천장)으로 한 방향으로 흐릅니다. 단, 휴대폰의 설정 변경만 거꾸로 거슬러 올라가죠.

하늘의 비행기 │ 1090MHz ADS-B 전파 ▼ [RTL-SDR 동글] ──USB──> [dump1090-fa] ──> aircraft.json (:8080) │ 서버가 ~1Hz로 폴링 │ (+ airplanes.live API 보충) ▼ ┌──────────────────────────────┐ │ server/ (Node·Express·ws) │ │ · 정규화(readsb 스키마) │ │ · 보강(항공사·기종·항로) │ │ · 위성 TLE 프록시(Celestrak) │ │ · 설정 저장 + WebSocket 방송 │ └───────────────┬──────────────┘ ┌───────────────────────┬───────────┴───────────┐ ▼ ▼ ▼ [디스플레이 /] [리모컨 /control] [REST /api/*] canvas 렌더러 + 휴대폰 설정 UI 디버그·외부 클라이언트 하늘 엔진 → 프로젝터 (실시간·양방향) ▲ │ └──── 설정 변경은 거꾸로 올라가 전체에 재방송 ────┘

핵심 설계 패턴 1 — 단일 진실원(Config) + 양방향 동기화

모든 설정은 shared/src/config.tsConfig 객체 한 곳에만 존재합니다. 서버는 이걸 config.json에 저장하고, 누가(휴대폰이든 REST든) 바꾸면 ConfigStore가 구독자에게 알리고, Hub연결된 모든 화면에 새 설정을 방송합니다. 그래서 휴대폰에서 회전 슬라이더를 움직이면, 천장 화면이 즉시 같이 돕니다. 재부팅해도 파일에 남아 유지되죠.

// hub.ts — 새 화면이 접속하면 현재 상태로 '초기 동기화'
private onConnect(ws) {
  this.send(ws, { type: "config", config: store.get() });   // 지금 설정
  this.send(ws, { type: "aircraft", ...snapshot });          // 지금 비행기
  this.send(ws, { type: "status", status: getStatus() });    // 소스 상태
}
// 휴대폰이 patchConfig를 보내면 → store.patch → 구독자 → 전체 재방송

핵심 설계 패턴 2 — 두 소스 합치기(radio + API merge)

내 라디오는 머리 위 비행기를 잘 잡지만, 멀어지거나 착륙하면 신호를 놓칩니다. 그래서 라디오 모드에서도 공개 API를 보조로 같이 폴링해, hex(기체 고유 ID) 기준으로 합칩니다. 단 신선도(seen)를 비교하되 라디오에 2초 가산점을 줘서, 내가 직접 받은 신호가 우선하고 라디오가 놓친 순간에만 API가 빈자리를 메웁니다.

// datasource.ts — 라디오에 -2초 편향을 줘서 우선권 부여
const rSeen = (r.seen ?? 0) - 2;   // 로컬 라디오가 더 '신선'하게 보이도록
const aSeen = existing.seen ?? 999;
byHex.set(r.hex, rSeen <= aSeen ? r : existing);

핵심 설계 패턴 3 — '스티키 보강'으로 라벨 깜빡임 제거

항로 정보(목적지 등)는 비동기로 천천히 채워집니다. 그런데 다음 스냅샷에서 그 값이 잠깐 비면 라벨이 "목적지 → — → 목적지"로 깜빡이겠죠. Skylight는 hex별로 한 번 알아낸 값은 절대 빈값으로 되돌리지 않는 스티키(sticky) 맵을 둬서 이 깜빡임을 없앱니다. 그리고 10분 넘게 안 보인 기체는 맵에서 지워 메모리를 관리합니다.

핵심 설계 패턴 4 — 1Hz 데이터를 60fps로 만드는 '시간 트릭'

비행기 신호는 1초에 한 번 옵니다. 그대로 그리면 1초마다 뚝뚝 순간이동하겠죠. Skylight는 두 가지 기법으로 부드럽게 만듭니다. 첫째, 일부러 약간 과거를 그립니다 — 항상 직전 두 위치 사이를 보간(tween)할 수 있도록. 둘째, 그 사이 구간은 geo.tsdeadReckon으로 진로·속도를 따라 위치를 추측 전진시켜 60fps로 채웁니다.

// geo.ts — 진로(track)와 속도(gs)로 dt초 뒤 위치를 추측 (추측항법)
export function deadReckon(m, trackDeg, gsKt, dtSec) {
  if (gsKt == null || gsKt <= 0) return m;
  const dist = gsKt * KT_TO_MS * dtSec;        // 노트→m/s 변환 후 거리
  const t = trackDeg * DEG;
  return { east: m.east + dist*Math.sin(t),
           north: m.north + dist*Math.cos(t) }; // 진행 방향으로 전진
}

핵심 설계 패턴 5 — '평평한 지구' 투영 + 올려다보기 보정

몇 km 반경 안에서는 지구를 평평하다고 봐도 오차가 거의 없습니다. 그래서 위경도를 단순한 곱셈으로 "동/북 미터"로 바꾸고(llToMeters), 회전·미러를 적용해 화면 픽셀로 투영합니다(project). 중요한 반전 포인트: 프로젝터는 위로 쏘고 사람도 위를 보기 때문에 좌우가 뒤집힙니다. 그래서 기본값이 mirrorX: true이고, 정확한 회전·미러 값은 공식이 아니라 실제 머리 위 비행기를 보며 손으로 맞추는 '보정(calibration)'입니다.

비유로 정리

이 아키텍처는 "방송국 구조"와 같습니다. 라디오(특파원)가 현장을 잡아 오면, 서버(편집국)가 사실 확인·자막(보강)을 붙여 1초 단위 뉴스로 가공하고, WebSocket(전파탑)이 모든 TV(화면)에 동시 송출합니다. 휴대폰 리모컨은 시청자가 편집국에 거꾸로 보내는 "이렇게 보여 주세요" 요청이고요. 그리고 '시간 트릭'은 24fps 필름을 부드럽게 보이게 하는 영사기의 원리와 같습니다.

5디렉토리 구조 해부

파일 30여 개의 작은 저장소 — 그래서 통째로 읽어 보기 좋은 교본

Skylight는 거대한 프로젝트가 아닙니다. 핵심 소스는 손에 꼽을 정도라, 아키텍처를 처음부터 끝까지 읽어 보기에 이상적인 교본입니다. 폴더별 역할을 지도로 그리면 이렇습니다.

skylight/ ├── shared/src/ ← 서버·화면이 공유하는 순수 코드 │ ├── config.ts · 설정 스키마 + 기본값 + mergeConfig (단일 진실원) │ ├── geo.ts · 좌표 수학: llToMeters / project / deadReckon │ ├── aircraft.ts · Aircraft 타입 정의 │ └── messages.ts · WebSocket 메시지 타입(서버↔클라) │ ├── server/src/ ← 데이터 수집·보강·방송 │ ├── index.ts · 진입점: 모든 부품을 조립 + REST + 정적 서빙 │ ├── datasource.ts · Poller: 폴링·정규화·소스 병합·스티키 보강 │ ├── hub.ts · WebSocket 허브(접속 시 초기 동기화 + 방송) │ ├── config-store.ts · config.json 읽기/쓰기 + 구독 │ ├── airports.ts · OurAirports CSV 다운로드·캐시(공항 좌표 조회) │ ├── tle.ts · 위성 TLE 가져와 캐시(메모리+디스크) │ └── enrich/ · 보강 데이터/로직 │ ├── routes.ts · adsbdb로 출발/도착 공항 채우기(+캐시) │ ├── tables.ts · 편명→항공사 / 기종코드→기종명 룩업 │ ├── airlines.json · 항공사 코드 표 │ └── types.json · 기종 코드 표 │ ├── web/ ← 두 페이지(디스플레이 + 리모컨) │ ├── index.html · 디스플레이 진입점 │ ├── control.html · 리모컨 진입점 │ ├── vite.config.ts · 두 진입점 빌드 설정 │ └── src/ │ ├── display/ · 천장에 비치는 화면 │ │ ├── renderer.ts · ★핵심 렌더러(~37KB): 비행기·꼬리·라벨 │ │ ├── aircraftGlyph.ts · 기종별 글리프(헬기 로터·프롭 회전) │ │ ├── celestial.ts · 해·달 위치(astronomy-engine) │ │ ├── main.tsx · 디스플레이 React 진입점 │ │ └── stars.ts · 별·별자리 │ ├── control/ · 휴대폰 설정 UI(Control.tsx) │ └── lib/ · connection.ts(WebSocket) / useStream.ts │ ├── pi-setup/ ← 라즈베리파이 어플라이언스 만들기 │ ├── provision-sd.sh · SD카드 헤드리스 WiFi+SSH │ ├── install-on-pi.sh · 드라이버+디코더+Node+앱+서비스 │ ├── setup-kiosk.sh · 부팅 시 크롬 전체화면 │ └── skylight-server.service · systemd 유닛 │ ├── scripts/ ← 개발 편의 스크립트 │ ├── run-dump1090-local.sh · 로컬에서 디코더 띄우기 │ ├── deploy-to-pi.sh · 파이로 rsync 배포 │ └── install-rtlsdr-fedora.sh · 드라이버 설치(Fedora) │ ├── pnpm-workspace.yaml · 모노레포 패키지 선언 └── package.json · dev/build/typecheck 스크립트(concurrently)
한눈에
읽는 순서 추천
shared/config.ts로 "무엇을 조절하는 앱인지" 감 잡기 → shared/geo.ts로 좌표 수학 이해 → server/index.ts로 부품 조립 보기 → server/datasource.tsPoller로 데이터 흐름 → web/src/lib/connection.ts로 클라이언트 수신 → renderer.ts로 그리기. 이 순서면 큰 그림이 막힘없이 들어옵니다.

6학습 포인트 (기술별)

이 작은 저장소 하나에 '실시간 시스템'의 교과서 패턴이 다 들어 있다

① WebSocket 실시간 양방향 통신

서버는 ws로 1초마다 비행기를 밀어내고(push), 클라이언트는 끊기면 자동 재접속합니다. 새로 접속한 화면에 현재 상태를 먼저 한 번 보내 주는 '초기 동기화' 패턴, 그리고 한 클라이언트의 변경을 모두에게 다시 뿌리는 '브로드캐스트' 패턴을 깔끔하게 볼 수 있습니다.

실습 아이디어

connection.ts를 흉내 내, 채팅·협업 커서·실시간 대시보드 같은 "한 사람이 바꾸면 모두에게 반영되는" 작은 앱을 만들어 보세요. 재접속·초기 동기화·브로드캐스트 세 가지만 익혀도 실시간 앱의 8할입니다.

② 폴링·정규화·소스 병합(데이터 파이프라인)

Poller는 서로 다른 두 소스(라디오/API)를 같은 readsb 스키마로 정규화하고, hex 기준으로 병합하며, 한 번 알아낸 값을 유지하는 스티키 캐시까지 갖췄습니다. "여러 불완전한 출처를 하나의 매끄러운 스트림으로 합치는" 실전 데이터 엔지니어링 패턴이죠.

③ 실시간 렌더링과 보간(애니메이션)

1Hz 데이터를 60fps로 만드는 "과거를 그려서 미래를 보간하는" 트릭과 deadReckon 추측항법은 게임·지도·트레이딩 차트 등 모든 실시간 시각화에 쓰이는 핵심 기법입니다. <canvas>에 직접 그려 React 리렌더 비용을 피하는 결정도 배울 점입니다.

④ 천문 계산(astronomy-engine · satellite.js · TLE)

위경도와 시각만으로 해·달·별·위성의 하늘 위치를 계산합니다. TLE(위성 궤도 데이터)를 받아 satellite.js로 ISS 위치를 푸는 부분은, 평소 접하기 어려운 우주·항법 도메인을 코드로 만져 보는 드문 기회입니다.

⑤ 설정을 '단일 진실원'으로 다루기

모든 설정이 Config 한 객체에 모이고, 파일에 저장되고, 변경이 구독으로 전파되는 구조는 규모가 큰 앱에서도 그대로 통하는 설계입니다. mergeConfig의 깊은 병합으로 부분 업데이트가 중첩 키를 날리지 않게 막는 디테일도 좋은 교본입니다.

⑥ 하드웨어 + 셀프호스팅 어플라이언스화

셸 스크립트만으로 라즈베리파이를 "켜면 켜지는 기기"로 만드는 과정(헤드리스 프로비저닝·systemd·키오스크)은 홈랩/셀프호스팅의 정석입니다. RTL-SDR·dump1090 조합은 그 자체로 무선·항공 취미의 입문 코스이기도 합니다.

7하드웨어/시스템 요구사항

"체험"은 노트북 한 대로 0원, "완성"은 약 30만 원 안쪽

중요한 점은 두 단계로 나뉜다는 것입니다. 코드만 돌려 보는 체험에는 하드웨어가 전혀 필요 없습니다.

0단계 — 체험(하드웨어 0개)

Node 20+ 와 pnpm만 있으면 됩니다. DATA_SOURCE=api pnpm dev 한 줄로 무료 공개 API에서 실제 비행기를 받아 브라우저에서 바로 봅니다.

완성 단계 — 천장 어플라이언스 부품표

부품권장메모
수신기RTL-SDR Blog V4 + 다이폴 안테나비행기가 거의 머리 위라 기본 다이폴로 충분 (약 4만 원)
컴퓨터Raspberry Pi 5 (8GB)해독+렌더링 동시 수행. 24시간 가동엔 능동 쿨링 권장
프로젝터1080p 프로젝터(위로 조준)레이저 단초점이 검정을 가장 깊게 내지만 과함. 아래 팁 참고
영상 연결micro-HDMI → HDMI파이 5는 micro-HDMI(미니 아님) 주의
마운트1/4-20 회전 스탠드위로 조준 + 안전 끈(테더) 필수
비용 절감 팁
비싼 프로젝터는 필요 없습니다

콘텐츠가 "검은 배경 위 점·선"이라 어두운 방에서는 200~400루멘짜리 저렴한 네이티브 1080p LED 프로젝터(약 15만 원대)가 오히려 더 깊은 검정을 냅니다. 단 세 가지만 확인하세요: ① "1080p 지원"이 아니라 네이티브 1920×1080, ② 조용한 팬, ③ 전원 켜자마자 HDMI 입력이 뜨는지. 제작자는 8피트(약 2.4m) 천장 아래 바닥에서 쏘면 1.35:1 투사비로도 약 1.7m 화면이 나온다고 안내합니다.

주의
프로젝터를 위로 매달 때의 안전

프로젝터를 천장으로 조준해 세우거나 매달 때는 반드시 안전 끈으로 이중 고정하세요. 그리고 SFO가 아닌 곳에서 쓰려면 centerLat/centerLon을 내 위치로 바꾸고, server/src/airports.ts의 활주로 좌표를 (OurAirports 데이터로) 내 동네 공항으로 교체해야 합니다. 별·해·달·위성은 좌표만 바꾸면 자동 계산됩니다.

8직접 해볼 수 있는 실습 과제

난이도별 5개 — 클론 → 설정 → 코드 수정 → 새 기능 → 응용 순
난이도 ★☆☆☆☆ · 입문

1. 라디오 없이 띄워 보고, 내 도시로 옮기기

pnpm installDATA_SOURCE=api pnpm dev로 띄우고, shared/src/config.tscenterLat/centerLon을 내가 사는 도시(또는 가까운 공항)로 바꿔 보세요. 화면(localhost:5173)과 리모컨(/control.html)을 휴대폰으로도 열어, 회전·테마를 바꿨을 때 화면이 실시간으로 따라오는지 확인합니다.

난이도 ★★☆☆☆ · 설정 탐험

2. 비상 스쿼크 강조 색·필터 만져 보기

geo.tsEMERGENCY_SQUAWKS(7500/7600/7700)와 config.tshighlightEmergency·palette.warn을 살펴보세요. 고도 필터(minAltitudeFt/maxAltitudeFt), 라벨 밀도(labelDensity), 꼬리 길이(trailSeconds)를 바꿔 가며 "내 취향의 천장"을 튜닝해 봅니다.

난이도 ★★★☆☆ · 코드 수정

3. 새 항공사/기종을 보강 표에 추가

server/src/enrich/airlines.json·types.json에 라벨이 "—"로 뜨는 항공사·기종을 직접 추가해 보세요. 그리고 tables.ts의 룩업이 어떻게 동작하는지, adsbdb 보강(routes.ts)과 어떤 순서로 합쳐지는지 추적합니다. 데이터 보강 파이프라인을 손으로 만져 보는 과제입니다.

난이도 ★★★★☆ · 새 기능

4. "가장 가까운 비행기" 알림 추가

서버 스냅샷에서 중심으로부터 거리(rangeMeters)를 계산해, 일정 거리 안에 비행기가 들어오면 WebSocket으로 type: "alert" 메시지를 새로 정의해 보내고, 클라이언트에서 받아 화면 모서리에 표시해 보세요. 새 메시지 타입을 shared/messages.ts에 추가하고 서버·클라 양쪽을 잇는 풀스택 흐름을 경험합니다.

난이도 ★★★★★ · 응용 프로젝트

5. 같은 구조로 '다른 실시간 오브제' 만들기

아키텍처(폴링→정규화→WebSocket→캔버스)를 그대로 빌려, 다른 공개 ADS-B/AIS/날씨 API를 소스로 꽂아 보세요. 예: 근처 배(AIS)를 벽에 띄우기, 버스 실시간 위치를 책상 모니터에. 소스 어댑터(fetchList·normalize)만 갈아끼우면 나머지 뼈대는 재사용됩니다. 이 레포의 진짜 가치는 "재사용 가능한 실시간 시각화 골격"임을 체감하게 됩니다.

9관련 기술 심화 학습 로드맵

4주 코스 — 실시간 웹부터 SDR·천문 계산까지
주차주제할 일
1주차실시간 웹 기초WebSocket(ws) 개념, 재접속·하트비트·브로드캐스트 패턴. connection.ts·hub.ts를 정독하고 미니 채팅앱 구현
2주차캔버스 렌더링·애니메이션requestAnimationFrame 루프, 보간(tween)·추측항법(dead reckoning), 좌표 투영. geo.ts + renderer.ts 구조 분석
3주차데이터 보강·캐싱외부 API 보강(adsbdb), 디스크 캐시·TTL·스티키 패턴. routes.ts·tle.ts 따라 만들기
4주차SDR + 천문 + 어플라이언스RTL-SDR/dump1090로 실제 신호 받기, astronomy-engine·satellite.js로 하늘 계산, 라즈베리파이 키오스크 배포
학습 전략

이 레포는 "작지만 끝까지 완성된" 풀스택 실시간 시스템이라, 한 주제씩 떼어 공부하기 좋습니다. 1~2주차(실시간 웹·캔버스)는 어떤 웹 개발자에게도 바로 쓸모가 있고, 3~4주차(SDR·천문)는 평소 만나기 힘든 도메인을 안전하게 맛보는 보너스 코스입니다.

10핵심 키워드 사전

이 글에 나온 용어를 한 줄씩 다시 정리
용어의미
ADS-B비행기가 자기 위치·고도·편명을 1090MHz로 1초마다 스스로 방송하는 시스템(암호화 없음)
RTL-SDR값싼 USB 동글을 소프트웨어 라디오로 쓰는 장치. 항공기 신호 수신의 표준 입문 장비
dump1090-faRTL-SDR 생전파를 비행기별 위치 JSON으로 해독하는 표준 디코더(FlightAware 판)
hex각 항공기의 24비트 고유 ID(ICAO 주소). 소스 병합·스티키 캐시의 키로 사용
readsb 스키마dump1090·airplanes.live가 공통으로 쓰는 aircraft JSON 형식. 하나의 정규화기로 둘 다 처리
WebSocket (ws)서버·클라이언트 양방향 실시간 통신. 비행기 위치 push + 설정 변경 수신
보간 / 추측항법1초 간격 데이터를 60fps로 매끄럽게. 진로·속도로 위치를 예측 전진시키는 deadReckon
스티키 보강한 번 알아낸 항로·항공사 값을 빈값으로 되돌리지 않아 라벨 깜빡임을 막는 캐시
TLE (Two-Line Element)인공위성의 궤도를 두 줄 숫자로 적은 표준 데이터. Celestrak에서 받아 satellite.js로 위치 계산
astronomy-engine위경도·시각으로 해·달·별의 하늘 위치를 계산하는 천문 라이브러리
satellite.jsTLE로 위성·ISS의 현재 위치를 계산하는 JS 라이브러리(클라이언트에서 실행)
단일 진실원(Config)모든 설정을 한 객체·한 파일에 모아 저장·전파. mergeConfig로 부분 업데이트 안전 처리
pnpm 워크스페이스한 저장소에 shared·server·web 패키지를 나눠 담고 서로 연결하는 모노레포 방식
키오스크 모드부팅 시 크롬을 전체화면으로 자동 실행해 '전용 기기'처럼 만드는 방식(라즈베리파이)
비상 스쿼크7500(납치)·7600(무선고장)·7700(비상) 코드. highlightEmergency로 강조 표시
다이폴 안테나RTL-SDR Blog V4에 기본 동봉되는 안테나. 머리 위 비행기 수신엔 이걸로 충분

11참고 링크