
이번 시간에는 웹 기반 문서 편집기 프로젝트의 FE 구조 설계서, BE 구조 설계서, 데이터베이스 설계, 개발 환경 셋업, 그리고 세부 UI 컴포넌트 구현까지 진행했습니다.
프론트엔드는 페이지 단위가 아닌 컴포넌트 단위로 설계합니다. 재사용 가능한 단위로 쪼개두면 나중에 화면이 추가되거나 수정될 때 훨씬 유연하게 대응할 수 있습니다.
components/
├── common/
│ ├── Button.tsx # 공통 버튼
│ ├── Input.tsx # 공통 인풋
│ ├── Modal.tsx # 공통 모달
│ └── Toast.tsx # 알림 토스트
├── layout/
│ ├── Header.tsx # 헤더
│ ├── Sidebar.tsx # 사이드바 (문서 목록)
│ └── Layout.tsx # 전체 레이아웃 래퍼
└── editor/
├── Editor.tsx # 마크다운 에디터
├── Preview.tsx # 실시간 미리보기
└── Toolbar.tsx # 서식 도구 모음
전역 상태와 서버 상태를 명확히 구분하여 관리합니다.
| 상태 종류 | 관리 도구 | 주요 데이터 |
|---|---|---|
| 서버 상태 | TanStack Query | 문서 목록, 문서 상세, 사용자 정보 |
| 전역 클라이언트 상태 | Context API | 로그인 여부, 테마 설정 |
| 로컬 상태 | useState | 에디터 입력값, 모달 열림 여부 |
const router = createBrowserRouter([
{ path: '/', element: <Home /> },
{ path: '/login', element: <Login /> },
{ path: '/signup', element: <SignUp /> },
{ path: '/editor', element: <Editor /> }, // 새 문서
{ path: '/editor/:id', element: <Editor /> }, // 기존 문서 편집
{ path: '/view/:id', element: <Viewer /> }, // 공개 문서 읽기
]);
# Vite + React + TypeScript 프로젝트 생성
npm create vite@latest document-editor -- --template react-ts
cd document-editor
npm install
# 라우팅
npm install react-router-dom
# 서버 상태 관리
npm install @tanstack/react-query
# 스타일링
npm install styled-components
npm install -D @types/styled-components
# 폼 관리
npm install react-hook-form
# HTTP 통신
npm install axios
# 마크다운
npm install react-markdown
vite.config.ts 에 alias를 설정하면 상대 경로 대신 절대 경로로 임포트할 수 있어 코드가 훨씬 깔끔해집니다.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
size?: 'small' | 'medium' | 'large';
scheme?: 'primary' | 'secondary' | 'danger';
isLoading?: boolean;
}
function Button({ size = 'medium', scheme = 'primary', isLoading, children, ...props }: ButtonProps) {
return (
<ButtonStyled size={size} $scheme={scheme} {...props}>
{isLoading ? '처리 중...' : children}
</ButtonStyled>
);
}
마크다운 에디터는 좌측 편집창 / 우측 미리보기 의 2단 레이아웃으로 구성합니다.
function EditorPage() {
const [content, setContent] = useState('');
return (
<EditorLayout>
<EditorPane>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="마크다운으로 작성하세요..."
/>
</EditorPane>
<PreviewPane>
<ReactMarkdown>{content}</ReactMarkdown>
</PreviewPane>
</EditorLayout>
);
}
문서를 일정 시간마다 자동 저장하는 기능은 커스텀 훅으로 분리합니다.
function useAutoSave(content: string, docId: number) {
useEffect(() => {
const timer = setTimeout(async () => {
await saveDocument(docId, content);
}, 2000); // 2초 뒤 자동 저장
return () => clearTimeout(timer);
}, [content, docId]);
}
src/
├── controllers/ # 요청을 받아 서비스 호출 후 응답 반환
├── services/ # 비즈니스 로직 처리
├── repositories/ # 데이터베이스 접근 레이어
├── models/ # 데이터 모델 정의
├── middlewares/ # 인증, 에러 처리 등 미들웨어
├── routes/ # API 엔드포인트 정의
└── utils/ # 유틸리티 함수
Controller → Service → Repository 의 3계층 구조로 분리하면 각 레이어의 역할이 명확해지고 테스트 작성이 쉬워집니다.
Users
├── id INT (PK, AUTO_INCREMENT)
├── email VARCHAR(255) UNIQUE NOT NULL
├── password VARCHAR(255) NOT NULL
├── name VARCHAR(100) NOT NULL
└── created_at DATETIME
Documents
├── id INT (PK, AUTO_INCREMENT)
├── title VARCHAR(255) NOT NULL
├── content TEXT
├── is_public BOOLEAN DEFAULT FALSE
├── author_id INT (FK → Users.id)
├── created_at DATETIME
└── updated_at DATETIME
deleted_at 컬럼으로 삭제 여부를 관리합니다. 복구 가능성을 열어두기 위함입니다.author_id, is_public 컬럼에 인덱스를 추가해 조회 성능을 향상시킵니다.bcrypt 로 해싱하여 저장합니다. 평문으로 저장하지 않습니다.# Express + TypeScript 프로젝트 초기화
npm init -y
npm install express
npm install -D typescript ts-node @types/express @types/node nodemon
# 인증 관련 패키지
npm install bcrypt jsonwebtoken
npm install -D @types/bcrypt @types/jsonwebtoken
# 데이터베이스
npm install mysql2
인증은 JWT(JSON Web Token) 방식으로 구현합니다.
1. 로그인 요청 → 서버에서 이메일·비밀번호 검증
2. 검증 성공 → Access Token 발급
3. 이후 요청 시 헤더에 토큰 포함 (Authorization: Bearer <token>)
4. 서버에서 토큰 유효성 검사 후 요청 처리
// 인증 미들웨어
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ message: '인증이 필요합니다.' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch {
return res.status(401).json({ message: '유효하지 않은 토큰입니다.' });
}
};
인증(Authentication) 은 사용자가 누구인지 확인하는 것이고, 인가(Authorization) 는 해당 사용자가 특정 리소스에 접근할 권한이 있는지 확인하는 것입니다. 예를 들어 로그인한 사용자가 자신의 문서에만 수정·삭제 권한을 가지도록 제어합니다.
// 문서 수정 전 소유자 확인
const doc = await documentService.findById(docId);
if (doc.authorId !== req.user.id) {
return res.status(403).json({ message: '권한이 없습니다.' });
}