리액트 Best Practice & Pattern 3탄

Sheryl Yun·2023년 8월 16일
1

리액트 Best Practice

목록 보기
4/4
post-thumbnail

토큰을 로컬스토리지가 아닌 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

  • rem 값에 해당하는 px 값을 알기 어려움
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)};
`;

input 변경을 위한 공통 함수

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) {
    	// API 요청
    }
}, [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")), // App 페이지 lazy 로딩
        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

profile
데이터 분석가 준비 중입니다 (티스토리에 기록: https://cherylog.tistory.com/)

0개의 댓글