외계행성 — NASA 데이터베이스를 탐색하는 UI 만들기
NASA Exoplanet Archive에서 실제 외계행성 데이터를 가져와 거주 가능성으로 분류하고 Canvas로 시각화한 기록
시작 — 5,000개가 넘는 실제 외계행성
2022년 NASA가 확인된 외계행성 수가 5,000개를 넘었다고 발표했습니다. 이 행성들은 실제 관측 데이터로 이루어진 공개 데이터베이스에 저장되어 있습니다.
단순히 목록을 보여주는 것이 아니라, 거주 가능성 기준으로 분류하고 각 행성의 물리적 특성을 시각적으로 탐색할 수 있는 UI를 만들고 싶었습니다.
API — TAP 서비스와 ADQL 쿼리
NASA Exoplanet Archive는 TAP(Table Access Protocol) 서비스를 제공합니다. SQL과 유사한 ADQL 쿼리로 원하는 컬럼만 선택해 가져올 수 있습니다.
const QUERY = `
SELECT pl_name, pl_rade, pl_bmasse, pl_insol, st_teff,
pl_orbper, sy_dist, disc_year, discoverymethod
FROM ps
WHERE pl_controv_flag = 0
`;
const FETCH_URL =
`https://exoplanetarchive.ipac.caltech.edu/TAP/sync` + `?query=${encodeURIComponent(QUERY)}&format=json`;
pl_controv_flag = 0 조건은 논란이 있는 후보 행성을 제외하고 확인된 행성만 가져오기 위한 필터입니다. 전체 쿼리 결과는 약 5,500개 행으로, 응답 크기가 상당해서 클라이언트 캐싱이 필수였습니다.
거주 가능성 분류
거주 가능성의 핵심 지표는 일사량(insolation flux, pl_insol)입니다. 단위는 지구 기준(Earth = 1)이며, 지구와 유사한 일사량 범위에 있는 행성을 거주 가능 후보로 분류합니다.
type Habitability = 'habitable' | 'hot' | 'cold' | 'unknown';
function getHabitability(p: Planet): Habitability {
if (p.pl_insol == null) return 'unknown';
if (p.pl_insol >= 0.25 && p.pl_insol <= 1.77) return 'habitable';
if (p.pl_insol > 1.77) return 'hot';
return 'cold';
}
0.25~1.77 범위는 Kopparapu et al. (2013)의 낙관적 거주 가능 구역(Optimistic HZ) 기준을 따랐습니다. 다만 일사량만으로는 진정한 거주 가능성을 판단할 수 없으며, 이 분류는 탐색의 시작점에 가깝습니다.
Canvas 시각화 — 크기와 온도로 표현
행성 목록을 단순 텍스트로 나열하는 대신, Canvas에 각 행성을 원으로 시각화했습니다.
- 원의 크기: 행성 반경(
pl_rade, 지구 반경 기준) — 로그 스케일 적용 - 원의 색상: 항성 유효 온도(
st_teff, K) — 차갑고 붉은 별(2,500K)에서 뜨거운 파란 별(30,000K)까지
function getTempColor(teff: number | null): string {
if (teff == null) return 'rgba(150, 150, 150, 0.7)';
// 2500K=붉은 적색왜성, 5778K=태양, 10000K=청백색
const t = Math.max(0, Math.min(1, (teff - 2500) / 10000));
const r = Math.round(255 * (1 - t * 0.6));
const g = Math.round(180 * t);
const b = Math.round(80 + 175 * t);
return `rgba(${r}, ${g}, ${b}, 0.85)`;
}
거주 가능 행성으로 분류된 것들은 shadowBlur로 초록색 글로우를 추가해 강조했습니다.
필터와 정렬
탐색을 돕기 위한 필터 시스템을 구현했습니다.
- 타입 필터: All / Habitable Zone / Earth-like (반경 0.5~1.5) / Giant (반경 > 6)
- 발견 방법: Transit / Radial Velocity / Direct Imaging / 기타
- 발견 연도: 슬라이더로 범위 지정
필터 결과 건수를 실시간으로 표시하고, 거주 가능 행성이 필터 결과에 포함되면 상단에 별도로 목록을 노출합니다.
클라이언트 캐시와 AbortController
데이터는 한 번 로드하면 exoplanet_data_v2 키로 localStorage에 저장합니다.
const abortRef = useRef<AbortController | null>(null);
async function fetchExoplanets() {
abortRef.current?.abort();
abortRef.current = new AbortController();
const cached = storageGet<Planet[] | null>('exoplanet_data_v2', null);
if (cached) {
setData(cached);
return;
}
const res = await fetch(FETCH_URL, { signal: abortRef.current.signal });
const json = await res.json();
const planets = parseRows(json);
storageSet('exoplanet_data_v2', planets);
setData(planets);
}
컴포넌트 언마운트 또는 재요청 시 이전 요청을 AbortController로 취소해 경쟁 상태(race condition)를 방지했습니다.
캔버스 성능 — 4K 해상도 캡
수천 개의 행성을 Canvas에 동시에 렌더링하면 픽셀 처리량이 상당합니다. 특히 거주 가능 행성에 적용되는 shadowBlur 글로우 효과는 픽셀 수에 비례해 비용이 증가합니다. 4K 디스플레이에서 캔버스 버퍼가 과도하게 커지는 것을 방지하기 위해, 총 픽셀 수를 1920 × 1080 기준으로 제한하는 캡을 적용했습니다.
5,000개 이상의 행성 점을 동시에 그리는 페이지인 만큼, 이 캡의 효과가 특히 체감되었습니다.
마치며
행성 목록을 처음 렌더링했을 때, 수천 개의 점이 Canvas에 펼쳐지는 것을 보며 잠시 멈췄습니다. 이것들이 실제로 존재하는 행성들이라는 사실 — 누군가의 관측이 데이터로 쌓이고, 그것이 이 화면 위 점 하나로 나타난다는 감각은 목록을 스크롤하는 것과는 전혀 다른 경험이었습니다.