단어 하나만 바꾸려다 사고 친 적 있으세요? 저는 코드에서 변수 이름 sum을 다른 걸로 바꾸려고 찾아 바꾸기를 돌렸다가, summary, sumo 속의 sum까지 죄다 바뀌어서 파일을 통째로 되돌린 적이 있어요. 진땀 뺐죠. 정규식으로 cat을 찾을 때도 똑같아요. categoryconcatenate 속의 cat까지 딸려오거든요. 이 "딸려옴"을 막아주는 게 오늘의 주제, 단어 경계(word boundary, 단어와 단어 아닌 것 사이) \b예요. 이거 하나면 "부분 말고 온전한 단어"만 콕 집을 수 있어요.

11.1 왜 부분 단어가 딸려올까요?

먼저 문제 상황부터 정확히 볼게요. cat category concatenate라는 글에서 cat을 찾으면 이렇게 돼요.


import re
re.findall(r"cat", "cat category concatenate")


# ['cat', 'cat', 'cat']


세 개나 잡혔어요. 하나씩 짚어보면 독립된 단어 cat 하나, category의 앞부분 cat, 그리고 concatenate 한가운데 숨은 cat까지예요. 정규식에서 그냥 cat이라고 적으면 "cat이라는 글자 조합"을 찾는 거지 "cat이라는 단어"를 찾는 게 아니거든요. 정규식은 우리가 생각하는 "단어"라는 개념을 기본적으로는 몰라요. 그냥 글자가 순서대로 나오면 무조건 잡는 거죠. 이 둘의 차이를 아는 게 오늘의 출발점이에요.

11.2 \b로 정확히 그 단어만 잡아요

온전한 단어만 잡으려면 양옆을 \b로 감싸요. \bcat\b는 "앞에도 단어 경계, 뒤에도 단어 경계인 cat", 즉 홀로 선 단어 cat이라는 뜻이에요.


re.findall(r"\bcat\b", "cat category the cat scatter")


# ['cat', 'cat']


이번엔 딱 두 개예요. 문장에 진짜 단어로 등장한 cat 두 개만 잡혔죠. 나머지가 왜 빠졌는지 볼까요? categorycat은 앞은 경계가 맞는데 뒤에 egory가 붙어 있어서 뒤쪽이 경계가 아니에요. scattercat은 앞뒤가 다 글자(s와 t)라 양쪽 다 경계가 아니고요. \b가 양쪽에서 "여기 진짜 단어 끝이야?"를 검사하니, 한쪽이라도 글자가 딱 붙어 있으면 탈락시키는 거예요.

11.3 경계가 있고 없고, 나란히 비교해요

차이를 확실히 느끼려면 같은 재료로 비교하는 게 최고예요. 경계 없이 cat만 쓰면요.


re.findall(r"cat", "cat category scatter")


# ['cat', 'cat', 'cat']


세 개가 다 잡혀요. 반면 \b로 감싸면 category, scatter 속의 것은 빠지고 홀로 선 cat만 남아요. 같은 대상인데 \b 유무가 "단어 검색"과 "글자 조합 검색"을 가르는 거예요. 아까 제 sum 사고도 \bsum\b로 찾았으면 summary는 건드리지 않았을 거예요. 코드에서 변수나 함수 이름을 바꿀 때, 검색창의 "단어 단위로 찾기" 체크박스가 바로 이 \b를 켜는 거라고 보시면 돼요. 그날 이후로 저는 이름을 바꿀 땐 무조건 이 옵션을 먼저 켜는 버릇이 생겼어요. 한 번 크게 데면 확실히 배우게 되더라고요.

11.4 정확히 세 글자짜리 단어만 뽑아요

\b는 길이 조건과 묶으면 더 강력해져요. \w{3}은 단어 글자 3개인데, 양옆을 \b로 감싼 \b\w{3}\b는 "정확히 세 글자짜리 온전한 단어"가 돼요.


re.findall(r"\b\w{3}\b", "the cat sat on a mat")


# ['the', 'cat', 'sat', 'mat']


세 글자 단어 the, cat, sat, mat만 잡혔어요. 두 글자 on과 한 글자 a는 조건에 안 맞아 빠졌죠. 여기서 \b가 왜 중요한지가 드러나요. 만약 경계가 없이 \w{3}만 썼다면, 긴 단어 속에서 아무 세 글자나 뚝 잘라 갔을 거예요. 경계로 감싸니 "단어를 통째로 봤을 때 딱 세 글자인 것"만 걸러진 거예요. 길이 조건과 경계를 같이 쓰면 이렇게 "특정 길이의 온전한 단어"를 정확히 뽑을 수 있어요.

11.5 우편번호 딱 5자리만 잡아요

실전에서도 이 요령이 그대로 쓰여요. 한국 우편번호는 5자리 숫자죠. \b\d{5}\b는 "앞뒤가 경계인 숫자 5개"예요.


re.findall(r"\b\d{5}\b", "우편 06236 서울")


# ['06236']


우편번호 06236이 정확히 잡혔어요. 여기서 \b가 중요한 이유가 있어요. 만약 123456처럼 6자리 숫자가 섞여 있으면, 경계가 없을 때 그 안의 앞 5자리 12345가 우편번호인 척 딸려올 수 있거든요. \b로 감싸면 "숫자 덩어리 전체가 정확히 5자리로 끝날 때"만 잡아서 그런 오탐을 막아줘요. 전화번호, 주민번호, 카드번호처럼 자릿수가 정해진 걸 다룰 때 이 \b는 필수 안전장치예요. 긴 숫자 뭉치에서 딱 원하는 자릿수만 안전하게 떼어낼 수 있으니까요.

11.6 숫자와 밑줄도 단어 글자예요

경계를 제대로 쓰려면 "단어 글자"의 범위를 정확히 알아야 해요. 단어 글자는 \w인데, 여기엔 영문과 숫자뿐 아니라 밑줄 _까지 포함돼요. 이게 초보에겐 은근히 의외예요. 그래서 경계는 글자와 숫자, 밑줄이 이어지는 동안엔 생기지 않아요. 예를 들어 cat3이나 cat_1에서는, t3 사이, t_ 사이에 경계가 없어요. 셋 다 단어 글자라 쭉 이어진 하나의 덩어리로 보거든요. 그래서 \bcat\b로 찾아도 cat3이나 cat_1은 안 잡혀요. 반대로 cat!이나 cat.처럼 뒤에 문장 부호가 오면, 부호는 단어 글자가 아니라서 t와 부호 사이에 경계가 생기고 cat이 잡혀요. "어디서 단어 글자가 끊기는가"가 곧 경계의 위치예요.

11.7 그래서 경계는 어디에 생기나요?

정리하면 \b단어 글자와 단어가 아닌 것 사이에 생기는 보이지 않는 위치예요. 단어 글자 옆에 공백이나 문장 부호처럼 단어 글자가 아닌 게 오거나, 글의 맨 앞과 맨 끝이면 거기가 경계예요. the cat을 예로 들면, cat 앞의 공백과 글자 c 사이가 하나, 그리고 t와 문장 끝 사이가 또 하나, 이렇게 두 곳에 경계가 있어요. 그래서 \b글자를 집어가지 않는 위치 기호예요. ^, $가 줄의 시작과 끝을 잡는 위치라면, \b는 단어의 시작과 끝을 잡는 위치라고 생각하면 딱 맞아요.

11.8 오늘 내용을 정리하면요

그냥 cat이라고 쓰면 category 속의 cat까지 딸려와요. 이건 정규식이 단어가 아니라 글자 조합을 찾기 때문이에요. 양옆을 \b로 감싼 \bcat\b온전한 단어만 잡아요. \b는 단어 글자와 비단어 사이의 위치라 글자를 집어가지 않고, 단어 글자엔 영문과 숫자뿐 아니라 밑줄도 들어간다는 점을 기억하면 경계가 어디 생기는지 헷갈리지 않아요. \b\w{3}\b\b\d{5}\b처럼 길이 조건과 묶으면 "정확히 그 길이인 온전한 단어"도 걸러낼 수 있어요. 찾아 바꾸기로 단어 하나만 안전하게 교체할 때, \b는 정말 든든한 안전장치가 되어줘요.