토큰을 로컬스토리지가 아닌 HTTP 쿠키에 저장
Bad
- 토큰을 로컬스토리지에서 가져와서 axios 헤더에 셋팅
const token = localStorage.getItem("token");
if (token) {
axios.defaults.headers.common["Authorization"] = token;
}
Good
- 토큰을 쿠키에서 가져와서 axios 헤더에 셋팅
import Cookies from "js-cookie";
const token = Cookies.get("token");
if (token) {
axios.defaults.headers.common["Authorization"] = token;
}
Better
No Code 😉
- 쿠키는 동일한 도메인의 모든 사이트에서 공유된다. 토큰을 요청에 매번 전달할 필요가 없다. 백엔드가 프론트와 같은 도메인이 아니라면 2번째 방법을 사용하라.
- 자바스크립트를 통해 쿠키 값(토큰)에 접근하는 걸 막기 위해 HttpOnly 속성을 사용하라. 리액트에선 라우팅 접근을 확인하기 위한 flag가 필요하다.
인증 토큰 또는 공통 헤더를 위한 인터셉터 사용
Bad
axios.get("/api", {
headers: {
ts: new Date().getTime(),
},
});
Good
axios.interceptors.request.use(
(config) => {
config.headers["ts"] = new Date().getTime();
return config;
},
(error) => {
return Promise.reject(error);
}
);
axios.get("/api");
Context API나 Redux로 인증 로직 처리
Bad
- 유저 정보(auth)를 변수로 선언
- 모든 Router 컴포넌트에 props로 일일이 전달
const auth = { name: "John", age: 30 };
return (
<Router>
<Route path='/' element={<App auth={auth} />} />
<Route path='/home' element={<Home auth={auth} />} />
</Router>
);
Good
- auth 로직은 Provider에만 있음
- Router는 별개의 로직으로 동작
return (
<Provider store={store}>
<Router>
<Route
path="/"
element={<App />}
/>
<Route
path="/home"
element={<Home />}
/>
</Router>
</Provider>
);
// 하위 컴포넌트에서 꺼내서 사용
const { auth } = useContext(AuthContext);
const { auth } = useSelector((state) => state.auth);
rem 유틸함수 사용
Bad
const Button = styled.button`
margin: 1.31rem 1.43rem;
padding: 1.25rem 1.5rem;
`;
Good
- toRem 함수의 인자에 px 값을 넣음
- px 단위로 알아보기 쉬움
const toRem = (value) => `${value / 16}rem`;
const Button = styled.button`
margin: ${toRem(21)} ${toRem(23)};
padding: ${toRem(20)} ${toRem(24)};
`;
Bad
- 상태마다 input과 onChange 함수 생성
const onNameChange = (e) => setName(e.target.value);
const onEmailChange = (e) => setEmail(e.target.value);
return (
<form>
<input type="text" name="name" onChange={onNameChange} />
<input type="text" name="email" onChange={onEmailChange} />
</form>
);
Good
- 하나의 onChange 함수를 만들고 들어오는 입력 값에 따라 setFormData 객체로 input 값들을 한번에 관리
- input 상태가 많아져도 동일한 함수 사용
const onInputChange = (e) => {
const { name, value } = e.target;
setFormData((prevState) => ({
...prevState,
[name]: value,
}));
};
return (
<form>
<input type="text" name="name" onChange={onInputChange} />
<input type="text" name="email" onChange={onInputChange} />
</form>
);
lazy loading을 위한 intersection observer 사용
Bad
element.addEventListener("scroll", function(e) {
});
Good
const useScroll = (element, options = {}): boolean => {
const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => {
const callback: IntersectionObserverCallback = (entries) => entries.forEach(
(entry) => setIsIntersecting(() => entry.isIntersecting)
);
const observer = new IntersectionObserver(callback, options);
if (element) observer.observe(element);
return (): void => element && observer.unobserve(element);
}, [element]);
return isIntersecting;
}
const ref = useRef<any>();
const isIntersecting = useScroll(ref?.current);
useEffect(() => {
if (isIntersecting) {
}
}, [isIntersecting]);
인증이 필요한 경우를 대비한 HOC 함수 만들어 적용하기
Bad
const Component = () => {
if (!isAuthenticated()) {
return <Redirect to="/login" />;
}
return <div></div>;
}
Good
- (아래부터) 컴포넌트: 컴포넌트에서 props를 받아서 JSX 반환, withAuth로 감싸서 export
- 라우트: component 자리에 withAuth로 감싸서 넣기
- withAuth HOC: 컴포넌트를 받아서 함수 반환
- 반환하는 함수: 인증이 안 됐을 경우 login 페이지로 redirect, 인증이 됐을 경우 props를 전달받는 컴포넌트 반환
const withAuth = (Component) => {
return (props) => {
if (!isAuthenticated()) {
return <Redirect to="/login" />;
}
return <Component {...props} />;
};
};
// 라우트
<Route path="/home" component={withAuth(Home)} />;
// 컴포넌트
const Component = (props) => <div></div>;
export default withAuth(Component);
라우트 정의에 라우트 객체 배열 사용
Bad
return (
<Router>
<Route path="/" element={<App />} />
<Route path="/about" element={<About />} />
<Route path="/topics" element={<Topics />} />
</Router>
);
Good
- 코드가 더 많지만 훨씬 더 유연성 있는 방법
- 수정 시 역할 분담
- route 경로를 추가할 경우 routes 배열의 라우트만 수정하면 됨
- 중첩된 children 라우트의 경우 createRoute 함수가 재귀로 돌면서 알아서 내부 children까지 반환
- 현재 createRoute 함수에서 role 여부로 withAuth 인증을 거칠지 결정
- 추가하고 싶은 로직이 있다면 createRoute 함수 안에 추가하면 됨
- 페이지 컴포넌트는 모두 lazy loading으로 동적 import
const routes = [
{
path: "/",
role: ["ADMIN"],
element: React.lazy(() => import("../pages/App")),
children: [
{
path: "/child",
element: React.lazy(() => import("../pages/Child"),
},
],
},
{
path: "/about",
role: [],
element: React.lazy(() => import("../pages/About")),
},
{
path: "/topics",
role: ["User"],
element: React.lazy(() => import("../pages/Topics")),
},
];
const createRoute = ({ element, children, role, ...route }) => {
const Component = role.length > 0 ? withAuth(element) : element;
return (
<Route key={route.path} {...route} element={<Component />}>
{children && children.map(createRoute)}
</Route>
);
};
return <Routes>{routes.map(createRoute)</Routes>;
참고자료
React best practices and patterns to reduce code - Part 3