티스토리 뷰
바닥부터 새로 짠 웹페이지-사이드바 v0.1.0을 릴리즈했습니다. 완전히 바닥부터는 아니고 기존 로직을 많이 참고하긴 했지만요.
웹페이지-사이드바?
기술적인 블로그 글이지만 이게 뭐하는 건지 설명은 해야될 것 같네요.
웹페이지-사이드바는 파이어폭스 확장기능으로, 비발디에 있던 옆면 툴바에서 웹페이지를 열 수 있던 기능이 그리워서 그나마 비슷하게 버튼을 누르면 사이트 목록이 나오고 그걸 누르면 사이드바에 웹사이트가 열리도록 만든 부가기능입니다.
일 하다가 SNS를 슬쩍 열어보는 딴 짓을 하기 제격인 부가기능이죠.
비슷한 확장기능이 없지는 않았었는데 걔는 내비게이션 바를 넣는다고 iframe으로 넣어놨더라구요. 몇몇 사이트는 iframe을 지원하지 않는 만큼 쓸 수가 없어서... 근데 그게 제가 자주가는 사이트여서... 결국 애드인을 급하게 하나 만들었었습니다.
왜 다시 짰는가
순수 자바스크립트는 유지보수와 지속적인 기능추가를 생각하면 어지간히 아키텍처를 잘 구성하지 않는 이상 유지보수가 피곤해질 수 있다고 판단, 이전부터 바닥부터 새로 짜는 걸 생각했었습니다.
매일 직접 쓰는 나 자신을 위한 애드인인데, 기능추가가 곤란하다면 배가 아프잖아요?
아 근데 전 노는 게 더 좋아서 당장 하진 않았었죠 (읍읍). 미루고 미루다 드디어 라는 느낌입니다.
시도 1: Rust yew
결론부터 말하자면 포기했습니다.
yew 프레임워크 (React 같은 것) 를 쓰는 건 확장기능의 브라우저 액션 (버튼 누르면 뜨는 팝업창) 에도 잘 뜨게 하고, 목록까지 나오게 했습니다.
파폭 버그로 우회한 건도 있긴 합니다. 스트리밍 방식으로 웹어셈블리를 읽어올 수 없더군요.
var ws_storage = series(
ws_storage_cargo,
parallel(
// js
function ws_storage_js(cb) {
return src('target/deploy/storage.js')
// workaround of https://bugzilla.mozilla.org/show_bug.cgi?id=1470182
.pipe(replace('typeof WebAssembly.instantiateStreaming === "function"', 'false'))
.pipe(dest('extension/storage/'))
},
// else
function ws_storage_others(cb) {
return src(['target/deploy/storage.wasm'])
.pipe(dest('extension/storage/'))
}
)
)
문제는 WebExtensions API 였죠. 이걸 일일히 다 래핑하고 JavaScript 객체랑 interop를 할 생각을 하니 머리가 아파오더군요... 이 확장기능은 TodoMVC 마냥 목록을 만들고 (여기까지는 단순) 이걸 로컬 스토리지 / 동기화 스토리지 (browser.storage.sync / browser.storage.local) 에 저장하고 불러오는게 핵심인데 저장하고 불러올 때마다 serde를 경유해서 직/역직렬화를 한다? 차라리 안 하는 게 낫겠더라구요.
그러나 React/Flux 계열의 프레임워크를 이해하는 데 큰 도움이 되었습니다. Rust가 type-safe한 언어이고, yew API가 깔끔하게 되어있던 덕입니다. 요즘 새로 나오는 UI 프레임워크들은 죄다 단방향 업데이트더라구요.
이걸 위해 작업했던 소스는 squash 한 상태로 내버려두기로 했으니 궁금하신 분은 확인해주세요.
시도 2: vue + vuex + typescript + parcel
Misskey도 vue를 쓰고 있고, React에 비해 vue 가 상대적으로 단순하고 깔끔해보여서 vue + vuex + TypeScript 조합을 쓰기로 했습니다. 웹팩이 속도 때문에 속터지는 것도 아니까 대신에 parcel을 쓰기로 했죠. 그리고 나중에 스타일링 용으로 SCSS를 쓰게 됩니다. pure CSS보다 편하더라구요.
- Vue: Virtual-DOM을 구현하는 UI 프레임워크
- Vuex: 단방향 업데이트 상태 저장소, Vue 용 (Flux / Redux 같은 거)
- TypeScript: JavaScript의 동적 / 약타입에 속 안 터지려고 선택 (당연한 선택)
- Parcel: 불꽃튀게 빠른 번들러
- Sass: CSS 슈퍼셋
미리 알았더라면
pinafore 같은 가볍고 빠른 클라이언트가 쓰는 Svelte 프레임워크를 미리 알았더라면 이걸 썼었겠죠. 근데 이미 때는 늦었더군요... 다음에 새 프로젝트를 할 때는 저걸 써봐야겠습니다.
프로젝트 초기 설정
TypeScript, Vue, Parcel 조합
써보시면 아시겠지만 Vue가 2.x 인 이상 아직 TypeScript 지원이 완전하지가 않습니다. 어딘가 삐꾸나는 일이 많더라구요. 어찌저찌 해결은 했습니다만. 거기다 대부분의 사용자는 Webpack을 쓰지 Parcel을 쓰진 않다보니 안 되는 걸 되게 한다고 용좀
깨나
꽤나 썼습니다.
WebExtensions와 Parcel
어떤 감사한 분께서 Parcel용으로 WebExtensions 빌드 플러그인을 만들어주셨는데 이게 또 윈도우에서 버그...라 gulp로 빌드 시 우회하도록 해야했습니다.
function manifest_copy(cb) {
return src(['src_workaround/manifest.json'])
.pipe(dest('extension/'))
}
올바른 자동 동기화
리스트를 다 짜고 browser.storage.sync
를 넣으려니 생각해보니 동기화가 문제더군요. 기존에는 그냥 로컬 값을 동기화 스토리지에 때려박았는데 이러면 여러 컴퓨터에서 일단 때려박고 보니까 동기화가 될 리가 만무했습니다. 즉, 기존 구현은 잘못되었습니다.
그렇다면 어떻게 해야할고... 라는 문제가 발생합니다. 저는 사용자가 스팀 클라우드마냥 어느게 더 최신인지 골라서 통으로 덮어쓰도록 하고싶지 않고, 알아서 각 항목별로 동기화가 되길 원했습니다. 그러다보니 항목별로 동기화를 해주려면 다음의 항목이 필요하더군요.
- 항목 유일 식별자: 로컬에 있는 거랑 싱크에 있는 거랑 같은 항목이야? 아님 다른 거야?
- 항목 업데이트일: 내 꺼랑 싱크랑 비교했을 때 어떤 게 더 최신이야?
- 항목 삭제 여부: 싱크에는 지워졌는데 로컬에는 남아있으면 로컬거도 지워야 하는데...
그래서 코드가... 코드가 생각보다 커졌습니다.
export class SiteState {
private _siteList: MappedList<string, Site> = // 생략
private _deletedSiteList: LimitedMappedList<string, CrudStamp> = // 생략
// version
v: string = '0.1.0';
static curVer: string = '0.1.0'; // for checking
constructor() { }
addSite(site: Site) { /* 생략 */ }
removeSite(site: Site) { /* 생략 */ }
static migrateOrCreate(siteState: SiteState | Array<object> | undefined | null): SiteState { /* 생략 */ }
mergeWith(siteState: SiteState): SiteState { /* 생략 */ }
static FromObject(siteState: object): SiteState { /* 생략 */ }
toObject() { /* 생략 */ }
}
알고도 이젠 지쳐서 안 고치는 버그도 있죠.
TypeScript
빨간 줄이 싫어서 <any>
캐스팅을 좀 많이 했습니다. 특히 WebExtensions API 쪽은 TypeScript 타입 정의 라이브러리가 있었지만서도 반환형 타입을 직접 정할수가 없더라구요.
Vue Vuex와 TypeScript
빨간 줄이 싫어서 vuex-class 도 넣었다가 뺐습니다. 데코레이터로 타입을 아직 정의할 수가 없더라구요. 하더라도 빨간 줄 나오고...
import { Vue, Component, Prop } from "vue-property-decorator";
// import { State, Getter, Action, Mutation } from "vuex-class"; // no type -> error line
@Component
export default class App extends Vue {
@Prop() siteUrl: string = "";
// @Action("addSite") addSiteAction: (site: Site) => void;
addSiteAction(site: Site): void {
this.$store.dispatch("addSite", site);
}
CSS
Scss와 App.vue
naru.cafe 홈페이지 만들면서 Sass를 써봤는데 편하더라구요. 당시에는 Sass 문법을 썼었는데 그것도 나름 괜찮았지만 역시 익숙한 CSS식 문법인 Scss가 좋더군요.
Vue는 좋은 게 Vue 파일에 Sass나 Stylepen 같은 걸 한 번에 넣을 수 있다는 게 좋은 것 같습니다.
한 줄 짜리 Grid
가장 골때리는 게 한 줄에 사이트 링크와 버튼을 쑤셔넣는 거였습니다. 사이트 URL은 여백을 가득 채워야 하고, 버튼은 지금은 한 개지만 나중에 늘어날 걸 고려하면 개수가 늘어나도 같은 CSS로 커버가 가능해야 했죠. 줄 별로 버튼 개수가 달라질 수도 있고요.
시도한 건 여러가지입니다.
처음에는 Grid로 하려고 했습니다. UWP / WPF에서도 그리드로 많이들 짜잖아요? 처음꺼 1fr
다음꺼 전부 auto
면 좋은데 이게 repeat
로 암시적으로 생성되는 거의 개수를 무제한으로 할 수 없는데다가 새로 생기는 게 아래로 내려가더라고요 (implicit row
). 드랍.
그 다음에 했던 게 flex
인데 이건 실패로 돌아갔습니다. 보통 웹페이지면 상관이 없었을텐데, 저는 버튼을 눌렀을 때 나오는 창의 크기가 내용에 맞춰졌으면 좋겠다고 생각했거든요. 근데 이게 flex일 땐 안 됩니다. 스크롤바가 생겨요. flex-grow: 1
이면 늘어나주는 건 좋은데 div 크기를 산정한 뒤 늘어나는 거 같더군요. 그게 문제였습니다. 포기.
드랍하고 table-cell
을 써봤는데 Input[type='text']
님께서 가로로 늘어나질 않는데다가 부모 <form>
이 이상하게 버튼 영역까지 자기 영역으로 산정 안 합니다... overflow: hidden;
을 주면 버튼이 사라지는데다 input
에 width: 100%;
를 주던 말던 고 크기 고대로입니다. 삽질에 삽질하다 포기.
그리고 다시 Grid로 돌아와서 행복을 찾았습니다. 자동생성 행 / 열의 방향을 지정할 수 있더라구요 (grid-auto-flow: column
). 역시 제대로 안 배우고 야매로 하면 안 돼...인데 전 귀찮아서 이것도 좋아요
.row-container {
display: grid;
grid-auto-flow: column;
grid-template-rows: 1fr;
grid-template-columns: 1fr;
.left {
width: 100%;
}
}
릴리즈
그렇게 산발적으로 조금씩 했던 게 드디어 빛을 봤고 그 많은 아무말 커밋을 squash... 아니 리셋하고 새로 커밋하고 릴리즈하게 되었습니다.
지치지만 만족스럽고 만족스럽지만 지치네요. 이 애드인 개발은 이제 한동안 쉴 겁니다 (물론 추가할 기능은 엄청 많지만요). 다음엔 뭘 먼저 해볼까 고민되네요.
- Total
- Today
- Yesterday
- exercism
- C#
- 오라클 클라우드
- kotlin당했다
- C++ FAQ
- 왜 생각이 안 났지
- vuex
- 오라클 클라우드 인프라
- rust-lang
- 시스어드민
- 업비트
- Sass
- 쿠버네티스
- upbit
- ActivityPub
- gitea
- javascript
- ArchLinuxARM
- pdf.js
- Oracle Cloud Infrastructure
- mvu
- c++
- 토이프로젝트
- OStatus
- K8s
- pleroma
- Godot Engine
- 개발기록
- 마스토돈
- scss
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |