여러 사람과 협업을 할수록 규칙은 중요하다.
규칙 없이 마구 개발하면 서로가 작성한 코드의 의도를 파악하는 데에 더 많은 시간을 쏟게 되고
오히려 혼자서 개발하는 것보다 못한 퍼포먼스를 낼 수도 있다.
여러 규칙 중 폴더와 파일을 어떻게 배치할지를 잘 정하는 게 중요하다고 생각한다.
아무리 세부 코드가 엉망이어도 파일 구분이 명확하면
엉망인 코드와 상관없이 다른 파일(또는 모듈)에서 좋은 코드를 짤 수 있고
추후 필요시 엉망인 코드를 손 볼 수 있다.
또한 코드 퀄리티 자체가 별로라고 해도 props 등의 인터페이스가 명확하고 제대로 작동한다면
그저 모듈을 사용하는 것일 뿐이지 세부적인 복잡한 로직을 들여다 볼 필요는 없다.
마치 node module을 쓰듯이 말이다.
필자가 여러 프로젝트를 진행하면서 괜찮았다고 생각하는 파일과 폴더 구조를 정리해보겠다.
컴포넌트 폴더 구조
리액트 프로젝트에서는 사실 다른 파일들의 위치는 어느 정도 정해져 있고
컴포넌트 파일들을 어디에 배치하는지가 관건이다.
컴포넌트부터 다뤄보겠다.
(이하 기본적으로 src/ 아래에 위치한다.)
Router.tsx
NextJS에서는 폴더 구조가 곧 라우트가 되기 때문에 필요 없지만
create-react-app으로 만든 일반 리액트 앱에서는 react router가 필요하다.
이를 활용한 route 설정을 이 파일에서 한다.
아래 코드는 react router 6를 이용한 예제다.
const Router = () => {
const { accessToken } = useContext(AccessTokenContext);
return (
<Routes>
{accessToken ? (
<Route element={<NavLayout />}>
<Route path="/reservation" element={<ReservationPage />} />
<Route path="/member" element={<MemberPage />} />
<Route path="/sales" element={<SalesPage />} />
<Route path="/voucher" element={<VoucherPage />} />
<Route path="*" element={<Navigate to="/reservation" />} />
</Route>
) : (
<>
<Route path="/signin" element={<SignInPage />} />
<Route path="*" element={<Navigate to="/signin" />} />
</>
)}
</Routes>
);
};
export default Router;
Router라는 이름 말고도 route와 관련된 어떤 이름을 써도 무방하다.
아예 route를 구분하지 말고 아래의 pages/index.tsx에 둬도 괜찮을 것 같다.
pages/
pages 폴더를 두고 위 route에 따라 랜딩되는 페이지 컴포넌트들을 둔다.
쉽게 말해 각 url에 해당하는 페이지 컴포넌트다.
위 코드에서 ReservationPage와 같이 Page로 끝나는 컴포넌트가 이에 해당한다.
다른 컴포넌트와의 구분을 위해서 ...Page로 이름을 명명하는 게 좋겠다.
이것 말고는 NextJS와 똑같이 따라하자.
depth가 생기는 경우에는 폴더로 바꿔서 /user/index.jsx로 한다.
자식 컴포넌트는 /user/example.jsx처럼 한다.
물론 컴포넌트 이름은 UserPage, ExamplePage로 하는게 좋겠다.
(NextJS의 방법이 궁금하면 https://nextjs.org/docs/routing/introduction)
components/
페이지 이외의 컴포넌트로 모든 페이지에서 공통으로 사용하는 common 폴더와 각 페이지 이름의 폴더들로 나눈다.
components/
common/ <- 공통 컴포넌트
Button/ <- Button 컴포넌트
user/ <- user 페이지의 컴포넌트
Select/ <- Select 컴포넌트
signin/ <- 로그인 페이지의 컴포넌트
TextInput/ <- TextInput 컴포넌트
profile/ <- 프로필 페이지의 컴포넌트
...
처음부터 공통으로 관리하면 베스트겠지만 기획과 디자인 시스템이 분명하게 잡혀있지 않다면 계속 바뀌게 되므로
각 페이지에서 필요한 컴포넌트를 만들고 필요에 따라 여러 곳에서 사용될 때 common으로 옮긴다.
각 컴포넌트는 폴더로 묶는다.
해당 컴포넌트 안에서만 사용하는 sub 컴포넌트나 아이콘, 스타일(CSS) 등
여러 요소들이 필요하게 되므로 폴더로 묶어서 관리한다.
components/
common/
Modal/
(index.jsx)
Modal.jsx <- 컴포넌트
styles.js <- styled components 코드
SubmitButton.jsx <- 모달 컴포넌트에서 사용되는 버튼 컴포넌트
CloseButton.jsx <- 모달 컴포넌트에서 사용되는 버튼 컴포넌트
CloseIcon.jsx <- CloseButton 컴포넌트에서 사용되는 아이콘 컴포넌트
Button/
...
위의 path는 components/common/Modal/Modal.jsx로 Modal이 중복되어 깔끔하지 못하기 때문에
components/common/Modal/index.jsx로 할 수도 있지만
모든 컴포넌트의 이름을 index.jsx로 하면 VS code의 탭을 보고 이름 분간하기가 어렵기 때문에 필자는 전자를 따랐었다.
더 좋은 방법은 index.jsx로 받고 여기서 다시 Modal.jsx를 불러오는 방법이다. (Material UI에서 쓰는 방식)
// index.js
export { default } from './Modal';
이렇게 하면 components/common/Modal로 깔끔하게 불러올 수 있다.
물론 위 컴포넌트는 모두 presentational 컴포넌트다.
presentational 컴포넌트는 내부에 api나 비지니스 로직과 관련된 state가 없는 재사용 가능한 컴포넌트를 말한다.
이와 반대로 container 컴포넌트에서는 api 로직이나 비지니스 로직, redux state 등이 들어가는데
다른 컴포넌트와 구분하기 위해서 ...Container라고 이름 붙인다.
feature/
container 컴포넌트는 presentational 컴포넌트와 마찬가지로 components/페이지/에 둔다.
...Container라는 이름 때문에 presentational 컴포넌트와 구분할 수 있다.
feature라는 폴더에 둔다.
feature/
CommentDrawer/
index.tsx
CommentDrawer.tsx
CommentItem.tsx
UserInfo/
index.tsx
UserInfo.tsx
...
아래는 container 컴포넌트 예제다.
getNotices가 api 함수이고 이런 로직이 컴포넌트 안에 있으면 재사용을 할 수 없다.
const NoticeContainer = () => {
const router = useRouter();
const page = useMemo(
() => (router.query?.page ? Number(router.query.page) : 1),
[router.query?.page]
);
const [openedId, setOpenedId] = useState<number | null>(null);
useEffect(() => {
if (router.query?.id) setOpenedId(Number(router.query.id));
else setOpenedId(null);
}, [router.query?.id]);
const { data } = useQuery(['notices', page], () => getNotices({ page }));
const notices = data?.boards || null;
/* ... */
return (
<Layout title="공지사항" fixedNav>
<Page paddingTop={60}>
<Table style={{ marginBottom: '50px' }}>
<TableRow head>
<TableCell justifyContent="center" paddingRight={30}>
번호
</TableCell>
<TableCell>제목</TableCell>
<TableCell justifyContent="center">등록일</TableCell>
</TableRow>
{/* ... */}
</Table>
</Page>
<ConsultingButton />
</Layout>
);
};
컴포넌트 이외
constants/
primary color와 같이 모든 페이지에서 주요하게 쓰이는 색 등의 상수를 여기에 둔다.
constants/
colors.js
velocity.js
// colors.js
export const PRIMARY_COLOR = '#4498f2';
export const ERROR_COLOR = '#ed1919';
styles/
global.css, normalize.css, fonts.css 등 전역 CSS 스타일과 관련된 파일을 둔다.
hooks/
공통으로 사용하는 커스텀 훅스를 둔다.
const useAlert = () => {
const dispatch = useDispatch();
const alert = (value, options) => {
// 에러 Alert
if (options?.error) {
if (typeof value === 'string') {
dispatch(setError(true));
dispatch(setMessage(value));
return;
}
/* ... */
} else {
// 일반 Alert
if (typeof value === 'string') {
dispatch(setError(false));
dispatch(setMessage(value));
}
}
};
return alert;
};
contexts/
전역에 쓰는 react context 모듈을 둔다.
contexts/
AccessTokenContext.jsx
ReactQueryContext.jsx
export const AccessTokenContext = createContext({
accessToken: '',
changeAccessToken: (token) => {
console.log(token);
},
});
export const AccessTokenContextProvider = ({
children,
}) => {
const [token, setToken] = useState(retrieveUserToken('access'));
const changeAccessToken = (newToken) => {
if (!newToken) removeUserToken('access');
else storeUserToken('access', newToken);
setToken(newToken);
};
return (
<AccessTokenContext.Provider
value={{ accessToken: token, changeAccessToken }}
>
{children}
</AccessTokenContext.Provider>
);
};
libs/
공통으로 사용하는 함수 등을 둔다.
libs/
axiosConfig.ts
userTokenStorage.ts
apis/
각종 api 함수를 둔다.
보통 서버의 라우트에 따라서 폴더링을 하는게 편하다.
그렇다고 너무 depth를 주면 복잡해지고
작은 규모의 프로젝트라면 두 depth 정도로 폴더링을 해도 충분하다.
아래의 reservations/index.ts는 GET, POST, DELETE 등 reservations/와 관련된 다양한 api 함수를 export 한다.
apis/
reservations/
index.ts
types.ts
shops/
index.ts
types.ts
typescript를 쓸 경우 서버 데이터의 타입을 나눠서 놓으면 분류하기 좋다.
redux/
프로젝트에서 redux로 상태 관리를 한다면 redux 또는 store라는 폴더를 둬서 관리한다.
'React > 일반' 카테고리의 다른 글
React의 JSX 문법 이해하기 (0) | 2021.11.13 |
---|---|
재사용 가능한 리액트 컴포넌트를 구현하는 방법 (0) | 2021.07.29 |
Create React App에서 SVG 파일을 간단히 불러오는 방법 (0) | 2021.03.18 |
React의 Virtual DOM이란? (0) | 2021.02.07 |
리액트 라우터의 기본 url(/)이 렌더링 되지 않게 하는 간단한 방법 (0) | 2020.06.22 |