* 프로그래머스, 타입스크립트로 함께하는 웹 풀 사이클 개발(React, Node.js) 5기 강의 수강 내용을 정리하는 포스팅.
* 원활한 내용 이해를 위해 수업에서 제시된 자료 이외에, 개인적으로 조사한 자료 등을 덧붙이고 있음.
웹 기반 문서 편집기 제작 프로젝트는 Node.js와 Express 기반의 서버 개발, 사용자 인증/인가, 백엔드 아키텍처 설계, 데이터베이스 설계를 중심으로 진행됩니다. 이 프로젝트를 통해 개발 환경 설정부터 실제 운영에 필요한 여러 구성 요소를 체계적으로 학습하고 실습할 수 있습니다.
소프트웨어 구조 설계서의 역할:
패키지 구조:
login, logout, users, notesuser, note프론트엔드 관점 추가 사항:
데이터베이스 스키마 정의:
users 테이블:
id: 정수, auto_increment, Primary Keyemail: 이메일 주소 (varchar)encrypted_password: 해시 처리된 비밀번호 저장notes 테이블:
id: 정수, auto_increment, Primary Keytitle: 텍스트content: 텍스트user_id: 외래키 (FK) – users 테이블의 id와 연동created_at, updated_at: 타임스탬프데이터베이스 초기화 파일 제공:
init-user.sql, init-db.sql, init-test-db.sql 테스트 데이터베이스 구성 및 배포:
localhost:30036)과 클러스터 내부 (CoreDNS 활용)에서의 접근 방법 제시프론트엔드 관점 추가 사항:
프로젝트 디렉토리 초기화 및 패키지 설치:
Express, dotenv, nodemon프로젝트 기본 설정:
package.json:
nodemon.json:
tsconfig.json:
환경 변수 및 설정 코드 작성:
.env 파일: NODE_ENV, PORT 등의 환경 변수 기록settings.ts: dotenv를 통해 환경변수를 불러와서 적용서버 애플리케이션 구성:
app.ts: Express 기본 설정, JSON 및 URL 인코딩 처리, 에러 핸들링 미들웨어 구성index.ts: app.ts를 불러와서 서버 리스너 (포트 바인딩) 구현프로덕션 빌드 및 테스트:
npm run build)를 통한 컴파일과, 결과물의 테스트 수행프론트엔드 관점 추가 사항:
.env)를 통해 API 서버 주소, 포트 등을 관리할 수 있습니다.nodemon과 같은 도구를 활용하여 프론트엔드와 백엔드가 동시에 변경사항을 반영하도록 구성할 수 있습니다.사용자 인증 (Authentication):
bcrypt 라이브러리를 사용하여 암호화된 비밀번호 비교사용자 인가 (Authorization):
id와 노트의 소유자 id 비교JWT를 활용한 로그인 처리:
POST /login:
access-token)에 저장하여 전송JWT 유효성 검증: 미들웨어를 통해 인증 상태 확인
CORS 정책 적용:
정책 목적:
설정:
CORS_ALLOWED_ORIGIN)를 통해 허용된 출처 지정프론트엔드 관점 추가 사항:
axios, fetch API 등을 사용하여 백엔드와 통신합니다.Redux, Context API 등으로 관리하여 UI와 연동합니다.create-react-app)에서는 프록시 설정을 통해 CORS 문제를 우회할 수 있습니다.프론트엔드 애플리케이션의 전반적인 구조와 상세 설계를 기술한 문서로, 전체 프로젝트의 FE 개발 기준을 제공합니다.
FE 개발 시 모듈별 역할과 책임을 명확히 하여, 유지보수와 확장이 용이하도록 설계함.
프론트엔드 개발 환경을 React 기반 Typescript 프로젝트로 설정하고, Craco를 활용하여 커스터마이징하는 방법을 다룹니다.
npx create-react-app frontend --template typescript
프로젝트 루트에서 위 명령어를 실행하여 Typescript 기반의 기본 스켈레톤 코드를 생성합니다.
npm install @craco/craco
"scripts": {
- "start": "react-scripts start",
+ "start": "craco start",
- "build": "react-scripts build",
+ "build": "craco build",
- "test": "react-scripts test",
+ "test": "craco test"
}
{
"compilerOptions": {
"(...)": "",
"jsx": "react-jsx",
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"include": [
"src"
],
"exclude": ["src/**/*.test.ts", "src/**/__mocks__/*.ts"]
}
경로 별칭을 통해 import 구문을 단순화합니다.
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
/** @type {import('webpack').Configuration} */
module.exports = {
plugins: [
{
plugin: {
overrideWebpackConfig: ({ webpackConfig }) => {
webpackConfig.resolve.plugins.push(new TsconfigPathsPlugin({}));
return webpackConfig;
},
},
},
],
};
npm startnpm run build
serve -s buildCI=true npm test.env 파일에 REACT_APP_API_BASE_URL (예: http://localhost:3031) 설정echo -n "" > ./build/env.js
echo "window._ENV={" >> ./build/env.js
for key in $(compgen -v | grep ^REACT_APP_); do
echo "$key:'${!key}'," >> ./build/env.js
done
echo "}" >> ./build/env.js
window._ENV = {
REACT_APP_API_BASE_URL: 'http://localhost:3031',
}
// settings.ts
const { REACT_APP_API_BASE_URL: API_BASE_URL = "" } = window._ENV ?? process.env;
export { API_BASE_URL };
// http.ts
import axios from "axios";
import { API_BASE_URL } from "@/settings";
export const httpClient = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
withCredentials: true,
});
프론트엔드 컴포넌트들의 동작 방식을 이해하고, 일부 페이지를 실제 코드로 구현하는 과정을 다룹니다.
이 자료에서는 정적인 페이지 구현과 API 연동이 필요한 페이지(예: 회원가입)의 구현 예시를 포함하고 있습니다.
App.test.tsx, index.css, logo.svg, reportWebVitals.ts 등 사용하지 않을 파일들을 삭제합니다..gitignore 파일 재정리)App.css, Index.template.tsx, mussg.svgnpm을 이용하여 다음 라이브러리를 설치합니다.
react-router-dom@tanstack/react-querystyled-componentsopen-colorimport React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
React 18의 새로운 Root API를 사용하여 DOM에 애플리케이션을 렌더링합니다.
import React from 'react';
import { RouterProvider } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { router } from "./router";
import "./App.css";
const queryClient = new QueryClient({
// 추가 설정
});
export const App: React.FC = () => {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
};
향후 AppErrorBoundary를 도입하여 오류를 처리할 예정입니다.
import { Route, createBrowserRouter, createRoutesFromElements } from "react-router-dom";
import { IndexPage } from "./pages/Index";
export const router = createBrowserRouter(
createRoutesFromElements(
<Route index Component={IndexPage} />
)
);
향후 RouteErrorBoundary를 도입하여 라우팅 오류를 처리할 예정입니다.
import { IndexTemplate } from "./Index.template";
export const IndexPage = () => {
return <IndexTemplate />;
}
import { Link } from "react-router-dom";
import { styled } from "styled-components";
import oc from "open-color";
import { ReactComponent as MussgImage } from "@/assets/mussg.svg";
export const IndexTemplate: React.FC = () => {
return (
<Container>
<MussgImage height="182" />
<AppTitle>Programmers Note Editor</AppTitle>
<AppDescription>
<strong>Programmers Note Editor</strong>는 (...)
<br />
메모는 클라우드에 저장되어 언제 어디서나 (...)
</AppDescription>
<StartLink to="login">무료로 시작하기</StartLink>
<Footer>© 2023 Grepp Co.</Footer>
</Container>
);
};
구조 설계에 따라 HOC(withUnauthenticated)를 적용해 로그인 상태에 따라 페이지 이동을 구현할 예정입니다.
import { useNavigate } from "react-router-dom";
import { useJoin } from "@/hooks/useJoin";
import { JoinTemplate, JoinTemplateProps } from "./Join.template";
export const JoinPage = () => {
const navigate = useNavigate();
const { join } = useJoin();
const handleSubmit: JoinTemplateProps["onSubmit"] = async ({ email, password }) => {
const { result } = await join({ email, password });
if (result === "conflict") {
return alert("이미 가입된 이메일입니다.");
}
result satisfies "success";
alert("회원가입이 완료되었습니다.");
navigate("/login");
};
return <JoinTemplate onSubmit={handleSubmit} />;
}
JoinTemplate 내부에 JoinForm이 포함되어 있습니다.
import React from "react";
export interface JoinFormProps {
onSubmit?(e: { email: string; password: string }): void;
}
export const JoinForm: React.FC<JoinFormProps> = (props) => {
return (
<Container>
<Title>회원가입</Title>
<Form
method="post"
onSubmit={(e) => {
e.preventDefault();
// ... 입력값 검증 및 처리 로직
if (password !== passwordConfirm) {
// 에러 처리 로직
}
props.onSubmit?.({ email, password });
}}
>
<InputContainer>
{/* ... */}
</InputContainer>
{/* ... */}
</Form>
</Container>
);
};
코드 일부는 상세 구현 내용에 따라 추가 구현이 필요합니다.
export const useJoin = () => {
const queryClient = useQueryClient();
const joinMutation = useMutation({
mutationFn: async (params: JoinParams) => {
const [error] = await requestJoin(params);
if (isAxiosError(error) && error.response?.status === 409) {
return { result: "conflict" as const };
}
if (error) {
throw error;
}
return { result: "success" as const };
},
onSuccess: async () => {
// 성공 시 추가 처리
},
});
return { join: joinMutation.mutateAsync };
};
HTTP 에러(409; conflict)의 경우 "conflict", 그 외에는 "success"를 반환하도록 처리합니다.
import { httpClient } from "@/utils/http";
export interface JoinParams {
email: string;
password: string;
}
export async function requestJoin(params: JoinParams) {
await httpClient.post("/users", params);
}
src/utils/http.ts에 axios를 이용한 API 호출 코드가 포함되어야 합니다.