관련지식
javascript, event, event delegation pattern

UI를 만들다보면 특정 입력폼은 Auto focus를 요구할때가 있습니다. 아래와 같은 경우죠.

  • 생년월일 6자리를 입력하면 성별 필드로 자동 이동
  • 보안카드 비밀번호 입력
  • 시작일과 종료일 입력
  • 핸드폰 번호 입력

그외에도 요구에 따라 발생할수 있겠죠. 자동 포커스 이동은 누구나 간단한 코딩으로 쉽게 구현할수 있지만, 각 입력 필드 한땀한땀 함수를 구현하는것이 아니라 일괄로 코딩하는 방법을 보여드리겠습니다.

샘플로 사용할 HTML은 은행 홈페이지에서 이체할때 보기 쉬운 보안카드 입력화면으로 하겠습니다. 보안카드 일련번호까지 입력하는 샘플이죠.

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
  7. <style>
  8. .secret-card {border:1px solid black; width:300px; height:200px}
  9. .secret-card div {padding:0; }
  10. .secret-card .row {clear:both; display:block; padding:10px;}
  11. .secret-card .serial {border:1px solid; width:30px; height:30px; display:inline-block; float:left;}
  12. .secret-card .pin {border:1px solid; width:60px; height:30px; display:inline-block;}
  13. .secret-card input {width:90%; height:16px; border:0;}
  14. </style>
  15. </head>
  16. <body>
  17. <div class="secret-card">
  18. <div class="row">
  19. <div class="serial"><input type="password" maxlength="1"/></div>
  20. <div class="serial"></div>
  21. <div class="serial"></div>
  22. <div class="serial"><input type="password" maxlength="1"/></div>
  23. <div class="serial"></div>
  24. <div class="serial"><input type="password" maxlength="1"/></div>
  25. <div class="serial"></div>
  26. </div>
  27. <div class="row">
  28. <div class="pin"><input type="password" maxlength="2"/></div>
  29. <div class="pin">●●</div>
  30. </div>
  31. <div class="row">
  32. <div class="pin">●●</div>
  33. <div class="pin"><input type="password" maxlength="2"/></div>
  34. </div>
  35. </div>
  36. <input type="button" id="btnNext" value="다음" onclick="alert('click')"/>
  37. <script>
  38. </script>
  39. </body>
  40. </html>

포커스 자동 이동

자동 이동을 하기 위해선 최소한의 룰이 필요합니다. 예를들어 보안카드는 일련번호 부분은 한자리, 보안카드 번호는 두자리를 입력하죠. 따라서 input 태그에 maxlength 값을 설정하였습니다. 이제 키 이벤트를 감지하여 maxlength 만큼 입력을 하면 다음 필드로 포커스를 옮길 것입니다. 어떻게 하면 좋을까요? 필드별로 각기 다른 함수를 정의하는 것은 그다지 좋은 방법이 아닙니다.

  1. var $inputs = $('.secret-card input[type="password"]');
  2. // var $inputs = $('.serial > input, .pin > input');
  3. $inputs.each(function(idx) {
  4. var $input = $(this);
  5. $input.on('keyup', function(e) {
  6. var val = $input.val();
  7. if(parseInt($input.attr('maxlength')) == val.length) {
  8. if(idx + 1 < $inputs.length) {
  9. $inputs[idx + 1].focus(); //다음 요소에 포커스
  10. }
  11. }
  12. });
  13. });

입력필드의 keyup 이벤트를 감지하여 이벤트가 발생한 필드 값의 길이가 maxlength 가 되면 다음 필드에 포커스를 넣는 간단한 로직입니다. 마지막 입력 필드에서는 자동으로 포커스가 되어야 할 대상을 모르므로 포커스 이동을 하지 않습니다.

포커스 들어갈때 필드 초기화

값을 모두 입력한 뒤, 잘못 입력한 필드가 있음을 깨닫고 다시 입력할 경우도 있겠죠. 그때 마우스를 클릭해서 포커스가 들어갈때 필드값을 초기화 시키면 일일이 삭제하는 번거로움이 줄어들것입니다.

  1. var $inputs = $('.secret-card input[type="password"]');
  2. // var $inputs = $('.serial > input, .pin > input');
  3. $inputs.each(function(idx) {
  4. ...
  5. }).on('focus', function(e) { //추가
  6. $(this).val(''); //포커스 들어갈때 초기화
  7. });

keyup 이벤트처럼 객체 하나하나에 이벤트를 등록해도 되지만 JQuery를 사용할 경우 그냥 배열에 적용해도 됩니다. 아니면 이벤트 델리게이션 패턴으로 적용하는게 더 좋습니다. keyup 이벤트는 필드 객체의 순서를 알아야 하기 때문에 위 방법을 택했을 뿐 $inputs.on('이벤트') 형태로 사용하는게 훨씬 간단합니다.

keyup 이벤트 삭제

지금까지 만들어진 기능은 maxlength 만큼 입력하면 자동으로 다음 포커스로 이동하고, 이미 입력했던 값을 수정하려고 클릭하면 입력된 값을 초기화 합니다. 그런데 한가지 불편한게 있습니다.
어떤 필드를 수정하려고 마우스 클릭 -> 수정 -> 다음 필드로 포커스 이동 -> 입력값 초기화 되어 모든 필드를 다시 입력해야 하기 때문입니다. 따라서 자동으로 포커스를 이동하는 기능은 한번만 동작하도록 수정하겠습니다.

  1. if(parseInt($input.attr('maxlength')) == val.length) {
  2. $input.off('keyup'); //이벤트삭제 추가
  3. ...
  4. }

$input.on('keyup', function(e) {}); 을 one() 함수로 바꾸는게 아닙니다. maxlength 값에 따라 keyup 이벤트가 여러번 발생되어야 할수 있으므로 maxlength 만큼 입력되었을때 삭제해야 합니다.

이제 입력된 필드를 수정할때에는 포커스가 자동으로 이동되지 않을 것입니다.

입력값 제한

입력값에 대한 제한/검증은 웹에서 항상 거론되는 기능입니다. input 속성에 number, date 등이 추가 됨으로서 상황은 호전되었지만, 하위 브라우저와의 호환성의 문제등으로 직접 구현해야 하는 경우는 얼마든지 있습니다. 보안카드 비밀번호도 숫자만 입력 가능하면 되겠죠. 샘플에서 일련번호 세칸은 숫자만 입력할수 있도록 수정해보겠습니다. 그럼 실수로 포커스 이동되는것을 막을수 있을것입니다.

data-valid="number" 추가)

  1. <div class="serial"><input type="password" maxlength="1" data-valid="number"/></div>
  2. <div class="serial"></div>
  3. <div class="serial"></div>
  4. <div class="serial"><input type="password" maxlength="1" data-valid="number"/></div>
  5. <div class="serial"></div>
  6. <div class="serial"><input type="password" maxlength="1" data-valid="number"/></div>
  1. function validCheck(rulename, val) {
  2. var isValid = true;
  3. switch(rulename) {
  4. case 'number':
  5. var re = /[^0-9]/g;
  6. isValid = !re.test(val);
  7. break;
  8. }
  9. return isValid;
  10. }
  11. $input.on('keyup', function(e) {
  12. var val = $input.val();
  13. if(!validCheck($input.data('valid'), val)) {
  14. $input.val(val.substring(0, val.length - 1));
  15. return;
  16. }
  17. if(parseInt($input.attr('maxlength')) == val.length) {
  18. ...
  19. }
  20. });

입력 필드에 data-valid 속성이 정의되면 정의된 룰에 따른 데이터 검증을 하고 포커스를 자동이동 시키지 않게 됩니다. 이로써 실수로 영문을 입력하여 포커스가 이동되는것을 막을수 있습니다.

이 검증 방법은 조금만 수정하면 날짜, 시간, 핸드폰 번호 등 고정된 규칙을 가진 필드에 유용하게 적용할수 있습니다.

함수화

위 소스를 공통 소스로 만들기 위해 또는 모듈화를 시킬려면 함수로 한번 감싸야 할 것입니다. 그때 함수에 파라미터를 추가하여 마지막 필드 다음에 자동으로 이동될 포커스를 수동으로 지정하도록 해보겠습니다.

  1. function autofocus(formSelector, nextFocus) {
  2. function validCheck(rulename, val) {
  3. ...
  4. }
  5. var $inputs = $(formSelector);
  6. ...
  7. if(parseInt($input.attr('maxlength')) == val.length) {
  8. $input.off('keyup'); //이벤트 삭제
  9. if(idx + 1 < $inputs.length) {
  10. $inputs[idx + 1].focus(); //다음 요소에 포커스
  11. }
  12. else { //추가
  13. if(nextFocus)
  14. $(nextFocus).focus();
  15. }
  16. }
  17. ...
  18. }
  19. autofocus('.secret-card input[type="password"]', '#btnNext'); //화면에서 호출

이제 화면에서는 autofocus() 함수만 호출하면 되겠네요!

최종샘플

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
  7. <style>
  8. .secret-card {border:1px solid black; width:300px; height:200px}
  9. .secret-card div {padding:0; }
  10. .secret-card .row {clear:both; display:block; padding:10px;}
  11. .secret-card .serial {border:1px solid; width:30px; height:30px; display:inline-block; float:left;}
  12. .secret-card .pin {border:1px solid; width:60px; height:30px; display:inline-block;}
  13. .secret-card input {width:90%; height:16px; border:0;}
  14. </style>
  15. </head>
  16. <body>
  17. <div class="secret-card">
  18. <div class="row">
  19. <div class="serial"><input type="password" maxlength="1" data-valid="number"/></div>
  20. <div class="serial"></div>
  21. <div class="serial"></div>
  22. <div class="serial"><input type="password" maxlength="1" data-valid="number"/></div>
  23. <div class="serial"></div>
  24. <div class="serial"><input type="password" maxlength="1" data-valid="number"/></div>
  25. <div class="serial"></div>
  26. </div>
  27. <div class="row">
  28. <div class="pin"><input type="password" maxlength="2"/></div>
  29. <div class="pin">●●</div>
  30. </div>
  31. <div class="row">
  32. <div class="pin">●●</div>
  33. <div class="pin"><input type="password" maxlength="2"/></div>
  34. </div>
  35. </div>
  36. &#768;` b&#96; c`
  37. <input type="button" id="btnNext" value="다음" onclick="alert('click')"/>
  38. <script>
  39. function autofocus(formSelector, nextFocus) {
  40. function validCheck(rulename, val) {
  41. var isValid = true;
  42. switch(rulename) {
  43. case 'number':
  44. var re = /[^0-9]/g;
  45. isValid = !re.test(val);
  46. break;
  47. }
  48. return isValid;
  49. }
  50. var $inputs = $(formSelector);
  51. // var $inputs = $('.serial > input, .pin > input');
  52. $inputs.each(function(idx) {
  53. var $input = $(this);
  54. $input.on('keyup', function(e) {
  55. var val = $input.val();
  56. if(!validCheck($input.data('valid'), val)) {
  57. $input.val(val.substring(0, val.length - 1));
  58. return;
  59. }
  60. if(parseInt($input.attr('maxlength')) == val.length) {
  61. $input.off('keyup'); //이벤트 삭제
  62. if(idx + 1 < $inputs.length) {
  63. $inputs[idx + 1].focus(); //다음 요소에 포커스
  64. }
  65. else {
  66. if(nextFocus)
  67. $(nextFocus).focus();
  68. }
  69. }
  70. });
  71. }).on('focus', function(e) {
  72. $(this).val(''); //포커스 들어갈때 초기화
  73. });
  74. }
  75. autofocus('.secret-card input[type="password"]', '#btnNext'); //화면에서 호출
  76. </script>
  77. </body>
  78. </html>