만약 다음과 같이 사용자에게서 ID를 받아오는 입력창과 버튼이 있다고 생각해 봅시다.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>회원가입</title> </head> <body> <form id="sign"> <input type="text" id="id" name="id" placeholder="아이디를 입력해 주세요." /> <input type="submit" /> </form> </body> </html> <script src="index.js" defer></script>
위 양식에서 사용자로부터 아이디를 입력받을텐데요. 이때 사용자에게 입력 받을 아이디에 대해서 제약 사항을 정해본다면 다음과 같이 지정할 수 있을 것 같습니다.
그렇다면 이 조건을 한 번 JS 코드로 적어볼까요?
document.getElementById("sign").addEventListener("submit", function (event) { event.preventDefault(); // 버튼을 누를 때 폼은 기본적으로 페이지를 이동시키므로 이러한 기본 동작을 막습니다. // 사용자 입력 값 받아오기 const email = document.getElementById("id").value; if (validateEmail(email)) { console.log("이메일이 유효합니다."); } else { console.log("이메일이 유효하지 않습니다."); } }); funct>ion validateEmail(email) { const atIndex = email.indexOf("@"); const dotIndex = email.lastIndexOf("."); // 기본적으로 이메일 형식을 id@domain.com 형식이므로 @의 인덱스와 .의 인덱스의 위치에 대한 검사를 실시함 // atIndex < 1 : 적어도 문자나 숫자 이후에 오는 형식인지 // dotIndex <= atIndex + 1 : . 기호가 @ 기호와 하나의 입력 형식보다 앞에 오지 않는지 // dotIndex >= email.length - 1 :. 기호가 전체 이메일 중 끝에 있지는 않은지 if (atIndex < 1 || dotIndex <= atIndex + 1 || dotIndex >= email.length - 1) { return false; } const userId = email.substring(0, atIndex); let hasEnglish = false; let hasDigit = false; // 2. 아이디중 아이디(이메일 앞에 오는)는 // 영어 및 숫자 혼합으로 5자 이상 입력하여야 한다. for (let i = 0; i < userId.length; i++) { const char = userId.charCodeAt(i); if ((char >= 65 && char <= 90) || (char >= 97 && char <= 122)) { hasEnglish = true; } if (char >= 48 && char <= 57) { hasDigit = true; } } return userId.length >= 5 && hasEnglish && hasDigit; }
분명 두 가지 단순한 조건임에도 불구하고 생각보다 많은 조건과 변수가 사용된 모습을 확인할 수 있습니다. 이처럼 우리가 특정 조건을 이용해 사용자에게 입력 형식에 제약을 주기 위해서는 생각보다 많고 복잡한 로직을 코드로 짜야만 하는데요.
하지만 이러한 작업을 정규 표현식으로 작성한다면 다음과 같이 깔끔하게 작성할 수 있게 됩니다.
document.getElementById("sign").addEventListener("submit", function(event) { event.preventDefault(); // 폼의 기본 제출 동작을 막습니다. const email = document.getElementById("id").value; if (validateEmail(email)) { console.log("이메일이 유효합니다."); } else { console.log("이메일이 유효하지 않습니다."); } }); function validateEmail(email) { // 이메일 형식 검사 const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailPattern.test(email)) { return false; } // 사용자 아이디(이메일 앞에 오는 부분) 조건 검사 const userId = email.split('@')[0]; const userIdPattern = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{5,}$/; return userIdPattern.test(userId); }
정규 표현식은 문자열에서 특정 문자 조합을 찾기 위한 패턴으로, 짧게 줄여 정규식이라고도 이야기를 하는데요.
이러한 정규식은 크게 리터럴 표기법과 RegExp 객체로 생성하는 법으로 생성할 수 있습니다.
let re1 = /ab+c/ // 정적으로 생성할 때 let re2 = new RegExp(re1); // 동적으로 전달 받아 사용할 때 let re3 = new RegExp("ab+c"); // 정적으로 생성할 때는 문자열로 감싸줘야함
그리고 이러한 정규식의 패턴 작성법은 다음과 같은 규칙으로 작성할 수 있는데요. 특히나 이스케이프 시퀀스 조합으로 같이 작성되기에 이스케이스 시퀀스를 모르실 분들을 위해 아래에 첨부를 해두겠습니다.
- ^ : 패턴 시작 (괄호 안에서는 부정을 의미)
- $ : 패턴 종료
- [0-9] : 괄호 안의 숫자로 범위 지정
- : 앞의 패턴이 한 번 이상 반복되어야 함
- [a-z] : 알파벳 소문자로 범위 지정
- [A-Z] : 알파벳 대문자로 범위 지정
- ? : 기호 앞에 정규식 단어가 있어도 되고, 없어도 되는 선택사항을 의미
- : 기호 앞에 정규식 단어가 0번 이상나타날 수 있음을 의미.
예를 들어 앞서 보여드린 정규식의 예시를 들어 설명을 해보겠습니다.
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- /.../ : 리터럴 표현식의 범위를 의미
- ^ : 패턴 시작을 알림
- [^\s@]+ : 단어가 공백 문자나(\s) @(@)를 포함하지 않는(^) 단어가 최소한 하나 이상(+) 있어야함
- @ : 말 그대로 @기호를 의미함.
- +. : 말 그대로 .기호를 의미함 이때 이스케이프 시퀀스로서의 의미인 백스페이스를 붙여줌.
- $ : 패턴 끝을 알림
정규식은 리터럴 표현식으로 지정을 하든, RegExp 객체로 지정을 하든 지정되는 순간 여러가지 메서드를 제공하는데요. 대표적으로 test, exec, search, match 메서드가 대표적인 메서드들입니다.
문자열이 정규 표현식과 일치하는지 여부를 확인하며, 결과값을 true 및 false로 반환해 줍니다.
// c 문자 앞에는 ab라는 단어가 하나 이상 포함해야 함 let re = new RegExp("ab+c"); // 타겟 문자열들 let target1 = "abc"; let target2 = "adc"; if (re.test(target1)) { console.log("올바른 값입니다."); } else { console.log("올바른 값이 아닙니다."); } if (re.test(target2)) { console.log("올바른 값입니다."); } else { console.log("올바른 값이 아닙니다."); }
문자열에서 정규 표현식과 일치하는 부분을 검색한 뒤 일치하는 결과를 포함하는 배열, 또는 일치하지 않은 경우 null을 반환합니다.
// abc라는 단어와 그 안에 포함된 (b)라는 단어를 따로 캡쳐 그룹으로 포착하도록 정규식을 정의 let re = new RegExp("a(b)c"); // 타겟 문자열들 let target1 = "fabcdeb"; let target2 = "addef"; let result1 = re.exec(target1); let result2 = re.exec(target2); console.log(result1) console.log(result2)
문자열에서 정규 표현식이 있는지를 검색한 후 찾은 경우 해당 단어의 첫번째 인덱스를 반환하고, 찾지 못한 경우 -1을 반환합니다. 특히 이 메서드는 정규식이 아니라 대상 문자열에서 호출하는 메서드임을 기억해야 합니다.
// c 문자 앞에는 ab라는 단어가 하나 이상 포함해야 함 let re = new RegExp("abc"); // 타겟 문자열들 let target1 = "edfabcab"; let target2 = "edfabdadc"; if (target1.search(re) > 0) { console.log( `해당 단어는 ${target1.search(re)} 번째 인덱스에 위치해 있습니다.` ); } else { console.log("못찾았습니다."); } if (target2.search(re) > 0) { console.log( `해당 단어는 ${target2.search(re)} 번째 인덱스에 위치해 있습니다.` ); } else { console.log("못찾았습니다."); }
문자열에서 정규 표현식과 일치하는 부분을 검색합니다. 일치하는 결과를 포함하는 배열을 반환하거나 일치하지 않으면 null을 반환합니다.
특히 해당 메서드는 정규식에서 호출하는 exec과 달리 문자열에서 호출하는 메서드이기 때문에 이 두 메서드의 반환값은 비슷하더라도 엄연히 호출 대상과 검색 대상의 범위에 따라 구분해서 사용되는 메서드임을 기억해야 합니다.
let re = new RegExp("a(b)c"); // 타겟 문자열들 let target1 = "fabcdeb"; let target2 = "addef"; let result1 = target1.match(re); let result2 = target2.match(re); console.log(result1) console.log(result2)
이스케이프 시퀀스는 정규식에서 사용되는 대표적인 시퀀스로, 해당 시퀀스가 무엇인지를 알아놓으면 다음에 사용할 때 조건에 맞는 적절한 시퀀스들을 사용할 수 있기에 소개를 드려볼까 합니다.