괄호로 조각을 저장하는 캡처 그룹을 익혔다면, 오늘은 그 저장한 조각을 패턴 안에서 다시 써먹는 기술을 배워요. 바로 역참조(앞에서 잡은 걸 되돌아 참조하기)예요. 여기에 더해 그룹에 이름을 붙여서 번호 대신 이름으로 다루는 방법도 같이 볼 거예요. 이 두 가지를 알면 "같은 게 두 번 나오는 경우"를 잡거나, 복잡한 정규식을 훨씬 읽기 좋게 만들 수 있어요. 처음 보면 기호가 좀 낯설 수 있는데, 예제를 하나씩 손으로 따라 치다 보면 금방 감이 올 거예요.

14.1 역참조가 뭐예요?

역참조는 앞에서 캡처한 그룹의 내용과 똑같은 문자열을 패턴 안에서 다시 요구하는 거예요. 표기는 \1처럼 역슬래시 뒤에 그룹 번호를 붙여요. \1은 "1번 그룹이 실제로 잡은 그 글자와 똑같은 것이 여기 또 나와야 해"라는 뜻이에요.


여기서 헷갈리기 쉬운 걸 짚고 갈게요. \1은 "1번 그룹의 패턴을 반복하라"가 아니라 "1번 그룹이 이번에 실제로 잡은 값을 반복하라"예요. 예를 들어 1번 그룹이 the를 잡았다면 \1은 딱 the만 요구해요. cat이 오면 안 되는 거죠. 이 "같은 값 반복"이라는 성질 덕분에 재밌는 걸 할 수 있어요.


비유하자면 이래요. 캡처 그룹이 방금 본 것을 메모지에 적어두는 거라면, 역참조는 그 메모지를 다시 펼쳐서 "아까 적은 거랑 똑같은 게 여기 또 나와?"라고 대조하는 거예요. 그래서 역참조는 "같은 게 반복되는 규칙"을 표현할 때 빛을 발해요. 중복된 단어, 열고 닫는 짝, 앞뒤가 대칭인 형태 같은 걸 잡을 수 있죠. 보통의 정규식은 "이런 모양"을 찾지만, 역참조가 들어가면 "앞에서 본 것과 같은 것"이라는, 한 단계 똑똑한 조건을 걸 수 있는 거예요.

14.2 실수로 두 번 쓴 단어를 어떻게 찾나요?

글을 쓰다 보면 the the처럼 같은 단어를 실수로 두 번 친 적 있으시죠? 이런 중복 단어를 역참조로 잡아낼 수 있어요. 상황은 문장에서 바로 붙어 두 번 반복된 단어를 찾는 거예요. 패턴은 이래요.


\b(\w+) \1\b


하나씩 뜯어볼게요. \b는 단어 경계(단어가 시작하거나 끝나는 자리)예요. (\w+)는 단어 하나를 잡아 1번 그룹에 저장하고요. 그다음 공백 한 칸이 있고, \1이 나와요. 이 \1이 바로 "방금 잡은 그 단어와 똑같은 게 또 와야 해"라는 조건이에요. 대상은 the the cat cat runs예요. 실행해볼게요.


import re


re.findall(r'\b(\w+) \1\b', 'the the cat cat runs')


결과는 이래요.


['the', 'cat']


반복된 thecat이 잡혔어요. runs는 뒤에 자기랑 똑같은 단어가 없으니 안 잡혔고요. 결과가 the the 통짜가 아니라 the만 나온 건, 캡처 그룹이 하나라서 findall이 그룹 값만 돌려주기 때문이에요. 이 함정은 앞의 캡처 그룹 이야기에서 다뤘던 그 규칙이에요. 아무튼 이 \b(\w+) \1\b 패턴 하나는 글 교정할 때 정말 유용하니 통째로 외워둘 만해요.


\b를 앞뒤에 감쌌는지도 잠깐 짚을게요. \b가 없으면 단어의 일부만 걸려서 엉뚱한 걸 잡을 수 있어요. 단어 경계로 양옆을 막아주면 "온전한 단어 하나"가 두 번 반복될 때만 깔끔하게 걸려요. 역참조와 단어 경계를 짝지어 쓰는 건 아주 흔한 조합이니 눈에 익혀두면 좋아요. 저는 원고를 넘기기 전에 이 패턴으로 한 번씩 훑어서 그 그, 있는 있는 같은 중복 오타를 잡곤 해요.

14.3 그룹에 이름은 어떻게 붙여요?

그룹이 서너 개만 넘어가도 "3번이 뭐였더라?" 하고 번호를 세는 게 귀찮아져요. 그래서 그룹에 이름을 붙일 수 있어요. 파이썬에서는 (?P<이름>...) 형태로 써요. 여는 괄호 뒤에 ?P와 꺾쇠로 감싼 이름을 넣는 거예요. 상황을 볼게요. 연도와 월을 번호가 아니라 이름으로 다루고 싶어요.


(?P<year>\d{4})-(?P<month>\d{2})


첫 그룹엔 year, 둘째 그룹엔 month라는 이름을 붙였어요. 대상은 2024-01이고요. 이름 그룹은 사전(키와 값이 짝지어진 자료)으로 꺼내기 좋아요.


m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})', '2024-01')


m.groupdict()


결과는 이래요.


{'year': '2024', 'month': '01'}


이름과 값이 짝지어져 나왔죠? 이제 m.group('year')라고 하면 2024가 나와요. 번호 대신 year라는 이름으로 부르니 코드만 봐도 뭘 꺼내는지 바로 읽혀요. 게다가 나중에 패턴 앞쪽에 그룹을 하나 더 끼워 넣어서 번호가 밀려도, 이름은 그대로라 코드를 안 고쳐도 돼요. 유지보수할 때 이 점이 정말 고마워요. 참고로 자바스크립트에서는 ?P 없이 (?<이름>...)로 써요. 표기만 살짝 다르고 개념은 같아요.

14.4 이름 그룹도 역참조가 되나요?

돼요. 그리고 이게 아주 실용적이에요. 이름 그룹을 역참조할 때는 (?P=이름) 형태로 써요. 상황을 하나 볼게요. <b>hi</b>처럼 여는 태그와 닫는 태그의 이름이 같은 짝만 잡고 싶어요. 패턴은 이래요.


<(?P<tag>\w+)>.*?</(?P=tag)>


앞의 (?P<tag>\w+)가 여는 태그 이름을 tag라는 이름으로 잡아요. 가운데 .*?는 사이 내용을 최소한으로 잡는 부분이고요. 마지막 (?P=tag)가 핵심이에요. "닫는 태그 이름이 아까 잡은 tag똑같아야 해"라는 조건이거든요. 대상은 <b>hi</b><i>yo</i>예요. 실행해볼게요.


re.findall(r'<(?P<tag>\w+)>.*?</(?P=tag)>', '<b>hi</b><i>yo</i>')


결과는 이래요.


['b', 'i']


b 짝과 i 짝이 각각 이름이 맞아떨어져서 잡혔어요. 만약 여는 태그와 닫는 태그 이름이 서로 달랐다면 (?P=tag) 조건에 걸려 매치가 안 됐을 거예요. 이렇게 열고 닫는 짝을 강제할 수 있다는 게 이름 그룹 역참조의 힘이에요.


다만 여기서 꼭 짚고 갈 게 있어요. 이건 교육용 예제일 뿐, 진짜 HTML을 다룰 땐 정규식을 쓰면 안 돼요. HTML은 태그 안에 태그가 겹겹이 들어가고 예외가 너무 많아서, 정규식으로 완벽히 처리하는 건 사실상 불가능하거든요. 실무에서는 HTML 파서(HTML 구조를 제대로 해석해주는 전용 도구)를 써야 해요. 정규식은 "이런 짝 맞추기도 되는구나" 정도로만 즐겨주세요. 실제로 태그가 서로 겹쳐 들어간 문서나, 같은 태그가 여러 겹 중첩된 경우엔 이 패턴이 금방 어긋나거든요. 정규식이 잘 어울리는 일과 안 어울리는 일을 구분할 줄 아는 것도 실력이에요.

14.5 오늘 배운 것 정리

역참조 \1은 앞에서 잡은 그룹의 실제 값과 똑같은 것을 다시 요구해요. 그래서 \b(\w+) \1\b로 실수로 두 번 쓴 단어를 잡을 수 있었죠. 그룹에 이름을 붙일 때는 (?P<이름>...)를 쓰고, 이름으로 꺼낼 땐 group('이름'), 이름으로 역참조할 땐 (?P=이름)을 써요. 이름을 쓰면 번호가 밀려도 안 흔들려서 유지보수가 편해요. 마지막으로 이름 역참조로 태그 짝을 맞추는 건 개념 학습용일 뿐, 진짜 HTML은 파서를 쓴다는 걸 꼭 기억해두세요. 기호가 많아 처음엔 복잡해 보여도, \1은 "같은 값 또", (?P<이름>...)는 "이 그룹에 이름표"라고만 잡아두면 어렵지 않아요.