본문 바로가기
C.W.K.
Stream
Lesson 03 of 04 · published

그대로 나가버린 자리표시자

~13 min · failure, tooling, silent-failure, design

Level 0흩어진 부품
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

뭐가 나갔나

소비자 하나가 템플릿 원본의 대상 목록에 추가됐어. 변수 블록은 같이 안 들어갔고. 흔한 누락이야. 매니페스트 한 줄, 어디서도 에러 없음.

치환은 뻔한 방식으로 구현돼 있었어. 변수 맵의 키마다 텍스트에서 바꿔치기. 맵이 없으면 반복문이 돌 게 없으니까 아무것도 안 바꾸고 원본을 그대로 돌려줬어. 그다음 배달이 서비스 워커를 하나 썼는데, 캐시 이름이 글자 그대로 치환 안 된 토큰이었고, 셸 목록은 값이 있어야 할 자리에 맨 자리표시자 토큰을 그대로 싣고 있었어. 두 번째가 문법 오류보다 나빠. 문법 오류가 아니거든. 맨 토큰은 유효한 식별자라서 파싱 때 아무도 불평 안 하고, 실패는 코드가 돌면서 그 이름이 아무것도 안 가리킬 때까지 기다려.

그 아래로는 전부 깨끗했어. 파일이 써졌고, 머리말도 맞고, 커밋이 들어갔고, 리뷰는 다른 서비스 워커랑 똑같이 생긴 서비스 워커를 봤어. 깨졌을 유일한 경로는 네트워크 없이 앱을 여는 거고, 그게 바로 아무도 일부러 안 밟는 경로야.

왜 이 부류가 보이는 것보다 고약한가

이 실패엔 진짜 위험한 가족의 일원이 되게 하는 성질이 셋 있어.

  • 나쁜 산출물이 형태가 멀쩡해. 맞는 종류의 파일이, 맞는 자리에, 맞는 머리말을 달고 있어. 의심을 부르는 구석이 없어.
  • 도구의 기본 동작이 그걸 만들었어. 아무것도 덮어쓰지 않았고 어떤 경고도 안 눌렀어. 키가 없다는 게 그냥 사건이 아니었어.
  • 피해 반경이 늦고 좁아. 몇 주 뒤에, 마침 오프라인인 사람한테, 오프라인 경로만 깨져. 책상 하나에서 쓰는 앱 가족이면 한참 동안 아무도 아닐 수 있고.

대안 실패랑 비교해봐. 배달이 거부하면서 어느 자리표시자가 살아남았는지, 어느 매니페스트 키가 그 값을 주는지를 찍는 거. 같은 결함, 몇 주 대신 십 초, 그리고 그걸 만든 사람이 그걸 보는 사람이야.

치환 안 된 자리표시자는 결함이고, 도구가 그걸 내보낼 수 있으면 안 돼. 템플릿 단계는 치환 뒤에 자리표시자 토큰이 안 남았다고 단언해야 해. 패턴 탐지는 시시해. 자리표시자는 애초에 grep 되라고 튀는 모양으로 고르니까. 그 단언 하나가 조용하고 늦고 형태 멀쩡한 오염 한 부류 전체를, 처방이 딸린 즉각적인 거부로 바꿔.

처방, 그리고 그 메시지의 모양

수리는 자리표시자 문법에 대한 정규식 하나를 쓰기 전 렌더링된 본문에 돌리는 것, 그리고 세 가지를 이름 대는 거부였어. 이 파일을 받았을 소비자와 대상, 어느 자리표시자가 살아남았는지, 그리고 그 값이 들어갈 매니페스트의 정확한 자리.

세 번째가 이걸 그냥 시끄러운 실패가 아니라 좋은 실패로 만들어. "치환 안 된 변수 발견"이라고만 하면 독자가 치환이 어떻게 설정되는지 알아내러 가야 해. 더할 매니페스트 키를 이름 대는 메시지는 배달 스크립트를 한 번도 안 읽어본 사람이 이 분 만에 고칠 수 있는 일로 바꿔.

Code

조용한 판본, 그리고 그걸 대체하는 거부·python
import re

TEMPLATE_VAR = re.compile(r"__KIT_[A-Z0-9_]+__")


def substitute(content: str, vars_map: dict[str, str] | None) -> str:
    """The original. With vars_map=None the loop body never runs, so
    the placeholders survive and the caller writes them to disk."""
    if vars_map:
        for key, value in vars_map.items():
            content = content.replace(key, value)
    return content


def unsubstituted(content: str) -> list[str]:
    """Placeholders still standing after substitution.

    Deliberately distinctive syntax: a placeholder must be impossible
    to confuse with real code, so that this check can be a plain
    regex over the rendered text with no parsing at all.
    """
    return sorted(set(TEMPLATE_VAR.findall(content)))


# At the deploy site, before writing THIS target:
expected_body = substitute(raw, (entry.get("vars") or {}).get(repo))

leftover = unsubstituted(expected_body)
if leftover:
    print(
        f"unsubstituted template var(s) for {repo}:{target_rel}"
        f" - {', '.join(leftover)}."
        f' Add them to "vars"."{repo}" for {entry["source"]}'
        " in the manifest.",
        file=sys.stderr,
    )
    return 2      # abort the RUN. And be honest about what that is
                  # NOT: targets written earlier in the walk stay
                  # written, because each is written as it is
                  # reached. Refusing late is not a rollback - it
                  # only stops the remaining writes.


# What the bad output looked like on disk - well-formed, wrong:
#
#   const CACHE = "__KIT_APP_SLUG__-shell-v1";   // a real cache,
#                                                // named nonsense
#   const SHELL = [__KIT_SHELL_LIST__];          // NOT a syntax error.
#                                                // A bare token is a
#                                                // valid identifier: it
#                                                // parses, then is
#                                                // undefined at RUNTIME

External links

Exercise

네 프로젝트의 템플릿이나 코드 생성 단계를 하나 골라서 질문 하나에 답해. 값이 없으면 뭘 해? 해봐. 변수를 하나 지우고 생성기를 돌려. 산출물이 자리표시자를 품은 파일이거나, 값이 있어야 할 자리에 빈 문자열이거나, 'None' 이라는 단어면, 치환 뒤 단언을 붙이고 그 메시지가 값을 정의할 자리를 이름 대게 만들어.
Hint
제일 위험한 결과는 크래시도 자리표시자도 아냐. 빈 문자열이야. 빈 문자열은 보통 유효한 문법이거든. 이름이 '' 인 캐시나 '' 인 URL 은 배달되고, 돌아가고, 템플릿 얘기는 한 번도 안 꺼내면서 깨져.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.