Dandy Now!
  • Vue.js Bootstrap 모달에서 Input 자동 포커싱 구현하기
    2025년 08월 28일 20시 51분 25초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    Vue.js Bootstrap 모달에서 Input 자동 포커싱 구현하기

    1. 개요

    1-1. 문제 상황

    Vue.js와 Bootstrap을 함께 사용하여 모달을 구현할 때, 모달이 열릴 때마다 특정 input 필드에 자동으로 포커스를 설정하는 것은 사용자 경험 향상에 중요한 요소이다. 하지만 다음과 같은 문제들이 발생할 수 있다.

    • 모달이 열릴 때 포커스가 즉시 해제되는 현상
    • X 버튼으로 모달을 닫고 다시 열 때 포커스가 작동하지 않는 문제
    • Bootstrap 모달의 렌더링 타이밍과 Vue 컴포넌트 라이프사이클 간의 동기화 이슈

    1-2. 해결 목표

    • 모달이 열릴 때마다 일관되게 첫 번째 input 필드에 포커스 설정
    • 모달을 여러 번 열고 닫아도 안정적인 포커스 동작 보장
    • Bootstrap 모달 이벤트와 Vue 컴포넌트 상태의 완전한 동기화

    2. 구현 방법

    2-1. SlotModal 컴포넌트 수정

    Bootstrap 모달의 네이티브 이벤트를 Vue 컴포넌트로 전달하기 위해 SlotModal 컴포넌트를 수정한다.

    <template>
      <div
        class="modal fade"
        :id="modalId"
        data-bs-backdrop="static"
        data-bs-keyboard="false"
        tabindex="-1"
        aria-labelledby="staticBackdropLabel"
        aria-hidden="true"
        ref="modalElement"
      >
        <div class="modal-dialog">
          <div class="modal-content modal-content-custom">
            <div class="modal-header">
              <h5 class="modal-title" id="staticBackdropLabel">
                <slot name="title"></slot>
              </h5>
              <button
                type="button"
                class="btn-close"
                data-bs-dismiss="modal"
                aria-label="Close"
              ></button>
            </div>
            <div class="modal-body"><slot name="body"></slot></div>
            <div class="modal-footer">
              <slot name="footer"></slot>
            </div>
          </div>
        </div>
      </div>
    </template>

    2-2. JavaScript 이벤트 리스너 등록

    Vue 템플릿에서 Bootstrap 이벤트를 직접 사용할 수 없으므로, JavaScript에서 이벤트 리스너를 등록한다.

    export default {
      props: {
        modalId: {
          type: String,
          default: 'myModal'
        }
      },
      emits: ['modal-hidden', 'modal-shown'],
      mounted() {
        // Bootstrap 모달 이벤트 리스너 등록
        if (this.$refs.modalElement) {
          this.$refs.modalElement.addEventListener(
            'hidden.bs.modal',
            this.handleModalHidden
          )
          this.$refs.modalElement.addEventListener(
            'shown.bs.modal',
            this.handleModalShown
          )
        }
      },
      beforeUnmount() {
        // 이벤트 리스너 정리
        if (this.$refs.modalElement) {
          this.$refs.modalElement.removeEventListener(
            'hidden.bs.modal',
            this.handleModalHidden
          )
          this.$refs.modalElement.removeEventListener(
            'shown.bs.modal',
            this.handleModalShown
          )
        }
      },
      methods: {
        handleModalHidden() {
          this.$emit('modal-hidden')
        },
        handleModalShown() {
          this.$emit('modal-shown')
        }
      }
    }

    2-3. UserFormModal 컴포넌트에서 이벤트 처리

    부모 컴포넌트에서 SlotModal의 이벤트를 받아 포커스 로직을 처리한다.

    <template>
      <slot-modal
        class="modal-position"
        :modalId="modalId"
        @keydown="handleModalKeydown"
        @modal-hidden="handleModalHidden"
        @modal-shown="handleModalShown"
        data-bs-backdrop="static"
        data-bs-keyboard="false"
      >
        <!-- 모달 내용 -->
      </slot-modal>
    </template>

    3. 포커스 로직 구현

    3-1. 모달 이벤트 핸들러

    Bootstrap 모달의 생명주기에 맞춰 포커스를 설정한다.

    methods: {
      // 모달이 완전히 열린 후 포커스 설정
      handleModalShown() {
        setTimeout(() => {
          this.attemptFocus()
        }, 50)
      },
    
      // 모달이 닫힌 후 정리 작업
      handleModalHidden() {
        this.$emit('cancel')
      },
    
      // 실제 포커스 설정 로직
      attemptFocus() {
        if (!this.isEditMode) {
          const usernameField = document.getElementById('userId')
          if (usernameField && usernameField.offsetParent !== null) {
            usernameField.focus()
          }
        } else {
          const passwordField = document.getElementById('userPassword')
          if (passwordField && passwordField.offsetParent !== null) {
            passwordField.focus()
          }
        }
      }
    }

    3-2. 요소 가시성 검증

    포커스를 설정하기 전에 요소가 실제로 화면에 보이는지 확인한다.

    • offsetParent !== null 조건을 사용하여 요소의 가시성을 검증
    • 숨겨진 요소나 렌더링되지 않은 요소에 포커스를 설정하는 것을 방지

    4. 주요 해결 포인트

    4-1. 타이밍 이슈 해결

    • Bootstrap의 shown.bs.modal 이벤트를 활용하여 모달이 완전히 열린 후 포커스 설정
    • setTimeout을 사용하여 DOM 렌더링 완료를 보장

    4-2. ESLint 오류 해결

    Vue 템플릿에서 @hidden.bs.modal 형식의 이벤트 리스너 사용 시 발생하는 ESLint 오류를 JavaScript 이벤트 리스너로 해결한다.

    // 오류 발생 코드
    <div @hidden.bs.modal="handleModalHidden"></div>
    
    // 해결 코드
    mounted() {
      this.$refs.modalElement.addEventListener('hidden.bs.modal', this.handleModalHidden)
    }

    4-3. 메모리 누수 방지

    컴포넌트가 언마운트될 때 이벤트 리스너를 정리하여 메모리 누수를 방지한다.

    beforeUnmount() {
      if (this.$refs.modalElement) {
        this.$refs.modalElement.removeEventListener('hidden.bs.modal', this.handleModalHidden)
        this.$refs.modalElement.removeEventListener('shown.bs.modal', this.handleModalShown)
      }
    }

    5. 결과 및 장점

    5-1. 개선된 사용자 경험

    • 모달이 열릴 때마다 자동으로 첫 번째 입력 필드에 포커스가 설정됨
    • 키보드 네비게이션이 향상되어 접근성이 개선됨
    • 사용자가 즉시 입력을 시작할 수 있어 작업 효율성이 증대됨

    5-2. 안정적인 동작

    • X 버튼으로 모달을 닫고 다시 열어도 일관된 포커스 동작
    • Bootstrap 모달과 Vue 컴포넌트 간의 완전한 동기화
    • 다양한 브라우저 환경에서 안정적인 작동 보장

    5-3. 유지보수성

    • 모듈화된 구조로 재사용 가능한 SlotModal 컴포넌트
    • 명확한 이벤트 기반 아키텍처로 디버깅 용이
    • 메모리 누수 방지를 통한 장기적인 안정성 확보

    이러한 구현을 통해 Vue.js와 Bootstrap을 사용한 모달에서 완벽한 자동 포커싱 기능을 구현할 수 있으며, 사용자 경험과 접근성을 크게 향상시킬 수 있다.


    728x90
    반응형
    댓글