개요
이 튜토리얼은 Electron, React, TypeScript, Tailwind CSS, Jotai를 활용하여 기본적인 Markdown 노트 앱을 구축하는 방법을 다룹니다.
기술 스택 및 의존성
- Electron: 크로스 플랫폼 데스크톱 애플리케이션 프레임워크
- React: Facebook이 개발한 프론트엔드 라이브러리
- Yarn: npm, pnpm과 유사한 패키지 관리자
- Vite: 차세대 프론트엔드 빌드 도구
- TypeScript: 타입 시스템이 추가된 JavaScript
- Tailwind CSS: 유틸리티 우선 CSS 프레임워크
- PostCSS: CSS 변환을 위한 JavaScript 라이브러리
- clsx: CSS 클래스명을 동적으로 결합하는 라이브러리
- tailwind-merge: Tailwind CSS 클래스 자동 병합 도구
- File system (Node.js): Node.js 파일 시스템 모듈
- Jotai: 경량 고성능 React 상태 관리 라이브러리
- Lodash: JavaScript 유틸리티 라이브러리
- MDXEditor: Markdown 에디터 컴포넌트
- react-icons: React 아이콘 라이브러리
- fs-extra: Node.js 비동기 파일 작업 라이브러리
개발 환경
본 튜토리얼은 VSCode를 기준으로 설명합니다.
VSCode 권장 확장 프로그램
- Auto Import - 자동 임포트 및 의존성 제안
- Auto Rename Tag - 쌍을 이루는 태그 자동 변경
- Tailwind CSS IntelliSense - Tailwind 스타일 자동 완성
- Prettier - 코드 포맷터
- ESLint - 코드Lint 및 오류 검사
- vscode-icons - 파일 아이콘
프로젝트 설정
Electron 프로젝트 생성
먼저 yarn을 설치합니다:
$ brew install yarn
macOS에 Homebrew가 없다면:
$ /bin/bash -c "$(curl -fsSL https://gitee.com/ineo6/homebrew-install/raw/master/install.sh)"
electron-vite를 사용하여 프로젝트를 생성합니다:
$ yarn create @quick-start/electron
설정 선택:
Project name: … markdown-notes
✔ Select a framework: › react
✔ Add TypeScript? … Yes
✔ Add Electron updater plugin? … No
✔ Enable Electron download mirror proxy? … No
의존성을 설치합니다:
cd markdown-notes
yarn
디렉토리 구조
프로젝트에 필요한 디렉토리를 생성합니다:
# 공통 타입 및 상수 디렉토리
mkdir src/shared
# 백엔드 유틸리티 메서드 디렉토리
mkdir src/main/lib
# 프론트엔드 커스텀 훅 디렉토리
mkdir src/renderer/src/hooks
# 프론트엔드 유틸리티 메서드 디렉토리
mkdir src/renderer/src/utils
# 프론트엔드 스토어 및 목업 데이터 디렉토리
mkdir src/renderer/src/store
mkdir src/renderer/src/store/mocks
전체 프로젝트 구조:
.
├──.vscode
├──build
├──node_modules
├──resources
│ └── icon.png
├──src
│ ├── main
│ │ ├── index.ts
│ │ └── lib
│ ├── preload
│ │ ├── index.d.ts
│ │ └── index.ts
│ ├── renderer
│ │ ├── index.html
│ │ └── src
│ │ ├── App.tsx
│ │ ├── assets
│ │ ├── components
│ │ ├── env.d.ts
│ │ ├── hooks
│ │ ├── main.tsx
│ │ ├── store
│ │ └── utils
│ └── shared
│ ├── constants.ts
│ ├── models.ts
│ └── types.ts
├── resources
├── electron-builder.yml
├── electron.vite.config.ts
├── package.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.web.json
└── README.md
preload란? Electron의 메인 프로세스와 렌더러 프로세스를 연결하기 위해 사용하는 특수 스크립트입니다.
애플리케이션 실행
yarn dev
Electron 통신 구조
프로젝트 src 디렉토리에는 main, preload, renderer 세 가지 주요 디렉토리가 있습니다.
Electron 애플리케이션은 메인 프로세스(main)와 렌더러 프로세스(renderer)로 구분됩니다. 메인 프로세스는 백엔드, 렌더러 프로세스는 프론트엔드에 해당하며, preload는 이两者 사이의 통신 다리 역할을 합니다.
통신 다이어그램
graph LR
Config[package.json] -->|시작| Main[src/main/index.ts]
subgraph 렌더러 프로세스
RedererIndex[src/renderer/index.html]-->|임포트|RedererMain[src/renderer/src/main.ts]
RedererMain-->|스타일|RedererCSS[src/renderer/src/assets/index.css]
RedererMain-->|루트 컴포넌트|App[src/renderer/src/App.tsx]
App-->|포함|components[src/renderer/src/components/*.tsx]
end
subgraph 프리로드
Preload[src/preload/index.ts] -->|contextBridge|RedererIndex
end
subgraph 메인 프로세스
Main -->|IPC 통신|Preload
end
윈도우 스타일 설정
애플리케이션의 외형을 개선하기 위해 타이틀 바를 숨기고 커스텀 윈도우 컨트롤을 구현합니다.
src/main/index.ts 파일을 수정합니다:
// ...
function createWindow(): void {
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
center: true,
title: 'MarkdownNotes',
frame: false,
vibrancy: 'under-window',
visualEffectState: 'active',
titleBarStyle: 'hidden',
trafficLightPosition: { x: 15, y: 10 },
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: true,
contextIsolation: true
}
})
// ...
보안 정책을 수정하여 인라인 스크립트 실행을 허용합니다:
src/renderer/index.html:
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
불필요한 파일을 정리합니다:
src/renderer/src/assets에서 main.css를 제외한 모든 파일 삭제main.css파일 내용을 비웁니다src/renderer/src/components/Version.tsx컴포넌트 삭제App.tsx를 아래와 같이 간소화합니다:
function App(): JSX.Element {
return (
<>
</>
)
}
export default App
Tailwind CSS 설정
설치
yarn add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
설치가 완료되면 프로젝트 루트에 tailwind.config.js와 postcss.config.js가 생성됩니다.
tailwind.config.js:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/renderer/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {}
},
plugins: [require('@tailwindcss/typography')]
}
추가 패키지 설치
yarn add -D tailwind-merge
yarn add -D clsx
yarn add -D react-icons
메인 스타일 정의
src/renderer/src/assets/main.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
#root {
@apply h-full;
}
html,
body {
@apply h-full;
@apply select-none;
@apply bg-transparent;
@apply font-mono antialiased text-white;
@apply overflow-hidden;
}
header {
-webkit-app-region: drag;
}
button {
-webkit-app-region: no-drag;
}
::-webkit-scrollbar {
@apply w-2;
}
::-webkit-scrollbar-thumb {
@apply bg-[#555] rounded-md;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
}
}
좌우 레이아웃 구성
React 컴포넌트를 생성하여 좌우 레이아웃을 구현합니다.
파일 확장자 설명
.ts: TypeScript 스크립트 파일.d.ts: 기존 JavaScript 코드를 TypeScript에서 사용하기 위한 타입 정의 파일.tsx: React + 타입 체크가 포함된 파일
컴포넌트 기본 템플릿:
import { ComponentProps } from 'react'
import { twMerge } from 'tailwind-merge'
export const 컴포넌트이름 = ({ className, ...props }: ComponentProps<'div'>) => {
return (
<div className={twMerge('flex justify-center', className)} {...props}>
</div>
)
}
src/renderer/src/components/AppLayout.tsx 생성:
import { ComponentProps, forwardRef } from 'react'
import { twMerge } from 'tailwind-merge'
export const RootLayout = ({ children, className, ...props }: ComponentProps<'main'>) => {
return (
<main className={twMerge('flex flex-row h-screen', className)} {...props}>
{children}
</main>
)
}
export const Sidebar = ({ className, children, ...props }) => {
return (
<aside
className={twMerge('w-[250px] mt-10 h-[100vh+10px] overflow-auto', className)}
{...props}
>
{children}
</aside>
)
}
export const Content = forwardRef<HTMLDivElement, ComponentProps<'div'>>(
({ children, className, ...props }, ref) => (
<div ref={ref} className={twMerge('flex-1 overflow-auto', className)} {...props}>
{children}
</div>
)
)
Content.displayName = 'Content'
App.tsx에 레이아웃 컴포넌트를 임포트합니다:
import { Content, RootLayout, Sidebar } from "./components/AppLayout"
function App(): JSX.Element {
return (
<>
<RootLayout>
<Sidebar className="p-2">
사이드바
</Sidebar>
<Content className="border-l bg-zinc-900/50 border-l-white/20">
콘텐츠 영역
</Content>
</RootLayout>
</>
)
}
export default App
드래그 가능한 헤더 추가
타이틀 바를 숨겼으므로 윈도우 드래그 기능을 위한 커스텀 헤더 컴포넌트가 필요합니다.
src/renderer/src/components/DraggableTopBar.tsx 생성:
export const DraggableTopBar = () => {
return <header className="absolute inset-0 h-8 bg-transparent" />
}
App.tsx에 헤더 컴포넌트를 추가합니다:
import { Content, RootLayout, Sidebar } from "./components/AppLayout"
import { DraggableTopBar } from "./components/DraggableTopBar"
function App(): JSX.Element {
return (
<>
<DraggableTopBar />
<RootLayout>
<Sidebar className="p-2">
사이드바
</Sidebar>
<Content className="border-l bg-zinc-900/50 border-l-white/20">
콘텐츠 영역
</Content>
</RootLayout>
</>
)
}
export default App
이제 상단 영역을 클릭하여 윈도우를 드래그할 수 있습니다.
사이드바 버튼 추가
공통 버튼 컴포넌트
src/renderer/src/components/Button/ActionButton.tsx 생성:
import { ComponentProps } from 'react'
import { twMerge } from 'tailwind-merge'
export type ActionButtonProps = ComponentProps<'button'>
export const ActionButton = ({ className, children, ...props }: ActionButtonProps) => {
return (
<button
className={twMerge(
'px-2 py-1 rounded-md border border-zinc-400/50 hover:bg-zinc-600/50 transition-all duration-100',
className
)}
{...props}
>
{children}
</button>
)
}
새 노트 버튼
src/renderer/src/components/Button/NewNoteButton.tsx 생성:
import { ActionButton, ActionButtonProps } from './ActionButton'
import { LuFileSignature } from 'react-icons/lu'
export const NewNoteButton = ({ ...props }: ActionButtonProps) => {
const handleCreation = async () => {
console.info("노트 생성")
}
return (
<ActionButton onClick={handleCreation} {...props}>
<LuFileSignature className="w-5 h-5 text-zinc-300" />
</ActionButton>
)
}
노트 삭제 버튼
src/renderer/src/components/Button/DeleteNoteButton.tsx 생성:
import { ActionButton, ActionButtonProps } from './ActionButton'
import { FaRegTrashCan } from 'react-icons/fa6'
export const DeleteNoteButton = ({ ...props }: ActionButtonProps) => {
const handleDelete = async () => {
console.info("노트 삭제")
}
return (
<ActionButton onClick={handleDelete} {...props}>
<FaRegTrashCan className="w-5 h-5 text-zinc-300" />
</ActionButton>
)
}
버튼 행 컴포넌트
src/renderer/src/components/ActionButtonRow.tsx 생성:
import { ComponentProps } from 'react'
import { NewNoteButton } from './Button/NewNoteButton'
import { DeleteNoteButton } from './Button/DeleteNoteButton'
export const ActionButtonRow = ({ ...props }: ComponentProps<'div'>) => {
return (
<div {...props}>
<NewNoteButton />
<DeleteNoteButton />
</div>
)
}
App.tsx에 버튼 행을 추가합니다:
import { ActionButtonRow } from "./components/ActionButtonRow"
// ... 기존 임포트 유지
function App(): JSX.Element {
return (
<>
<DraggableTopBar />
<RootLayout>
<Sidebar className="p-2">
<ActionButtonRow className="flex gap-2 mb-4" />
</Sidebar>
<Content className="border-l bg-zinc-900/50 border-l-white/20">
</Content>
</RootLayout>
</>
)
}
macOS의 다크 모드를 활성화하고 애플리케이션을 실행하면 다음과 같이 표시됩니다:
현재 스타일은 다크 모드 전용으로 최적화되어 있습니다.
Mock 데이터 활용
스토어 디렉토리에 테스트 데이터를 추가합니다.
미리보기 기능 추가
날짜 포맷 함수를 작성합니다.
Markdown 에디터 통합
MDXEditor를 설치합니다:
yarn add @mdxeditor/editor
yarn add -D @tailwindcss/typography
스크롤바 스타일을 개선합니다:
src/renderer/src/assets/index.css:
@layer base {
// ...
::-webkit-scrollbar {
@apply w-2;
}
::-webkit-scrollbar-thumb {
@apply bg-[#555] rounded-md;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
}
}
Markdown 에디터 컴포넌트를 생성합니다:
// ...
return <MDXEditor
key={selectedNote.title}
markdown={selectedNote.content}
// ...
/>
노트 제목 추가
Jotai를 설치합니다:
yarn add jotai
상태 관리를 위한 Jotai atoms을 설정합니다.
노트 생성 및 삭제 기능 구현
파일 시스템을 활용하여 노트를 영구적으로 저장합니다.
파일 시스템 설정
mkdir ~/MarkdownNotes
cd ~/MarkdownNotes
echo "Note1의 내용" > Note1.md
echo "Note2의 내용" > Note2.md
필요한 패키지 설치
yarn add fs-extra
yarn add -D lodash
타입 검사를 실행합니다:
yarn typecheck:web
파일 쓰기 기능 구현
src/shared/types.ts:
// ...
export type WriteNote = (title: NoteInfo['title'], content: NoteContent) => Promise<void>
환영 화면 추가
첫 실행 시 표시할 환영 페이지를 구성합니다.
빌드 및 배포
개발 모드 실행
yarn dev
프로덕션 빌드
yarn build
yarn start
macOS 앱 빌드
yarn build:mac
참조 자료
- Build a Markdown Notes app with Electron, React, Typescript, Tailwind and Jotai - YouTube
- 빠른 시작 | Electron