본문으로 건너뛰기

이메일 저장하기

· 약 9분
arch-spatula

이메일 저장하는 방법입니다. 하지만 이 방법을 적용하기 전에 왜 저장해야 하는가? 이것은 ux 문제입니다.

프론트엔드 엔지니어를 위한 짧은 UX 지식

보통 PM과 디자이너가 요구사항을 정의할 때 무엇이 좋은 UX를 제공하는지 정하는 기준 중 하나는 action cost입니다. 이 action cost라는 것은 사용자가 제품을 사용하기 위한 노력 비용 혹은 동작 비용이라고 직역할 수 있습니다. 프론트엔드 엔지니어가 UX 소양을 갖고 있다는 것은 action cost 관리를 잘한다는 것입니다.

대표적인 경우 검색의 자동완성도 해당합니다. 검색을 위해 모든 것을 다 입력하기 전에 사람들이 많이 찾는 정보를 제시하고 선택할 수 있게 합니다. 모든 입력을 위한 노력을 아껴줄 수 있습니다.

참고로 프론트엔드 엔지니어의 작업은 대부분은 서버 요청비용 관리입니다. 최소한의 요청으로 실제 비용관리를 잘하는 프론트엔지니어가 UX 소양이 있는 엔지니어보다 훨씬더 가치가 있습니다. 이 action cost 관점에서 사용자의 동작최적화는 PM과 디자이너의 역할입니다.

왜 이메일을 저장해줘야 하는가?

action cost 관점으로 생각해보겠습니다. 특별한 기능지원이 없다면 유저는 매번 서비스를 접근하고자 할 때마다 들여야 하는 동작 즉 이메일 입력, 비밀번호 입력이 필요합니다. 여기서 이메일을 저장해주면 비밀번호 입력만 하면 되기 때문에 노력비용을 반으로 줄일 수 있습니다. DAU를 높이기 위해서 동작을 이런식으로 간소회 시킬 수 있습니다.

많은 서비스들이 사용자 정보를 저장하고 다시 방문했을 때 이메일, 비밀번호도 없이 로그인할 수 있게 해주는 것도 이런 이유로 지원해줍니다. 노력비용이 쌓이지 않고 url, 즐겨찾기 클릭 정도 수준의 노력 비용으로 낮은 지불의사에 맞춰줄 수 있습니다.

여기서 더 좋은 것은 실제 서비스처럼 비밀번호 입력 노력도 덜어주는 것입니다. 하지만 지금은 보류하겠습니다. 그 짐은 미래의 저에게 전가하겠습니다.

이메일 저장하기 적용

useInput
import React, { useCallback, useRef, useState } from 'react';

export function useInput(init = '') {
const [inputVal, setInputVal] = useState(init);
const inputRef = useRef<HTMLInputElement>(null);

const changeInputVal = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setInputVal(e.target.value);
},
[]
);

const resetInputVal = useCallback(() => {
setInputVal(init);
}, [init]);

const focusInput = useCallback(() => {
inputRef.current?.focus();
}, []);

return { inputVal, changeInputVal, resetInputVal, focusInput, inputRef };
}

useInput이 이렇게 있습니다. 생각보다 사용이 많고 다른 hook들도 의존을 많이 합니다. util 계층에 있는 hook으로 분류하는 것이 좋을 것 같습니다.

// ... import 생략

function Component() {
const {
inputVal: emailValue,
changeInputVal: changeEmail,
inputRef: emailRef,
focusInput: focusEmail,
} = useInput(localStorage.getItem('email') ?? '');

const [isChecked, setIsChecked] = useState<boolean>(
!!localStorage.getItem('email')
);

// ...생략

const signIn = async () => {
// ... api 호출 생략
if (isChecked) localStorage.setItem('email', emailValue);
};

const handleSaveEmail = () => {
if (isChecked) {
localStorage.removeItem('email');
setIsChecked(false);
} else {
localStorage.setItem('email', emailValue);
setIsChecked(true);
}
};

return (
<>
<input
type="email"
onChange={changeEmail}
value={emailValue}
customRef={emailRef}
placeholder="user@email.com"
/>
<input type="checkbox" value={isChecked} onClick={handleSaveEmail} />
</>
);
}

export default Component;

이렇게 소비하면 됩니다. 저장할지말지를 storage를 기준으로 보존하고 상태를 제어합니다.

이렇게 하고도 새로고침해도 여전히 input에 이메일이 남아있을 것입니다.

custom hook 추출

useEmailSave.ts
import { STORAGE_KEY } from '@/constant/config';
import { useInput } from '..';
import { useState } from 'react';

export function useEmailSave() {
const {
inputVal: emailValue,
changeInputVal: changeEmail,
inputRef: emailRef,
focusInput: focusEmail,
} = useInput(localStorage.getItem(STORAGE_KEY.EMAIL) ?? '');

const [isChecked, setIsChecked] = useState(
!!localStorage.getItem(STORAGE_KEY.EMAIL)
);

const storeEmail = () => {
if (isChecked) localStorage.setItem(STORAGE_KEY.EMAIL, emailValue);
};

const handleSaveEmail = () => {
if (isChecked) {
storeEmail();
setIsChecked(false);
} else {
localStorage.setItem(STORAGE_KEY.EMAIL, emailValue);
setIsChecked(true);
}
};

return {
emailValue,
changeEmail,
emailRef,
focusEmail,
handleSaveEmail,
storeEmail,
isChecked,
};
}

이렇게 정의하고 조합합니다. useInput은 더 상위 계층인 util로 간주하고 호출로 주입받습니다. 그리고 이메일을 localStorage에 읽고 쓰는 로직을 모두 몰아 넣습니다.

function Component() {
const {
emailValue,
emailRef,
storeEmail,
focusEmail,
changeEmail,
handleSaveEmail,
isChecked,
} = useEmailSave();

// ...생략

const signIn = async () => {
// ... api 호출 생략
storeEmail();
};

return (
<>
<input
type="email"
onChange={changeEmail}
value={emailValue}
customRef={emailRef}
placeholder="user@email.com"
/>
<input type="checkbox" value={isChecked} onClick={handleSaveEmail} />
</>
);
}

export default Component;

소비할 때는 이렇게 호출하면 됩니다. 로직처리할 함수를 꺼내서 조합하면 됩니다.

결과

최종 적용은 이렇게 됩니다.