프로젝트 소개
스탑워치 / 타이머기능을 통해 실시간으로 공부시간을 기록하고, 가입한 그룹 내에 공부량을 시각화하여 보여주는 웹사이트
개발 기간
23. 10. 23 ~ 23. 11. 10 (약 2주)
기술 스택
프론트 엔드 - 2명
- Vite
- ReactJS
- JavaScript
- Redux-Toolkit
- TailwindCSS
- Socket.IO-client
백엔드 - 3명
- Express
- Mongoose
- JWT (jsonwebtoken)
- Socket.IO
- MongoDB, AWS EC2, NGINX
협업 툴
프론트 엔드 기술 스택 선택 이유
Vite VS create-react-app
매번 create-react-app으로만 했었는데 vite가 기존의 번들러와 비교하여 개발 속도가 월등히 빠르다고 하고, 요즘 Vite를 사용하여 프로젝트 하는 곳이 많아 이번 기회에 써보기로 했다.
작은 프로젝트에는 별 차이가 없겠지만 react 설치부터 시작해서 개발환경을 구축하는 과정이 create-react-app보다 간단하고 빠르다는 점이 마음에 들었다.
Redux-Toolkit VS Zustand
Zustand가 상태관리 라이브러리 중 사용이 간편하고 진입장벽이 낮다고 해서 사용할까 말까 많이 망설였다.
하지만 Redux-Toolkit을 사용한 이유는 아직 채용 공고를 보면 Redux-Toolkit을 사용하는 곳이 많다.
그리고 Redux-Toolkit을 먼저 사용해봐야 Zustand를 나중에 배웠을 때 장점이 보일 것 같아 일단은 Redux-Toolkit 상태관리 라이브러리를 선택했다.
TypeScript VS JavaScript
요즘 현업에서 많이 쓰는 TypeScript로 시작했었으나, 사용 중간에 타입 에러 잡는다고 시간을 많이 쏟아 프로젝트 기간 내에 못 맞출 거 같아 JavaScript로 전향했다.
프로젝트 끝나면 TypeScript로 리팩토링을 꼭 하고 싶다.
TailwindCSS VS Emotion
이전에 Emotion을 써봤고, 나는 클래스명과 변수명을 짓는데 시간을 꽤 사용한다.
TailwindCSS는 클래스명 생각할 시간에 개발에 더 집중할 수 있기 때문에 이번 기회에 사용했다.
하지만 처음 사용해 봐서 class명 찾는다고 시간을 더 쓴 거 같다...
폴더 구조
- components
- common - 여러 페이지에서 사용되는 공통 컴포넌트 모음
- layout - 전체 화면을 담당하는 컴포넌트 (Header, main, Footer)
- data : 공통으로 사용되는 data 모음 (카테고리 등등)
- hooks : 공통으로 사용되는 hooks 모음
- pages : 페이지를 담당하는 폴더 - 각 페이지마다 한번씩 쓰는 컴포넌트는 여기서 관리
- reducers : redux slice 모음
- store : 리덕스 세팅
- utils : 공통으로 사용되는 axios나 함수 모음
- 가독성을 유지하기 위해, React 컴포넌트는 .jsx 확장자를 사용하고, 일반적인 JavaScript 함수를 모아둔 파일은 .js 확장자를 사용
초기 세팅 (공통 로직) - 라이브러리의 도움을 받아 효율적으로 구현
레이아웃
공통적인 컴포넌트를 처리하기 위해 React-Router-Dom에 있는 <Outlet /> 속성을 이용했다.
components/layout/index.jsx
import { Outlet } from 'react-router-dom';
import Footer from './Footer';
import Header from './Header';
export default function Layout() {
return (
<div>
<Header />
<main>
<Outlet />
</main>
<Footer />
</div>
);
}
App.jsx
import { Route, Routes } from 'react-router-dom';
import Layout from './components/layout';
import MainPage from './pages/MainPage';
import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage';
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<MainPage />} />
<Route path="/login" element={<LoginPage />} />
</Route>
</Routes>
);
}
export default App;
<Route path="/" element={<Layout />}> 로 감싸주면 children와 같은 효과를 내어 내부 로직이 Layout 컴포넌트의 main 태그 안에 들어가게 된다.
이렇게 감싸주면 내부의 MainPage와 LoginPage의 컴포넌트에 매번 Layout 컴포넌트로 감싸주는 중복 로직을 제거 할 수 있다.
React-Router-Dom 공식 사이트 (Outlet)
https://reactrouter.com/en/main/components/outlet
CSS
자주 사용하는 색상 추가 및 tailwindcss 속성 커스텀을 위해 tailwind config 파일에 옵션을 추가하고, 공통으로 사용되는 heading 태그나 section 태그 통일성을 위해 tailwindcss에서 지원하는 @layer base 를 사용하여 기본 스타일을 지정해 주었다.
tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}', 'node_modules/flowbite-react/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
primary: '#22a3cc',
semi_primary: '#bce3ef',
secondary: '#bfd9ff',
danger: '#e74c3c',
},
container: {
screens: {
lg: '1024px',
md: '768px',
sm: '640px',
xs: '390px',
},
padding: {
DEFAULT: '1rem',
sm: '0',
},
},
},
},
plugins: [require('flowbite/plugin')],
};
src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
font-family: 'Roboto', system-ui, sans-serif;
}
textarea {
resize: none;
}
section + section {
@apply mt-12;
}
h2 {
@apply text-2xl font-bold mb-8;
}
h3 {
@apply text-lg font-semibold mb-5;
}
}
@layer base 속성 안에다가 기본 스타일을 지정하는 이유
CSS 우선 순위 때문이다. @layer 밖에다 적으면 h2 태그를 커스텀 할 일이 생길 때 CSS 우선 순위 때문에 해당 클래스명이 안 먹을 수 있다. @layer 종류에는 base, components, utilities 등등 있다.
@apply 속성은 css 파일 내에서도 tailwindcss 클래스명을 사용할 수 있게 해준다.
tailwindcss 공식 사이트 (base styles)
https://tailwindcss.com/docs/adding-custom-styles#adding-base-styles
참고 자료
axios 요청
axios 인스턴스로 매번 백엔드 똑같은 주소(http://localhost:8000)를 적어야 하는 중복을 없앴다.
또한, 인터셉터 메소드를 이용하여 매번 headers에 토큰값을 넣어야 하는 사용자 인증 로직을 처리하였다.
- 인터셉터 메소드 : then 또는 catch로 처리되기 전에 요청과 응답을 가로챌수 있습니다.
- request에서는 요청을 보내기 전에 하는 작업이 가능하다.
- response에서는 서버에서 받은 응답이 then과 catch로 처리되기 전에 인터셉터로 가로채서 원하는 작업들을 추가할 수 있다.
utils/axios.js
import axios from 'axios';
export const API = axios.create({
baseURL: import.meta.env.VITE_APP_API_URL,
});
API.interceptors.request.use(
function (config) {
config.headers.Authorization = 'Bearer ' + localStorage.getItem('accessToken');
return config;
},
function (error) {
return Promise.reject(error);
}
);
API.interceptors.response.use(
function (response) {
return response;
},
function (error) {
if (error.response.data.msg === '토큰 만료') {
window.location.reload();
}
return Promise.reject(error);
}
);
pages/Notice/NoticePage/index.jsx
import { API } from '../../../utils/axios';
export default function NoticePage() {
const [notice, setNotice] = useState([]);
useEffect(() => {
const fetchNotices = async () => {
try {
const response = await API.get('/notices');
const data = await response.data;
setNotice(data);
} catch (err) {
console.error(err);
}
};
fetchNotices();
}, []);
return (
<div className="container">
{...}
</div>
);
}
위 코드를 실행하면 매번 요청때마다 http://localhost:8001/api 가 기본주소로 등록이 되고, 매 요청마다 따로 설정 안해도 Request Headers Authorization 에 토큰값을 실어서 보낼 수 있다.
axios 공식 사이트 (인스턴스)
https://axios-http.com/kr/docs/instance
axios 공식 사이트 (인터셉터)
https://axios-http.com/kr/docs/interceptors
다음 블로그에서는 redux 세팅과 로그인 여부에 따른 제한 접근 라우팅 구현에 대해 적어야 겠다.