Inputmask 오픈소스에 PR 올리기 (ShadowDOM에서 정상 동작하지 않는 이슈)

호박고구마·2023년 11월 28일
0

개요

현재 진행하고 있는 프로젝트에서 Inputmask 오픈소스 라이브러리를 사용해서 마스킹 처리 하는 기능을 개발하게 되었다.
그런데 개발 환경에서는 동작이 잘 되었는데 실제 빌드된 환경에서는 제대로 동작하지 않는 상황이 발생했다.

위 gif 처럼 일반적인 DOM에서는 휴대폰 번호 마스킹 처리가 정상적인데, Shadow DOM에서는 input에 포커싱 하자마자 포커싱이 이상하게 잡히고 아예 입력이 안됐다.


원인

이 프로젝트는 먼저 Vue 기반으로 개발한 후에 빌드된 결과물이 WebComponent로 특정 DOM의 하위에 부착되는 형태로 구성되었다.
개발 환경에서는 일반 DOM에서 개발했기 때문에 별 이상이 없었지만, 신기하게 빌드 되어서 ShadowDOM 하위에 위치하면 제대로 동작하지 않았다.
결국 Inputmask 라이브러리 내부에서 ShadowDOM 하위에 있는 특정 타겟을 찾지 못하는 게 아닐까 하는 의심이 들어서 Inputmask 소스를 하나하나 까보기 시작했다.

하나하나 디버깅해본 결과 역시나 내부적으로 마스킹 처리하는 타겟을 찾는 부분이 문제였다.


if (input === (input.inputmask.shadowRoot || input.ownerDocument).activeElement) ...

위 코드 처럼 현재 마스킹 처리하고자 하는 Inputmask 인스턴스와 실제 DOM 타겟이 맞는지 확인하는 코드들이 있었다.
여기서 문제가 된 부분은 input.ownerDocument였다. 내 추측으로는 위 코드는 결국 input 엘리먼트의 최상단 Document안에서 실제 포커싱 된 객체를 찾고자 하는 코드 같은데, input 엘리먼트가 속한 최상단 객체를 찾고자 ownerDocument로 접근 하는 게 문제였다.

MDN에 따르면 ownerDocument는 가장 탑 레벨의 Node 객체를 반환한다.

이 경우 항상 DOM의 가장 최상단인 일반 Document 객체만을 반환하게 되는데, 문제는 ShadowDOM 하위에 있는 엘리먼트에서 ownerDocument로 접근하는 경우에도 실제 해당 엘리먼트가 속한 최상단 DOM(Shadow Root)가 아니라 일반 Document를 반환한다는 점이다.
ShadowDOM은 일반적인 DOM과 별개의 DOM으로 캡슐화되어 있다. 따라서 최상단 DOM 내부에서는 캡슐화 된 ShadowDOM의 특정 엘리먼트를 찾을 수가 없다.
따라서 저 코드로 인해 결국 실제 포커스 된 input 엘리먼트를 찾지 못하게 되는 이슈가 발생했고, 그래서 정상적으로 동작하지 않았다.

결국 input.ownerDocument로 특정 input 엘리먼트가 속한 최상단 DOM을 찾고자할 때, input이 일반적인 DOM 하위에 위치한 것 뿐만 아니라 ShadowDOM 하위에 위치할 수 있다는 점까지 신경써서, ShadowDOM에 위치한다면, Shadow Root를 반환할 수 있도록 변경되어야 했다.


해결

구글링 결과, getRootNode() 라는 API를 찾았다. ownerDocument와 비슷하게 해당 Node의 최상단 DOM을 반환하는 API인데, ShadowDOM인 경우 ShadowDOM의 Root를 반환해준다.

문제 됐던 위 코드를 아래와 같이 바꾸니 빌드된 환경, 즉 Shadow DOM 하위에서도 정상 동작했다.


if (input === (input.inputmask.shadowRoot || input.getRootNode()).activeElement) ...

Inputmask github에서 최신 버전으로 git fork후 코드 수정을 한 다음 빌드 및 테스트까지 한 다음 문제가 없는 것 같아, PR을 올렸다.

PR 올린 후에 2주 정도 기다렸는데, Merge 되지는 않아서 우선 내부 프로젝트에만 변경된 소스가 적용되도록 수정했다. 언젠가 Merge 되길 바라며...

결론

사실 이렇게 정리하고 보니 별 거 아닌 것 같아 보이지만, 실제로 디버깅해서 원인 파악하는 데까지 이틀 정도 걸렸다.
그리고 그 과정에서 기타 시행 착오들도 있었다.

  • node_modules에 설치된 패키지를 바라보는 게 아니라 fork 떠서 수정 중인 라이브러리를 바라보게 하려고 pnpm link를 처음 사용해봤다.
  • 빌드된 dist 파일 대신 실제 코드를 바라보게 하려고 package.json을 까보면서 exports 구문이 뭘 의미하는지 새삼 알게 되었다.
  • 마지막에 라이브러리 수정 후에 빌드된 결과물이 제대로 동작하지 않아서 webapck 설정도 까보고 프로젝트 빌드나 테스트 등을 관리하는 툴인 grunt에 대해서도 알게 되었다.

이래저래 시행착오를 거치면서 PR 까지 올리니까 뿌듯하다. 별 거 아니지만 새삼 다른 사람이 만들어놓은 오픈 소스 라이브러리를 까보는 게 또 흔한 기회는 아닌 것 같아서 재밌었다.

0개의 댓글