GraphQL 포맷터가 문자열 속 #을 주석으로 착각하지 않는 법
GraphQL 문법에서 #은 그 줄 끝까지를 주석으로 취급합니다. 문제는 이 규칙이 "코드 영역"에만 적용되어야 하는데, 포맷터·최소화 도구를 정규식 하나로 대충 짜면 문자열 리터럴 안의 #까지 똑같이 주석 시작으로 오인해버린다는 점입니다. 이 가이드는 왜 이런 사고가 발생하는지, 그리고 GraphQL 포맷터가 실제로 이 문제를 어떻게 피하고 있는지 코드 레벨에서 확인합니다.
1. 문제의 시작: 정규식은 문맥을 모른다
가장 단순한 GraphQL 주석 제거 코드는 src.replace(/#[^\n]*/g, '') 한 줄로 짤 수 있습니다. 이 정규식은 줄에서 # 이후 전부를 지웁니다. 문제는 GraphQL 쿼리 안에 filter: "가격 #1위"처럼 사용자가 넣은 문자열 값에 #이 우연히 포함될 때 벌어집니다. 정규식은 그 #이 문자열 안에 있는지, 코드 영역에 있는지 구분할 문맥 정보가 없으므로 #1위" 뒤에 이어지는 나머지 쿼리 구문까지 통째로 주석으로 지워버립니다. 이는 SQL 압축기가 --를 문맥 없이 지우다가 문자열 안의 --에 걸려 뒤 구문을 통째로 날려먹는 것과 정확히 같은 부류의 버그입니다.
2. 실제로 어떤 입력에서 터지는가
예를 들어 다음과 같은 쿼리를 생각해 보겠습니다.
{ product(bio: "Loves #hashtags, {coffee}") { name price } }문맥을 고려하지 않는 포맷터라면
"Loves 까지만 남기고 #hashtags, {coffee}") { name price } } 전체를 주석으로 인식해 삭제합니다. 결과물은 괄호가 안 닫힌 채 잘려나간 깨진 쿼리가 되고, 이 상태로 서버에 전송하면 GraphQL 파서가 Syntax Error를 던집니다.
실무에서는 상품명에 해시태그가 들어가거나, 검색 필터 값에 순위·번호를 #1 형태로 표기하는 경우가 흔하기 때문에 이 버그는 우연이 아니라 반드시 마주치는 케이스입니다.
3. 안전한 처리 순서: 문자열을 먼저 격리한다
해법은 순서를 바꾸는 것입니다. 주석을 지우기 전에 먼저 문자열 리터럴 전체를 찾아 임시 자리표시자(placeholder)로 바꿔치기해 "보호"해두고, 그 상태에서 주석 제거·공백 정리·구두점 정리를 수행한 뒤, 마지막에 자리표시자를 원래 문자열로 되돌립니다. 이렇게 하면 #이 문자열 안에 있었는지 코드 영역에 있었는지 애매할 일이 없습니다 — 문자열은 애초에 처리 대상에서 빠져 있기 때문입니다.
4. 실제 도구 코드 확인
GraphQL 포맷터의 graphql-formatter.html를 열어 보면 이 순서가 그대로 구현돼 있습니다. extractGqlStrings() 함수가 """..."""(블록 문자열)와 "..."(일반 문자열, 이스케이프 포함)를 정규식 /"""[\s\S]*?"""|"(?:\\.|[^"\\])*"/g로 먼저 찾아 배열에 저장하고, 그 자리에 널 문자로 감싼 인덱스(\u00000\u0000 형태)를 심습니다. 이후 gqlFormat()·gqlMinify()는 이 마스킹된 문자열에 대해서만 #[^\n]* 주석 제거와 공백 정리를 수행하고, 처리가 끝난 뒤 restoreGqlStrings()가 자리표시자를 실제 문자열로 되돌립니다. 즉 주석 제거 로직이 문자열 내용을 아예 볼 수 없는 구조라, #이 문자열 안에 있었든 코드 영역에 있었든 오작동할 여지가 없습니다.
| 단계 | 동작 |
|---|---|
| 1. 문자열 추출 | 모든 "..."/"""..."""를 찾아 배열에 저장하고 자리표시자로 치환 |
| 2. 주석 제거 | 마스킹된 텍스트에서 # 이후 줄 끝까지 삭제 (문자열은 이미 빠져 있음) |
| 3. 공백·구두점 정리 | 들여쓰기 재구성 또는 최소화 수행 |
| 4. 문자열 복원 | 자리표시자를 원래 문자열 값으로 되돌림 |
5. 직접 검증해보기
위 위험 예시(bio: "Loves #hashtags, {coffee}")를 GraphQL 포맷터에 그대로 붙여넣고 포맷 또는 최소화를 실행하면, 문자열 값이 손상 없이 그대로 유지되고 뒤따르는 { name price } 구문도 정상적으로 출력되는 것을 확인할 수 있습니다. 반대로 도구 코드에서 extractGqlStrings 호출을 생략하고 곧바로 replace(/#[^\n]*/g,'')만 적용하도록 바꿔보면(테스트 목적으로만) 같은 입력에서 즉시 구문이 잘려나가는 것을 재현할 수 있습니다 — 이는 문맥 보호 로직이 실제로 결과를 좌우한다는 뜻입니다.
자주 묻는 질문
Q. 문자열 안에 콤마나 중괄호가 있어도 문제 없나요?
네. 문자열 리터럴 전체가 자리표시자로 치환된 상태에서 구조 파싱이 이뤄지므로, 문자열 내부의 콤마·중괄호·콜론이 GraphQL 구문 구조로 오인될 일이 없습니다.
Q. 블록 문자열(""")도 같은 방식으로 보호되나요?
네. 정규식이 """...""" 패턴을 "..."보다 먼저 우선 매칭하도록 작성돼 있어 여러 줄 설명 문자열도 통째로 보호됩니다.
Q. 이스케이프된 따옴표(\")가 있는 문자열도 정확히 잡히나요?
네. 정규식의 (?:\\.|[^"\\])* 부분이 백슬래시로 이스케이프된 문자를 문자열의 일부로 소비하도록 처리하므로, 문자열 중간의 \" 때문에 문자열이 조기 종료되지 않습니다.
Q. 이 문제는 GraphQL에만 있는 건가요?
아니요. 주석 기호가 문자열 안에 우연히 등장할 수 있는 언어라면 동일한 위험이 있습니다. SQL의 --, 셸 스크립트의 # 등도 문맥 없이 정규식만으로 주석을 제거하면 같은 방식으로 깨질 수 있습니다.