오픈소스를 개발 프로젝트를 진행하였습니다.
서비스 개발이 아닌 오픈소스 개발 중의 고민과 느낀 점에 대해서 이야기해보려고 합니다.
오픈소스 프로젝트에서 가장 중요한 첫 단계는 어떤 주제로 개발할지 정하는 것입니다. 이 과정은 생각보다 많은 시간이 소요되며, 프로젝트의 방향성을 결정짓는 핵심적인 단계입니다.
당시 저는 타임 트래커(Time Tracker) 서비스를 개발 중이었고, 이와 관련된 문제를 해결하기 위한 라이브러리를 만들어보자는 아이디어를 떠올렸습니다. 이를 기반으로, 시간표와 관련된 라이브러리 개발이라는 주제를 선정했습니다.
프로젝트의 초기 목적은 간단하면서도 실용적인 기능을 구현하는 것이었습니다. 구체적으로는, 할일 목록을 입력받아 이를 시간표 형식으로 적절히 렌더링하는 라이브러리를 만드는 것이 목표였습니다. 이를 위해 다음과 같은 세부 목표를 설정했습니다
리스트 형식의 할일 목록을 입력받는다
사용자가 간단히 할일을 정의하고 이를 라이브러리에 전달할 수 있도록 입력 형식을 정의합니다.
시간에 맞게 적절한 위치에 할일을 렌더링한다
시간표 상에서 각 할일이 시각적으로 이해되기 쉽게 배치되도록 렌더링 로직을 설계합니다.
프로젝트 초기에는 하나의 레포지토리에 라이브러리 소스코드와 데모 페이지를 함께 포함시켰습니다. 그러나 이러한 방식은 다음과 같은 문제가 있었습니다:
이를 해결하기 위해 Yarn Berry의 Workspace를 사용하여 모노레포 구조를 도입했습니다. 이를 통해 데모 페이지와 라이브러리를 각각 별도의 패키지로 관리할 수 있었고, 다음과 같은 장점을 얻었습니다:
프로젝트의 목표 중 하나는 경량성과 독립성을 가진 라이브러리를 구현하는 것이었습니다. 이를 위해 라이브러리 자체의 구현에는 어떠한 추가 의존성도 사용하지 않고 작성했습니다.
반면, 데모 페이지는 다음과 같은 이유로 일부 라이브러리를 활용했습니다:
이와 같은 분리를 통해 라이브러리는 본연의 경량성을 유지하면서도, 데모 페이지에서는 사용자 경험을 강화할 수 있었습니다.
렌더링 측면에서는 useRef를 렌더링 시점 이후에 영역을 잡을 수 있게 했다.
따라서 사용자가 만든 div에 ref를 주고 해당 요소의 사이즈를 가져와 적절한 위치를 잡는다.
할일 목록에 대한 시간을 가지고 요소의 사이즈 height를 알고 어디에 위치하면 좋을지 x축으로 위치를 계산한다.
이후 겹치는 요소가 있을때는 겹치는 요소를 한 묶음으로 묶고 그들을 x축으로 나누어 표시하는 로직을 짰다.
Headless 컴포넌트로 설계하였다.
처음의 설계에서는 style 관련 porps를 많이 넘겨받는 구조로 설계하였으나, 너무 속성이 많아지고 불필요하게 코드가 많아지는 것을 느겼다. 따라서 스타일이 존재하지 않고 로직만이 존재하는 Headless 컴포넌트로 수정하였다.
timetable을 그리기 위해 필요한 정보를 만들 수 있는 로직 등을 custom hook으로 제공하고 사용자는 hook을 사용하여 원하는 디자인으로 timetable을 그릴 수 있게 설계하였습니다.
렌더링의 핵심은 useRef를 활용하여 DOM 요소의 크기와 위치를 동적으로 계산하는 방식입니다. 이를 통해 사용자가 정의한 div 요소를 기반으로 적절한 위치에 렌더링할 수 있도록 설계했습니다.
div에 ref를 부여하고, 해당 요소의 사이즈와 위치 정보를 가져옵니다.height)를 계산한 뒤, 적합한 세로축 위치를 결정합니다.초기 설계에서는 style 관련 props를 다수 받아 스타일과 로직을 함께 처리하는 구조로 시작했습니다. 그러나 이러한 방식은 다음과 같은 문제를 일으켰습니다:
이 문제를 해결하기 위해, Headless 컴포넌트로 구조를 변경했습니다. Headless 컴포넌트는 스타일을 포함하지 않고, 오직 로직만 제공하여 사용자가 원하는 디자인으로 자유롭게 스타일링할 수 있도록 합니다.
사용자 경험을 단순화하고 코드 재사용성을 높이기 위해, Custom Hook을 도입했습니다.
라이브러리는 간단히 아래 명령어로 설치할 수 있습니다.
npm i react-custom-timetable
이 라이브러리는 Headless 구조로 제공되며, 핵심 기능은 useTimeTable이라는 커스텀 훅을 통해 사용할 수 있습니다.
useTimeTable 훅은 할일 목록(taskList)을 입력받아, 각 요소의 위치와 스타일 정보를 자동으로 계산해주는 기능을 제공합니다. 아래는 사용 예제입니다
import "./styles.css";
import "./reset.css";
import useTimeTable from "react-custom-timetable";
import { exampleTaskList } from "./timetableMockData";
import { TaskListItem } from "./TaskListItem";
function App() {
const { taskListWithAutoPosition, timeTableCallbackRef } = useTimeTable({
taskList: exampleTaskList,
});
return (
<div
style={{
height: "500px",
border: "1px solid black",
boxSizing: "border-box",
position: "relative",
overflow: "hidden",
}}
ref={timeTableCallbackRef}
>
{taskListWithAutoPosition.map((task, index) => (
<TaskListItem task={task} style={task.style} key={index} />
))}
</div>
);
}
taskList 전달:useTimeTable에 원본 할일 목록(taskList)을 전달합니다.exampleTaskList는 각 할일의 시간 정보와 기본 데이터를 포함합니다.useTimeTable은 ref 영역을 기준으로 각 할일의 위치와 스타일 정보를 계산합니다.taskListWithAutoPosition: 위치 정보(style)가 포함된 새로운 할일 목록.timeTableCallbackRef: 타임테이블 영역을 계산하기 위한 ref 콜백.taskListWithAutoPosition을 순회하며 각 할일을 렌더링합니다.style 속성을 기반으로 설정됩니다.
위 코드를 실행하면, 지정된 높이와 너비를 가진 타임테이블에서 할일 목록이 자동으로 적절한 위치에 배치되어 표시됩니다. 겹치는 일정은 가로축으로 나뉘어 배치되며, 사용자는 각 요소의 스타일을 자유롭게 정의할 수 있습니다.
이 프로젝트는 배포를 위해 라이브러리는 esbuild를 사용하여 코드 크기를 최소화하고, 의존성을 제거하여 경량성을 극대화했습니다. 또한, 스타일 라이브러리를 사용하지 않았기 때문에 번들 크기를 더욱 줄일 수 있었습니다. 이러한 과정에서 번들러가 작성된 코드를 어떻게 해석하고 묶어내는지 배울 수 있었습니다.
esbuild.config.js 파일을 통해서 빌드하기 위한 다양한 설정을 하였습니다.
라이브러리 출력 형식을 설정하고 최적화 옵션을 추가했습니다.
/* eslint-disable no-undef */
import { context } from 'esbuild';
import { sassPlugin } from 'esbuild-sass-plugin';
import inlineImage from 'esbuild-plugin-inline-image';
const watch = process.argv.includes('--watch');
const baseConfig = {
entryPoints: ['src/index.ts'],
outdir: 'dist',
bundle: true,
sourcemap: true,
plugins: [
sassPlugin({
type: 'local-css',
}),
inlineImage(),
],
external: ['react', 'react-dom'],
};
Promise.all([
context({
...baseConfig,
format: 'cjs',
outExtension: {
'.js': '.cjs',
},
}).then((ctx) =>
watch ? ctx.watch() : ctx.rebuild().then(() => ctx.dispose()),
),
context({
...baseConfig,
format: 'esm',
}).then((ctx) =>
watch ? ctx.watch() : ctx.rebuild().then(() => ctx.dispose()),
),
]).catch((error) => {
console.log('Build fail');
console.log('error', error);
process.exit(1);
});
또한, package.json 파일을 설정하여 main 및 module 필드를 설정하여 배포된 패키지를 사용자가 설치하였을 때 프로젝트가 올바르게 작동하도록 구성했습니다.
{
"name": "react-custom-timetable",
"description": "custom timetable for react",
"version": "1.0.4",
"keywords": [
"timetable",
"scheduler"
],
"repository": {
"type": "git",
"url": "git+https://github.com/dmdgpdi/react-custom-timetable.git"
},
"contributors": [
"dmdgpdi",
"haejinyun"
],
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"files": [
"dist"
],
"scripts": {
"build": "yarn clean && yarn build:tsc && yarn build:js",
"build:tsc": "yarn tsc",
"build:js": "node esbuild.config.js",
"build:tsc:watch": "yarn tsc --watch",
"build:js:watch": "node esbuild.config.js --watch",
"build:watch": "concurrently \"yarn build:tsc:watch\" \"yarn build:js:watch --watch\"",
"clean": "rimraf -rf dist",
"lint": "eslint '.'",
"test": "yarn jest"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"devDependencies": {
"@eslint/js": "9.10.0",
"@jest/globals": "^29.7.0",
"@types/node": "^22.7.4",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"concurrently": "^9.0.1",
"esbuild": "^0.24.0",
"esbuild-plugin-inline-image": "^0.0.9",
"esbuild-sass-plugin": "^3.3.1",
"eslint": "8.57.1",
"eslint-plugin-react-hooks": "^4.6.2",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rimraf": "^6.0.1",
"sass": "^1.77.8",
"sass-embedded": "^1.79.4",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
},
"license": "MIT",
"packageManager": "yarn@4.4.1"
}
이후 package의 버전을 올린 뒤
npm publish
빌드된 결과물을 NPM에 배포했습니다.

react-custom-timetable npm 링크
코드 샌드 박스
라이브러리를 직접 개발하고 배포하는 과정을 경험하며, 번들링과 배포의 흐름을 보다 깊이 이해할 수 있었습니다. 번들러가 코드를 어떻게 처리하고, 빌드 결과물이 어떤 구조로 나오는지 체계적으로 학습 할 수 있는 시간이었습니다.
번들링 과정에서 소스 코드가 어떤 방식으로 하나의 산출물로 묶이는지 이해할 수 있게 되며, 직접 배포한 결과물을 확인하며 실제 사용자가 라이브러리를 다운받고 사용되기까지의 사이클을 이해할 수 있었습니다.
나아가, 필요할 경우 node_modules 내부를 탐색하며 타입 정의나 구조를 분석하고, 필요한 기능을 선택적으로 활용하는 방법을 익혔습니다.
이후 내가 필요하다고 느끼는 것이 있다면 직접 개발하여 라이브러리로 제공하고 사용할 수 있겠다는 용기와 다양한 아이디어를 얻을 수 있었습니다.