본문 바로가기메뉴 바로가기


브라우저 Web Worker 다루기 with 오피스 문서 텍스트 추출 및 암호해제

카카오 사내 기술 세미나 Techtalk 아홉 번째 이야기입니다.

안녕하세요. 톡플랫폼개발팀 bishop.cho입니다. 현재 메일클라개발셀에서 프론트엔드 업무 개발을 담당하고 있습니다. 기존에는 자바스크립트로 동작하는 오피스 에디터 및 뷰어 개발을 했던 경험이 있습니다.

여기에서는, 토이 프로젝트로 텍스트 추출 라이브러리 개발 및 이를 Web Worker와 접목시킨 경험을 바탕으로, Web Worker로 오피스 문서 텍스트를 추출하고 암호 해제하는 방법에 대해 이야기하고자 합니다.


Web Worker 소개

Web Worker란?

다음은 Web Worker와 관련한 간단한 워크플로우입니다.

왼쪽은 Web Worker를 사용하지 않은 상태이고, 오른쪽은 Web Worker를 사용한 상태입니다.

자바스크립트는 브라우저에서 싱글 스레드 기반으로 동작합니다. 따라서, Web Worker를 사용하지 않았을 때에는 UI 동작과 데이터 핸들링이 순차적인 방식으로 처리가 되는 반면, Web Worker를 사용하면 병렬 처리가 가능합니다.

자바스크립트 엔진 동작 방식에 대해서 좀 더 자세히 살펴보겠습니다.

왼쪽에 태스크 큐, 마이크로 태스크 큐, 애니메이션 프레임이 있습니다.

일반적인 이벤트에 해당하는 콜백은 태스크 큐, Promise1 콜백은 마이크로 태스크 큐, 그리고 requestAnimation2에 해당하는 애니메이션 프레임 큐까지 각각의 동작을 담고 있는 태스크를 스택에 담아서 하나씩 처리합니다. 다시 스택이 비면 브라우저 이벤트 루프의 우선순위에 따라서 순차적으로 큐에 있는 항목을 스택에 넣어 처리합니다.

웹 API의 일부 함수는 멀티 스레드로 처리가 되는 것도 있지만, 사용자가 직접 데이터를 처리하는 부분은 싱글 스레드에서 동작되므로 데이터 처리가 많아질수록 병목현상이 생겨 렌더링 프레임에 영향을 미치게 됩니다.

이런 병목현상을 처리하기 위해서 Web Worker를 사용하게 됩니다. 그림을 통해 설명하겠습니다.

UI를 처리하는 메인스레드 외, 각각의 데이터를 처리할 수 있는 Web Worker를 필요한 개수만큼 생성하여 스레드처럼 사용할 수가 있습니다.

메인스레드와 Web Worker는 메시지 방식으로 서로 통신하며 데이터를 주고받습니다. Web Worker 자체는 단순해서 메인 스레드에는 Web Worker로 로드할 URL을 넘겨주고 메시지를 주고받는 콜백을 등록하여 사용하면 됩니다. Web Worker로 사용될 스크립트에서도 마찬가지입니다. 메시지 콜백만 등록하여 사용하면 됩니다. Pub/Sub(발행/구독) 구조를 생각하면 될 것 같습니다.

1Promise는 자바스크립트 비동기 처리에 사용되는 객체로 어떤 작업의 중간 상태를 나타내는 오브젝트입니다. 미래에 어떤 종류의 결과가 반환됨을 약속해 주는 오브젝트로 비동기 작업이 맞이할(Pending) 미래의 완료(Fulfilled) 또는 실패(Rejected)와 그 결괏값을 나타냅니다.

2https://developer.mozilla.org/ko/docs/Web/API/Window/requestAnimationFrame

Web Worker는 IE10 이상이면 동작되는 Web API이므로 사실상 모든 브라우저에서 사용 가능합니다.

Web Worker를 사용하지 않는 이유

이처럼 Web Worker가 많은 장점이 있는데 잘 사용되지 않는 이유에 대해서 정리해 보았습니다.

  • 첫 번째로, 서버에서 대부분의 데이터를 처리하게 때문에 굳이 Web Worker를 사용하게 될 일이 없습니다(Web Worker를 사용할 만큼 부하가 걸리는 작업을 안 함).
  • 두 번째로, Dom 제어 및 Window 객체의 일부 함수가 메인 스레드에서만 가능하고 Web Worker에서는 사용이 불가능한 부분이 있습니다.
  • 세 번째로는, 단순 계산식에서는 Web Worker를 사용하지 않는 것이 더 빠르기도 하고, 메인 스레드와 Web Worker 간 데이터 전송 시에는 비용이 발생하기 때문입니다.

Web Worker를 사용하는 경우

반면, Web Worker를 사용하는 경우는 다음과 같습니다. 

  • 첫 번째로, 화면 동작에 영향을 미치는 연산 동작, 즉, 바이너리 파일 핸들링이나 복잡한 계산이 필요한 경우에 유용합니다. 
  • 두 번째로, 백그라운드에서 지속적인 작업을 해야 하거나 메인 스레드 영향을 미치지 않고 작업을 하는 경우에 유용하게 사용될 수 있습니다.
  • 세 번째로는, 멀티 스레드로 개발했을 때 사용자 환경 개선에 도움이 되는 경우로 볼 수 있습니다. 싱글 페이지 애플리케이션으로 개발되는 프로젝트라면, 사용했을 때 도움이 되는 부분이 있을 것으로 생각됩니다.

오피스 파일 텍스트 추출

텍스트 추출을 사용하는 이유

먼저, 오피스 파일 텍스트 추출은 어디에 사용하는지가 궁금하실 수 있을 것 같아 정리해 보았습니다.

  • 첫 번째로, 파일을 서버에 업로드 또는 다운로드하기 전에 파일의 내용을 확인하기 위해 사용할 수 있습니다.
  • 두 번째로, 메타데이터 및 특정 데이터를 추출하는 작업을 네트워크 사용 없이 즉각적으로 처리를 할 수 있습니다.
  • 세 번째로는, 검색, 인덱스 생성 및 암호 해제를 메신저나 메일 등에서 사용할 수가 있습니다. Web 및 Android, iOS 등에서도 webView 어댑터를 통해서 원 소스 멀티 유즈로 사용이 가능합니다.

프론트에서 작업하는 이유

굳이 왜 프론트에서 작업해야 하나 의문이 들 수 있습니다. 

다양한 프로젝트를 진행해 본 경험상 서비스의 종류는 다양하고 볼륨이 커질수록 제약이 많이 생깁니다. 개인 정보보호 여부 또는 서버 전송 등의 다양한 경우의 수가 많으나, 프론트에서는 이런 제약이 있어 자유로울 수 있는 부분이 있습니다.

즉, 개인 정보 보호 등으로 인해 직접적으로 분석해서 활용할 수 없는 경우가 있고, 현재는 PC, 모바일의 성능이 충분히 빨라져서 네트워크 전송 및 처리하는 것보다 속도가 빠를 수 있으며, 기존 서비스에 연동하거나 새로 구축하는 것에 비해 스크립트로 가볍게 처리할 수 있는 장점이 있습니다. 

텍스트 추출 라이브러리

텍스트 추출하는 라이브러리에 대해서 간략히 설명을 드리도록 하겠습니다.

MS 오피스 문서는 크게 2007년 이전과 이후로 나뉩니다. 2007년 이전 포맷은 컴파운드 바이너리 파일 구조(Compound Binary File Structure)를 띄고 있고, 2007년 이후 포맷은 패키지 파일 구조(Package File Structure)로 XML 형태로 데이터를 저장하고 있습니다. 한글 파일에 경우 2007년 이전의 컴파운드 바이너리 파일 구조이고, 이는 또한 윈도우 FAT 시스템과 유사한 구조이기도 합니다.

구분설명구조 예시
OLECompound Binary
File Structure
예) doc, xls, ppt, hwp
OOXML
Package Files Structure
Office Open XML
예) docx, xlsx, pptx

OLE(Object Linking & Embedding) 기반 텍스트 추출

2007년 이전 포맷인 컴파운드 바이너리 파일 구조(Compound Binary File Structure)에 대해 조금 더 자세히 살펴보겠습니다.

이러한 구조체에 대한 정보는 MS에서 제공하는 공식 문서를 보면 몇 바이트를 읽어야 하는지에 대한 명세가 있습니다. 이를 바탕으로 데이터를 읽으면 되는데, 파일을 어레이 버퍼로 변환하여 스펙에 있는 바이트 수만큼 읽어서 처리를 하면 됩니다.

다음은 한글문서파일 형식입니다. 역시나, 읽어야 하는 바이트 수를 명세에 맞게 순차적으로 읽으면 텍스트가 있는 위치를 찾아 정보를 처리할 수 있습니다.

출처 – hancom.com  한글문서파일형식3.0 .hwp

버퍼에 있는 항목을 2 또는 4 바이트만큼 읽어서 데이터 있는 포인트를 찾을 수가 있습니다. 주석에 나온 것처럼 텍스트가 아닌 정보가 있는 경우, 임의의 위치를 점프하여 텍스트가 있는 위치를 찾아갈 수 있습니다.

OOXML(Office Open XML)  기반 텍스트 추출

오피스 2007년 이후 포맷인 OOXML 포맷에 대해 설명드리겠습니다. 

OOXML은 Package File Structure를 가지고 있습니다. 이를 해제하면(docx, xlsx, pptx 파일의 확장자를 zip으로 변경하고 압축 해제) XML로 된 파일들이 나타납니다. 이 XML을 파싱 하여 w:t 태그의 속성을 찾으면 워드의 텍스트를 찾을 수 있습니다. 

엑셀의 경우에는 시트나 함수 같은 요소가 있어서 조금 복잡하긴 하지만, 몇 개 파일의 데이터를 조합하면 텍스트를 추출할 수 있습니다. 

다음은 docx 워드 파일에서 텍스트를 추출하는 부분의 소스코드입니다.

header, document, footer의 세 가지 종류 파일은 여러 개가 존재할 수 있는데, 각각 머리글, 바닥글, 본문에 해당하는 데이터를 담고 있습니다.

문서의 XML 파일 정보를 재귀로 순회하면서 w:t 태그 안에 있는 값을 담으면 텍스트를 추출할 수 있습니다. 

워드 내의 차트와 같은 요소는 엑셀 파일 형태로 저장됩니다. 코드 상단에 보이는 /.xlsx/ 부분의 경우, xlsx 파일이면 엑셀 파서를 통해서 텍스트를 추출할 수 있도록 하고 있습니다.

암호 해제

암호 해제를 하는 방법에 대해서 설명드리도록 하겠습니다.

docx, xlsx, pptx 파일을 압축 해제를 하면 encryptedPackage 파일과 encryptedInfo 파일을 확인할 수 있습니다. encryptedInfo 파일에는 압축을 해제하는데 필요한 정보가 담겨 있습니다. 이 파일에서 어떤 알고리즘 인지에 대한 정보와 salt key, hash spin 횟수 정보를 얻을 수가 있는데, 대부분은 sha512 알고리즘으로 암호화되어 있습니다. 

이러한 복호화 키 알고리즘을 바탕으로 spin 횟수만큼 패스워드와 함께 해시 digest 함수를 반복 실행하면 복호화 할 수 있는 키를 생성할 수 있습니다. 

이를 토대로 암호화된 파일 패키지인 encryptedPackage 파일을 복호화 키로 4kb 바이트만큼 읽어 decrypt(복호화)를 수행하면 원본 파일을 복원할 수 있습니다. 

원본 파일인지 알 수 있는 방법은 첫 번째 4kb만큼만 읽어서 파일의 시그니처 정보를 확인하면 암호가 맞았는지 검증할 수 있습니다. 

Web Worker 연동 테스트

Web Worker와 텍스트 추출 라이브러리를 연동하는 데모에 대해서 소개하겠습니다.

데모 페이지 구성

Web Worker를 사용했을 때와 사용하지 않았을 때의 차이를 직접 확인해 보고자, 다음과 같이 데모 페이지를 구성해 보았습니다. 

데모 페이지는 스톱워치와 파일 업로드 창으로 구성했습니다. 스톱워치는 텍스트 추출 중에 UI의 프레임이 어떻게 동작하는지를 확인하기 위해 추가했습니다. 또한, 텍스트 변환 결과를 화면 하단에서 직접 확인해 볼 수 있도록 구성했습니다.

오른쪽의 initial password 입력란의 경우, 암호가 걸린 파일에 대해서 미리 값을 입력할 수 있도록 했습니다. 

한편, Worker를 사용하는 데모에서는 4개의 Worker를 미리 생성해 놓고, 파일을 업로드하면 파일마다 빈 Worker를 찾아서 텍스트를 추출할 수 있도록 Worker pool을 만들어 테스트를 진행했습니다.

* 참고 데모 페이지: https://workerdemo.github.io

테스트 방법

  1. doc, docx, xls, xlsx, ppt, pptx, pdf, hwp 파일 문서의 텍스트 추출
  2. docx, xlsx, pptx의 암호 파일만 해제
  3. zip으로 압축된 파일의 경우 압축된 파일 중에서 호환하는 문서만 다시 추출
  4. Web Worker를 사용하는 경우 zip 안의 항목에 대해서도 Worker pool에 추가하여 스레드로 동작

Non Worker: Web Worker 미사용 시(main-nonworker.js)

먼저 Web Worker를 사용하지 않았을 경우입니다. 

  1. 파일 목록으로부터 개별 파서 Promise 생성
  2. Promise.all로 모든  Promise 순회 후 텍스트 추출

선택한 파일을 순차적으로 DocToText 라이브러리로 텍스트 추출을 비동기로 요청하고, promise all로 모든 파일에 대한 처리가 끝나면 텍스트를 렌더링 하도록 했습니다.

테스트 시, 스톱워치를 구동시킨 다음, 업로드할 파일을 선택했습니다. 약 500개 정도의 다양한 파일들을 업로드해, 변환 시에 어느 정도 속도가 나오는지 그리고 그리고 프레임에 영향을 미치는지를 확인해 보았습니다. 

Web Worker를 사용하지 않기 때문에 스톱워치를 켜고 파일을 변환하면 프레임이 끊기는 프레임 드롭 현상이 나타났습니다. 

Worker: Web Worker 사용 시(main-worker.js)

Web Worker를 사용했을 경우입니다.

먼저, 초기에 기본 Worker를 4개 생성하여 테스트를 진행했습니다. 

오른쪽의 Worker를 생성하는 부분은 HTTP 리퀘스트로 Worker 파일을 가져온 후, createObjectUrl로 URL을 생성하여 Worker 생성 시에 넘겨 주도록 했습니다. Worker를 생성할 때 바로 URL을 넘겨줘도 되지만, 여러 개를 생성할 때에는 디스크 캐시로 스크립트를 불러오는 시간이 맞지 않아서 URL로 한번 생성 후에 재활용하는 방식을 사용했습니다. 

마지막으로 Worker가 돌아가는 방식은 task를 리스트로 넣고, task가 끝나면 idle Worker를 찾아 파일의 텍스트를 계속하여 추출할 수 있도록 했습니다.

다음은 메인 스레드와 Web Worker 간에 메시지로 데이터를 주고 받는 소스입니다. 

왼쪽 메인 스레드의 postMessage를 통해 파일을 Worker로 넘겨주면 오른 쪽의 Worker에 등록된 메시지 콜백에서 텍스트를 추출하거나 중간에 패스워드를 입력받는 등의 동작을 서로 주고받습니다. 

최종적으로 Worker에서 동작이 마무리되면 text result 메시지와 함께 데이터를 메인 스레드로 전달하며 Worker는 할 일을 마무리합니다.

마지막으로, Worker가 더 이상 사용되지 않을 때에는 worker terminate을 호출하여 메모리를 해제할 수 있습니다.

Web Worker를 사용했을 경우에 대한 테스트도, 동일하게 우선 스톱워치를 켜고 총 500개 파일을 선택했습니다. 프레임 드랍 없이 파일 변환이 되고 있는 것을 확인할 수 있었습니다.

테스트 결과: Worker vs. Non Worker 차이

Worker를 썼을 때와 안 썼을 때의 작업관리자 화면에서 CPU 사용률에 대한 정보를 확인할 수 있는데요. Non Worker 상태일 때는 작업 탭이 1개이지만 Worker를 사용했을 때에는 메인 스레드를 포함하여 5개로 늘어난 것을 확인할 수 있습니다. 

여러 개의 파일에서 텍스트 추출할 때 Non Worker에서 실행하면 CPU의 최대 동작 수치가 100% 근처에서 동작하지만, Worker를 사용했을 경우에는 321%까지 사용되었습니다. 

실제 10개의 파일 변환을 기준으로 봤을 때 Non Worker는 0.3초, Worker는 0.1초입니다. 그리고 스톱워치 또한 최종 결과를 렌더링 할 때를 제외하곤 프레임 드롭도 없습니다.

테스트 결과: Worker vs. Non Worker 성능 비교

Worker를 사용했을 때와 사용하지 않았을 때의 속도 차이입니다. 

8개, 16개, 32개의 파일을 각각 텍스트로 추출했을 때 평균 2배 이상 빠르게 텍스트를 추출하는 것을 확인할 수 있습니다.

이처럼 워커를 사용하면 병목 없이 더 빠르게 사용 가능합니다. 


지금까지 웹 워커를 활용하여 텍스트 추출을 하는 방법에 대해서 다뤄봤습니다. 앱, 서버 개발을 거쳐 프론트엔드 개발을 주력으로 하게 되면서 맡은 개발 포지션에 따라서 생각도 바뀌게 되는 것 같습니다.

다양한 비즈니스에 어떤 방식으로 접근했을 때 더 효율적인지 고민할 수 있는 기회가 생기면 좋겠습니다.  다소 생소할 수 있는 기술이지만 다른 서비스를 개발할 때 참고하거나 영감을 얻는 데 도움이 되길 바랍니다. 감사합니다.

bishop.cho
bishop.cho 메일클라개발셀에서 메일 프론트엔드 개발을 하고 있습니다. 다른 언어에는 있지만 자바스크립트에선 없는 라이브러리를 포팅하거나 새로 만드는데에 관심이 있습니다.
Top Tag
2021
2021-new-krew
Ad Platform
Ad tech
adaptive-hash-index
adt
adtech
agile
agilecoach
ai
algorithm
Algorithm/ML
Algorithm/Ranking
almighty-data-transmitter
Analyzer
android
angular
anycast
App2App
applicative
Architecture
arena
ast
async
aurora
babel
babel7
Backend
BApp
bgp
big-data
binary
ble
blind-recruitment
block
Block Chain
blockchain
bluetooth
brian
business
Cache
cahtbot
canvasapi
Caver
cch
cd
CDR
ceph
certificate
certification
CF
cgroup
chrome
ci
cite
client
clojure
close-wait
cloud
cloudera-manager
clustered-block
cmux
cnn
code-festival
code-review
codereview
coding
coding test
Collaborative Filtering
competition
Compliance
component
conference
consul
container
contents
contest
contribution
cookie
core-js@3
Corporate Digital Responsibility
couchbase
COVID-19
cpp
Data
data-engineering
DB
deep-learning
Dependency
dependency-graph
desktop
dev
dev-session
dev-track
developer
developer relations
developers
devops
digitalization
digitaltransformation
dns
docker
dr
Electron
employeecard
emscripten
eslint
extract-text
Feature List
Featured
Feedback
friendstime
front-end
frontend
functional-programming
funfunday
fzf
garbage-collection
gawibawibo
GC
github
globalpollution
go
graphdb
graphql
Ground X
growth
ha
hadoop
hate speech
hbase
hbase-manager
hbase-region-inspector
hbase-snashot
hbase-table-stat
hbase-tools
hri
ICPC Sinchon
id
if kakao
ifkakao
infrastructure
innodb
internship
ios
item
Java
javascript
javascript web-assembly
JCMM
JIRA
jsconf
jsconfkorea
json
k8s
kafka
kakao
kakao-Career-Boost-Program
kakao-commerce
kakao-games
kakaoarena
kakaobrain
kakaocommerce
kakaocon
kakaoenterprise
kakaok
kakaokey
kakaokrew
kakaomap
kakaopage
kakaotalk
KAS
KCDC
khaiii
Klaytn
Klip
kubernetes
l3dsr
l4
License
links
Linux
load-balancing
MAB
Machine Learning
machine-learning
map
marathon
meetup
melon
mesos
message
Messaging
microservice
Microsoft TypeScript
mm
mobil
mocking
monad
monorepo
ms-office
MSA
mtre
mysql
mysql-realtime-traffic-emulator
nand-flash
network
new
new-krew
nfc
Nickface
nomad
ocp
olive
onboarding
open
open source
opensource
openstack
OpenWork
OSS
page
parallel
PBA
performance
planning poker
Platform
polyfill
programming-contest
project-structure
pycon
python
quagga
react
reactive-programming
reactor
recap
recommendation
recommendation system
recruitment
redis
redis-keys
redis-scan
related-blind
Renderer
rest
Rome
rubics
ruby
rxjs
s2graph
scala
scalaz
seminar
Serve
server
service
service worker
sharding
shopping
Shuffle Partition
socket
spark
spark-streaming
SpringBoot
ssd
Statistics/Analysis
Stomp
storage
storm
style-guide
summer internship
support
System
talk
talkchannel
tcp
tech
Techtalk
test
thread
Thread-Debugging
time-wait
tmux
Topic Modeling
typescript
Untact
update
User Story
vim
vim-github-dashboard
vim-plugin
vue
vue.js
WASM
web-cache
web-worker
webapp
webgl
WebSocket
webworkers
weekly
work
workplatform
개인화 추천
길찾기
라이선스
연관 추천
오픈소스
오픈소스검증
의존성분석
일하는방식
협업
All Tag
2021
2021-new-krew
Ad Platform
Ad tech
adaptive-hash-index
adt
adtech
agile
agilecoach
ai
algorithm
Algorithm/ML
Algorithm/Ranking
almighty-data-transmitter
Analyzer
android
angular
anycast
App2App
applicative
Architecture
arena
ast
async
aurora
babel
babel7
Backend
BApp
bgp
big-data
binary
ble
blind-recruitment
block
Block Chain
blockchain
bluetooth
brian
business
Cache
cahtbot
canvasapi
Caver
cch
cd
CDR
ceph
certificate
certification
CF
cgroup
chrome
ci
cite
client
clojure
close-wait
cloud
cloudera-manager
clustered-block
cmux
cnn
code-festival
code-review
codereview
coding
coding test
Collaborative Filtering
competition
Compliance
component
conference
consul
container
contents
contest
contribution
cookie
core-js@3
Corporate Digital Responsibility
couchbase
COVID-19
cpp
Data
data-engineering
DB
deep-learning
Dependency
dependency-graph
desktop
dev
dev-session
dev-track
developer
developer relations
developers
devops
digitalization
digitaltransformation
dns
docker
dr
Electron
employeecard
emscripten
eslint
extract-text
Feature List
Featured
Feedback
friendstime
front-end
frontend
functional-programming
funfunday
fzf
garbage-collection
gawibawibo
GC
github
globalpollution
go
graphdb
graphql
Ground X
growth
ha
hadoop
hate speech
hbase
hbase-manager
hbase-region-inspector
hbase-snashot
hbase-table-stat
hbase-tools
hri
ICPC Sinchon
id
if kakao
ifkakao
infrastructure
innodb
internship
ios
item
Java
javascript
javascript web-assembly
JCMM
JIRA
jsconf
jsconfkorea
json
k8s
kafka
kakao
kakao-Career-Boost-Program
kakao-commerce
kakao-games
kakaoarena
kakaobrain
kakaocommerce
kakaocon
kakaoenterprise
kakaok
kakaokey
kakaokrew
kakaomap
kakaopage
kakaotalk
KAS
KCDC
khaiii
Klaytn
Klip
kubernetes
l3dsr
l4
License
links
Linux
load-balancing
MAB
Machine Learning
machine-learning
map
marathon
meetup
melon
mesos
message
Messaging
microservice
Microsoft TypeScript
mm
mobil
mocking
monad
monorepo
ms-office
MSA
mtre
mysql
mysql-realtime-traffic-emulator
nand-flash
network
new
new-krew
nfc
Nickface
nomad
ocp
olive
onboarding
open
open source
opensource
openstack
OpenWork
OSS
page
parallel
PBA
performance
planning poker
Platform
polyfill
programming-contest
project-structure
pycon
python
quagga
react
reactive-programming
reactor
recap
recommendation
recommendation system
recruitment
redis
redis-keys
redis-scan
related-blind
Renderer
rest
Rome
rubics
ruby
rxjs
s2graph
scala
scalaz
seminar
Serve
server
service
service worker
sharding
shopping
Shuffle Partition
socket
spark
spark-streaming
SpringBoot
ssd
Statistics/Analysis
Stomp
storage
storm
style-guide
summer internship
support
System
talk
talkchannel
tcp
tech
Techtalk
test
thread
Thread-Debugging
time-wait
tmux
Topic Modeling
typescript
Untact
update
User Story
vim
vim-github-dashboard
vim-plugin
vue
vue.js
WASM
web-cache
web-worker
webapp
webgl
WebSocket
webworkers
weekly
work
workplatform
개인화 추천
길찾기
라이선스
연관 추천
오픈소스
오픈소스검증
의존성분석
일하는방식
협업

위로