Day 61 - 웹 기반 문서 편집기 제작 프로젝트 (1)

이유승·2025년 3월 17일

* 프로그래머스, 타입스크립트로 함께하는 웹 풀 사이클 개발(React, Node.js) 5기 강의 수강 내용을 정리하는 포스팅.

* 원활한 내용 이해를 위해 수업에서 제시된 자료 이외에, 개인적으로 조사한 자료 등을 덧붙이고 있음.

웹 기반 문서 편집기 제작 프로젝트 (1)

1. 프로젝트 개요

웹 기반 문서 편집기 제작 프로젝트는 Node.js와 Express 기반의 서버 개발, 사용자 인증/인가, 백엔드 아키텍처 설계, 데이터베이스 설계를 중심으로 진행됩니다. 이 프로젝트를 통해 개발 환경 설정부터 실제 운영에 필요한 여러 구성 요소를 체계적으로 학습하고 실습할 수 있습니다.


2-01. BE 구조 설계서

주요 내용

소프트웨어 구조 설계서의 역할:

  • 전체 시스템의 구조를 기술하고 정의하는 문서로, 개발 과정에서 가장 중요한 기준이 됨.
  • 프로젝트 초기 단계에서 반드시 산출해야 하는 문서.

패키지 구조:

  • Route: 클라이언트의 HTTP 요청을 처리하는 경로를 담당
    예: login, logout, users, notes
  • Model: 데이터 구조 및 비즈니스 로직을 처리
    예: user, note
  • Utility: 공통 기능 (예: MySQL 연동 등)을 제공
  • Middleware: 요청 처리 과정에서 인증(authentication), 인가(authorization) 등 부가 기능 수행

프론트엔드 관점 추가 사항:

  • API 문서 이해: 프론트엔드 개발자는 백엔드에서 제공하는 API 엔드포인트(Route)의 역할과 응답 형식을 정확히 이해할 필요가 있습니다.
  • 데이터 흐름 파악: Model과 Middleware의 역할을 파악하여, 프론트엔드에서 데이터 요청 시 어떤 에러 (예: 401, 403 등)가 발생할 수 있는지 미리 예측하고 에러 처리 로직을 구성하는 것이 중요합니다.

2-02. 데이터베이스 설계

주요 내용

데이터베이스 스키마 정의:

  • users 테이블:

    • id: 정수, auto_increment, Primary Key
    • email: 이메일 주소 (varchar)
    • encrypted_password: 해시 처리된 비밀번호 저장
  • notes 테이블:

    • id: 정수, auto_increment, Primary Key
    • title: 텍스트
    • content: 텍스트
    • user_id: 외래키 (FK) – users 테이블의 id와 연동
    • created_at, updated_at: 타임스탬프

데이터베이스 초기화 파일 제공:

  • init-user.sql, init-db.sql, init-test-db.sql
    • 실습을 위해 빈 데이터베이스 생성 및 테스트 데이터 주입

테스트 데이터베이스 구성 및 배포:

  • Docker Desktop & Kubernetes:
    • k8s manifest 파일을 이용한 데이터베이스 인스턴스 실행
    • Deployment, Service(NodePort), Persistent Volume(PV) 및 Persistent Volume Claim(PVC) 설정
    • 로컬 (예: localhost:30036)과 클러스터 내부 (CoreDNS 활용)에서의 접근 방법 제시

프론트엔드 관점 추가 사항:

  • API 데이터 연동: 프론트엔드에서 데이터를 요청할 때, 데이터베이스의 스키마 구조를 이해하면 응답 데이터를 보다 효과적으로 처리할 수 있습니다.
  • 에러 핸들링: 초기화 파일을 통한 테스트 데이터 주입으로 백엔드에서 발생 가능한 데이터 관련 에러를 미리 확인하고, 프론트엔드에서도 이에 따른 에러 메시지를 사용자에게 전달할 수 있는 구조를 고려합니다.

2-03. 개발 환경 셋업

주요 내용

프로젝트 디렉토리 초기화 및 패키지 설치:

  • 기본 패키지: Express, dotenv, nodemon

프로젝트 기본 설정:

  • package.json:

    • 메인 스크립트 지정, 시작 및 빌드 스크립트 정의
  • nodemon.json:

    • 감시할 파일, 무시할 파일, 실행 명령어 설정
  • tsconfig.json:

    • TypeScript 컴파일 옵션 (타겟, 라이브러리, 모듈 해석 등) 설정

환경 변수 및 설정 코드 작성:

  • .env 파일: NODE_ENV, PORT 등의 환경 변수 기록
  • settings.ts: dotenv를 통해 환경변수를 불러와서 적용

서버 애플리케이션 구성:

  • app.ts: Express 기본 설정, JSON 및 URL 인코딩 처리, 에러 핸들링 미들웨어 구성
  • index.ts: app.ts를 불러와서 서버 리스너 (포트 바인딩) 구현

프로덕션 빌드 및 테스트:

  • 빌드 명령어 (npm run build)를 통한 컴파일과, 결과물의 테스트 수행

프론트엔드 관점 추가 사항:

  • 개발 환경 통합: 프론트엔드 개발 시에도 동일한 환경 변수 관리 (.env)를 통해 API 서버 주소, 포트 등을 관리할 수 있습니다.
  • 로컬 개발 편의성: nodemon과 같은 도구를 활용하여 프론트엔드와 백엔드가 동시에 변경사항을 반영하도록 구성할 수 있습니다.

2-04. 사용자 인증 및 인가

주요 내용

사용자 인증 (Authentication):

  • 절차:
    • 사용자가 제출한 인증 정보 (이메일, 암호화된 비밀번호)를 검증하여 유효한 사용자인지 확인
    • 비밀번호 해시: bcrypt 라이브러리를 사용하여 암호화된 비밀번호 비교

사용자 인가 (Authorization):

  • 절차:
    • 특정 자원 (예: 노트)에 접근 시, 요청 사용자가 해당 자원에 접근할 권한이 있는지 확인
    • 예: 로그인한 사용자의 id와 노트의 소유자 id 비교

JWT를 활용한 로그인 처리:

  • POST /login:

    • 요청 본문에서 이메일과 비밀번호를 추출하여 검증
    • 성공 시 JWT 생성 (이메일 정보 인코딩) 후 쿠키 (access-token)에 저장하여 전송
  • JWT 유효성 검증: 미들웨어를 통해 인증 상태 확인

CORS 정책 적용:

  • 정책 목적:

    • 정해진 URL로부터 서비스된 FE 코드에 의한 요청만 허용하여, 악의적 요청을 차단
  • 설정:

    • 환경 변수 (CORS_ALLOWED_ORIGIN)를 통해 허용된 출처 지정

프론트엔드 관점 추가 사항:

  • API 호출 시 인증 처리:
    프론트엔드에서는 로그인 후 JWT 토큰을 쿠키나 로컬 스토리지에 저장하고, 이후 API 요청 시 자동으로 포함시켜야 합니다.
  • CORS 이슈:
    프론트엔드 개발 시, CORS 정책으로 인해 API 호출이 차단되지 않도록 백엔드에서 설정한 CORS 정책과 일치하는지 확인해야 합니다.
  • 에러 상태 처리:
    401 (Unauthorized), 403 (Forbidden) 등의 에러 발생 시 사용자에게 알맞은 메시지와 재로그인 등의 안내를 제공하는 UI/UX 설계가 필요합니다.

프론트엔드 연동 및 추가 고려 사항

1. API 통신 구현

  • HTTP 클라이언트:
    axios, fetch API 등을 사용하여 백엔드와 통신합니다.
  • 상태 관리:
    API 응답 데이터를 Redux, Context API 등으로 관리하여 UI와 연동합니다.

2. 사용자 인증 토큰 관리

  • 토큰 저장 위치:
    쿠키 또는 로컬 스토리지에 저장하며, 보안상 HTTPS, HttpOnly 옵션 등을 고려합니다.
  • 자동 갱신:
    토큰 만료 시 자동 재인증 또는 갱신 로직을 구현할 수 있습니다.

3. 에러 핸들링 및 사용자 피드백

  • 에러 코드에 따른 처리:
    401, 403 등의 에러 발생 시 사용자에게 재로그인 또는 에러 원인 안내
  • 로딩 상태 및 예외 처리:
    API 호출 시 로딩 스피너, 실패 시 에러 메시지 UI 제공

4. CORS와 프록시 설정

  • 개발 환경 설정:
    프론트엔드 개발 서버 (예: create-react-app)에서는 프록시 설정을 통해 CORS 문제를 우회할 수 있습니다.
  • 배포 환경:
    백엔드의 CORS 정책과 FE 배포 도메인이 일치하도록 설정하여, 실제 서비스 시 문제가 발생하지 않도록 주의합니다.

3-01. FE 구조 설계서와 상세 설계서

개요 및 목표

프론트엔드 애플리케이션의 전반적인 구조와 상세 설계를 기술한 문서로, 전체 프로젝트의 FE 개발 기준을 제공합니다.

패키지 구조 구성 요소

  • Page Components:
    페이지 단위의 컴포넌트로, 화면 전체 레이아웃 및 각 페이지별 기능 구현.
  • (Smaller) Components:
    재사용 가능한 UI 컴포넌트들 (버튼, 인풋, 카드 등)
  • API 함수:
    백엔드와의 통신을 담당하는 함수 모음.
  • React Query Hooks:
    서버 상태 관리를 위한 React Query 기반의 커스텀 훅 모음.
  • Utility:
    공통 유틸리티 함수 및 도구 (예: 날짜 포맷팅, 검증 함수 등)

구조 설계의 의의

FE 개발 시 모듈별 역할과 책임을 명확히 하여, 유지보수와 확장이 용이하도록 설계함.

프론트엔드 관점 추가 사항

  • API 및 데이터 흐름에 맞춰 각 컴포넌트가 백엔드와 어떻게 연동되는지 충분히 이해해야 합니다.
  • 재사용성과 확장성을 고려한 컴포넌트 분리와 폴더 구조의 일관성이 중요합니다.

3-02. 개발 환경 셋업

주요 내용 및 단계

프론트엔드 개발 환경을 React 기반 Typescript 프로젝트로 설정하고, Craco를 활용하여 커스터마이징하는 방법을 다룹니다.

1. React App 생성

npx create-react-app frontend --template typescript

프로젝트 루트에서 위 명령어를 실행하여 Typescript 기반의 기본 스켈레톤 코드를 생성합니다.

2. Craco 설치 및 설정

  • Craco 설치:
npm install @craco/craco
  • package.json 수정:
    기존의 react-scripts 명령어를 Craco 명령어로 변경합니다.
"scripts": {
- "start": "react-scripts start",
+ "start": "craco start",
- "build": "react-scripts build",
+ "build": "craco build",
- "test": "react-scripts test",
+ "test": "craco test"
}

3. 경로 별칭 설정

  • tsconfig.json 예시:
{
  "compilerOptions": {
    "(...)": "",
    "jsx": "react-jsx",
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": [
    "src"
  ],
  "exclude": ["src/**/*.test.ts", "src/**/__mocks__/*.ts"]
}

경로 별칭을 통해 import 구문을 단순화합니다.

4. Craco 설정 파일 작성

  • craco.config.js 예시:
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;
        },
      },
    },
  ],
};

5. 개발 환경 확인

  • 실행 및 빌드 테스트:
    • 개발 서버 실행:
    npm start
    • 빌드 테스트:
    npm run build
    serve -s build
    • 테스트 실행:
    CI=true npm test

6. 환경 변수 설정 및 백엔드 연동

  • 개발 환경 변수:
    .env 파일에 REACT_APP_API_BASE_URL (예: http://localhost:3031) 설정
  • Production build 시:
    별도의 방식으로 환경 변수를 전달합니다.
  • 빌드 시 환경 변수 파일 생성 (build/env.js):
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',
}
  • API 호출 설정 (settings.ts 및 http.ts):
// 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,
});

프론트엔드 관점 추가 사항

  • Craco와 경로 별칭을 활용하면 프로젝트 구조가 깔끔해지고, 유지보수가 쉬워집니다.
  • 환경 변수 관리 및 빌드 시 동적 환경 변수 주입은 배포 환경에서 매우 중요합니다.

3-03. 세부 UI 컴포넌트 설계와 구현

주요 내용 및 목표

프론트엔드 컴포넌트들의 동작 방식을 이해하고, 일부 페이지를 실제 코드로 구현하는 과정을 다룹니다.
이 자료에서는 정적인 페이지 구현과 API 연동이 필요한 페이지(예: 회원가입)의 구현 예시를 포함하고 있습니다.

1. 프로젝트 파일 정리 및 초기 설정

  • 불필요 파일 삭제:
    App.test.tsx, index.css, logo.svg, reportWebVitals.ts 등 사용하지 않을 파일들을 삭제합니다.
  • Git 리포지토리 셋업:
    Backend와 Frontend를 함께 관리하는 리포지토리 구성 (예: .gitignore 파일 재정리)
  • 배포 파일 목록:
    App.css, Index.template.tsx, mussg.svg

2. 라이브러리 설치

npm을 이용하여 다음 라이브러리를 설치합니다.

  • react-router-dom
  • @tanstack/react-query
  • styled-components
  • open-color
    추가로 필요한 라이브러리도 동일한 방법으로 설치합니다.

3. DOM Root 생성

  • src/index.tsx:
import 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에 애플리케이션을 렌더링합니다.

4. React App 생성 및 기본 설정

  • src/App.tsx:
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를 도입하여 오류를 처리할 예정입니다.

5. 라우터 구현

  • src/router.tsx:
import { Route, createBrowserRouter, createRoutesFromElements } from "react-router-dom";
import { IndexPage } from "./pages/Index";

export const router = createBrowserRouter(
  createRoutesFromElements(
    <Route index Component={IndexPage} />
  )
);

향후 RouteErrorBoundary를 도입하여 라우팅 오류를 처리할 예정입니다.

6. Index 페이지 구현

  • src/pages/Index.tsx:
import { IndexTemplate } from "./Index.template";

export const IndexPage = () => {
  return <IndexTemplate />;
}
  • src/pages/Index.template.tsx:
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)를 적용해 로그인 상태에 따라 페이지 이동을 구현할 예정입니다.

7. 회원가입 페이지 구현

  • src/pages/Join.tsx:
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이 포함되어 있습니다.

  • src/components/JoinForm.tsx:
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>
  );
};

코드 일부는 상세 구현 내용에 따라 추가 구현이 필요합니다.

8. API Hook 및 호출 코드 구현

  • API Hook (src/hooks/useJoin.ts):
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"를 반환하도록 처리합니다.

  • API 호출 코드 (src/apis/requestJoin.ts):
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 호출 코드가 포함되어야 합니다.

9. 프론트엔드 개발 방향 예시

  • 정적 페이지 (초기 화면) 구현 과정
  • API 연동이 필요한 페이지 (회원가입) 구현 과정
    이후 단원에서는 테스트 케이스 설계, 구현 및 TDD 적용에 대해 다룹니다.

프론트엔드 관점 추가 사항

  • 컴포넌트 단위로 UI를 분리하고, React Query를 통한 서버 상태 관리를 효과적으로 구현합니다.
  • HOC 및 에러 경계(Error Boundary)를 통해 사용자 경험(UX) 및 안정성을 향상시킬 수 있도록 합니다.

profile
프론트엔드 개발자를 준비하고 있습니다.

0개의 댓글