몇 달 동안 변환기에는 제 눈에 보이지 않는 버그가 있었습니다. 변환기가 만든 박스 섀도가 전부 버려지고 있었던 것입니다. 잘못 렌더링된 것이 아니라 폐기되었습니다. 변환도 성공했고 가져오기도 성공했고 요소도 모두 들어왔는데, 섀도만 그냥 없었습니다.
예외도 없었습니다. 콘솔 경고도 없었습니다. 실패한 요청도 없었습니다. 로그에도 아무것도 남지 않았습니다. 페이지를 직접 만든 사람이 아니라면 무언가 빠졌다는 사실을 알 수 없었을 것이고, 변환 품질이 그저 그렇다고 생각했을 것입니다.
이 일을 기록해 두는 이유는 문제의 구조가 보편적이기 때문입니다. Elementor는 하나의 사례일 뿐입니다. 일반화하면 이렇습니다. 인식하지 못하는 키를 무시하는 시스템에 데이터를 쓰면, 오타 하나나 낡은 가정 하나가 영구적이고 조용한 데이터 손실로 이어집니다.
왜 보이지 않는가
Elementor는 각 요소를 JSON으로 저장합니다. 요소 유형과 settings 객체로 이루어져 있습니다. 렌더링할 때는 등록된 컨트롤을 차례로 돌면서 settings 객체에서 각 컨트롤의 값을 가져옵니다. 등록된 컨트롤에 해당하지 않는 키는 한 번도 조회되지 않습니다. 거부되는 것도 아닙니다. 그냥 읽히지 않을 뿐입니다.
이것은 합리적인 설계입니다. 플러그인을 업데이트해도 기존 데이터가 살아남고, 새 버전에서 생긴 알 수 없는 키가 이전 버전을 망가뜨리지도 않습니다. 그리고 바로 그 때문에 잘못된 키 이름에는 아무런 피드백도 돌아오지 않습니다. 값을 검증하는 API라면 잘못된 키는 쓰는 시점에 오류가 되고, 다음 테스트 실행에서 잡힙니다. 무시하는 API에서는 아무 반응이 없고, 몇 달 뒤 사용자가 발견하거나 끝내 아무도 발견하지 못합니다.
이름을 그냥 찾아보면 안 되는 이유
뻔한 해법은 컨트롤 이름을 문서와 대조해 보는 것입니다. 문제는 그중 상당수가 어디에도 적혀 있지 않다는 점입니다. 이름이 생성되기 때문입니다.
Elementor에는 그룹 컨트롤이 있습니다. 타이포그래피, 테두리, 배경, 박스 섀도처럼 재사용할 수 있는 컨트롤 묶음입니다. 그룹을 이름과 함께 위젯에 붙이면 개별 컨트롤 ID는 런타임에 조합됩니다. 그룹 이름, 밑줄, 그리고 각 필드 이름 순서입니다. 따라서 title로 등록한 타이포그래피 그룹은 title_font_size라는 컨트롤을 만듭니다. 이 문자열이 등장하는 파일은 어디에도 없습니다. grep으로 찾을 수도 없습니다. 규칙을 알고 직접 적용해야 합니다.
바로 이런 상황에서 기억, 포럼 답변, 오래된 블로그 글은 쓸모없는 정도를 넘어 해롭습니다. 자신 있게 구체적이지만 자주 틀리고, API는 틀렸다고 알려 주지 않습니다.
버그: 섀도는 컨트롤 다섯 개가 아니라 하나입니다
변환기는 섀도 하나에 키 일곱 개를 쓰고 있었습니다. 섀도를 켜는 키 하나, 그리고 색상, 가로, 세로, 흐림, 퍼짐, 위치를 각각 별도의 값으로 썼습니다. 꼼꼼해 보입니다. 하지만 거의 전부 틀렸습니다.
박스 섀도 그룹이 등록하는 필드는 정확히 두 개입니다. 섀도 자체는 하나의 복합 컨트롤이고, 그 값은 가로, 세로, 흐림, 퍼짐, 색상을 함께 담은 객체 하나입니다. 두 번째 필드는 외부/내부(inset) 위치입니다. 팝오버 토글까지 합치면 실제 키는 세 개이고, 그룹 자체의 이름이 box_shadow이기 때문에 셋 모두 접두사가 두 번 붙습니다.
쓰고 있던 키 일곱 개 중 다섯 개는 존재하지 않는 키였습니다. 여섯 번째는 이름은 맞았지만, 기대하는 객체 대신 "yes"라는 문자열을 받고 있었습니다. 일곱 번째는 거의 맞았지만 접두사가 빠져 있었습니다.
아무리 추측해도 알아낼 수 없었을 세부 사항이 하나 있습니다. “Outline”의 값은 빈 문자열이 아니라 공백 한 칸입니다. 이 값은 옵션 배열에만 있고 다른 어디에도 없습니다.
두 번째 버그: 이름은 맞는데도 아무 효과가 없었습니다
같은 검토에서 flex_grow와 flex_shrink도 나왔습니다. 플렉스 부모가 요소를 늘리거나 줄이지 못하게 하려고, 너비를 명시한 요소에 쓰던 키입니다. 이것도 틀렸습니다. 이 컨트롤들은 다른 이름으로 등록된 그룹에 속해 있어서 접두사가 필요했습니다.
그런데 접두사를 고쳤더라도 여전히 동작하지 않았을 것이고, 이 부분이 기억해 둘 만한 점입니다. 두 필드 모두 조건이 걸려 있어서, 그룹의 크기 컨트롤이 “custom”으로 설정되어 있을 때만 적용됩니다. 조건은 패널에 보이느냐만이 아니라 CSS 생성 여부까지 좌우합니다. 키 이름이 맞는 것은 필요조건일 뿐 충분조건이 아닙니다.
실제 해결책은 두 시도보다 더 간단했고, 처음부터 소스 안에 있었습니다. 크기 컨트롤에는 정확히 “늘어나지도 줄어들지도 않음”을 뜻하는 “none” 옵션이 있습니다. 조건부 컨트롤 세 개 대신 조건 없는 컨트롤 하나면 됩니다. 소스를 읽은 덕분에 버그를 고쳤을 뿐 아니라, 원래 만들려던 것보다 더 나은 구현을 얻었습니다.
나머지 키 전부 점검하기
읽어서 찾아낸 버그가 두 개였습니다. 아무도 읽어 보지 않은 키가 아직 90여 개 남아 있었습니다. 그래서 다음 단계는 기계적인 작업이었습니다. 공식 플러그인 소스를 파싱해 위젯별로 확실한 컨트롤 이름 집합을 만들고, 변환기가 출력할 수 있는 모든 키와 diff로 비교했습니다.
점검 스크립트는 맞는 답을 내기 전에 틀린 답을 네 번 냈습니다. 실패 하나하나가 일반적인 교훈이 됩니다.
- 이름은 다음 줄에 있는 경우가 많습니다. 컨트롤을 등록하는 코드는 이름을 호출 다음 줄에 두는 경우가 많습니다. 한 줄짜리 패턴은 이를 놓쳤고, 모든 위젯이 잘못된 키 다섯 개(고급 탭 전체)를 출력하는 것처럼 보였습니다. 표준 컨트롤을 알 수 없는 키로 표시하는 검사기는 버그를 보고하는 것이 아니라 자신의 맹점을 보고하는 것입니다.
- 같은 일을 하는 두 가지 관용 표현. 어떤 그룹은 한 변수 이름으로 필드를 만들고, 다른 그룹은 다른 변수 이름을 씁니다. 첫 번째 방식만 매칭하자 그룹 전체가 조용히 빠졌습니다. 공교롭게도 바로 조사 중이던 그 그룹까지 포함해서였습니다.
- 인자 배열은 중첩됩니다. 첫 번째 닫는 괄호에서 매칭을 멈추자 처음 나오는 중첩 배열에서 잘려 버렸고, 그 등록은 통째로 건너뛰었습니다. 완전히 유효한 레이아웃 키 여러 개가 처음에 “invalid”로 나온 것은 이 때문이었습니다.
- 캡처 범위를 넓히면 다른 것까지 삼킵니다. 앞의 문제를 넓은 범위를 잡는 방식으로 고치자, 매칭 하나가 그 안에 있는 다른 등록까지 삼켜 버렸습니다. 한 위젯의 확인된 컨트롤 수가 281개에서 60개로 줄었는데도 diff는 여전히 성공이라고 보고했습니다.
이 코드베이스에만 해당하는 것도 하나 더 있습니다. Button 위젯 파일에는 컨트롤 등록이 하나도 없습니다. 모두 트레이트(trait) 안에 있습니다. 첫 실행에서는 Button에 컨트롤이 전혀 없다고 보고했습니다.
이 오류들이 어느 방향으로 틀렸는지 보세요. 세 개는 잘못된 경보를 냈습니다. 성가시긴 해도 저절로 바로잡힙니다. 조사해 보면 도구가 틀렸다는 것을 알게 되기 때문입니다. 나머지 하나는 잘못된 통과를 냈고, 이쪽이 위험합니다. 확인된 정상 이름이 적을수록 보고되는 문제도 적어집니다. 이런 실패는 조용하고, 오히려 안심하게 만듭니다.
다른 검사를 믿을 수 있게 해 주는 검사
그래서 이번 작업 전체에서 남길 만한 생각은 하나로 모입니다.
“모두 정상”이라고 보고하는 검사기는, “정상이 아님”을 보고할 수 있다는 것을 증명하기 전까지는 아무 가치가 없습니다.
그래서 점검은 음성 대조로 끝납니다. 이미 알려진 잘못된 키 이름 다섯 개를 다시 넣어 보고, 그중 하나라도 통과하면 실행 전체를 실패로 처리합니다. 나중에 추출기가 고장 나더라도(패턴이 넓어지거나, 파일을 건너뛰거나, 캐시가 꼬이거나) 이 단언이 먼저 빨간불을 켜므로 “모두 통과” 줄이 거짓말을 할 틈이 없습니다.
여기에 기준 두 가지를 더 둡니다. 반드시 존재하는 컨트롤 이름 목록은 모두 찾아내야 하고, 전체 개수는 절대 조용히 줄어들면 안 됩니다. 두 번째 기준이 있었다면 281개가 60개로 무너진 일을 잡아냈을 것입니다.
고친 상태를 유지하기
diff 결과가 깨끗해졌을 때의 최종 상태는 이렇습니다. 위젯 유형 9개에 걸쳐 키 205개를 검사했고, 알 수 없는 키는 하나도 없었습니다. 그다음 이 상태가 유지되도록 지속적 통합(CI) 작업을 추가했습니다. 섀도가 들어간 페이지를 변환한 뒤, 출력에 복합 키가 있는지, 그리고 예전의 개별 이름이 하나도 되살아나지 않았는지를 단언합니다.
순수 함수의 단위 테스트가 아닙니다. 실제로 배포되는 산출물을 대상으로, 이미 한 번 일어났던 바로 그 실수를 확인하는 단언입니다.
일반화하면
알 수 없는 키를 무시하는 곳에 데이터를 쓴다면(페이지 빌더, CMS 메타 테이블, 분석 페이로드, 기능 플래그 데이터, 세상에 있는 설정 형식의 절반이 그렇습니다), 다음을 지키세요.
- 유효한 키 집합은 기준이 되는 원본에서 직접 뽑아내세요. 기억이나 문서에 의존하지 마세요. 문서는 의도를 설명할 뿐이고, 생성되는 이름은 런타임에만 존재합니다.
- 출력하는 키를 그 집합과 기계적으로 비교하세요. 키 90개는 눈으로 검토할 수 있는 양이 아니고, 틀린 키는 맞는 키와 똑같아 보입니다.
- 먼저 검사기를 의심하세요. 확신하던 부분을 실행 결과가 문제로 지적한다면, 코드보다 도구가 틀렸을 가능성이 훨씬 높습니다.
- 음성 대조를 추가하세요. 검사기가 실패할 수 있다는 것을 증명하세요. 그렇지 않으면 통과는 아무 의미가 없습니다.
- CI에서 배포 산출물을 대상으로 단언하세요. 이런 종류의 버그는 누군가 출력 코드를 리팩터링하는 순간 다시 돌아오기 때문입니다.
이제 섀도는 제대로 동작합니다. 더 값진 수확은, 구조적으로 오류를 낼 수 없는 API를 상대로 몇 달 동안 “오류 없음”을 정상이라는 증거로 여겨 왔다는 사실을 깨달은 것입니다.