반복 코드 131줄을 정규식으로 한 번에 바꿔도 괜찮았던 이유
혼자 운영하는 서비스는 점검을 자꾸 미루게 됩니다. 기능이 돌아가고 있으면 코드를 다시 들여다볼 이유가 생기지 않고, 그 사이에 같은 패턴이 계속 복사됩니다. 저도 얼마 전 하루를 잡고 운영 중인 서비스 코드를 전체 점검했는데, 관리자 API 라우트 파일 70개에 완전히 똑같은 인증 가드 한 줄이 131번 반복되고 있었습니다.
if (!(await requireAdmin())) return NextResponse.json({ error: "unauthorized" }, { status: 401 });처음에는 그대로 둘까 했습니다. 동작에는 아무 문제가 없었고, 한 줄짜리 반복은 리팩토링 대상 중에서도 가성비가 낮은 편이라고 생각했습니다. 그런데 세어 보니 이미 균열이 있었습니다. 두 파일은 공용 헬퍼를 import하는 대신 똑같은 함수를 파일 안에 다시 정의해 두었고, 한 파일은 NextResponse.json이 아니라 Response.json으로 응답을 만들고 있었습니다. 로직이 틀린 건 아니지만, 401 응답의 모양을 바꿔야 하는 날이 오면 131곳을 찾아다녀야 한다는 뜻입니다. 반복은 시간이 지나면 이렇게 조금씩 부식됩니다.
그래서 응답까지 만들어 주는 헬퍼를 하나 추가했습니다.
export async function adminGuard(): Promise<NextResponse | null> {
return (await requireAdmin())
? null
: NextResponse.json({ error: "unauthorized" }, { status: 401 });
}호출부는 두 줄이 됩니다.
const deny = await adminGuard();
if (deny) return deny;문제는 70개 파일을 어떻게 고치느냐였습니다. 손으로 열어서 고칠 수는 없으니 perl 정규식 일괄 치환을 택했는데, 돌리기 전에 한 가지를 먼저 확인했습니다. 치환 대상이 정말 전부 같은 문자열인가. grep으로 세어 보니 한 줄 형태가 40곳, 나머지는 if 블록으로 감싼 세 줄 형태였고, 블록 안의 응답 문장은 전부 동일했습니다. 여기서 다르게 생긴 변형이 하나라도 나왔다면 일괄 치환은 접었을 겁니다.
치환 자체는 한 번에 됐는데, import 정리에서 헛짚었습니다. "파일에 requireAdmin이라는 단어가 더 이상 없으면 import 문을 adminGuard로 바꾼다"라는 조건을 걸었는데, import 문 자체에 requireAdmin이 들어 있으니 이 조건은 영원히 거짓입니다. 치환을 돌리고 세어 보니 본문은 다 바뀌었는데 import는 하나도 안 바뀌어 있었습니다. 조건을 단어 존재가 아니라 호출 형태인 requireAdmin()의 존재 여부로 바꾸고 나서야 제대로 돌았습니다. 정규식으로 코드 고칠 때 조건문이 자기 자신을 참조하고 있지 않은지, 저는 이번에 한 번 데이고 배웠습니다.
일부러 안 바꾼 곳도 있습니다. 세 개 라우트는 가드 다음 줄부터 session.user.id 같은 세션 정보를 써야 해서, true/false만 돌려주는 가드로는 대체할 수 없었습니다. 이런 파일까지 기계적으로 밀었다면 컴파일은 되는데 동작이 바뀌는 사고가 났을 겁니다. 일괄 치환은 예외를 미리 분류해 둔 만큼만 안전합니다.
마지막은 검증입니다. 타입체크는 치환 전에도 한 번 돌려서 기준선이 0 오류인 걸 확인해 뒀고, 치환 후에 다시 돌려 0 오류, 프로덕션 빌드까지 통과를 확인했습니다. 최종 diff는 73개 파일에 +382/-461이었습니다. 지운 줄이 더 많은 리팩토링은 오랜만이라 기분이 좋았습니다.
돌아보면 이번 작업이 무모하지 않았던 건 정규식이 정교해서가 아닙니다. 치환 대상이 전부 동일한 리터럴임을 먼저 세어서 확인했고, 예외가 되는 파일을 미리 골라냈고, 타입체크와 빌드라는 그물이 아래에 깔려 있었기 때문입니다. 셋 중 하나라도 없었다면 저는 70개 파일을 손으로 고치는 쪽을 택했을 겁니다.
같은 점검에서 겪은 다른 이야기:
함께 읽기
- 중복처럼 보이는 코드를 함부로 합치면 안 되는 이유오래 운영한 코드베이스를 점검하다 보면 비슷하게 생긴 코드 뭉치를 발견하고 "이거 중복이네, 합치자"라는 충동이 듭니다. 중복 제거는 리팩토링의 기본이라고 배우기도 했고, 지우는 작업은 손도 빠릅니다. 저는 얼마 전 그 충동대로 갔다가 큰일 날 뻔했습니다.
- git push 후 자동 검사가 시작되는 이유: Continuous Integration(CI) 입문코드를 수정하고 로컬에서 잘 동작하는 것까지 확인했습니다. 이제 아래처럼 GitHub에 올립니다.
- Next.js 앱에서 클릭이 한 박자 느리게 느껴질 때 살펴본 것들서버가 멀면 응답이 느린 건 어쩔 수 없습니다. 제 경우 오리진 응답 지연에 CDN 우회 경로까지 겹쳐 페이지 응답에 1초 가까이 걸리는 상황이었고, 네트워크 쪽은 당장 손댈 수 없었습니다. 그래서 응답 속도 대신 "눌렀을 때 반응하는 속도"를 올리는 쪽으로 방향을 잡았습니다. 느린 것과 느리게 느껴지는 것은 생각보다 …
- Cloudflare를 붙였는데 한국에서 오히려 느려진 이유한국 사용자를 대상으로 운영 중인 데모 사이트에서 응답 지연을 점검했습니다. 도메인은 Cloudflare 프록시를 거치게 해뒀고, 원본 IP와 오리진을 보호하기 위해 켜둔 상태였습니다. 그러다 사이트를 손보는 중에 "버튼이 한 박자 늦게 눌리는 것 같다"는 얘기가 나왔고, 확인해 보니 앱 코드가 아니라 이 프록시 경로가…
- RAG 챗봇이 "그건 언제 바뀐 거야?"에 답하게 만들기사내 문서를 근거로 답하는 RAG 챗봇을 도입하면 대부분 비슷한 지점에서 막힙니다. 첫 질문에는 그럴듯하게 답하는데, 이어서 "그럼 그건 언제 바뀐 거야?" 같은 질문을 던지면 갑자기 엉뚱한 문서를 근거로 들고 옵니다. 사용자는 "AI가 헛소리를 한다"고 느끼고, 도입은 거기서 멈춥니다.