[React] - React Router v6, 폼 컨트롤

Lee Jeong Min·2021년 12월 31일
0
post-thumbnail

React Router v6

react-router-dom 타입 패키지 설치

yarn add -D @types/react-router-dom

참고 사이트: https://reactrouter.com/docs/en/v6/api#navigate

App.js

import { Routes, Route, Link, Navigate } from 'react-router-dom';
import { Home, SignIn, SignUp } from 'pages';

function PageNotFound(props) {
  console.log(props);
  return (
    <div role='alert'>
      <h2>페이지를 찾을 수가 없습니다</h2>
      <Link to='/'>홈 페이지</Link>를 이용하여 네비게이션 하세요
    </div>
  );
}

export default function App() {
  return (
    <Routes>
      <Route path='/' element={<Home />} />
      <Route path='signin' element={<SignIn />} />
      <Route path='signup' element={<SignUp />} />
      <Route path='page-not-found' element={<PageNotFound />} />
      {/* Redirect (v5) -> Navigate (v6) 컴포넌트 사용 */}
      <Route path='*' element={<Navigate to='page-not-found' replace={true} />} />
    </Routes>
  );
}

React Router v5에 있던 redirect가 없어지고 위와 같이 사용하여 페이지가 없는 경우 처리를 해줄 수 있다.

참고 사이트: https://reactrouter.com/docs/en/v6/api#navlink

NavLink 컴포넌트 스타일을 확장하는 스타일 컴포넌트로 만들어 사용하면 React Router v6 API 방법을 그대로 사용할 수 있을 거라 생각되지만 정상 작동되지 않는다.

왜냐하면 className prop에 설정된 함수 식을 styled-components 라이브러리에서 문자 값으로 변환 처리하여 함수 실행 자체가 안됨

=> 고차 컴포넌트(HOC)를 사용하여 문제를 해결해야 한다!

// NavLink
// ⬇
// LinkWrapper - Forwardref  (activeclassName, activeStyle)
// ⬇
// styled(LinkWrapper)

이러한 구조로 바꾸어 주어야 함.

Navigation.styled.js

...

const LinkWrapper = forwardRef(
  ({ activeClassName, activeStyle, className, style, ...restProps }, ref) => {
    return (
      <NavLink
        ref={ref}
        className={({ isActive }) =>
          [className, isActive ? activeClassName : null].filter(Boolean).join(' ')
        }
        style={({ isActive }) => ({
          ...style,
          ...(isActive ? activeStyle : null),
        })}
        {...restProps}
      />
    );
  }
);

중간에 LinkWrapper라는 고차 컴포넌트를 사용!

Switch -> Routers 개선사항

v5까지 사용되었던 컴포넌트가 v6부터 로 변경
단순히 컴포넌트 이름만 바뀐 것이 아니라, 훨씬 강력한 기능을 제공

  • 상대 경로 활용 - , 모두 에 상대적인 라우트 설정이 가능해 예측이 쉽ek.
  • 버그 문제 개선 - 기존의 순서에 의존한 스위칭 대신, 매칭율이 높은 라우트를 선택
  • 경로 구성 집중 - 중첩된 라우트를 구성할 경우, 이전과 달리 모든 라우트를 한 곳에서 관리

children → element 변경 이점

v5까지 사용되었던 children prop 대신, v6부터는 element prop을 사용한다. render props 패턴 또는 고차 컴포넌트(HOC) 사용 보다 element prop 사용이 훨씬 쉽다.

v5 버전 코드

// 간단한 설정
<Route path=":userId" component={Profile} />

// 그런데 어떻게 <Profile /> 요소에 `animate` prop를 전달?
// 다소 복잡한 render props 패턴 사용
<Route
  path=":userId"
  render={routeProps => (
    <Profile routeProps={routeProps} animate={true} />
  )}
/>

v6 버전 코드


// 간단한 설정으로 component 대신 element가 사용되고,
// React의 <Suspense />와 사용법이 유사
<Route path=":userId" element={<Profile />} />

// 그런데 어떻게 <Profile /> 요소에 `animate` prop를 전달?
// <Profile /> 요소에 `animate` prop을 설정 -> 매우 쉬워짐
<Route path=":userId" element={<Profile animate={true} />} />

Form 컨트롤

Controlled 컴포넌트

React 컴포넌트는 폼을 통해 입력된 사용자의 값을 제어 할 수 있다. React를 통해 값이 관리되는 입력 요소는 컨트롤 컴포넌트(Controlled Component)이다.

FormInput 컴포넌트 작성

FormInput.js

import React from 'react';
import { A11yHidden } from 'components';
import { Control, Label, Input } from './FormInput.styled';
import { node, bool, string, objectOf, any } from 'prop-types';

export function FormInput({
  id,
  label,
  type = 'text',
  value = null,
  invisibleLabel = false,
  children = null,
  forwardRef = null,
  onChange = null,
  inputProps = {},
  ...restProps
}) {
  return (
    <Control {...restProps}>
      {invisibleLabel ? (
        <A11yHidden as='label' htmlfor={id}>
          {label}
        </A11yHidden>
      ) : (
        <Label htmlFor={id}>{label}</Label>
      )}

      <Input
        ref={forwardRef}
        type={type}
        id={id}
        placeholder={children}
        value={value}
        onChange={onChange}
        readOnly={value && !onChange}
        {...inputProps}
      />
    </Control>
  );
}

FormInput.propTypes = {
  id: string.isRequired,
  label: string.isRequired,
  type: string,
  invisibleLabel: bool,
  children: node,
  inputProps: objectOf(any),
  restProps: any,
};

FormInput.styled.js

const styled = require('styled-components/macro');

export const Control = styled.div`
  padding: 4px;
  background: rgba(200 200 200 / 10%);
  margin-bottom: 4px;
`;

export const Label = styled.label`
  margin-bottom: 4px;
  font-size: 0.875rem;
`;

export const Input = styled.input`
display: block;
width: 100%;
padding: 0.3em 0.6em
font-size: 1rem;
border-bottom: 2px solid currentColor`;

SignIn.js

import { useState, useRef, useEffect } from 'react';
import 'styled-components/macro';

import { Link } from 'react-router-dom';
import { FormInput } from 'components';

export function SignIn() {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);
  const [email, setEmail] = useState('dlwoabsdk@naver.com');
  const [password, setPassword] = useState('000');

  useEffect(() => {
    // console.log('mounted');
    // console.log('emailRef.current', emailRef.current);
    // console.log('passwordRef.current', passwordRef.current);
  }, []);

  const handleChange = e => {
    const { name, value } = e.target;

    switch (name) {
      case 'email':
        setEmail(value);
        break;
      case 'password':
        setPassword(value);
    }
  };

  return (
    <>
      <h2>로그인 폼</h2>
      <form
        css={`
          border: 3px solid currentColor;
          border-radius: 4px;
          padding: 2rem;
        `}
      >
        <FormInput
          forwardRef={emailRef}
          type='email'
          id='userMail'
          label='이메일'
          inputProps={{
            autoComplete: 'user-name',
            name: 'email',
            value: email,
            onChange: handleChange,
          }}
        >
          dlwoabsdk@naver.com
        </FormInput>
        <FormInput
          forwardRef={passwordRef}
          type='password'
          id='userPass'
          label='패스워드'
          inputProps={{
            autoComplete: 'current-password',
            name: 'password',
            value: password,
            onChange: handleChange,
          }}
        >
          대소문자 조합 6자리 이상 입력
        </FormInput>
        <button type='submit'>로그인</button>
      </form>
      <p>
        회원가입 정보가 없다면? <Link to='/signup'>회원가입</Link> 페이지로 이동해 가입하세요.
      </p>
    </>
  );
}

현재 onChange로 전달되는 handleChange의 경우 하위로 전달하기 때문에 성능을 위해 useCallback을 사용하여 함수를 기억해두어 메모되어있는 함수가 반환되게 만든다.


  // 컴포넌트가 리 렌더링 될 때마다, 성능 저하를 막기 위해서 함수를 기억해두어 메모되어있는 함수가 반환되어
  // 성능 저하를 막을 수 있다.
  const handleChange = useCallback(e => {
    const { name, value } = e.target;

    switch (name) {
      case 'email':
        setEmail(value);
        break;
      case 'password':
        setPassword(value);
    }
  }, []);

또한 useMemo를 사용하여 컴포넌트를 기억해두어 이메일이나 패스워드 부분의 상태가 변하면서 나머지 부분이 리렌더링이 되는 것을 막는다.

  // 컴포넌트(값) 메모
  const memoizedEmail = useMemo(
    () => (
      <FormInput
        type='email'
        id='userMail'
        label='이메일'
        inputProps={{
          autoComplete: 'user-name',
          name: 'email',
          value: email,
          onChange: handleChange,
        }}
      />
    ),
    [email, handleChange]
  );

  const memoizedPassword = useMemo(
    () => (
      <FormInput
        type='password'
        id='userPass'
        label='패스워드'
        inputProps={{
          autoComplete: 'current-password',
          name: 'password',
          value: password,
          onChange: handleChange,
        }}
      >
        대소문자 조합 6자리 이상 입력
      </FormInput>
    ),
    [password, handleChange]
  );

...

return (
  ...
  {memoizedEmail}
  {memoizedPassword}
  ...
)

url에 따른 타이틀 변경

참고 사이트: https://create-react-app.dev/docs/adding-custom-environment-variables/

setDocumentTitle.js

const { REACT_APP_TITLE: documentTitle } = process.env;

export function setDocumentTitle(newTitle) {
  document.title = `${newTitle} - ${documentTitle}`;
}

seo, 접근성 관련 패키지 설치

참고사이트: https://www.npmjs.com/package/react-helmet-async

패키지 설치

yarn add react-helmet-async

이를 사용하여 페이지의 link, head 태그들을 조절할 수 있음!
react-helmet는 업데이트가 잘 안되고 있어서 react-helmet-async를 사용!

React form 관련 라이브러리 - FORMIK

참고사이트: https://formik.org/

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글