함수와 입력 경계
120분 안팎
학습 목표
검색어 정규화를 작은 함수로 분리합니다.
개념
왜 정규화를 함수로 분리합니까
사용자가 책을 검색할 때 앞뒤 공백 때문에 결과가 달라지면 화면마다 조건을 고치는 일이 생깁니다. 폼, URL, 테스트가 같은 규칙을 사용하도록 입력을 한 함수에 모읍니다. 이번 함수는 값을 받아 정규화한 문자열을 반환합니다. 화면 상태나 전역 검색어를 읽지 않으므로 브라우저 없이 입력과 반환값을 비교할 수 있습니다.
function normalizeQuery(value)는 value라는 매개변수를 받는 함수 선언입니다. 호출 normalizeQuery(" 책 ")의 문자열은 인자입니다. return은 계산 결과를 호출자에게 돌려주고 그 호출의 실행을 끝냅니다. console.log는 관찰용 출력이며 반환값을 대신하지 않습니다. 함수 안에서 출력만 하면 호출자는 undefined를 받습니다. 계산 함수와 출력을 맡는 코드를 분리합니다.
입력 경계에서 문자열인지를 typeof로 확인합니다. null, 숫자, 배열, 객체와 누락된 값은 빈 문자열로 반환하는 것이 이번 계약입니다. String(value)를 먼저 호출하면 null은 "null", 숫자는 숫자 글자로 바뀌어 의도하지 않은 검색이 됩니다. 기본 매개변수 값만 설정하면 undefined에는 적용되지만 null에는 적용되지 않습니다. 그래서 타입 판정을 앞에 둡니다.
계약을 코드로 옮깁니다
trim은 양 끝의 공백을 제거하고 내부 공백은 그대로 둡니다. replace(/\s+/g, " ")는 연속 공백 문자를 한 칸으로 바꿉니다. 정규식의 \s는 공백 문자, +는 하나 이상, g는 전체 일치에 적용한다는 뜻입니다. 탭과 줄바꿈도 대상입니다. 마지막 toLowerCase는 영문 대소문자 차이를 줄입니다. 순서를 고정하면 같은 입력이 같은 결과를 냅니다.
정규화는 문자열을 새로 계산하며 원래 문자열 변수에 재대입하지 않습니다. normalizeQuery(normalizeQuery(x))가 한 번 적용한 값과 같은지를 확인하면 반복 호출에도 규칙이 안정적인지 알 수 있습니다. 이 성질을 멱등성이라고 부르지만 이름보다 검색 조건이 매번 더 바뀌지 않는다는 의미가 중요합니다. 같은 검색을 두 화면에서 처리할 때 예측하기 쉬워집니다.
한글 책을 소문자 함수에 통과시켜도 그 글자를 다른 검색어로 바꾸지 않습니다. 그러나 이 함수는 오타 수정, 초성 검색, 유니코드 조합형 정규화나 언어별 검색을 제공하지 않습니다. 여러 단어의 공백을 한 칸으로 유지하므로 "책 나눔"과 "책나눔"은 다릅니다. 사용자가 기대할 수 있는 범위를 계약에 적고 기능을 추가할 때 새 테스트로 결정합니다.
function normalizeQuery(value) {
if (typeof value !== 'string') return '';
return value.trim().replace(/\s+/g, ' ').toLowerCase();
}함수의 책임을 좁힙니다
정규화 함수에서 지역 필터나 화면 안내 문구까지 만들지 않습니다. 정규화된 빈 문자열은 이름 제한 없음이라는 데이터 계층의 표현입니다. 앞 모듈 HTML은 required로 빈 제출을 막는 계약이므로 데이터 함수의 빈 검색과 UI 빈 제출 정책은 서로 구분합니다. 뒤 DOM 모듈에서 정책을 바꿀 때 폼 안내와 테스트를 함께 고칩니다.
따라하기에서는 JSON.stringify를 사용해 반환 문자열의 따옴표와 공백을 눈으로 확인합니다. 단순 console.log는 양끝 공백을 구분하기 어려울 수 있습니다. 탭이나 줄바꿈은 입력 JSON에서 \t와 \n으로 표현합니다. 화면 표시용 공백과 실제 문자열 공백을 구별하고 실패한 테스트의 기대값과 실제값을 먼저 비교합니다.
흔한 실수와 오류 읽기
TypeError에서 trim is not a function이 보이면 문자열이 아닌 값에 메서드를 호출했는지 살핍니다. undefined에 trim을 읽는 오류라면 타입 검사 이전에 호출했을 수 있습니다. 모든 입력에 try/catch를 씌워 빈 값으로 숨기기보다 계약의 비문자열 처리를 첫 줄에 둡니다. 정답 출력이 undefined라면 return 누락이나 줄바꿈 직후 자동 세미콜론 삽입을 확인합니다.
호출마다 독립된 값을 계산합니다
매개변수 value는 호출할 때 전달된 값에 연결됩니다. 함수 밖의 검색어 변수와 이름이 같아도 같은 저장 공간이라는 뜻은 아닙니다. 지역 변수를 사용하면 다른 호출의 중간 결과를 읽는 실수를 줄일 수 있습니다. 정규화 결과는 호출자가 const q = normalizeQuery(raw)처럼 받아 사용합니다. raw를 바꾸지 않았다는 사실과 q가 올바른 문자열이라는 사실을 각각 확인합니다.
중간 결과로 규칙을 확인합니다
입력 문자열의 양끝 제거 결과, 연속 공백 축소 결과, 소문자 변환 결과를 차례로 적어 봅니다. BOOK과 Fair 사이에 탭 두 개가 있으면 trim만으로 내부 탭은 사라지지 않습니다. replace가 한 칸으로 바꾸고 toLowerCase가 book fair를 만듭니다. 각 메서드는 새 문자열을 반환하므로 호출만 하고 반환값을 버리면 다음 단계에 변경 결과가 전달되지 않습니다.
return의 위치를 읽습니다
return 바로 뒤에서 줄을 바꾸고 다음 줄에 계산식을 쓰면 자동 세미콜론 삽입 때문에 undefined가 반환될 수 있습니다. 이 예제에서는 반환할 식을 return과 같은 줄에 둡니다. 타입 검사에서 빈 문자열을 반환한 호출은 그 자리에서 끝나므로 아래 trim은 실행하지 않습니다. 숫자나 null이 들어왔을 때 어디에서 호출이 끝나는지 손으로 추적하면 오류를 예방할 수 있습니다.
정규화 함수의 검증 기준
정상 영문, 한글, 공백만 있는 입력, 탭과 줄바꿈, null을 각각 넣습니다. 빈 문자열은 반환할 수 있는 정상 값이므로 결과가 비었다는 이유만으로 함수 실행 실패로 판단하지 않습니다. 기대 문자열을 JSON.stringify로 표시하면 따옴표 안 공백과 이스케이프를 확인할 수 있습니다. 같은 입력을 연속 호출하고 정규화 결과를 다시 넣어도 값이 유지되는지 비교합니다.
검색 입력 경계를 연결합니다
미션에서는 입력 출처가 폼인지 URL인지와 관계없이 normalizeQuery가 같은 문자열 규칙을 적용합니다. 함수가 DOM 요소를 읽도록 만들면 테스트에서 화면을 준비해야 하고 URL 처리와 재사용하기도 어려워집니다. 호출자가 원시 값을 전달하고 반환 문자열로 다음 검색을 수행하게 합니다. 화면 안내를 바꾸는 작업은 이 함수의 반환 계약을 바꾸지 않고 호출자에서 처리할 수 있습니다.
따라하기
정상 입력 추적
계산 결과를 먼저 예상합니다. 아래 코드를 Node의 실행 창이나 별도 example.cjs 파일에 입력합니다. 파일 실행 명령은 node example.cjs입니다. 각 단계는 독립된 예제이므로 이전 변수 선언과 섞지 않습니다.
function normalizeQuery(value) {
if (typeof value !== 'string') return '';
return value.trim().replace(/\s+/g, ' ').toLowerCase();
}
console.log(JSON.stringify(normalizeQuery(" BOOK Fair ")));실행 결과
"book fair"
경계와 빈 값 확인
조건 하나를 바꿔 결과를 비교합니다. 아래 코드를 Node의 실행 창이나 별도 example.cjs 파일에 입력합니다. 파일 실행 명령은 node example.cjs입니다. 각 단계는 독립된 예제이므로 이전 변수 선언과 섞지 않습니다.
function normalizeQuery(value) {
if (typeof value !== 'string') return '';
return value.trim().replace(/\s+/g, ' ').toLowerCase();
}
console.log(JSON.stringify([null,12,"\t 책 \n"].map(normalizeQuery)));실행 결과
["","","책"]
호출 계약 점검
원본과 반환값의 책임을 확인합니다. 아래 코드를 Node의 실행 창이나 별도 example.cjs 파일에 입력합니다. 파일 실행 명령은 node example.cjs입니다. 각 단계는 독립된 예제이므로 이전 변수 선언과 섞지 않습니다.
function normalizeQuery(value) {
if (typeof value !== 'string') return '';
return value.trim().replace(/\s+/g, ' ').toLowerCase();
}
const q = normalizeQuery(" 책 나눔 ");
console.log(q === normalizeQuery(q));실행 결과
true
브라우저 실습과 미션으로 옮기기
입력은 JSON 값 한 개입니다. 문자열이면 양끝 공백 제거→연속 공백 문자 한 칸→영문 소문자 순으로 정규화합니다. 문자열 외의 값은 빈 문자열로 반환합니다. 결과를 JSON 문자열로 출력합니다. 단어 사이 공백은 지우지 않습니다.
아래 실습 starter의 TODO를 구현하고 모든 제공 입력을 실행합니다. 출력은 JSON 한 개만 남깁니다. Node 로컬에서는 main.cjs에 코드를 저장하고 다음 명령으로 입력 파일을 전달할 수 있습니다. input.json에는 아래 tests의 입력 한 개를 저장합니다.
node main.cjs < input.json미션에서는 계산 함수를 assets/data.js에 옮깁니다. 표준 입력용 require와 console.log는 브라우저에 복사하지 않습니다. npm ci 후 npm test로 데이터 계약과 앞 모듈 문서·CSS 검사를 확인합니다.
확인 문제
실습
입력은 JSON 값 한 개입니다. 문자열이면 양끝 공백 제거→연속 공백 문자 한 칸→영문 소문자 순으로 정규화합니다. 문자열 외의 값은 빈 문자열로 반환합니다. 결과를 JSON 문자열로 출력합니다. 단어 사이 공백은 지우지 않습니다.
모범 답안
const input = JSON.parse(require('fs').readFileSync(0,'utf8'));
function normalizeQuery(value) {
if (typeof value !== 'string') return '';
return value.trim().replace(/\s+/g, ' ').toLowerCase();
}
console.log(JSON.stringify(normalizeQuery(input)));
더 읽기
면접 질문
- 매개변수와 인자의 차이, return과 console.log의 차이를 설명해 주세요.
- 검색어 정규화에서 타입 검사를 먼저 하는 이유는 무엇인가요?