웹 에이전시에서 개발을 하다 보니까 프론트엔드에서 프로젝트마다 자주 필요하지만, 자체 개발을 하려면 시간이 많이 드는 것들이 있다.
- 로그인 및 회원가입 폼
- 페이지네이션
- 필터
- 테이블 폼
주로 위와 같은 것들인데 이런 요소들을 기획서에서 본다면, 기획에 따라서 구현 산정 시간이 기본 4H+ 로 잡혔던 것 같다.
물론 요즘 MUI 같은 UI 라이브러리들이 잘 나와서 이런 것들을 잘 활용하면 빠르게 개발할 수 있지만, 자체 개발을 해야 하는 경우가 한 번씩은 생기기 마련이다.
그래서 해커톤이나 자체 개발을 할 때 시간을 좀 단축할 수 있는 템플릿을 만들어 두면 좋을 것 같다는 생각이 들어서 이번에는 폼과 관련된 로직을 만들어 보았다.
실행 방법
프론트 파일의 경우 위의 프로젝트를 git clone 한 뒤
$ yarn install
$ yarn dev
를 입력해주면 된다.
실제 테스트를 해보고 싶다면 아래 백엔드를 다운로드한 뒤,. env에 mongoDB에 관한 세팅을 해주고
$ npm install
$ npm start
를 입력해주면 된다.
주요 기능
메인 화면
Default
메인 페이지는 처음에 간단한 모달이 보여지고 "닫기"를 누를 시 본 페이지를 사용할 수 있다.
특정 사황에 대한 에러처리
만약 잘못된 경로로 접근할 시 에러 메세지를 보여줄 수 있도록 해두었다.
또한 ErrorBoundary를 이용하여서 특정 상황에서 에러가 발생했을 시 에러를 반환하게 해 두었다.
로그인
Default
Submit 예외 처리
로그인 페이지에서는 로그인 기능과 예외 처리를 할 수 있도록 해두었다.
mutation의 isPending을 사용해 처리 되고 있음을 유저에게 보여줌으로써 UX를 챙길 수 있도록 하였다.
예외 상황에 대한 처리는 axios와 useMutation hook을 이용해 에러 메세지를 상황에 맞게 띄울 수 있도록 했다.
회원가입
Default
Input 예외 처리
Submit 예외 처리
회원가입 페이지에서는 Input과 Select, Dropdown field에 대해서 validate 할 수 있도록 해두었다.
로그인과 같이 로딩 상태와 예외 상황에 대한 처리를 해두었다.
validation에 대한 메세지를 유저에게 보여주는 타이밍에 관해서 고민을 많이 했는데, 하나씩 검증이 되고 회원가입 버튼을 눌렀을 때 입력이 안된 필수 값이 있다면 메시지를 반환하게 끔 해두었다.
고민 한 부분
Axios Interceptor 구현
비동기 통신 라이브러리로 axios를 사용해서, 인터셉터를 설정해 입력과 출력에 관한 설정을 해두었다.
응답을 받을 때는 DX를 높히기 위해 최대한 상황에 따른 처리를 할 수 있도록 해 두었다.
선언적인 에러처리
앞서 예외처리한 부분에 나와있지만 사용자 경험을 개선할 수 있도록 노력했다.
이를 위해서 react-query와 Suspense, ErrorBoundary를 사용해 로딩과 에러에 대한 처리를 하였다.
Login 페이지에서 비동기 처리가 이뤄지는 부분을 LoginForm 컴포넌트로 따로 분리하고 에러와 로딩 처리를 위해 ErrorBoundary와 Suspense로 감싸주었다.
(사실 LoginForm을 구성하는 요소 중에서 비동기로 fetching 되는 데이터가 없기 때문에 LoginSkeleton이 보이진 않지만.. 연습 삼아 강제로 지연시키면 보일 수 있게 한 번 만들어 봤다.)
컴포넌트 분리에 대한 고민
LoginForm을 컴포넌트로 분리해서 Login 페이지의 코드가 간결해 졌다.
하지만 Login 페이지의 구성을 파악하기 위해선 LoginForm을 확인해야 되는 불편함이 생겼다.
컴포넌트를 구성할 때 최대한 재사용성을 생각하면서 만들어서 대다수의 컴포넌트를 합성 컴포넌트 패턴을 이용해 만들었는데, 약간 불필요하게 코드의 길이가 길어지는 단점이 있다.
사실 Input 컴포넌트만 아니면 LoginForm을 분리할 필요도 없이 Login 페이지 내에서 모든 구성을 보여주는 것이 DX적으로는 더 좋은 판단이었을 것 같다.
합성 컴포넌트 패턴의 장단점
해당 프로젝트의 다른 목적 중 하나는 합성 컴포넌트 패턴을 최대한 사용해려고 하여서 대부분의 컴포넌트를 합성 컴포넌트 패턴으로 작성했다. 내가 이 패턴을 잘 사용한 지는 모르겠는데.. 합성 컴포넌트 패턴을 사용했을 때 느껴지는 점은 다음과 같았다.
장점 1. 사용처에서 사용가능한 컴포넌트를 명확히 알 수 있다
위의 두 컴포넌트는 합성 컴포넌트 패턴으로 만들었다.
합성 컴포넌트 패턴은 쉽게 말하면 연관된 컴포넌트들이 내부 상태와 로직을 공유하면서도 유연한 UI 구성을 가능하게 하는 패턴이다.
합성 컴포넌트 패턴은 다음과 같이 Object.assign을 통해 사용가능한 컴포넌트들을 담아두는 구조를 띄는데, 이를 통해 사용처에서 해당 컴포넌트 내에 사용가능한 컴포넌트가 무엇이 있는지 명확히 알 수 있어서 좋았다.
장점 2. 쉽게 파악이 가능한 컴포넌트 구조
합성 컴포넌트 패턴으로 컴포넌트를 구성하면 역할에 맞게 컴포넌트가 잘 분리되어 있어, 화면을 보지 않아도 페이지가 어떻게 생겼는지 쉽게 파악가능해서 좋았다.
장점 3. 유지보수 및 재활용성이 좋다
합성 컴포넌트 패턴의 가장 큰 장점은 유지보수와 재활용성이라 생각한다.
각 페이지에 맞게 필요한 형태에 따라 컴포넌트의 구성을 달리 하면 되어서 한 컴포넌트를 가지고 여러 페이지에서 다르게 보이게 할 수 있어서 좋았다.
만약 합성 컴포넌트를 쓰지 않았다면 부모 컴포넌트에서 props를 전달해서 각 페이지마다 다르게 보여줘야 했을 텐데, 이는 유연성 부족과 복잡한 상태관리를 야기했을 것 같다.
단점 1. 과한 추상화로 인해 생기는 불필요한 컴포넌트
해당 Input 컴포넌트를 보면 꽤나 많은 컴포넌트들이 내포되어있다고 느껴진다.
그 이유는 InputGroup이나 InputFieldset 같이 wrapping을 담당하는 컴포넌트들 때문인데, 다른 컴포넌트들은 각자의 역할이 명확한 반면 두 컴포넌트는 사실 정렬의 기능을 하는 것 말고는 큰 의미가 없다.
지금 돌이켜 보면 이런 부분들은 오히려 사용처에서 처리가 되었어야 했나 라는 생각이 들기도 한다.
적절한 추상화를 통해 불필요한 컴포넌트가 생기지 않도록 주의해야겠다는 생각이 들었다.
단점 2. 길어지는 컴포넌트 길이
합성 컴포넌트 패턴은 가독성 적인 측면에서 불편함이 있었다.
한 컴포넌트 내에 props가 많아지게 되면 eslint 때문에 줄내림이 발생해 괜히 코드의 길이가 길어져 복잡하게 보이는 것 같다.
이런 점 때문에 Login 페이지 내에서 작성해도 되었을 컴포넌트를 LoginForm이라는 컴포넌트로 묶어서 관리하게 되었는데, 이런 고민을 해보면서 좋은 컴포넌트를 설계하는 방법에 대해서 조금 더 찾아봐야겠다는 생각이 들었다.
단점 3. 과한 자율성으로 인한 문제
자식 컴포넌트들 간의 의존성이 필요한 컴포넌트는 굳이 합성 컴포넌트 패턴을 사용하지 않는 것이 낫다고 생각했다.
Dropdown 컴포넌트의 경우엔 그냥 컴포넌트 합성을 통해 구현했는데, 그 이유는 Button 만 있거나 List만 있다면 의미가 없는 컴포넌트이기 때문이다.
그래서 처음엔 Dropdown을 합성 컴포넌트 패턴으로 구현을 했다가 과한 자율성은 오히려 문제를 발생시킬 수 있겠다는 생각이 들어서 다음과 같이 코드를 작성했다.
Form의 각 Field를 관리하기 위한 hook 구성
폼을 구성하기 위해선 생각보다 많은 로직을 필요로 한다.
그래서 어떻게 하면 효율적으로 각 필드를 관리할 수 있을지 생각해 보았고 다음과 같이 설계를 하고 코드를 짰다.
useField (필드 레벨 추상화)
↓
useForm (폼 레벨 추상화)
↓
useSignupForm (비즈니스 로직 추상화)
---
util/validator -> 따로 분리
다음과 같이 hook을 구성하니까 다음과 같은 장점들이 있었다.
- 각 hook들을 개별적으로 써도 됨.
- LoginForm, SignupForm 등 여러 Form에 대해 대응가능
- 비즈니스 로직과 UI 로직의 분리
각각에 필요한 기능들을 미리 생각해 보고 코드를 작성하니 보다 재활용성이 높은 코드를 작성할 수 있었다.
타입 관리
이번 개발을 하면서 가장 시간이 많이 든 곳이 있다면 타입을 관리하는 부분인 것 같다.
툭하면 eslint에러가 나고 타입이 맞지 않다고 하니까 이를 해결하기 위해 타입 단언(as)을 사용한 부분도 일부 있다.
위의 코드와 같은 경우에 newValue는 string 타입을 갖게 되는데, setValue의 경우 타입을 제네릭으로 선언해 두어서 계속해서 타입에러가 나서 어쩔 수 없이 타입 단언을 사용했다.
타입 단언을 사용하지 않으려는 이유는 틀린 타입이더라도 타입을 강제해서 에러를 무시한다는 점 때문인데, 잘못 사용하면 타입스크립트를 사용하는 의미를 없애 버릴 수도 있겠다는 생각이 들었다.
다만 해당 부분의 경우 string 타입의 값이 안정적으로 올 것이라는 생각이 들어 안전하다고 생각해 타입 단언을 부득이하게 사용하게 되었다.
후기
이번 프로젝트를 통해 좋은 코드와 UX, DX에 대해서 한 번 되돌아보는 계기가 되었다.
코딩을 하면서 느끼는 점은 Best는 없을 수 있지만 Better은 무조건 존재한다는 점이다.
그래서 프로젝트를 처음에 완성하고도 여러 번 갈아엎으면서 리팩토링을 하였는데, 리팩토링을 하면서 과한 추상화가 아닌가 돌아보게 되고 어디까지가 좋은 코드라고 할 수 있는지 많은 고민을 했던 것 같다.
이 부분에 있어선 github에서 다른 프로젝트들의 코드를 살펴보는 게 많은 도움이 되었던 것 같다.
아는 게 많아질수록 스스로의 부족함이 보이는 느낌이랄까.. 참고할 수 있는 많은 자료들을 모아놔야겠다.
여튼 앞으로 이런 간단한 프로젝트들을 몇 가지 해보면서 실험도 해보고.. 좋은 코드를 작성하기 위해서 노력해야겠다는 생각이 들었다.
'활동 > 개인 프로젝트' 카테고리의 다른 글
네이버 특정 블로그에서 가장 많이 사용한 단어 알아보기! (0) | 2023.08.20 |
---|---|
Monte Carlo Walker (ref. Nature Of Code) (0) | 2022.02.25 |
Gaussian line (ref. Nature Of Code) (0) | 2022.02.20 |
맥주 음미 노트 (개발X / 디자인X / Figma) (0) | 2021.11.29 |
Random Walker (0) | 2021.11.03 |