의욕 넘치던 시절엔 소프트웨어 공학 수업에서 배웠던 모든 걸 직접 해본다는 사실에 들떴었다.
(모두를 놀라게했던 당시 플로우 차트...)
아쉽게도 기획/설계 단계에서 여러 번 피봇팅을 거치면서 점점 저러한 합의나 문서화의 과정이 간소화되었다. 덕분에 약소한 와이어프레임만 보고 작업하다보면 이 부분은 어떻게 처리하지? 고민하게 될 때가 많다. 그 중 하나가 오늘 주제인 유효성 검사였다.
스프링을 한 번도 사용해보지 않은 나지만, 다른 팀원의 코드 리뷰를 위해 코드를 유심히 보고 있으면 대충 감이 오는 부분들이 있다. 듣기로는 스프링에서는 Validation에 대한 코드가 제공되어 간단한 annotation을 붙이면 유효성 검사가 자동으로 수행된다고 한다.
위 코드는 스크랩을 추가하는 부분에서의 코드인데, 유효성 검사의 메시지를 통해
NotBlank
는 공백으로 이루어졌는 지에 대한 검사URL
은 링크 형식인지에 대한 검사Size
는 입력 가능한 글자 수 제한을 지키고 있는 지에 대한 검사임을 알 수 있다.
진행중인 프로젝트에서는 다양한 input을 사용하고 있기 때문에 몇몇 유효성 검사는 중복되어 적용될 것이라고 판단했다. 따라서 유효성 검사와 관련된 hook을 만들어 보았다.
export function useIsBlank(text: string) {
return /^\s*$/.test(text);
}
해당 코드는 chatGPT의 도움을 받았다. 정규표현식을 사용하여 띄어쓰기, 탭, 줄바꿈 등의 공백을 표현하고, 이로 이루어졌는지 검사하는 코드로 작성하였다.
chatGPT를 이용하여 해당 함수를 만들 때 이 함수가 정말 공백으로만 이루어진 것을 걸러주는지 의심이 들었다. (실제로 처음 몇 번은 잘못 짜주었다.) 이를 확인해보기 위해 테스트 코드를 작성해 확인해보았다.
it('useIsBlank을 통해 공백으로 이루어졌는지 검사한다.', () => {
// 공백을 잡아내는지, 연속으로 사용해도 잡아내는지 확인
expect(useIsBlank('')).toBe(true);
expect(useIsBlank(' ')).toBe(true);
expect(useIsBlank(' ')).toBe(true);
// 줄바꿈이나 탭을 잡아내는지 검사
expect(useIsBlank(`\n`)).toBe(true);
expect(useIsBlank(`\t`)).toBe(true);
// 공백에 일반 문자가 섞여있을 때 false를 return하는지 확인
expect(useIsBlank(' a')).toBe(false);
expect(useIsBlank('a ')).toBe(false);
expect(useIsBlank(' a ')).toBe(false);
// 공백이 아예 없는 경우도 false로 판단하는지 확인
expect(useIsBlank('a')).toBe(false);
});
해당 함수는 검사할 문자열과 제한길이를 매개변수로 받아 둘을 비교하는 단순한 로직으로 구성하였다. 다만, 유효성 검사와 관련된 모든 정보를 이 hook에서 가지고 있게 하기 위해 글자수 제한을 export 가능한 상수로 빼내어 한 번에 제어할 수 있도록 하였다.
// 스크랩 링크 최대 길이 상수
export const SCRAP_LINK_MAX_LENGTH = 2083;
export function useIsLessThanLengthLimitation(text: string, limit: number) {
return text.length <= limit;
}
유효성 검사를 통과하지 못했을 경우 해당 내용을 사용자한테 3가지 방법을 통해 알리기로 했다.
const validation = () => {
if (!useIsLessThanLengthLimitation(textAreaValue, SCRAP_LINK_MAX_LENGTH)) {
return `최대 ${SCRAP_LINK_MAX_LENGTH}글자까지만 입력 가능합니다.`;
}
if (!useIsEntered(textAreaValue)) {
return ' ';
}
...
return 'success';
}
const isValidationSuccess = () => validation() === 'success';
위와 같이 필요한 유효성 검사 hook을 검사하고 false가 return되면 필요한 문구를 받고, 모두 통과할 경우 'success'라는 문구를 반환하는 함수를 만들었다. isValidationSuccess는 유효성 검사 자체가 성공 / 실패했는지 여부를 판단하기 위해 validation 함수 결과가 'success'인지를 판단하도록 만들었다.
<OutlinedInput
...
error={!isValidationSuccess()}
/>
현재 MUI에서 제공하고 있는 input을 사용하고 있는데, 이 컴포넌트들은 props에 에러가 발생했는지 여부를 error 속성에 넣어주면 이에 맞추어 테두리를 붉게 만들 수 있다.
유효성 검사를 실패했을 경우, 사용자에게 어떤 이유로 동작할 수 없는지 이유를 알려줘야 한다. 별도의 모달을 만들거나 토스트 등을 통해서도 알릴 수 있겠지만, 최대한 사용자의 시선 근처에 알려주기 위해 formHelperText를 사용하였다. (마찬가지로 MUI에서 제공중이다.)
<FormControl>
<OutlinedInput/>
<FormHelperText>
<Typography
color={isValidationSuccess() ? theme.color.Gray_060 : '#f44336'}
>
{isValidationSuccess() ? `${textAreaValue.length} / ${MAX_MEMO_LENGTH}자` : validation()}
</Typography>
</FormHelperText>
</FormControl>
input
과 FormHelperText
를 FormControl
이라는 컴포넌트로 감싸고, FormHelperText에 validation의 성공 유무에 따라 글자를 조건부 렌더링해주면 된다. 추가로 validation 결과에 따라 조건부 렌더링으로 글자 색도 바꿀 수 있다.
<Button
disabled={!isValidationSuccess()}
>
등록
</Button>
마찬가지로 MUI에서 제공하는 버튼을 사용하면 validation 결과에 따라 disabled의 boolean 값을 다르게 설정하여 유효성 검사를 통과했을 때에만 버튼을 누를 수 있도록 설정할 수 있다. 더불어 버튼 색상도 누르고 싶지 않은 회색으로 바뀐다.
예전에 보안 관련 멘토링을 들었을 때 input을 통해 공격을 하는 방법이 있으니 신경을 써야한다는 소리를 들은 적이 있었다. 그 때 들었던 보안 문제는 XSS 공격
으로 악의적인 사용자가 input text에 스크립트를 삽입하여 사용자의 정보를 탈취하거나 비정상적인 행위를 하려고 하는 공격이었다.
다행히도, React는 JSX에 삽입된 값을 렌더링하기 전에 이스케이프하는 특성이 있다. 이스케이프란 특정 문자가 변환되는 행위이다. 이 특정 문자 중에는 스크립트 태그를 삽입하기 위해 필요한 부등호가 존재한다. 즉, <은 <
으로, >은 >
으로 변경되기 때문에 이스케이프로 변환되면 태그로써의 기능을 상실하여 XSS 공격이 방지된다. (역시 믿고쓰는 리액트...!👍👍👍)
인터넷에 리액트/프론트엔드로 유효성 검사하는 것에 대해 검색하면 잘 나오지 않는다. 당연하다. 왜냐하면 아주 유명한 라이브러리가 있기 때문이다.
React Hook Form
은 hook 기반으로 쉽게 form 형식을 다룰 수 있도록 도와주는 라이브러리라고 한다.
yup
은 React Hook Form과 자주 같이 사용되는 라이브러리로, 객체 스키마 유효성 검사에 사용된다.
현재는 직접 만든 코드로 유효성 검사를 실행하면서 자주 쓰일 기능을 hook으로 빼는 연습과 관련 테스트 코드 짜는 연습을 해볼 수 있었다. 하지만 유효성 검사는 다른 부분보다도 정확해야할 코드이기 때문에 점차 위 라이브러리로 변경해나가야 할 것 같다.