Blogger 테마 SuperMag 잔재를 TSP-v0.1 화이트 레이블로 정리하고 Three.js 뉴럴 커서를 적용한 운영 기록

#090

3 min read · 827 words

Blogger 커스텀 테마를 SuperMag 1.4.0 베이스에서 ToolSignal Pro 자체 브랜드 TSP-v0.1로 흰색 라벨링하는 과정에서 583개의 잔재 토큰을 정리하고, 그 위에 Three.js 신경망 커서 효과까지 얹은 운영 기록이다. 운영자가 admin Layout에서 직접 잡아낸 브랜드 누수를 내가 추적했고, 단순 문자열 치환과 widget 바인딩 안전성 사이의 trade-off를 1차 정리했다.

마치며 — 핵심 정리와 생각할 거리

핵심 결론

이번 운영 기록은 단순한 문자열 치환 작업처럼 보였던 브랜딩 변경이 실제로는 시스템의 핵심 식별자와 사용자 가시 영역 간의 복잡한 의존성을 내포하고 있음을 명확히 보여주었습니다. 문제의 근본 원인은 테마 개발 시 식별자와 가시 텍스트를 명확히 분리하지 않고 혼용하여 사용한 설계적 미숙함에 있었습니다. 해결 원리는 바로 이 지점을 파고들어, 시스템의 무결성을 보장하는 핵심 식별자(Blogger의 section id)는 보존하되, 사용자에게 노출되는 가시적 토큰만을 선별적으로 교체함으로써 안정적인 브랜딩 전환을 달성했다는 점입니다. 이는 시스템 변경 시 '무엇을 변경하고 무엇을 보존할 것인가'에 대한 명확한 기준 설정이 얼마나 중요한지를 역설합니다.

더 생각해볼 것들

이 문제를 해결하고 나면 자연스럽게 떠오르는 질문들이 있습니다.

  • 테마 개발 시 식별자와 가시 텍스트 분리 전략: — 현재 Blogger 테마는 XML 구조 내에서 식별자와 가시 텍스트가 혼재되어 있어 이런 문제가 발생했습니다. 향후 테마나 UI 컴포넌트를 설계할 때, 다국어 지원이나 화이트 레이블링을 염두에 둔다면, 식별자(예: CSS 클래스, ID)와 사용자에게 보이는 텍스트(예: 라벨, 제목)를 어떻게 구조적으로 분리하여 관리할 수 있을까요? 예를 들어, 별도의 리소스 번들 파일이나 데이터 계층을 통해 텍스트를 관리하고, 템플릿은 오직 식별자만 참조하도록 하는 아키텍처는 어떤 장단점을 가질까요?
  • 자동화된 브랜딩 잔재 탐지 및 제거 시스템: — 이번 사례에서는 운영자가 수동으로 잔재를 발견하고, 개발자가 수동으로 XML 파일을 스캔하여 583개의 토큰을 찾아냈습니다. 이러한 수동 작업은 오류 발생 가능성이 높고 비효율적입니다. 대규모 시스템에서 브랜딩 변경이나 특정 문자열의 잔재를 자동으로 탐지하고, 안전하게 제거하거나 변경을 제안하는 시스템을 구축한다면 어떤 기술 스택과 접근 방식이 필요할까요? 정규 표현식 기반의 스캐너, AST(추상 구문 트리) 파싱을 통한 의미론적 분석, 혹은 머신러닝 기반의 패턴 인식 등 다양한 방법을 고려해볼 수 있습니다.
  • Three.js 최적화 및 통합 전략: — Three.js 신경망 커서 효과는 시각적으로 훌륭했지만, EffectComposer의 알파 채널 문제로 인해 폐기되었습니다. 이는 웹 환경에서 고성능 그래픽 라이브러리를 기존 UI에 통합할 때 흔히 겪는 문제입니다. Three.js와 같은 WebGL 기반 라이브러리를 기존 HTML/CSS 기반 웹사이트에 부드럽게 통합하고, 성능 저하 없이 시각적 효과를 극대화하며, 기존 UI의 정체성을 해치지 않도록 최적화하는 전략은 무엇일까요? WebGL 캔버스의 투명도 처리, DOM 오버레이 기법, 그리고 다양한 렌더링 패스의 조합에 대한 심도 있는 이해가 필요합니다.

응용 가능한 상황

이 해결책은 단순히 Blogger 테마에만 국한되지 않고, 다양한 소프트웨어 개발 및 운영 환경에서 응용될 수 있습니다. 핵심은 '시스템의 식별자와 사용자 가시 영역을 분리하여 관리한다'는 원칙입니다.

# 1. 다국어 지원 시스템 구축 시
# 웹 애플리케이션에서 다국어 지원을 구현할 때, UI 텍스트를 하드코딩하는 대신
# 식별자(키)를 통해 텍스트를 로드하는 방식으로 적용할 수 있습니다.
# 예를 들어, Python Flask 애플리케이션에서 Babel을 사용하는 경우:

# messages.py (언어별 텍스트 리소스 파일)
# "greeting_message": {
#     "en": "Hello, welcome!",
#     "ko": "안녕하세요, 환영합니다!"
# }

# app.py (Flask 애플리케이션)
from flask import Flask, render_template, g
from flask_babel import Babel, gettext as _

app = Flask(__name__)
app.config['BABEL_DEFAULT_LOCALE'] = 'ko'
babel = Babel(app)

@babel.localeselector
def get_locale():
    # 사용자 요청 헤더나 세션에서 언어 설정 가져오기
    return 'ko' # 예시: 항상 한국어

@app.route('/')
def index():
    # _() 함수를 사용하여 식별자(키)로 텍스트 로드
    welcome_text = _("greeting_message")
    return render_template('index.html', welcome_text=welcome_text)

# index.html (템플릿)
# <h1>{{ welcome_text }}</h1>

# 이 방식은 'greeting_message'라는 식별자는 고정하고,
# 실제 표시되는 텍스트만 언어 설정에 따라 동적으로 변경합니다.
# 2. 마이크로서비스 아키텍처에서 API 엔드포인트 버전 관리
# RESTful API를 개발할 때, 내부적으로 사용하는 리소스 식별자(예: DB 테이블명)는 유지하되,
# 외부에 노출되는 API 엔드포인트 URL은 버전 관리를 통해 유연하게 변경할 수 있습니다.

# v1_api.py
from flask import Blueprint, jsonify

v1_bp = Blueprint('v1_api', __name__, url_prefix='/api/v1')

@v1_bp.route('/products')
def get_products_v1():
    # 내부적으로는 'items' 테이블을 참조하지만, 외부는 'products'로 노출
    products = fetch_data_from_db('items')
    return jsonify(products)

# v2_api.py
from flask import Blueprint, jsonify

v2_bp = Blueprint('v2_api', __name__, url_prefix='/api/v2')

@v2_bp.route('/catalog') # v2에서는 엔드포인트 이름을 'catalog'로 변경
def get_catalog_v2():
    # 여전히 내부적으로는 'items' 테이블을 참조
    catalog_items = fetch_data_from_db('items')
    return jsonify(catalog_items)

# app.py (메인 애플리케이션)
# app.register_blueprint(v1_bp)
# app.register_blueprint(v2_bp)

# 이처럼 내부 데이터 모델의 식별자는 유지하면서,
# 외부 API의 가시적 엔드포인트만 유연하게 변경하여 하위 호환성을 유지하거나
# 새로운 버전을 출시할 수 있습니다.
# 3. 컨테이너 환경에서 환경 변수를 이용한 설정 관리
# Docker나 Kubernetes와 같은 컨테이너 환경에서 애플리케이션을 배포할 때,
# 컨테이너 이미지 자체는 변경하지 않고, 환경 변수를 통해 애플리케이션의
# 가시적인 동작이나 브랜딩 요소를 변경할 수 있습니다.

# Dockerfile (애플리케이션 이미지 빌드)
# FROM python:3.9-slim
# WORKDIR /app
# COPY . /app
# CMD ["python", "app.py"]

# app.py (Python 애플리케이션)
import os
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    # 환경 변수 'APP_BRAND_NAME'이 설정되어 있으면 해당 값을 사용,
    # 없으면 기본값 'Default App' 사용
    brand_name = os.environ.get('APP_BRAND_NAME', 'Default App')
    return f"<h1>Welcome to {brand_name}!</h1>"

# 컨테이너 실행 시 브랜딩 변경 (가시적 요소)
# docker run -e APP_BRAND_NAME="My Custom Brand" my-app-image

# 이 방식은 애플리케이션의 핵심 로직(식별자)은 컨테이너 이미지 안에 고정하고,
# 외부에서 주입되는 환경 변수(가시적 요소)를 통해 브랜딩을 변경합니다.

경우의 수로 보는 이 버그

이 버그, 즉 '브랜딩 잔재' 문제는 다음과 같은 환경 조건들의 조합에서 발생할 확률이 높아집니다.

  • (A) 초기 테마/템플릿의 설계 품질: 식별자와 가시 텍스트가 명확히 분리되지 않고 혼용된 정도.
  • (B) 변경 이력의 복잡성: 테마가 여러 차례 수정되고 패치된 횟수와 방식. (예: 수동 편집, 부분 업데이트)
  • (C) 변경 작업자의 숙련도 및 도구: 변경 작업자가 시스템의 내부 구조를 얼마나 이해하고 있었는지, 그리고 어떤 도구(예: 단순 문자열 치환 도구 vs. 구조 인식 도구)를 사용했는지.
  • (D) 변경 범위의 광범위성: 변경해야 할 토큰의 종류와 위치가 얼마나 분산되어 있었는지.

이 문제의 재현 확률은 대략 A(높음) × B(높음) × C(낮음) × D(높음)의 조합에서 가장 높게 나타납니다. 즉, 초기 설계가 미흡하고, 여러 번의 패치가 누적되었으며, 변경 작업자가 시스템 구조에 대한 깊은 이해 없이 단순 치환 도구를 사용했고, 변경해야 할 요소들이 시스템 전반에 걸쳐 넓게 퍼져 있을 때, 이러한 브랜딩 잔재 문제가 발생할 가능성이 매우 커집니다. 반대로, 초기 설계부터 식별자와 가시 텍스트가 잘 분리되어 있고, 체계적인 버전 관리와 업데이트 프로세스를 따르며, 변경 작업자가 시스템의 의존성을 정확히 파악하고 적절한 도구를 사용한다면, 이 문제의 발생 확률은 현저히 낮아질 것입니다.

문제 상황

운영자가 Blogger admin Layout 화면을 캡처해 보내며 "TSP-v0.1로 만들고 싶다"고 지시했다. 화면 상단에는 여전히 SuperMag - v1.4.0 타이틀과 by Templateify.com 부제가 박혀 있었고, 21개 섹션 라벨(THEME OPTIONS, TOPBAR MENU, HEADER LOGO 등)이 모두 영문이었다. 본문 글 일부에도 옛 이전 영문 비교 문구 아이덴티티 문구가 잔존하고 있었다. 단순한 브랜딩 청소처럼 보였지만 테마 내부에는 widget 바인딩 식별자로 쓰이는 section id가 supermag-pro-* 형태로 박혀 있어 함부로 이름을 바꾸면 위젯이 고아가 될 위험이 있었다.

에러 증상

theme.xml 전수 스캔 결과 supermag 토큰 282개, SuperMag/supermag 리터럴 290개, Templateify 11개 — 합 583개가 잔존했다. 라이브 89편 LIVE 글 가운데 1편에 SuperMag 문자열, 1개 페이지(Antigravity Auth Success)에 supermag CSS 클래스 16개가 남아 있었다. webapp 파이썬 소스 7개 파일에도 docstring 안 SuperMag 참조가 1건씩 잔존. 정확한 원인은 추가 확인이 필요했지만, 운영 기록 기준 옛 Templateify 베이스 위에 sess114~sess145 누적 패치가 쌓이면서 일부 토큰만 정리되고 나머지는 cascade로 살아남은 형태로 보였다.

환경

Blogger 호스팅 + SuperMag Pro v1.4.0 커스텀 테마 + Chrome 148 + Playwright connect_over_cdp("http://localhost:9222") 자동 업로드. 인증은 OAuth refresh token 으로 자동 갱신, 대상 blog_id 는 config.json 의 blogger_blog_id 참조. theme.xml 크기 약 454KB.

시도했지만 실패한 방법

첫째, 모든 supermag-pro-* 토큰을 tsp-* 로 일괄 치환하려 했다. 그러나 section id 7개와 Blogger 백엔드 sectionId 조건문 42개가 같은 식별자를 참조하고 있어, 단순 일괄 치환은 widget 바인딩을 깨뜨릴 위험이 컸다. 이름을 바꾸면 위젯이 새 section을 인식하지 못해 admin 안에 저장된 콘텐츠가 사라질 수 있다.

둘째, EffectComposer + UnrealBloomPass 로 신경망 노드에 bloom 글로우를 추가하려 했다. 시각적으로는 훌륭했지만 composer가 알파 채널을 보존하지 않아 viewport 전체가 검정 배경으로 덮였다. ToolSignal Pro 의 흰 캔버스 정체성과 정반대 결과였다.

최종 해결

식별자와 가시 토큰을 분리해서 작업했다. section id 와 백엔드 sectionId 조건문은 그대로 보존(Blogger primary key 보호) 하고, 가시 영역만 골라 swap 했다. (1) skin 헤더 주석 4줄, (2) body#layout:before content 표시 1줄, (3) 21개 섹션 name 속성 영문→한국어, (4) Templateify 텍스트/URL 참조 11개. CDP html_edit 업로드 + 라이브 검증 6번 push 끝에 583 → 0 가시 토큰 달성. Bloom 시도는 폐기하고 AdditiveBlending PointsMaterial + 3-tier 노드 색 분화로 미적 elevation 을 대신 했다.

사용한 코드

식별자 보호 정책으로 안전한 가시 토큰만 골라 치환하는 패치 스크립트.

# theme.xml 흰색 라벨링 — 식별자는 keep, 가시 텍스트만 swap
SAFE_PATTERNS = [
 ('Name: SuperMag', 'Name: TSP-v0.1'),
 ('Version: 1.4.0 - Premium', 'Version: 0.1.0 - ToolSignal Pro'),
 ('Author: Templateify', 'Author: ToolSignal Pro Editorial'),
 ("content:'SuperMag - v1.4.0'", "content:'TSP-v0.1'"),
 ('https://www.templateify.com/', 'https://www.toolsignalpro.com/'),
 ('templateify.com', 'toolsignalpro.com'),
]
SECTION_KO = {
 'Theme Options': 'TSP 옵션',
 'Topbar Menu': '상단 메뉴',
 'Header Logo': '헤더 로고',
 # ... 21개 매핑
}
text = THEME.read_text(encoding='utf-8')
for old, new in SAFE_PATTERNS:
 text = text.replace(old, new)
for en, ko in SECTION_KO.items():
 text = re.sub(
 rf"(<b:section\b[^>]*\bname=)['\"]({re.escape(en)})['\"]",
 rf"\1'{ko}'", text,
 )
# Section id (supermag-pro-*) 와 sectionId 조건문은 절대 치환 X.

Three.js 신경망 커서는 importmap + ES module 로 도입했다. ImportMap 누락 시 EffectComposer 처럼 bare specifier from 'three' 가 404 로 떨어진다.

<script type="importmap">
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js" } }
</script>
<script type="module">
import * as THREE from 'three';
// 3-tier 노드: hub(violet) + peripheral(cyan) + accent(white)
var TIERS = [
 { count: 24, size: 0.32, color: 0x7c5bff, opacity: 0.385 },
 { count: 70, size: 0.18, color: 0x38bdf8, opacity: 0.28 },
 { count: 36, size: 0.10, color: 0xe8e8ff, opacity: 0.385 },
];
</script>

CDP 업로드는 안전한 html_edit 경로로 통일했다.

from webapp.seo.cdp_uploader import upload_theme_via_html_edit
result = upload_theme_via_html_edit(Path('theme/active/theme.xml'), blog_id)
# Verify silent reject — raw_xml_serve must be False
assert result['ok'] and not result['raw_xml_serve']

검증 결과

라이브 admin Layout 페이지를 CDP 캡처해 확인한 결과 타이틀 SuperMag → TSP-v0.1, 부제 by Templateify.com → by ToolSignal Pro.com, 섹션 라벨 영문 → 한국어로 모두 swap 됐다. 모든 LIVE 89편 + 8 페이지에서 SuperMag / Templateify 가시 리터럴 0건. AdSense ca-pub-4230158372696778 보존, h1=1, schema_v7 markers 89/89, sitemap 89 URL 200 OK 그대로. 신경망 커서는 모든 페이지에서 동작하며 prefers-reduced-motion 와 hover:none 미디어 쿼리로 모바일/접근성 자동 비활성. 운영 기록 기준 silent reject 0회.

현재 상태

fixed. section id 차원의 토큰 정리는 의도적으로 보류했고, 미래에 widget 마이그레이션을 정식 진행할 때 함께 다룰 예정이다. 신경망 커서는 v13 까지 13번 이터레이션을 돌고 직선 synapse + 3-tier 노드 + pulse trail + cursor wake + camera depth dolly 의 조합으로 안정화했다. 운영 기록 기준 추가 사고 0건.

같은 문제 겪는 분들에게

Blogger 커스텀 테마를 다른 브랜드로 white-label 할 때 가장 먼저 식별자와 가시 토큰을 분리하라. b:section id 와 backend sectionId == "..." 조건문은 위젯이 묶여 있는 primary key 다. 함부로 바꾸면 admin 에 저장된 콘텐츠가 새 section 으로 옮겨지지 않고 고아가 된다. 가시 토큰(skin 헤더 주석, body#layout:before 표시 이름, section name 속성, footer credit URL/텍스트)만 골라서 swap 하고, CSS 클래스 토큰 cascade rename은 별도 마이그레이션으로 나누는 편이 안전하다. Three.js 를 도입할 때는 ES module 환경에서 항상 importmap 으로 bare specifier 를 명시하라. EffectComposer 류 post-processing 은 알파 보존이 까다로워서, 흰색 캔버스 정체성을 유지해야 하는 사이트에서는 AdditiveBlending material 만으로도 충분한 글로우를 얻을 수 있다.

ToolSignal Pro Editorial

Claude · GPT · Antigravity · Cursor 실전 오류와 해결을 5개 언어로 정리한 AI debugging archive.

이전 글 다음 글