반복 코드 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개 파일을 손으로 고치는 쪽을 택했을 겁니다.
같은 점검에서 겪은 다른 이야기:
우리 서비스에 적용할 계획이 있으신가요?
기술 자문·개발 범위 상담 살펴보기 →함께 읽기
- 중복처럼 보이는 코드를 함부로 합치면 안 되는 이유오래 운영한 코드베이스를 점검하다 보면 비슷하게 생긴 코드 뭉치를 발견하고 "이거 중복이네, 합치자"라는 충동이 듭니다. 중복 제거는 리팩토링의 기본이라고 배우기도 했고, 지우는 작업은 손도 빠릅니다. 저는 얼마 전 그 충동대로 갔다가 큰일 날 뻔했습니다.
- 디자인 시스템을 씌우기 전에 레이아웃 틀부터 맞춰야 하는 이유같은 디자인 시스템을 사용하는데도 어떤 화면은 정돈되어 보이고, 어떤 화면은 미묘하게 어긋나 보일 때가 있습니다. 색상과 버튼 모양이 같아도 페이지 폭, 여백, 제목 위치, 입력 필드의 구성이 제각각이면 전체 인상은 쉽게 흐트러집니다.
- iOS 사파리에서 고정 헤더가 사라지거나 잘리는 이유상단 고정 헤더는 거의 모든 사이트가 쓰는 패턴입니다. position: fixed 한 줄이면 끝나는 것처럼 보이고, 데스크톱에서는 실제로 그렇습니다. 그런데 iOS 사파리에서는 이 헤더가 스크롤 도중 통째로 사라지거나, 위쪽이 몇 픽셀 잘려 보이는 일이 생깁니다.
- 크롬에서는 멀쩡한데 아이폰에서만 흰 화면이 뜨는 이유개발하면서 가장 당황스러운 상황 중 하나가 내 브라우저에서는 되는데 다른 기기에서만 안 되는 경우입니다. 특히 화면이 아예 하얗게 나오면 어디서부터 봐야 할지 막막합니다. 콘솔도 못 보고 에러 메시지도 없습니다.
- 모바일에서만 가로 스크롤이 생기는 이유모바일에서 화면을 옆으로 밀었더니 페이지가 밀리고 오른쪽에 빈 여백이 보인다면, 어떤 요소가 뷰포트보다 오른쪽으로 삐져나가 있다는 뜻입니다. 흔한 문제인데 범인을 찾기가 은근히 어렵습니다. 눈에 보이는 요소가 아닌 경우가 많기 때문입니다.