Dormitory(기숙사) 줄임말인 Dotori 프로젝트는 기숙사 자습신청 등 잡다한 것들이 모두 사감 선생님의 수기로 작성되어야 한다는 점에서 불편함을 느끼게 되어서
기숙사의 자습신청, 안마의자 사용, 규정 위반, 기상음악 신청 등을 웹사이트로 간편하게 관리 할 수 있도록 만든 웹사이트다.
기존에 만들어진 도토리는 디자이너가 따로없어서 사용자가 디자인이 아쉽다는 평이 많았고 파일구조도 제대로 짜고 만든게 아니라 잘 구조화되지 않았고 인증로직도 제대로 짜여져 있지 않는등 여러가지 문제로 인해 도토리 v2 를 내기로 했다.
이번에 v2에는 디자인변경, 다크모드, Next.js 로 migration, 사감쌤과 1대1문의 기능등을 추가하기로 했다.
Frontend : 3명, Backend: 3명, Android: 4명, IOS: 3명, Design: 1명으로 구성되어있고
Frontend Leader 역할을 맡아 프로젝트 초기설정과 개발의 전반적인 리드를 했다.
기존 dotori 프로젝트 파일구조는 component라고 해놓고 너무 중구난방 느낌으로 코드를 짜놔서 처음부터 잘 구조화하면 좋을 것 같다는 선배의 조언을 바탕으로 v2 는 파일구조를 어떻게 할까 많이 생각해봤다.
그러다가 파일을 구조단위로 나눠서 관리하는 Atomic 디자인 패턴을 보게되어서
components 안에 페이지단위로 폴더를생성하고 각 폴더안에 atoms, molecules, organisms,templates 폴더를 만들어서 관리하고 common 폴더를 만들어서 공통으로 사용되는 컴포넌트관리를 하는 방식으로 하기로 했다.
SSR 은 초기 HTML 데이터를 빠르게 받아와 사용자가 빠르게 화면구조를 볼 수 있고 SEO가 좋다는 장점이 있지만
서버는 항상 각 요청이 올때마다 HTML파일을 생성하기 때문에 컨텐츠 캐시가 되지 않아 서버 부화가 걸릴 수 가있고 TTV와 TTI 간에 시간 간격이 존재하고 매번 페이지를 요청할 때마다 새로 고침 되는 단점이 있다.
그래서 SWR 을 같이사용해 초기에 SSR로 가지고온데이터를 SWRConfig 로 캐싱해서
기존에 CSR 의 장점인 서버부화를 감소시키고 TTV와 TTI 간에 시간 간격이 없게 했다.
또 SWR 의 기능을 이용해서 페이지에 다시 포커스했을때나 자습신청시간(8시)과 안마의자 신청시간(8시 20분)이 되면 자동으로 재랜더링이 되게 설정했다.
ex) home.tsx
const HomePage: NextPage<{
fallback: Record<string, myProfileType> &
Record<string, noticePageProps> &
Record<string, applyPageProps> &
Record<string, applyPageProps>;
}> = ({ fallback }) => {
UseThemeEffect();
return (
<>
<SEOHead title={'| 홈페이지'} />
<SWRConfig value={fallback}>
<MainTemplates>
<SideBar />
<HomeTemplates>
<TimeBoard />
<Profile />
<MealBoard />
<NoticeBoard />
<SelfStudyBoard />
<MassageBoard />
</HomeTemplates>
</MainTemplates>
</SWRConfig>
<ChannelBtn />
</>
);
};
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const { Authorization } = await getToken(ctx);
const role = getRole(ctx);
const header = {
headers: { Authorization },
};
const promises = [
apiClient.get(MemberController.myProfile, header),
apiClient.get(NoticeController.getNotice(role), header),
apiClient.get(SelfstudyController.selfStudyInfo(role), header),
apiClient.get(MassageController.massage(role), header),
];
try {
const [
{ data: myData },
{ data: noticeData },
{ data: selfStudyData },
{ data: massageData },
] = await Promise.all(promises);
return {
props: {
fallback: {
[MemberController.myProfile]: myData,
[NoticeController.getNotice(role)]: noticeData,
[SelfstudyController.selfStudyInfo(role)]: selfStudyData,
[MassageController.massage(role)]: massageData,
},
},
};
} catch (e) {
return { props: {} };
}
};
export default HomePage;
미들웨어는 request가 완료되기 전에 코드를 실행해준다.
따라서 로그인 여부에따른 url 직접접근설정을 middleware 에 처리해 한파일에 가능하게 헀다.
(로그인이 되있는데 인증페이지로 이동하려고할때, 로그인이 안되있는데 인증페이지 이외의 페이지로 이동하려고할때)
ex) middleware.ts
import { NextRequest, NextResponse, userAgent } from 'next/server';
export const middleware = async (req: NextRequest) => {
const ua = userAgent(req);
const RefreshToken = req.cookies.get('RefreshToken');
const isAuthPage = ['/signin', '/signup', '/'].some(
(i) => i == req.nextUrl.pathname
);
if (ua.isBot) {
return NextResponse.redirect(new URL('/signin', req.url));
}
if (RefreshToken && isAuthPage) {
return NextResponse.redirect(new URL('/home', req.url));
} else if (!RefreshToken && !isAuthPage) {
return NextResponse.redirect(new URL('/signin', req.url));
}
};
export const config = {
matcher: [
'/signin',
'/signup',
'/home',
'/selfstudy',
'/massage',
'/penalty',
'/notice',
'/notice/:path*',
'/',
],
};
next.js 기본적으로 모든 페이지를 pre-rendering 한다.
웹 페이지에서는 프로모션 페이지같은 큰 JS 파일을 모두 불러올 때까지 Rendering 을 멈추어서 promotion 같은 페이지들은 pre-rendering 하면 오히려 더 랜더링 시간이 길어지게 되는 현상이 발생하게된다.
그래서 dynamic import 를 사용하여 ssr 설정을 지운뒤 csr 로 불러와 랜더링성능을 향상시켰다.
ex) signin.tsx
const SignInForm = dynamic(
() => import('../components/SignIn/organisms/SignInForm'),
{ ssr: false, loading: () => <DotoriLogo /> }
);
처음 url 직접접근방식을 구현할떄 ssr 에서 Authorization 여부에 따라 redirect 하도록 했다
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const { Authorization } = await getToken(ctx);
if (!Authorization) {
return {
redirect: {
destination: '/signin',
permanent: false,
},
}
}
return { props: {} };
}
이 방식도 나쁘진 않았는데 각 페이지마다 이런 설정을 해줘야됐고 구지 getServerSideProps 을 안써도 되는 페이지에서도 적용해야됬고 이것때문에 베포환경에서 에러가 떳다. 그래서 더 좋은 방법을 찾아보던중 현재의 Middleware 로 바꾸게됬다.
홈페이지에서 다크모드로 설정한후 새로고침하면 흰배경이 됐다가 다시 검은색 배경이 되는 이슈가있었다. 한번 적용을 해봤는데 app.tsx 에는 window, document 를 지원을 안해서 쿠키에 접근을 못했다 그러다 _document 에는 접근할 수 있다는 글을 보고 다크모드 쿠키여부에따라 document 에 theme 를 미리 정해줘서 깜빡임을 막을 수 있었다.
ex) _document.tsx
const themeInitializerScript = `
(function () {
document.body.dataset.theme = document.cookie.match('(^|;)?themeCookie=([^;]*)(;|$)')[2] || (window.matchMedia?.('(prefers-color-scheme: dark)').matches ? "dark" : "light");
})();
`;
export default function Document() {
return (
<Html lang="ko">
<Head>
<link rel="icon" href="/favicon.ico" />
</Head>
<body>
<script dangerouslySetInnerHTML={{ __html: themeInitializerScript }} />
<Main />
<NextScript />
</body>
</Html>
);
}
프로젝트를 하면서 실제 학교학생들이 사용중인 프로젝트라 더 열심히 하게됐고 베포하게됐을때 성취감도 더 큰것 같다.
또 여러사용자가 사용하니까 만들당시에는 문제없어보였던게 베포되고나서 많은 이슈와, 피드백이 나와서 다음엔 좀더 사용자입장에서 만들어야겠다고 생각했다.
열심히 이슈찾아주고 피드백해준 학교친구들과 개발할때 더 좋은코드로 짤 수 있게 코멘트 해준 학교선배님들에게 감사의 인사를 전한다. 🙇♂️