Leaving dots everyday.

What is Ajax / Ajax API

Context

Shopify AJAX API 문서를 읽다가 “AJAX API”라는 명칭이 정확히 무엇을 뜻하는지 헷갈려서 찾아봤다.

What I Learned

AJAX 자체의 의미

AJAX = Asynchronous JavaScript And XML

페이지를 새로고침하지 않고 서버와 데이터를 주고받는 브라우저 패턴이다. XML은 이름에만 남아 있고, 현재는 JSON을 주로 사용한다. fetch, XMLHttpRequest 등이 AJAX를 구현하는 수단이다.

Shopify에서 말하는 “AJAX API”

일반 API와 달리 스토어 프론트엔드 JS에서 직접 호출하도록 열어둔 unauthenticated HTTP 엔드포인트 묶음이다. 이름에 AJAX가 붙은 건 역사적·용도적 이유에 가깝다.

일반 APICart AJAX API
사용 주체외부 시스템 / 백엔드스토어 테마 JS
인증필요불필요 (guest cart 가능)
상태 관리-세션 기반 cart state

흔한 오해

  • ❌ “AJAX API = 다른 종류의 API” → 그냥 HTTP 엔드포인트
  • ❌ “fetch랑 AJAX는 다른 기술” → fetch는 AJAX를 구현하는 방법 중 하나
  • ❌ “특별한 프로토콜” → 그냥 HTTP

Note

“AJAX API”라는 이름은 기술적 특이성보다 **설계 의도(프론트엔드에서 바로 쓸 수 있는 unauthenticated 엔드포인트)**를 나타내는 표현에 가깝다.

aiohttp ClientTimeout: 단계별 타임아웃 설정

Context

이미지 다운로드 로직에서 SoftTimeout 에러가 연쇄 발생했다. 원인을 추적해보니 dead host에 대해 session.get(url)에 timeout이 없어서, OS 레벨 TCP connect timeout(~30s)이 3번 발동하는 구조였다.

3 × 30s (TCP timeout) + 6s (retry sleep) ≈ 96s / URL

dead host 하나가 96초짜리 병목이 되어 전체 세션을 잡아먹고 있었다.

What I Learned

aiohttp.ClientTimeouttotal 하나만 있는 게 아니라 연결 단계별로 따로 걸 수 있다.

필드적용 범위
total전체 작업 (연결 + 요청 + 응답 읽기) 최대 시간
connect새 연결 수립 또는 풀에서 여유 연결 대기 최대 시간
sock_connect실제 소켓 연결(peer 연결) 최대 시간
sock_read응답 데이터를 peer로부터 읽는 사이 최대 대기 시간

기본값:

aiohttp.ClientTimeout(
    total=5*60,       # 5분
    connect=None,     # 제한 없음
    sock_connect=None,
    sock_read=None,
)

dead host 문제처럼 연결 자체가 안 되는 케이스total만으로는 대응이 늦다. sock_connect를 짧게 걸면 연결 시도 단계에서 일찍 끊을 수 있다.

timeout = aiohttp.ClientTimeout(
    total=10,
    sock_connect=3,  # 연결 자체가 3초 안에 안 되면 포기
    sock_read=5,
)
async with session.get(url, timeout=timeout) as resp:
    ...

Note

timeout=None(기본값)으로 놔두면 OS 레벨 TCP timeout에 의존하게 된다. Linux 기준 약 30s인데, retry까지 붙으면 한 URL당 수십-수백 초가 소요될 수 있다. 외부 리소스를 fetch하는 코드엔 반드시 명시적 timeout을 걸자.

Athena Partition과 Partition Projection

Context

S3에 적재된 raw event data를 Athena로 쿼리하려다가 partitioning이 안 된 테이블이라는 걸 알게 됐다. 쿼리 효율을 높이기 위해 partition projection을 적용했다.

What I Learned

1. Partition vs Index — 같은 범주가 아니다

항목PartitionIndex
목적읽을 데이터 양 자체를 줄임데이터 찾는 속도를 빠르게
데이터 위치물리적으로 분리됨그대로 있음
작동 방식특정 partition만 스캔전체 중에서 빠르게 lookup
효과I/O 감소 (big data에서 핵심)CPU/lookup 최적화
Athena/S3매우 중요 (거의 필수)없음

Partition은 데이터를 물리적으로 나눠 저장하고, 쿼리 시 일부만 읽도록 만드는 구조다.

s3://bucket/
  ├── event_date=2026-04-25/
  ├── event_date=2026-04-26/
  └── event_date=2026-04-27/
WHERE event_date = '2026-04-27'
-- → 해당 폴더만 읽음 (partition pruning)

Athena는 index가 없다. 성능은 오직 얼마나 적은 S3 데이터를 읽느냐에 달려 있다.

partition 있음 → 필요한 파일만 읽음 → 빠름 + 저렴
partition 없음 → 전체 스캔 → 느림 + 비쌈

2. Partition Projection

partition 메타데이터를 실제로 저장하지 않고, 규칙으로 계산해서 사용하는 방식

전통적인 방식은 S3에 데이터를 올린 뒤 Glue Crawler를 돌리거나 ALTER TABLE ADD PARTITION을 실행해 메타데이터를 별도로 등록해야 한다.

Partition Projection은 DDL에 규칙만 정의하면 Athena가 경로를 직접 계산한다.

TBLPROPERTIES (
  'projection.enabled'='true',
  'projection.event_date.type'='date',
  'projection.event_date.range'='2026-01-01,NOW',
  'storage.location.template'='s3://bucket/${event_date}/'
)

쿼리 실행 시 내부 동작:

WHERE event_date = '2026-04-27'
→ template에 대입 → s3://bucket/2026-04-27/
→ 이 경로만 읽음 (metadata lookup 없음)

partnerid / eventtype / event_date 같이 partition 조합이 많을수록 projection이 유리하다. Crawler 없이 관리할 수 있다.

3. 실무 주의사항

반드시 partition 조건을 WHERE에 명시해야 한다.

-- ❌ 위험 — 모든 partition 조합을 스캔 시도 → 느림 / 비용 폭발
SELECT * FROM my_table LIMIT 100;

-- ✅ 올바른 방식
SELECT * FROM my_table
WHERE partnerid = 'A'
  AND event_date = '2026-04-27';

injected 타입으로 설정된 partition 컬럼은 Athena가 가능한 값 목록을 모른다. 쿼리에서 반드시 직접 지정해야 한다.

Note

Projection은 partition 수가 많아질수록 강력하다. 반대로 WHERE에 partition 조건을 빠뜨리면 전체 스캔보다 더 나쁜 결과가 나올 수 있으니 팀 내 쿼리 가이드를 명확히 해두는 게 좋다.

Smoke Testing: 전체를 돌리기 전에 최소한만 검증하기

Context

여러 레이어를 활용하는 파이프라인을 설계하고 구현하는 과정에서 “smoke test부터 돌려보자”는 말이 나와서 정확한 의미를 확인했다.

What I Learned

Smoke testing이란 시스템의 핵심 기능이 기본적으로 동작하는지 빠르게 확인하는 가벼운 테스트다. “이 빌드를 더 깊이 테스트할 가치가 있는가?”를 먼저 판단하는 게 목적이다.

어원은 전자기기 분야에서 왔다 — 회로 기판에 전원을 처음 연결했을 때 연기(smoke)가 나지 않으면 기본적으로 괜찮다고 보는 데서 유래했다.

특징

  • 전체 테스트 스위트를 돌리기 전 최소한의 통과 기준만 확인
  • 빠르게 실행되고, 실패 시 즉시 멈춤
  • “작동은 하는가?”에만 집중 — 세부 동작의 정확성은 다음 단계

파이프라인에서의 활용 예시

[smoke test]
- 각 레이어가 오류 없이 실행되는가?
- 입력/출력 형식이 맞는가?
- 빈 결과나 예외 없이 끝까지 흐르는가?

Note

복잡한 파이프라인일수록 smoke test가 유용하다. 전체를 완성하기 전에 각 레이어가 연결되는 골격만 먼저 통과시켜보면, 설계 단계의 문제를 일찍 발견할 수 있다.

Monkey Patching: 런타임에 함수를 덮어씌우는 기법

Context

유저 이벤트 로그를 심다가, Target의 필터 UI가 URL을 바꿀 때 브라우저가 아무 이벤트도 발생시키지 않는다는 걸 알게 됐다. popstate는 뒤로가기/앞으로가기에서만 발생하고, 사이트가 직접 pushState / replaceState로 URL을 갱신할 때는 표준 이벤트가 없다.

그래서 monkey patching으로 직접 훅을 걸었다.

What I Learned

Monkey patching이란 런타임에 이미 정의된 객체/함수의 동작을 외부에서 덮어쓰거나 확장하는 기법이다. 원본 소스 코드는 건드리지 않고, 실행 중인 메모리상의 객체를 바꿔치기한다.

실제 적용 코드

['pushState', 'replaceState'].forEach((m) => {
    const original = history[m];
    history[m] = function (...args) {
        const ret = original.apply(this, args); // 원본 반드시 호출
        window.dispatchEvent(new Event('locationchange'));
        return ret;
    };
});

브라우저 내장 history.pushState를 우리 함수로 교체한다. 이후 누가 호출하든 우리 버전이 실행되고, 커스텀 이벤트를 발생시켜 URL 변화를 감지할 수 있다.

언제 유용한가

  • 브라우저/서드파티 API에 훅을 걸 때 — 수정 불가능한 내장 API를 덮어씌우는 것은 가능
  • 긴급 버그 패치 — upstream 수정 전 임시 우회
  • 계측/모니터링 — Sentry, DataDog 같은 APM 도구들이 fetch, XMLHttpRequest, console.error 등을 몽키 패칭해서 에러/요청을 자동 수집

주의점

위험내용
전역 영향한 번 덮어씌우면 페이지 전체 모든 호출이 영향받음
업스트림 변경원본 API 시그니처가 바뀌면 패치가 조용히 고장날 수 있음
디버깅 난이도스택트레이스에 패치된 함수가 찍혀 원인 추적이 어려워짐
원본 호출 누락original.apply(this, args)를 빼먹으면 원래 동작이 사라짐

Note

가능하면 표준 이벤트/옵저버를 먼저 찾고, 없을 때 최후 수단으로 쓰는 게 좋다. 이번 케이스는 “브라우저가 history 변화에 대한 표준 이벤트를 제공하지 않는다”는 명확한 이유가 있어서 정당화되는 사례다.

Articles