티스토리 뷰

바닥부터 새로 짠 웹페이지-사이드바 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;을 주면 버튼이 사라지는데다 inputwidth: 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
링크
«   2024/04   »
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
글 보관함