개발새발 로그

oWhat 프로젝트를 진행하면서.. 본문

React

oWhat 프로젝트를 진행하면서..

이즈흐 2024. 1. 18. 23:17

팀 프로젝트를 진행하면서 만났던 문제나 느꼈던 점, 아쉬웠던 점을 정리해보려고한다.

기억이 아직 남아있을 때 얼른 적어야지!

 

나중에 이 부분들을 따로 나눠서 검색하기 용이하게 만들 예정이다!!

 

 

1. 리액트 쿼리를 사용한 이유?

- 비동기 데이터를 관리하는데 편리

- Mobx나 Redux를 사용하다보면 BoilerPlate 형태의 코드가 발생하게 되는데, 리액트 쿼리는 비교적 코드의 양이 적고 구조가 단순하여 추후 유지보수가 용이해서

- Caching을 통해 애플리케이션 속도를 향상 - 동일한 데이터에 대한 중복요청을 제거함

- 가비지 컬렉터를 이용해 서버 쪽 데이터 메모리를 관리

- 리액트 Hooksdhk 유사한 인터페이스로 사용이 편리

- 비동기 과정을 선언적으로 관리 가능 

 

정리하자면 리액트 쿼리는 데이터 요청, 변경에 관련된 코드를 단순화 시켜줌
이를 통해 서버와의 데이터 요청과 응답을 처리하는 코드를 작성하는데 더 적은 시간을 투자할 수 있음
결과적으로 애플리 케이션의 성능과 유지보수성을 높임

 

BoilerPlate 형태?

- 최소한의 변경으로 여러 곳에서 재사용되지만, 반복적인 코드로 인해 많은 양의 코드를 양산하는 것

 

 

 

2. vite를 사용한 이유?

- ES build를 사용하여 종속성을 미리 묶는다.

- ES build는 Go Lang으로 작성되어 js 기반의 번들러보다 10~100배 빠른 속도로 종석성을 사전 번들링한다.

- HMR 제공

- 번들을 생성하는 과정이 필요없어서버의 시작속도가 매우 빠름

- 모듈화된 컴포넌트의 수정사항을 브라우저로 확인 가능

HMR 이란

- Hot Module Replace 의 줄임말로 브라우저를 새로고침하지 않아도 웹팩으로 빌드한 결과물이 웹 애플리케이션에 실시간으로 반영될 수 있게 도와주는 설정이다

 

정리하자면

- ES Build는 Golang을 써서 빠름 - 개발단계에서 빠름
- 빌드할 때는 rollup을 써서 정교하게 해준다.

결과적으로 Vite를 사용함으로써 빠른 개발 환경을 제공하고, 빌드 결과물이 가벼워서 웹 애플리케이션의 초기 로딩 속도를 향상시킬 수 있도록 도와줍니다.

3. vite가 빠른 이유?

 - ES 모듈을 지원하기 전까지 js 모듈화를 네이티브 레벨에서 진행 못했음

 - 그래서 번들링이라는 것이 나옴 (Webpack,Rollup, Parcel)

 - 하지만 모듈의 개수가 증가할 수 록 js 기반의 도구는 성능 병목 현상 발생, 서버 가동에 오랜시간을 기다려야 했음

 - 이를 해결하기 위해 브라우저에서 지원하는 ES 모듈 및 네이티브 언어로 작성된 js 도구 등을 활용

 - vite의 사전번들링 기능은 ES Build를 사용, Go로 작성되어 10~100배 빠름

 - vite는 HTTP 헤더를 활용하여 전체 페이지의 로드 속도를 높입니다. 필요에 따라 소스 코드는 304 Not Modified로, 디펜던시는 Cache-Control: max-age=31536000,immutable을 이용해 캐시됩니다. 이렇게 함으로써 요청 횟수를 최소화하여 페이지 로딩을 빠르게 만들어 줌

 

  1. ESM (ECMAScript Modules) 기반의 개발 서버: Vite는 개발 서버에서 모듈 단위로 파일을 서빙하며, 이는 빠른 개발 환경을 제공합니다. 모듈 단위로 서빙하면 필요한 모듈만 로드되고 의존성이 해결되기 때문에 초기 로딩 속도가 빠릅니다.
  2. 빠른 번들링 알고리즘: Vite는 Rollup을 기반으로 한 번들링을 사용합니다. Rollup은 Tree-shaking(불필요한 코드 제거)과 같은 최적화 기술을 사용하여 번들 크기를 최소화하고 실행 속도를 향상시킵니다.
  3. HMR (Hot Module Replacement): Vite는 빠른 개발 주기를 지원하기 위해 HMR을 내장하고 있습니다. 이를 통해 코드 수정 후에도 브라우저를 새로고침하지 않고 변경 사항을 즉시 반영할 수 있습니다.
  4. 캐시를 활용한 효율적인 빌드: Vite는 캐시를 활용하여 변경되지 않은 파일은 다시 빌드하지 않고 캐시된 결과를 사용하여 빠른 빌드를 제공합니다.
  5. 웹 네이티브 ES 모듈 지원: Vite는 브라우저에서 네이티브 ES 모듈을 지원하는 것에 중점을 두고 있습니다. 이는 번들링이 아닌 네이티브 모듈의 형태로 파일을 서빙하며 성능상의 이점을 제공합니다.

ES Build란?

 JavaScript 번들을 빠르게 읽을 수 있는 CLI, NPM package 입니다.

Go와 Javascript / TypeScript로 개발되었으며 2020년 초에 처음 출시되었습니다.

esbuild는 잘 짜여진 documentation과 쉬운 CLI 환경을 갖추고 있으며 결과적으로 매우 빠릅니다.

Es Build가 빠른 이유

  1. Go 언어 사용: Esbuild는 Go 언어로 작성되었습니다. Go 언어는 컴파일 속도가 빠르고 효율적인 메모리 관리를 제공하기 때문에 Esbuild는 경쟁 도구보다 빠른 성능을 발휘할 수 있습니다.
  2. 병렬 처리 및 캐시 활용: Esbuild는 병렬 처리를 통해 여러 작업을 동시에 처리할 수 있습니다. 또한, 변화가 없는 부분에 대한 캐싱을 효과적으로 사용하여 중복된 작업을 최소화하고 빌드 속도를 높입니다.
  3. Tree-shaking 및 압축: Esbuild는 트리 쉐이킹과 코드 압축을 효과적으로 수행하여 불필요한 코드를 제거하고 번들 크기를 최소화합니다.
  4. V8 엔진 사용: Esbuild는 V8 JavaScript 엔진을 사용하여 코드를 효율적으로 실행합니다. V8 엔진은 Google Chrome에서 사용되는 엔진으로 높은 성능을 자랑합니다.
  5. 모듈 번들링과 플러그인 시스템: Esbuild는 모듈 번들링을 지원하며, 여러 모듈을 하나로 결합하여 최적의 번들을 생성할 수 있습니다. 또한, 다양한 플러그인을 통해 기능을 확장할 수 있습니다.
  6. 미리 컴파일: Esbuild는 코드를 미리 컴파일하여 실행 속도를 향상시킵니다. 이는 번들링 후에도 빠른 실행을 보장합니다.

Go Lang이란?

- Google에서 개발한 오픈소스 프로그래밍 언어

- 간결하고 강력한 문법을 가지고 있음

  1. 간결한 문법: Go 언어는 간결하고 읽기 쉬운 문법을 가지고 있어 개발자들이 코드를 이해하고 유지보수하기 쉽습니다.
  2. 정적 타이핑: Go는 정적 타이핑 언어로서, 변수의 타입을 컴파일 시간에 결정하며, 이를 통해 코드의 안정성을 높입니다.
  3. 가비지 컬렉션: Go 언어는 자동으로 메모리를 관리하는 가비지 컬렉션을 지원하여 개발자가 명시적으로 메모리를 해제할 필요가 없습니다.
  4. 동시성 지원: Go는 경량 스레드인 고루틴(Goroutine)과 채널(Channel)을 이용한 강력한 동시성 지원을 제공합니다. 이를 통해 병렬 프로그래밍이 쉬워지고 높은 성능을 발휘할 수 있습니다.
  5. 효율적인 컴파일 속도: Go 언어는 빠른 컴파일 속도를 가지고 있어 개발자가 코드를 수정하고 테스트하는 데 시간을 절약할 수 있습니다.
  6. 모듈 시스템: Go 언어는 간단하면서도 강력한 모듈 시스템을 제공하여 코드의 구조화와 의존성 관리를 용이하게 합니다.
  7. 크로스 플랫폼 지원: Go는 여러 플랫폼에서 컴파일되고 실행될 수 있는 크로스 플랫폼 언어로 설계되었습니다.

3. Storybook을 사용한 이유?

 

 

4. Carousel 컴포넌트를 만들면서 어려웠던 점

 

1. slide 컴포넌트를 traslateX를 이용해서 구현하면 기능이 한정적으로 될 것 같다는 생각을 함
 translate로 버튼을 누르면서 정해진 너비에 맞춰 이동은 가능하지만 이후 마우스 이벤트로 했을 때 translate값이 동적으로 바뀌어야 가능했음

Tailwind를 사용하는 상황에서 동적으로 스타일은 변경하는 것이 좋지않은 방법일 수 있다고 생각함.

 이보다 쉬운 방법으로 div 컨테이너에 ref를 등록하고, ref의 scrollLeft값을 빼고 더하는 로직을 이용해서 버튼으로 scroll값을 증감시켜 나열된 데이터를 볼 수 있게 할 수 있음

2. scroll값을 이용해 기능을 구현할 때 전체 너비와 Carousel아이템의 너비가 필요했음 그래서 children의 요소 하나의 너비를 갖고와서 그 값으로 silde의 최대 너비와 scroll되는 값을 정하려고함
 하지만 children의 너비를 갖고오기 위해서는 해당 요소가 마운트된 후에 ref를 등록하고 width를 가져와야했음
 그렇게 되면 해당 값을 또 ref 나 useState에 저장을 해야하는데 그러면 렌더링이 한번 더 일어나게 됨
 이 부분이 불필요하다고 생각해서 props로 너비 자체를 받아오도록 함 - 상위 컴포넌트에서 children의 너비를 결정

const Carousel = ({
  children,
  itemsToShow = 4,
  childSize = 100,
  useButton = false,
  groupGap = 5,
  className,
  ...props
}: CarouselProps) => {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const itemToShowWidth = itemsToShow * (childSize + groupGap) + childSize / 2;
  const totalCarousels = Children.count(children);

  const avatars = Children.toArray(children).map((child: ReactNode) => {
    if (isValidElement(child)) {
      return cloneElement(child as JSX.Element, {
        style: { width: `${childSize}px` },
        className: `${child.props.className || ''} snap-start`,
      });
    }
  });

...

}

3. scroll값이 변할 때 transition 효과를 적용하는 방법

scroll이 부드럽게 되는 효과를 주고 싶었음.
그래서 scroll-smooth 라는 속성을 적용하고 스크롤 될 때 아래와 같이 구현함

const handleMouseMove = (e: React.MouseEvent | React.TouchEvent) => {
    if (!isDragging || !containerRef.current) return;

    const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;

    const x = clientX ;
    const walk = (x - startX) * 2; // 드래그 동작에 비례하여 더 큰 이동을 원할 때 조절하기 위함
    containerRef.current.scrollLeft = scrollLeft - walk;
  };

walk값을 통해서 스크롤이 더 부드럽게 구현됨

이후 스크롤 될 때 Intersection Observer를 이용해서 애니메이션도 가능
4. slide 마우스 드래그시 기준을 잡는 방법
https://tailwindcss.com/docs/scroll-snap-align

 

5. useRef를 사용할 때 주의할 점 : 캡처링 오류

The ref value 'containerRef.current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy 'containerRef.current' to a variable inside the effect, and use that variable in the cleanup function.

->경고 메시지는 containerRef.current 값이 useEffect의 정리(clean-up) 함수가 실행되는 시점에 변경될 가능성이 있다는 것을 알려주고 있습니다. 이러한 경우를 방지하기 위해 useEffect 내부에서 containerRef.current 값을 변수에 복사하고, 정리 함수에서는 이 변수를 사용하도록 수정해야 합니다.

이 오류는 아래와 같이 작성했기때문에 생겼다.

  useEffect(() => {
  
   ...
 

    if (containerRef.current) {
      containerRef.current.addEventListener('scroll', handleScroll);
    }

    return () => {
      if (containerRef.current) {
        containerRef.current.removeEventListener('scroll', handleScroll);
      }
    };
  }, [containerRef]);

 

useRef로 선언한 somRef는 current 속성을 통해 접근해야 하는데 여기서 containerRef.current는 가변성이고

cleanup 함수가 실행되는 순간에는 화면이 업데이트 되고 난 뒤기 때문에 containerRef.current의 값은 null로 세팅된다.

https://ko.legacy.reactjs.org/blog/2020/08/10/react-v17-rc.html#effect-cleanup-timing

5. 디자인 시스템

 oWhat 프로젝트는 디자인 시스템 방법론인 아토믹 디자인을 활용하여 디자인 시스템을 구축했다

https://fe-developers.kakaoent.com/2022/220505-how-page-part-use-atomic-design-system/

 

아토믹 디자인을 활용한 디자인 시스템 도입기 | 카카오엔터테인먼트 FE 기술블로그

정호일(harry) 카카오페이지에서 웹 프론트엔드를 개발하고 있습니다. 집보다 밖에 돌아다니는 걸 좋아합니다.

fe-developers.kakaoent.com

처음에 컴포넌트를 만들 때 Tailwind CSS의 cva를 이용해서 각 컴포넌트의 기본 스타일링 값을 정해진 단위로 지정해줬습니다.

즉 small,large와 같은 단위로 나누고, 컴포넌트를 사용할 때 정해진 단위를 props로 내려주는 것이었습니다.

프로젝트를 진행하다가 문제가 있었는데

color 값이나 크기 값과 같이 광범위하게 활용하는 값들의 단위는 어떻게 할 것인지였습니다.

 

두 가지의 의견이 있었는데

첫번째는 모든 값에 단위를 만드는게 스타일 통일에 좋지 않겠느냐 였습니다.

크기 값이나 마진 값을 자유롭게 주면 각각 다른 컴포넌트가 형성되지않을까 생각하게 되었고,

미리 이런 값들은 모두 단위로 만들어서 통일되게 하자는 의견이었습니다.

두번째는 광범위하게 활용되는 값들은 아토믹 컴포넌트 사용시 자율적으로 넣어주는 것이었습니다.

너무 한정적인 단위를 하게되면 추후 사용시 문제가 발생하지 않을까에 대해 예상했고,

컴포넌트를 사용할 때 props로 스타일 값을 넘겨줘서 재사용성을 높이는게 좋지않을까였습니다.

 

그래서 저희가 선택한 방법은 각 컴포넌트의 필수적인 스타일링 요소나 속성 값은 Tailwind의 cva로 미리 단위를 지정해서 사용하도록 하고, 
크기나 color값과 같은 광범위한 값들은 props로 내려주는 방법을 채택했습니다.

이 방법을 채택한 이유는 아무래도 크기나 색상은 어떻게 될지 예상할 수 없었고,

이미 프로젝트가 일부 진행된 상태에서 다시 단위를 지정하는 것은 많은 시간이 필요했습니다.

결과적으로 프로젝트 완성을 우선시 했기 때문이었습니다.

 

 

6. Tailwind css 사용한 이유 및 장점

  • className을 고민하지 않아도 된다. 클래스명과 관련한 컨벤션에 대해 고민하지 않아도 되고, 이를 통해 개발 시간을 단축할 수 있다.
  • 순수 CSS를 다루기 때문에 성능이 개선될 수 있다.
  • 사전에 정의된 스타일을 사용하기 때문에 일관된 스타일을 적용할 수 있다.
  • 다크모드설정이 편리하다
  • 기존에 제공하는 애니메이션이 있다.
  • 부모요소를 hover했을 때 자식요소도 hover 적용하는 group이라는 편리한 속성이 있었다.

Tailwind css를 사용하면서 어려웠던 점

1. Tailwind에서 지원하는 class-varience-authority를 사용하면서 제한적인 스타일의 문제

디자인 시스템을 구축하기 위해 도입한 것이었는데 각 컴포넌트의 varients를 구성하는 과정에서 기준을 모호하게 잡았다.

크기 값이나 색상값 경우에는 너무 광범위해서 varients에서 따로 지정하지 않고 props로 className을 받아서 사용했다.

그렇게 되면 기존의 디자인 시스템을 구축해 디자인 통일성을 만들려했었던 cva를 사용하는 이유가 없어져버렸다.

결과적으로 varients를 사용하면서 props로 스타일링을 하게되었다.

이유는 프로젝트를 진행하면서 예상할 수 없는 px단위의 문제들과

이를 다시 재구축하기에는 시간 비용이 들게 되었고, 프로젝트 완성을 우선순위로 두었기 때문에 두 방식 모두 사용하게 되었다.

 

2. tailwind 설정파일에서 테마 및 사용자 정의 클래스를 정의하고 twmerge사용

tailwind는 뒤에 클래스명을 선언한다고 해서 덮여 씌워지지 않는다.

그래서 아래와 같이 사용했다.

import { ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export const cn = (...inputs: ClassValue[]) => {
  return twMerge(clsx(inputs));
};

 

이때 프로젝트를 진행하면서 문제가 발생했는데

tailwind.config.js에서 정의한 사용자 정의 클래스로 스타일을 지정하면

아무리 className으로 twmerge시에 뒤에 선언한다고 해도

사용자 정의 클래스로 지정한 스타일이 우선해서 

커스텀하게 사용이 불가능해졌다.

 

3. Tailwind의 twmerge의 중복제거의 한계 때문에 생긴 문제

tailwind css 에서 "text-h1 text-primary-color"와 같이 커스텀 해서 속성을 사용하면, 실질적으로 브라우저에는 "text-h1"만 나타나 적용되는 현상이 있다.

이는 Tailwind의 twmerge의 문제로 나타났고,
stackoverflow에서 twmerge 개발자의 답변으로 "공식 문서의 예시를 참고하여 커스텀을 추가"하거나, "상수로 관리해서 tailwind에서 정의한 기본 속성 활용하기의 두 가지 방법"을 추천했다.

const TYPHOGRAPHY = {
  title: 'text-[14px] font-bold',
  description: 'text-[12px]',
  date: 'text-[10px] text-[#BFBFBF]',
  icon: 'text-[10px]',
}

https://velog.io/@doggopawer/tailwind-%EC%A4%91%EC%B2%A9-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%98%A4%EB%A5%98

 

3.Tailwind를 사용할 때 동적인 스타일 값을 넣어야 할 때 cva를 사용

보통 mx-${size}와 my-${size}와 같이 스타일 값을 동적으로 만들어야할 때가 있다.
그러나 이런 접근 방식은 Tailwind CSS의 핵심 철학과 맞지 않을 수 있었다.
Tailwind CSS는 일반적으로 미리 정의된 클래스를 사용하여 스타일을 적용하고, 동적으로 클래스를 생성하는 것은 이를 어길 수 있다.
더 좋은 방법은 Tailwind CSS의 디자인 시스템에 맞춰서 클래스를 선언하고, JavaScript에서는 그 클래스들을 결합하여 사용하는 것입니다. 
이렇게 하면 CSS와 JavaScript의 역할이 분명히 나누어지며 유지 보수가 쉬워집니다.

import { cva } from 'class-variance-authority';

export const badgeVariants = cva(
  'absolute inline-flex rounded-full shadow-xl',
  {
    variants: {
      corner: {
        'top-left': ' left-[12%] top-[17%] -translate-x-1/2 -translate-y-1/2',
        'top-right': 'right-[12%] top-[17%] -translate-y-1/2 translate-x-1/2',
        'bottom-left':
          'bottom-[17%] left-[12%] -translate-x-1/2 translate-y-1/2',
        'bottom-right':
          'bottom-[17%] right-[12%] translate-x-1/2 translate-y-1/2',
      },
      badgeType: {
        online: '[&>span]:bg-online',
        offline: '[&>span]:bg-gray-700 ',
        alarm: '[&>span]:bg-primary',
      },
      size: {
        xsmall: '[&>:last-child]:h-2 [&>:last-child]:w-2',
        small: '[&>:last-child]:h-3 [&>:last-child]:w-3',
        medium: '[&>:last-child]:h-4 [&>:last-child]:w-4',
        large: '[&>:last-child]:h-5 [&>:last-child]:w-5',
        xlarge: '[&>:last-child]:h-6 [&>:last-child]:w-6',
      },
    },
    defaultVariants: {
      corner: 'top-right',
      badgeType: 'online',
      size: 'large',
    },
  },
);

 

 

 

7. cloneElement 사용법

나는 cloneElement를 사용할 때 이렇게 사용했다.

const avatars = Children.toArray(children).map((child: ReactNode) => {
    if (isValidElement(child)) {
      return cloneElement(child as ReactElement, {
        style: { width: `${childSize}px` },
        className: `${child.props.className || ''} snap-start`,
      });
    }
  });

as 키워드를 쓰는 것이 뭔가 적절하지 못한 것 같아서 팀원의 코드를 살펴보았다.

const target =
    isValidElement<HTMLAttributes<ReactElement>>(targetElement) &&
    cloneElement(targetElement, {
      ...(eventType === 'click' && { onClick: toggleVisibility }),
      ...(eventType === 'hover' && {
        onMouseEnter: showTooltip,
        onMouseLeave: hideTooltip,
      }),
    });
childrenToArray(children, 'Tab.Item') as ReactElement<TabItemProps>[]
    ).map(element => {
      return cloneElement(element, {
        ...element.props,
        key: element.props.label,
        active: element.props.label === currentActive,
        onClick: () => {
          setCurrentActive(element.props.label);
        },
      });
    });

다양한 방법이 있는 것 같아서 가장 적절한 방법을 찾아야할 것 같았다.

 

 

8. CSS 기본값을 더 확실히 알자

color의 기본값을 몰라서 찾아봤었다.

canvasText라는데 검색해보니 브라우저의 기본값이라고 한다.

 

기본값 모음

https://www.w3schools.com/cssref/css_default_values.php

 

 

8. props가 나을까 children이 나을까?

Props로 컴포넌트에 값을 전달하는 경우
1. Props를 통해 값을 전달하면 명시적이고 명확한 API를 가진다. 부모 컴포넌트에서 어떤 값이 전달되는지 쉽게 확인할 수 있습니다.
2. TypeScript에서 Props를 사용하면 타입체크가 가능하다.

Children을 사용해서 컴포넌트를 출력하는 경우:
1. 부모 컴포넌트에서 자식 컴포넌트에게 여러 값을 전달할 수 있다. 더 유연한 사용이 가능하다.
2. 부모 컴포넌트에서 전달되는 값의 형태나 구조에 크게 의존하지 않는 더 높은 재사용성을 가진다.
3. 부모 컴포넌트가 자식 컴포넌트의 구조나 구현에 대해 덜 알아도 되고, 자식 컴포넌트가 더 독립적으로 동작한다. 즉 컴포넌트간의 의존성이 감소한다.

제가 정리한 특징입니다.

그래서 결론은 현재 어떤 요구사항인지 고려를 해야한다고 합니다.

- 전달되는 값이 고정적이고 일관된 구조를 가지면서 자주 변경되지 않는다면, Props를 사용하는 것이 명확성과 타입 안정성 면에서 더 좋을 수 있습니다.
- 자식 컴포넌트가 복잡한 구조를 가지거나, 여러 요소를 조합해야 하는 경우, Children을 사용하여 유연하게 처리하는 것이 효과적일 수 있습니다.

 

 

9.  회원가입 페이지를 만들면서 만났던 상황들

회원가입 페이지를 만들면서 다양한 경험을 했다.

 

1. 이메일, 사용자 이름, 비밀번호를 입력하는Input이 동일한 기능과 validation을 갖고 있다는 점이다.

이점을 염두해두고 개발을 시작했지만 

이메일, 사용자 이름, 비밀번호는 각각 다른 validation을 갖고 있고, onChange될 때마다 검증을 해야했으며

이메일은 검증 후에 중복확인 API완료라는 조건이 있었고,

비밀번호는 password와 confirmPassword 값을 서로 비교해야하는 과정이 필요했다.

그래서 처음에는 아래와 같은 파일 구조로 회원가입을 만들게 되었다

 

이 방식으로 하게 되니까 정리는 되었지만 너무 많은 파일을 생성하고 있고, 

중복이 되고있다는 느낌이 들었다,

그래서 모든 validation과 form데이터를 통합해서 관리하고 검증해주는 useForm을 만들었다.

import { ChangeEvent, useEffect, useState } from 'react';

interface FormValues {
  [key: string]: string;
}

interface FormIsValid {
  [key: string]: boolean;
}

interface FormValidate {
  [key: string]: (value: string) => boolean;
}

interface useFormParams {
  initialValues: FormValues;
  isValidinitialValues: FormIsValid;
  validate: FormValidate;
}

const useForm = ({
  initialValues,
  isValidinitialValues,
  validate,
}: useFormParams) => {
  const [values, setValues] = useState<FormValues>(initialValues);
  const [isValid, setIsValid] = useState<FormIsValid>(isValidinitialValues);
  const [isCompleted, setIsCompleted] = useState(false);

  const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
    const { name, value } = e.target;

    const result = validate[name](value);

    setValues(prevValues => ({ ...prevValues, [name]: value }));
    setIsValid(prevIsValid => ({ ...prevIsValid, [name]: result }));

    // confirmPassword 필드에 대한 유효성 검사 함수가 등록되어 있다면 실행 - 재사용성 감소의 원인
    if (name === 'password' && validate['confirmPassword']) {
      const confirmPasswordResult = value === values.confirmPassword;
      setIsValid(prevIsValid => ({
        ...prevIsValid,
        confirmPassword: confirmPasswordResult,
      }));
    }
  };

  const areAllValid = (obj: FormIsValid): boolean => {
    return Object.values(obj).every(value => value);
  };

  useEffect(() => {
    const result = areAllValid(isValid);
    setIsCompleted(result);
  }, [isValid]);

  return {
    values,
    isValid,
    isCompleted,
    handleChange,
  };
};

export default useForm;

- 이 코드를 아래와 같이 사용하면 된다.

  const { values, isValid, isCompleted, handleChange } = useForm({
    initialValues: {
      email: '',
      password: '',
      username: '',
      confirmPassword: '',
    },
    isValidinitialValues: {
      email: false,
      password: false,
      username: false,
      confirmPassword: false,
    },
    validate: {
      email: value => isValidEmail(value),
      password: value => isValidPassword(value),
      username: value => isValidUsername(value),
      confirmPassword: value =>
        isValidPasswordMatch({
          value,
          newPassword: values.password,
        }),
    },
  });

이때 중복확인 버튼을 누른 후에 타이핑이 되면 에러메시지가 없어지는 상황과

현재 모든 validation이 끝났을 때 바로 회원가입 버튼이 활성화 되는 함수를 useEffect로 처리해줬다.

  useEffect(() => {
    setIsEmailDuplicate(true);
    setEmailCheckMessage('');
  }, [values.email, setEmailCheckMessage, setIsEmailDuplicate]);

  useEffect(() => {
    onRegisterCompleted(isCompleted && !isEmailDuplicate);
  }, [isCompleted, isEmailDuplicate, onRegisterCompleted]);

이 useForm을 만들어서 정말 간편하게 값들을 관리하고 검증할 수 있었다.

하지만 email 값이 하나가 바뀌면 모든 state들이 다시 재설정되므로 불필요한 렌더링이 발생할 수 있겟다고도 생각했다.

 

이메일 & 패스워드 & 사용자 이름 정규표현식

export const isValidEmail = (value: string) =>
  /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/.test(value);

export const isValidPassword = (value: string) =>
  value.length >= 8 &&
  /[a-zA-Z]/.test(value) &&
  /\d/.test(value) &&
  /[!@#$%^&*(),.?":{}|<>]/.test(value);

export const isValidUsername = (value: string) =>
  value.length >= 3 && value.length <= 8 && /^[가-힣a-zA-Z0-9]+$/.test(value);

export const isValidPasswordMatch = ({
  value,
  newPassword,
}: {
  value: string;
  newPassword: string;
}) => {
  return newPassword === value;
};

 

 

label을 사용하는 이유

접근성 (Accessibility): <label> 요소는 웹 접근성을 향상시키는 데 도움이 됩니다. 화면 판독기를 사용하는 사용자들은 <label>을 통해 해당 양식 요소와 관련된 텍스트를 듣고 양식 요소를 이해할 수 있습니다.

클릭 영역 확대: <label>을 사용하면 사용자가 해당 라벨 텍스트를 클릭할 때, 라벨과 연결된 양식 요소가 자동으로 선택됩니다. 이는 사용자 경험을 향상시키며, 특히 작은 체크박스나 라디오 버튼과 같은 작은 요소에 대한 편의성을 제공합니다.

포커스 및 키보드 탐색: <label>을 사용하면 사용자가 키보드로 양식 요소를 탐색할 때 라벨과 연결된 양식 요소에 쉽게 접근할 수 있습니다. 이는 키보드 사용자에게 편의성을 제공하며, 키보드만을 이용할 수 있는 일부 사용자들에게 도움이 됩니다.

시맨틱 마크업 (Semantic Markup): <label>은 시맨틱 마크업의 한 예입니다. 시맨틱 마크업은 문서 구조를 명확하게 표현하여 검색 엔진 및 개발자 도구에게 의미 있는 정보를 제공합니다. <label>을 사용하면 양식 요소와 관련된 텍스트 설명을 시맨틱하게 표현할 수 있습니다.

label 사용법

<form>
      <label htmlFor="username">
        <Text>이메일</Text>
      </label>
      <input
        type="text"
        value={username}
        id="username"
        onChange={e => setUsername(e.target.value)}
      />

      <label htmlFor="password">
        <Text>비밀번호</Text>
      </label>
      <input
        id="password"
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
      />

      <button onClick={handleLogin}>Login</button>
 </form>

 

label의 다른 사용법

다른 형태의 토글버튼을 만들고 싶을 때 

  <label>
      <input
        type="checkbox"
        style={{ display: 'none' }}
      />
      <button>Check</button>
   </label>


label안에 요소를 클릭했을 때 input의 check가 바뀌는 것이다.

input요소는 스타일링을 할 수 없기때문에 label요소를 꾸미고 input요소는 숨기면 된다.

 

728x90
반응형
LIST