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
라는 고차 컴포넌트를 사용!
v5까지 사용되었던 컴포넌트가 v6부터 로 변경
단순히 컴포넌트 이름만 바뀐 것이 아니라, 훨씬 강력한 기능을 제공
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} />} />
React 컴포넌트는 폼을 통해 입력된 사용자의 값을 제어 할 수 있다. React를 통해 값이 관리되는 입력 요소는 컨트롤 컴포넌트(Controlled Component)이다.
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}
...
)
참고 사이트: 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}`;
}
참고사이트: https://www.npmjs.com/package/react-helmet-async
패키지 설치
yarn add react-helmet-async
이를 사용하여 페이지의 link, head 태그들을 조절할 수 있음!
react-helmet는 업데이트가 잘 안되고 있어서 react-helmet-async를 사용!
참고사이트: https://formik.org/