home

더 쉽게! 더 빠르게! 본인인증 개선기

안녕하세요. 아이들나라 Frontend팀 이윤호입니다.
아이들나라는 지난 3월 말 통합회원 프로젝트 과제 중 하나로 본인인증의 UI/UX를 개선하였습니다. 이번 글에서는 개선 과제를 진행하며 생각했던 고민과 개선 과정을 소개하고자 합니다.
본인인증은 통합회원 회원가입(아이들나라, 부모나라) 과정과 아이들나라 홈페이지 웹 구독 상품 결제 전, 아이들나라 APP 내 민감정보에 대한 14세 미만 법정대리인 보호자 인증 화면에서 만나볼 수 있습니다.

기존 본인인증은 어땠을까?

새로운 본인인증을 소개하기 전, 아이들나라에서 기존에 사용되던 본인인증은 어떠했는지를 몇 가지 이야기해 보면 좋을 것 같습니다.
▲ 기존 본인인증 화면
이번 본인인증 개선 Task를 진행했던 PO와 PD의 공통되는 의견은 다음과 같았습니다.
1.
약관 체크박스 및 여러 입력 필드가 한 페이지에 모두 나열되어 부담스럽게 느껴집니다.
2.
불필요한 위•아래 스크롤과 함께 시선 이동이 발생합니다.
3.
입력 필드를 직접 터치해야해서 번거롭습니다.
본인인증은 회원가입 과정에서도 필수로 진행되기 때문에 회원가입을 마음먹은 사용자에게 서비스의 첫인상을 결정짓게 될 화면입니다. 사용자가 회원가입 중 본인인증 단계에 진입했을 때 약관동의 체크박스들과 나열된 4개의 입력필드를 마주하게 된다면 부담스러운 첫인상이 될 수 있을 것입니다.
그리하여 개편된 본인인증

그래서 어떻게 바뀌었을까?

신규 본인인증 화면은 입력 필드 전환간 인터렉션이 추가되었으며, 다소 정적이었던 기존 본인인증 화면에 비해 동적인 UI로 탈바꿈되었습니다. 또한, 이번 본인인증 개편은 사용자 경험 개선을 중점으로 진행되었기 때문에 기존에 사용되던 PASS 인증이 과감히 제거되었습니다.
▲ 입력 과정들을 거치고 나면 윙크하는 유삐가 있는 인증완료 화면을 만나볼 수 있어요.
앞서 나열해보았던 기존 본인인증이 갖는 아쉬운 점을 토대로 UI/UX 개선을 진행하며 우리가 기대한 결과는 다음과 같았습니다.
1.
화면 내에 많은 정보가 한꺼번에 노출되지 않아 입력에 대한 부담을 덜어줍니다.
2.
입력이 필요한 입력 필드가 상단에 활성화되어 시선이 항상 고정되어 있습니다.
3.
입력 필드들을 클릭해가면서 활성화시키지 않아도 되며, CTA 버튼(’다음’ 버튼)을 누르면 자동으로 다음 입력 필드로 이동되어 더 간편한 사용자 경험을 제공합니다.

개선된 인증 Flow

본인인증은 회원가입 뿐만 아니라 결제 화면을 비롯한 몇가지 경로에서 추가로 사용되고 있었습니다.
아이들나라 APP 회원가입, 민감정보(14세 미만 법정대리인) 보호자 인증
부모나라 APP 회원가입
아이들나라 홈페이지 회원가입, 구독 상품 결제 전
진입 경로에 따라 본인인증 페이지의 시작점이 다르기도 하며 회원가입과 구독 결제 전 진행되는 본인인증 과정에선 14세 미만 사용자가 인증을 시도한다면 법정대리인 인증(보호자 인증)을 진행하는 절차가 추가되기 때문에 Step 별로 노출되는 화면에 대한 적절한 상태관리가 필요했습니다.
페이지 전환 없이 ’다음’ 버튼을 클릭 할 때마다 입력 필드가 하나씩 등장하는 Step 형태의 화면으로 변경되면서 구현 과정에서는 기존 본인인증의 SMS 문자 인증로직(KCB 본인인증)을 유지하면서 새로운 UI에 맞는 로직을 작성해야 했습니다.
▲ 기본 본인인증 Flow
▲ 14세 미만 법정대리인 인증 Flow

본인인증 Step 구현하기

앞서 설명한 본인인증 Flow의 요구사항에 맞추어, 미리 나올 수 있는 화면의 시나리오들을 코드 상에 작성해두었습니다. 다음과 같이 객체 배열 구조로 Flow에 대한 로직을 관리하였고, 객체 안의 값들이 하나의 Step을 구성하고 있습니다.
그중에 inputsKeys key는 현재 Step에 어떤 입력 필드를 노출시킬 것인지를 분기하는 역할을 합니다. 위의 예시에서는 4개의 입력 필드가 화면에 보여지고 4개의 state를 Form에 가지고 있습니다.
법정대리인(보호자 인증) Flow까지 진행된다면 Form에 최대 12개의 입력 값을 저장하여 관리해야하기 때문에 입력 필드들을 통해 입력받는 값은 비제어 컴포넌트 방식인 react-hook-form을 사용하여 상태를 다루었습니다.
react-hook-form은 state를 최소화하여 렌더링 횟수가 최소화 됨으로 퍼포먼스 최적화에 이점을 줍니다.
 법정대리인 인증 경로 접근 시
Flow 별로 미리 작성된 Step을 조합해 진입 경로에 따라 다른 화면을 보여줄 수 있습니다.
const [certificationStep, setCertificationStep] = useState<CertStepType[]>(DEFAULT_CERT_STEP); ... useEffect(() => { // 아이들나라 앱 민감정보 동의 경로 접근 시 보호자 인증만 진행 if (enterType === EnterCertType.PARENT_CERT) { const parentCertStep: CertStepType[] = [ ...CERT_TERMS_AGREEMENT_STEP, ...PARENT_CERT_STEP, ]; setCertificationStep(parentCertStep); setCurrentStep(parentCertStep[0]); } }, [enterType])
TypeScript
복사
입력 필드 에러 핸들링
본인인증 화면에서 입력 필드들의 상태를 확인하여 입력된 값이 없거나 유효성 검사에 실패했다면 다음 Step으로 이동하는 것을 막을 필요가 있었습니다.
react-hook-form의 watch, errors를 사용한다면 Form의 에러 핸들링을 쉽게 할 수 있습니다. isValid 라는 Validate 상태를 지원해주기도 하지만, Input에 입력이 될 때마다 값이 바뀌는 점 때문에 사용하지 않았습니다.
현재 Step의 입력 필드(inputKeys)들의 입력값 상태(watch)와 에러 상태(errors)를 확인하는 코드를 간단하게 작성할 수 있습니다.
const { watch, formState: { errors }} = useForm<RequestSMSCertForm>(); const formValues: RequestSMSCertForm = watch(); const isFormValid: boolean = useMemo(() => { const { inputKeys } = currentStep; // 현재 Step의 입력 상태 확인 const isFormFieldEmpty: boolean = inputKeys.some(key => !formValues[key]); const hasFormErrors: boolean = inputKeys.some(key => errors[key]); return !isFormFieldEmpty && !hasFormErrors; }, [currentStep, formValues, errors]); const handleClickNextButton = () => { if (!isFormValid) { // 다음 Step으로 이동 막기 return; } ... }
TypeScript
복사
▲ Step에 대한 hook form + validation 예시

크로스 브라우징 이슈

새로운 본인인증 화면에선 사용자 액션을 최소화하기 위해 다음과 같은 요구사항이 있었습니다.
입력을 마친 후 ‘다음’ 버튼 클릭하면 다음 입력 필드가 자동으로 포커싱되어야 한다.
모바일 브라우저의 경우 입력을 마친 후 바로 버튼을 누를 수 있도록 하단 ’다음’ 버튼을 키패드 위로 위치시켜야 한다.
하지만, 구현하는 단계에서 IOS 모바일 브라우저에서 발생하는 크로스 브라우징 이슈를 해결해야 했습니다.
1. 키패드가 올라오지 않는 문제
IOS 모바일 브라우저에서 입력 필드 Input에 input.focus 메서드를 실행하여 자동 포커싱을 주어도 키패드가 올라오지 않았습니다. async function 내에서 input.focus를 실행할 경우 모바일 Safari 비동기 동작의 이슈로 제대로 작동하지 않는 현상입니다.
포커스가 되어 있는 상태에서는 포커싱를 옮기는 건 가능하다는 점을 이용하여, 새로 렌더링되는 Input에 포커싱 하기 전 DOM에 fakeInput을 생성해 포커싱을 먼저 준 뒤 Timeout function의 Callback으로 해당 Input에 포커싱을 주어 해결할 수 있었습니다.
async function setFocusTimeout(executeFocus: () => void, delay: number) { return new Promise<boolean>((resolve) => { const fakeInput: HTMLInputElement = document.createElement('input'); fakeInput.setAttribute('type', 'text'); fakeInput.style.position = 'absolute'; fakeInput.style.opacity = '0'; fakeInput.style.height = '0'; fakeInput.style.fontSize = '16px'; fakeInput.readOnly = true; document.body.prepend(fakeInput); fakeInput.focus({ preventScroll: true, }); setTimeout(() => { executeFocus(); fakeInput.remove(); resolve(true); }, delay); }); } // 사용 구간 const inputFocusHandler = (targetInputKey: CertificateFormNames) => { ... setFocusTimeout(() => { setFocus(targetInputKey); // 다음 입력 필드 포커싱(hook form setFocus) }, EVENT_INPUT_TRIGGER_DELAY); };
TypeScript
복사
▲ fakeInput 유틸함수 예시
적용 전
적용 후
2. 버튼이 키패드에 가려지는 문제
iOS 모바일 브라우저에서는 Document가 키패드의 가상영역 뒤로 감춰지게 됩니다. iOS 브라우저는 키패드 높이를 포함한 화면 전체가 Viewport 영역이고 안드로이드 브라우저는 키패드 높이를 제외한 영역을 Viewport 영역으로 사용하기 때문입니다.
본인인증 화면 하단에 보여지는 ‘다음’ 버튼은 position 속성이 fixed로 Document 하단에 위치해있기 때문에 키패드 영역에 버튼이 가려지는 문제가 있었습니다.
이를 간단히 해결하기 위해 window.visualViewport 이벤트 리스너에 resize 이벤트를 등록하였습니다. 키패드가 올라올 때 가상영역의 크기가 변화하기 때문에 키패드의 상태를 감지할 수 있었고, 키패드 상단 위치를 계산한 offsetY 값을 버튼에 전달해주었습니다.
useEffect(() => { const updateButtonPosition = (event: Event) => { const { height, offsetTop } = event.target as VisualViewport; const offsetY: number = height - containerRef.current.getBoundingClientRect().height + offsetTop; buttonWrapRef.current.style.transform = `translateY(${offsetY}px)`; }; window.visualViewport.addEventListener('resize', updateButtonPosition); return () => { window.visualViewport.removeEventListener('resize', updateButtonPosition); }; }, []);
TypeScript
복사
▲ visualViewport resize 이벤트 예시
적용 전
적용 후

챌린지 곁들이기

본인인증의 UI/UX 개선에 대한 이야기는 여기까지 마치며, 이번 본인인증 개선 과제를 진행하며 Frontend팀에서 사용하던 기존 본인인증이 가지고 있는 구조 측면의 문제점 개선을 진행해보았던 이야기를 덧붙여보고자 합니다.
아이들나라 Frontend팀은 어드민을 제외한 서비스향 프로젝트들을 TurboRepo를 활용해 모노레포로 관리하고 있습니다.
모노레포를 통해 도메인의 성격이 다른 프로젝트들을 한 레포지토리 안에서 관리하며 다음과 같은 모노레포가 가져오는 이점들을 챙기고 있습니다.
모노레포로 묶인 모든 프로젝트를 쉽게 검색 및 수정할 수 있다.
유사한 기능을 가진 컴포넌트나 함수를 추상화하고 공통 모듈화하여 중복 작업을 줄일 수 있다.
package.json의 종속성 관리나 환경설정(ESlint, TSConfig 등)을 구성하기 쉽다.

모노레포 안티패턴?

하지만 Frontend팀에서는 기존 본인인증을 개발하며 생긴 모노레포 내 구조적인 문제점이 한가지 있었습니다.
기존 본인인증webview 프로젝트 안에 개발되어 있었습니다. 본인인증 기능이 필요한 memberwww 프로젝트에서는 기존 본인인증 컴포넌트를 가져와 사용하기 위해 webview 프로젝트에 대한 패키지 의존성이 생기게 되었습니다.
이러한 구조는 Frontend팀에서 기존 본인인증 개발 이후에 관련된 많은 양의 코드를 추상화 및 공통화할 여유가 되지않아 생긴 기술 부채이며, 동시에 모노레포를 사용하며 생길 수 있는 안티패턴 정도로 이야기해보고 싶습니다.
의존성 관리의 방해
프로젝트간 의존성이 생기게 되면서 의존성 관리에 직접적인 방해가 생길 수 있습니다.
Frontend팀은 패키지 매니저로 Yarn v1을 사용하고 있습니다. Yarn v1은 중복해서 설치되는 node_modules 때문에, 디스크 공간을 아끼기 위해 아래 예시와 같이 호이스팅 기법을 사용하여 의존성 트리를 평탄화합니다.
memberwww 프로젝트의 패키지는 의존성 트리가 바뀌면서 원래 require() 할 수 없었던 모듈을 불러올 수 있게 되었습니다. 이런 현상을 유령 의존성 현상이라고 부르기도 합니다.
버전 관리의 문제
현재 레포지토리에 구축된 배포 환경 기준으로는 webview 프로젝트에 포함되어있는 본인인증 관련한 코드가 변경되어도 member, www 프로젝트는 배포가 이루어지지 않아서 버전 관리에 실패하게 됩니다.
다른 프로젝트를 build 하기 위해서 의미없는 변경점을 커밋하여 배포를 진행하고 있던 상황입니다.

재사용 가능한 본인인증 만들기

그래서, 모노레포 Packages 디렉토리 아래에 iframe을 사용한 본인인증 컴포넌트를 작성하여 코드를 공유하는 것을 생각했습니다.
신규 본인인증은 비즈니스 도메인에 맞게 webview 프로젝트에서 통합회원 프로젝트인 member로 코드를 이관하여 개발하였고, iframe은 member 도메인의 신규 본인인증 페이지를 호출하도록 하였습니다.
iframe을 사용한 이유?
다양한 페이지에서 같은 콘텐츠를 여러 번 사용해야 할 때, iframe을 사용하여 중복 작업을 피할 수 있습니다.
또, iframe 안의 콘텐츠는 외부 웹 페이지와 분리되어 독립적으로 동작하기 때문에, 외부 콘텐츠의 변화가 현재 페이지에 영향을 주지 않을 수 있습니다.
iframe을 사용하며 webview, member, www 도메인으로 흩어져있던 본인인증 도메인이 member 하나로 관리되면서 본인인증에 대한 이슈 트래킹이 수월해지게 된 것도 이점이라고 볼 수 있습니다.
모듈 컴포넌트 구현하기
다음과 같이 iframe과 인증 결과처리에 대한 로직을 가지고 있는 모듈 컴포넌트를 작성하였습니다.
const CertificateIframeModule FC<PropTypes> = ({ certType, getCertInfo, onCancel, }) => { /** Hook에서 부모 window, Iframe 간 메시지로 전달 받아 인증 완료/취소 처리 */ useIframeMessage(getCertInfo, onCancel); return createPortal( <ModalContainer> <IframeWrapper src={iframeSrc} // member 도메인 본인인증 페이지 호출 sandbox="..." /> </ModalContainer>, document.body, ); }; export default const CertificateIframeModule;
TypeScript
복사
▲ 본인인증 모듈 컴포넌트 예시 (iframe + portal)
iframe을 담은 본인인증 모듈 컴포넌트를 작성하여 Packages 디렉토리를 통해 코드를 공유하는 TurboRepo의 일반적인 구조로 돌려놓을 수 있었습니다.
의도했던 대로 모듈 컴포넌트를 모노레포 프로젝트 어디서든 불러와 사용해도 패키지 의존성 없이 본인인증 화면을 노출할 수 있게 되었습니다.

마치며

아이들나라에서 다양한 화면을 구현하고, 그 과정에서 겪게되는 크로스 브라우징 이슈들을 해결하며 알게된 것들이 값진 경험으로 쌓이는 것 같습니다. 평소, 유저 인터랙션과 동적인 UI 요소가 많이 포함된 페이지를 개발해보고 싶었기 때문에 즐겁게 구현에 집중할 수 있었습니다.
아이들나라 사용자들이 더 좋은 사용 경험을 가질 수 있도록 지속적으로 개선해나가도록 하겠습니다.
읽어주셔서 감사합니다.