초기 미션 목표
압축/압축해제 프로그램의 구현 방법에는 크게 2가지가 있습니다.
- 여러 파일 들을 각자 압축 후 하나의 파일로 합치기 (zip 방식)
- 여러 파일 들을 하나의 파일로 합치고, 이 파일을 압축하기 (tar.gz 방식)
처음 프로젝트를 구상할 때는 2번 방식으로 구상했지만,
제가 최종적으로 만들고 싶은 것은 zip 압축 프로그램이어서 1번 방식으로 선회했습니다.
여기서 저는 의문이 들었습니다.
여러 파일 들을 각자 압축하는 것은 쉽습니다.
하지만 압축된 파일들을 어떻게 하나로 합치고,
압축해제 시에 합쳐진 파일들은 어떻게 다시 여러 파일들을 나눌 수 있을까?
그래서 인터넷에 자료를 찾아보다가 ZIP의 specification에서 해답을 찾을 수 있었습니다.
여러 파일들을 하나의 파일로 합치는 법
ZIP 포맷을 만든 Pkware사의 specification에는 다음과 같이 나와있습니다.
ZIP 파일 내부
4.3.6 Overall .ZIP file format:
[local file header 1]
[encryption header 1]
[file data 1]
[data descriptor 1]
.
.
.
[local file header n]
[encryption header n]
[file data n]
[data descriptor n]
[archive decryption header]
[archive extra data record]
[central directory header 1]
.
.
.
[central directory header n]
[zip64 end of central directory record]
[zip64 end of central directory locator]
[end of central directory record]
각 파일들의 헤더
4.3.7 Local file header:
local file header signature 4 bytes (0x04034b50)
version needed to extract 2 bytes
general purpose bit flag 2 bytes
compression method 2 bytes
last mod file time 2 bytes
last mod file date 2 bytes
crc-32 4 bytes
compressed size 4 bytes
uncompressed size 4 bytes
file name length 2 bytes
extra field length 2 bytes
file name (variable size)
extra field (variable size)
이 형식을 보고 비슷하게 구현하면 될 것 같았습니다.
.compressed
저는 ZIP파일의 형식보다 간소화된 .compressed의 포맷을 정의했습니다.
.compressed file format:
[filecount] (4byte)
[pathLength] (4byte)
[pathBuffer]
[compressedLength] (4byte)
[compressedContent]
[pathLength] (4byte)
[pathBuffer]
[compressedLength] (4byte)
[compressedContent]
.
.
.
ZIP 형식이나 .compressed 형식같이 (데이터의 길이 - 데이터)로 나타내는 것을 TLV라고 합니다.
https://ko.wikipedia.org/wiki/%ED%98%95%EC%8B%9D-%EA%B8%B8%EC%9D%B4-%EA%B0%92
형식-길이-값 - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. 형식-길이-값 또는 TLV(type-length-value 혹은 tag-length-value의 약자)는 통신 프로토콜에서 필수적이지 않은 항목의 자료를 부호화하는 방식이다. 항목의 형식(자료형)
ko.wikipedia.org
중간 구현 결과
위에 정의한 스펙을 따라서 구현한 결과는 다음과 같습니다.
압축 영상
압축 해제 영상
중간 구현에서 아쉬운 점
파일을 압축/압축해제를 한다는 1차 목표는 달성했지만 아쉬운점이 많았습니다.
우선 사용자가 제시된 선택지를 입력해서 압축할 파일과 폴더를 선택하는 것이 사용성이 떨어졌습니다.
처음 기획할 때는 사용자가 파일의 절대경로나 상대경로를 입력하는 것보다
프로그램에서 파일 탐색기를 제공하여 탐색하는 것이 사용성이 좋을 것이라고 생각했습니다.
근데 막상 구현을 해보니 생각보다 직관적이지 않고, 이 프로그램을 개발한 저조차 사용할 때 햇갈렸습니다.
사용자가 압축 진행률을 볼 수 없다는 점도 아쉬웠습니다.
큰파일을 압축할 때, 아무런 피드백이 없어서 저는 프로그램에 에러가 발생한 줄 알았습니다.
추가적으로, 이 프로그램을 사용하려면 해당 리포지토리를 클론하고, 빌드해서 실행해야 하는것도 불편했습니다.
[개선] 파일 선택 방식 변경
파일탐색기 형식의 방식이 불편하여 파일 경로를 입력하는 방식으로 바꾸기로 결정했습니다.
다만, 이 프로젝트를 진행하면서 많은 시간을 파일탐색기를 개발하는데 사용하였는데요,
이 코드를 싹다 지우는것은 아깝다고 느껴졌습니다. (나중에 변심해서 이 방식이 더 좋았다고 생각할 수 있잖아요?)
그래서 해당 코드를 지우기보다, 해당 코드에서 인터페이스를 추출후에 인터페이스의 구현체를 만드는, 전략패턴을 적용하기로 했습니다.
기존 InteractiveFileSelector 클래스에서 FileSelector 인터페이스를 추출했습니다.
export default interface FileSelector {
selectTarget: () => Promise<string>;
selectTargetDirectory: () => Promise<string>;
}
이 인터페이스를 구현하여 TextFileSelector를 만들었습니다.
import ERROR_MESSAGE from '../constants/errorMessage.js';
import Input from '../view/Input.js';
import FileSelector from './FileSelector.js';
import fs from 'node:fs';
import path from 'node:path';
class TextFileSelector implements FileSelector {
async selectTarget() {
const userInput = (await Input.readLineAsync()).trim();
const resolvedPath = path.resolve(userInput);
this.isExistFileOrDirectory(resolvedPath);
return userInput;
}
async selectTargetDirectory() {
const userInput = (await Input.readLineAsync()).trim();
const resolvedPath = path.resolve(userInput);
this.isExistDirectory(resolvedPath);
return userInput;
}
private isExistFileOrDirectory(pathString: string) {
if (!fs.existsSync(pathString)) {
throw new Error(ERROR_MESSAGE.NOT_EXISTING_FILE_OR_DIRECTORY);
}
}
private isExistDirectory(pathString: string) {
if (!fs.existsSync(pathString)) {
throw new Error(ERROR_MESSAGE.NOT_DIRECTORY(pathString));
}
}
}
export default TextFileSelector;
그리고 App.ts에서 TextFileSelector를 의존성으로 주입했습니다.
class App {
...
constructor() {
...
this.compressController = new CompressController(new TextFileSelector());
...
}
...
}
export default App;
이전 방식과 사용자가 경로를 넣는 방식을 비교했을 때 사용성이 개선되었습니다.
[개선] 빌드 & 배포
이 프로젝트를 실행하려면 프로젝트를 clone하고 빌드후에 실행해야해서 불편했습니다.
이를 개선하기 위해 npm에 프로젝트를 배포했습니다.
(npm에 어떻게 배포했는지는 아래 글을 봐주세요!)
https://bobostown.tistory.com/37
위 과정을 통해 사용자는 npx 명령어를 통해 제 프로그램을 실행할 수 있게 되었지만,
제가 배포를 위해 npm publish 명령어를 입력해야 한다는 불편함이 남아 있었습니다.
npm 배포 자동화
이를 자동화하기 위해 Github action을 사용하여 배포 자동화를 했습니다.
name: Deploy to NPM
on:
push:
tags:
- v*
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22.19'
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Test package
run: npm run test
- name: Build package
run: npm run build
- name: Publish to NPM
run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
tsup으로 빌드 결과물 용량 줄이기
추가적으로 배포에서 아쉬운점이 있었습니다.
tsc로 typescript를 javascript로 컴파일하다 보니 파일들이 번들링이 안되있고, minification과 tree-shaking이 안되있어서 빌드 결과물의 용량이 크다는 점에 있었습니다.
빌드 결과물의 용량이 크다는 것은 사용자가 프로그램을 실행할 때 npm registry에서 다운로드하는 시간이 길어진다는 것을 의미하기에 이를 개선해야된다고 생각했습니다.


이를 개선하기 위해 tsup을 사용하여 프로젝트를 빌드했습니다.
tsup으로 빌드를 하니 빌드 결과물의 크기가 46% 감소했습니다.

최종 결과물
https://www.npmjs.com/package/open-mission-compressor
느낀점 및 아쉬운점
사용성이 많이 개선이 되었지만
처음 프로젝트를 시작할 때 세운 계획과 비교하면 아쉬움이 많이 남습니다.
오픈미션을 계획할때는 ZIP을 압축하고 압축해제할 수 있는 프로그램을 만들고, 다양한 압축 포맷 지원, 그리고 이를 rust로 재 작성하여 node.js 프로그램과 rust 프로그램간의 성능 비교를 해볼 생각이었습니다.
그런데 아무래도 프론트엔드를 개발하는 입장로써 node.js를 상대적으로 깊게 사용해본 것이 이번이 처음이고,
압축/압축해제를 구현하기 위해 사전조사를 하느라 시간이 많이 소모되어서 node.js 압축 프로그램 개발까지만 끝낼 수 있었습니다.
제가 만든 압축 프로그램의 한계도 명확합니다.
- 표준이 아닙니다. 제 프로그램으로 압축한 파일만 압축 해제 할 수 있습니다.
- 경우에 따라 압축된 결과물의 용량이 더 클 수 있습니다.
- 압축 시간이 오래 걸립니다.
- node.js의 filesystem과 path module을 사용하는 부분등 많은 부분을 테스트 하지 못했습니다.
결과물은 아쉽지만 개발 과정에서 압축/압축해제를 수행하는 다양한 알고리즘(inflate, lz77, huffman)들을 알 수 있었고, 알고리즘을 실제로 구현해볼 수 있는 기회가 되었습니다. (node.js의 zlib 사용없이 직접 구현해보고 싶었습니다.)
npm 배포도 이번에 처음 해봤는데, CLI 프로그램이나 라이브러리를 배포하는 방법을 배울 수 있었습니다.
또, node.js로 프로그램 개발하라고 하면 할 수 있는 자신감도 생겼습니다.😁
과제를 구현하며 조사한 자료
압축 알고리즘 정리
https://bobostown.tistory.com/32
[압축 알고리즘] LZ77 알고리즘 이란?
LZ77 알고리즘은 ZIP 압축에 사용되는 Deflate 알고리즘을 구성하는 압축 알고리즘입니다.Deflate 알고리즘은 LZ77 알고리즘으로 1차 압축 후에 2차로 허프만 코드 알고리즘을 사용하여 압축합니다. 현
bobostown.tistory.com
https://bobostown.tistory.com/33
[압축 알고리즘] LZ77 구현
압축 알고리즘을 Node.js로 구현해 봤습니다.Node.js의 Buffer와 LCS알고리즘을 사용하여 구현했습니다. LZ77 구현Search buffer size와 Look ahead buffer size는 임의의 값이 아닌 Deflate 알고리즘의 Spec인 32KB와 258
bobostown.tistory.com
node.js 모듈 및 클래스
https://bobostown.tistory.com/34
[node.js] node:fs
fs modulenode.js에서 파일 시스템에 접근할 수 있는 방법은 2가지입니다.promise-based API인 node:fs/promisescallback 및 sync API인 node:fs모든 파일 시스템 작업은 동기식, 콜백 방식, 프로미스 기반 형태로 제공
bobostown.tistory.com
https://bobostown.tistory.com/35
[node.js] node:path
path modulepath 모듈은 파일과 디렉터리 경로 작업을 위한 유틸리티를 제공합니다. path 모듈은 node.js가 실행되는 운영체제에 따라 기본 동작이 달라집니다.예를 들어, Windows 운영체제에서 path 모듈
bobostown.tistory.com
https://bobostown.tistory.com/
Bobostown
bobostown.tistory.com
npm에 CLI 프로그램 배포하기
https://bobostown.tistory.com/37
ZIP Specification
https://pkwaredownloads.blob.core.windows.net/pkware-general/Documentation/APPNOTE-6.3.9.TXT
'우아한 테크코스 8기 프리코스' 카테고리의 다른 글
| [우테코 8기 프리코스] 파일 압축 개념 정리 (0) | 2025.11.06 |
|---|---|
| [우테코 8기 프리코스] 오픈 미션 시작 (0) | 2025.11.05 |
| [우테코 8기 프리코스] 3주차 과제 후기 (0) | 2025.11.04 |
| [우테코 8기 프리코스] 1주차 과제 후기 (0) | 2025.10.17 |
