새로운 강의는 이제 https://memi.dev 에서 진행합니다.
memi가 Vue & Firebase로 직접 만든 새로운 사이트를 소개합니다.

바로가기


Electron 2 일렉트론뷰로 우아한 데스크탑 앱 만들기 UI

9 분 소요

최근 유행하는 UI 흐름에 대해 간단히 설명드립니다.

이번 챕터는 사실 일렉트론이랑 상관은 없습니다. 디자인과 관련되어 있으니 유관련이신 분들은 참고하세요..

예전 NEMBV 프로젝트 진행할때 https://fkkmemi.github.io/nembv/nembv-00-intro/

언급했었지만 다시 정리하면

  • 기기해상도 원페이지 프로그래밍:
    모바일, 태블릿, 데스크탑을 한페이지로 만드는 추새 입니다.(eg: 인스타 같은 거 보면 데탑에선 좌우로 사진이 배치되다가 모바일에서는 스크롤하기 좋게 사진이 하나만 나오죠)

  • 색상 사용의 단일화:
    배경이나 기본 프레임 외에 UI는 상황별 색상 한 8가지(primary, success, warning, error, info, etc..) 정도만 놓고 만듭니다. 쓰잘데 없는 다양한 색깔은 집중도가 떨어지기 때문이죠..

  • 메뉴 정리(룩앤필):
    모바일 앱이 사용이 많아지면서 익숙해진 사람들은 아디다스 아이콘이 메뉴 버튼으로 대부분 인지 하기 때문에 메뉴 접근성이 모바일 앱처럼 갑니다. 수평으로 늘어 놓던 옛 스타일은 다른 용도로 쓰죠(소셜, 인포등)

  • 위드아웃 디자이너:
    이제 백엔드 개발자나 데탑 개발자도 UI를 다룰 수 있게 되었습니다. 바로 복잡한 css 직접 핸들링이 없기 때문이죠.. 뷰티파이나 부트스트랩등의 css프레임웤들이 도와줍니다.(일러로 한땀한땀 버튼 만들던 전 좀 씁쓸하네요)

  • 프론트엔드 3대장(angular, react, vue):
    스크립트와 화면과 따로 놀던 시대에서 이제는 데이터 자체가 곧 화면입니다. set과 get 같은 절차 없이 바인딩되서 화면이 꾸며집니다.

  • 어떤 요소든 손가락만하게:
    모바일 중심으로 가면서 깨알 같은 버튼을 여기저기 나열한 페이지들은 유행이 지났습니다.
    처음엔 무식하게 큰 버튼들이 위화감을 주었지만..마우스가 아닌 손가락으로 클릭하게 만든 버튼이 포함된 페이지들이 결국 트렌드가 되었습니다.

vuetify를 그냥 쓰기만한다고 해서 공대감성이 해제되지는 않습니다.

모던웹 트렌드는 디자이너가 아니더라도 감각은 필요합니다.(일명 풀스택 비스무리한 개발자를 실리콘밸리에서도 많이 찾습니다..)

엔드유저 상황의 접근성과 배치가 중요합니다.(나란히 놓여 있는 버튼 2개중 하나가 단 1px만 높아도 생각보다 많은 이질감을 줍니다. 그것은 곳 품질의 하락입니다.)

디자이너로 태어나지 않은 분들도 이제 실체를 보여주는 개발에도 관심을 가져보시기 바랍니다.

대부분의 경우 재밌는 만화는 그림체도 좋죠

색상

일렉트론 기본 칙칙한 다크 테마부터 바꿔봅시다.

src/renderer/App.vue

<v-app dark>

alt dark

<v-app>

alt white

테마는 두가지 뿐이네요 어떻게 아냐고요?

개발하는 방법은 늘 레퍼런스 사이트를 꾸준히 보면 됩니다.

https://vuetifyjs.com/ko/components/api-explorer

여기서 v-app을 찾아보면 props라는 곳이 있는데 dark 밖에 없죠

디폴트 false 라는 말은 위의 예시처럼 dark 라는 태그가 없다는 것입니다.

청소

기본 레이아웃이 이것저것 보여주려고 쓸데 없는 버튼들이 있는데 불필요한 것들은 청소해봅니다.

src/renderer/app.vue

<template>
  <div id="app">
    <v-app>
      <v-navigation-drawer
        v-model="drawer"
        app
      >
        <v-list>
          <v-list-tile 
            router
            :to="item.to"
            :key="i"
            v-for="(item, i) in items"
            exact
          >
            <v-list-tile-action>
              <v-icon v-html="item.icon"></v-icon>
            </v-list-tile-action>
            <v-list-tile-content>
              <v-list-tile-title v-text="item.title"></v-list-tile-title>
            </v-list-tile-content>
          </v-list-tile>
        </v-list>
      </v-navigation-drawer>
      <v-toolbar app>
        <v-toolbar-side-icon @click.native.stop="drawer = !drawer"></v-toolbar-side-icon>

        <v-toolbar-title v-text="title"></v-toolbar-title>
        <v-spacer></v-spacer>
        <v-btn
          icon
        >
          <v-icon>battery_std</v-icon>
        </v-btn>
      </v-toolbar>
      <v-content>
        <v-container fluid fill-height grid-list-md>
          <v-slide-y-transition mode="out-in">
            <router-view></router-view>
          </v-slide-y-transition>
        </v-container>
      </v-content>

      <v-footer app>
        <v-spacer></v-spacer>
        <span>&copy; 2017 everybody go &nbsp; </span>
      </v-footer>
    </v-app>
  </div>
</template>

<script>
  export default {
    name: 'elecapp',
    data: () => ({
      drawer: true,
      items: [
        { icon: 'cloud', title: '메뉴바꿈!', to: '/' },
        { icon: 'bubble_chart', title: 'Inspire', to: '/inspire' },
        { icon: 'title', title: 'test', to: '/test' },
        { icon: 'attachment', title: 'testFile', to: '/testFile' },
        { icon: 'archive', title: 'testDB', to: '/testDB' },
        { icon: 'brightness_low', title: 'testUI', to: '/testUI' }
      ],
      title: 'Electron Test'
    })
  }
</script>

<style>
  @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons');
  /* Global CSS */
</style>

alt white

깔끔하네요 우선~

배치의 예술

https://vuetifyjs.com/ko/layout/grid

모바일시대가 되며 부트스트랩부터 구체화되서 발전하고 있는 그리드 시스템입니다.

이제부터 모바일, 태블릿, 데스크탑등의 사이즈를 바꿔가며 확인해보시기 바랍니다.

그리드 시스템을 위한 3요소

v-container / v-layout / v-flex

항상 이런 3 계층으로 이루어져야합니다.

일렉트론 기본 구성

위의 소스중 중간쯤 보면 v-container가 있고 router-view라는 곳이 만든 페이지들 표시하는 곳입니다.

모든 페이지가 v-container 안에 들어가 있는거죠..

src/renderer/app.vue

<v-container fluid fill-height grid-list-md>
  <v-slide-y-transition mode="out-in">
    <router-view></router-view>
  </v-slide-y-transition>
</v-container>
  • fluid는 좌우로 꽉찬 화면을 의미합니다.

  • grid-list-md는 각 요소들간의 여백입니다. 없으면 답답하게 꽉꽉 붙어있죠..

어떻게 테스트하냐고요? 실행시켜놓고 testDB.vue 같은 페이지를 grid-list-md를 지우고/살리고 저장해보면 됩니다.

그리드시스템 테스트

테스트를 위한 파일을 추가해보겠습니다.

src/renderer/components/testUI.vue

<v-layout row wrap>
    <v-flex xs12 sm6 md4 lg3 xl2>
      <v-card color="red">
        a
      </v-card>
    </v-flex>
    <v-flex xs12 sm6 md4 lg3 xl2>
      <v-card color="blue">
        b
      </v-card>
    </v-flex>
    <v-flex xs12 sm6 md4 lg3 xl2>
      <v-card color="red">
        c
      </v-card>
    </v-flex>
    <v-flex xs12 sm6 md4 lg3 xl2>
      <v-card color="blue">
        d
      </v-card>
    </v-flex>
    <v-flex xs12 sm6 md4 lg3 xl2>
      <v-card color="red">
        e
      </v-card>
    </v-flex>
    <v-flex xs12 sm6 md4 lg3 xl2>
      <v-card color="blue">
        f
      </v-card>
    </v-flex>
  </v-layout>

https://vuetifyjs.com/ko/layout/breakpoints

위 링크를 참조해보면.

12가 너비의 맥스인 것이고(bootstrap등 다른 프레임도 마찬가지입니다.)

xs12의 경우 600px이하(곧 모바일)에서는 12개를 다 쓰겠다는 것 곧 너비를 다쓴다는 것입니다.

xs6로 해두면 12 나누기 6 = 2개가 나오겠죠.

실제 구현 모습..

  • xs: mobile
    alt xs
  • sm: tablet
    alt sm
  • md: notebook
    alt md
  • lg: desktop
    alt lg 특이한 것은 옆에 사이드 메뉴가 있죠? lg사이즈에서 v-navigation-drawer가 살아나는 것을 알수 있습니다.
  • xl: 4k monitor
    모니터가 후져서 이건 테스트가 안되네요…

사실 처음 디자인 운운했던것 중 가장 큰 요소가 바로 이 그리드 시스템입니다.

정렬만 제대로 해주면 왠만한 UI 때려 밖아도 그럴싸합니다.

vuetify가 이미 설계가 잘 되어있기 때문에 저 3요소만 잘 이용해주면 배치의 문제는 끝난 것입니다.

코드 줄여보기

처음 접하시는 분들은 vuetify와 vue가 뭔지 잘 모르시겁니다. 겁내지마세요.. 저도 둘다 한지 얼마 안되었답니다.

vuetify가 위와 같이 화면을 이쁘게 구성하는 css 프레임웤이라고 하면..

vue.js는 정적인 html을 편리하게 사용가능하게 합니다.

위의 소스를 바꿔 보겠습니다.

src/renderer/components/testUI.vue

<template>
  <v-layout row wrap>
    <v-flex xs12 sm6 md4 lg3 xl2 v-for="(value, index) in grids">
      <v-card dark :color="index % 2 ? 'red' : 'blue'">
        {{value}}
      </v-card>
    </v-flex>
    <v-flex xs12>
      <v-btn @click="add">추가</v-btn>
      <v-btn @click="remove">삭제</v-btn>
    </v-flex>
  </v-layout>
</template>

<script>
  export default {
    name: 'testUI',
    data: () => ({
      grids: ['', '', '', '', '', '', '', '']
    }),
    mounted () {
    },
    methods: {
      add () {
        this.grids.push('a')
      },
      remove () {
        this.grids.pop()
      }
    }
  }
</script>

<style scoped>
</style>

alt lgvue

html 코드가 비약적으로 줄면서 8개나 표시하네요~

추가 삭제로 데이터를 지우지만(배열 추가: push, 삭제: pop) 화면도 반영됩니다.

vue의 할일은 저런 것입니다.

v-for로 이터레이팅(반복작업)도 해서 값(value)도 얻고 몇번째(index)도 얻고..

인라인 조건으로 색상도 바꾸고..

이런 홀짝 조건 문은

index % 2 ? 'red' : 'blue'

이런 것과 같습니다.

if (index % 2) color = 'red'
else color = 'bule'

간단한 조건은 html에 저렇게 쑤셔 넣을 수 있어서 좋죠~

color 앞에 콜론(:)이 어떤 조건이나 값에 바인딩이 되어있다는 것입니다.

더 복잡한 조건은 method나 computed 등에서 할 수 있습니다.

콤포넌트 사용

이제 오와 열을 잘 맞출 수 있으니 vuetify 콤포넌트를 섞어서 모양을 내보겠습니다.

v-card, v-list

v-card는 모양을 낼때 제일 중요한 콤포넌트입니다.

정리해서 넣기 좋은 박스(윈도우 어플이라면 panel 같은..)입니다..

https://vuetifyjs.com/ko/components/cards

위 공식 도큐에 자세히 나와있습니다..

이중 제가 또 강조 하고 싶은 것은

권장하는 공식 도큐의 가이드라인을 지켜라! 입니다.

v-card 아래에는 v-card-title, v-card-text, v-card-action 등으로 해야 이쁘게 나옵니다.

비정상

<v-card-title>
  <v-card>
    이런식으로하면 당연히 요상하게 나옵니다.
    <div>sfwefw</div>  
  </v-card>
</v-card-title>

권장

<v-card>
  <v-card-title>제목</v-card-title>
  <v-card-text>어쩌고저쩌고 높이가 가변<br>ddd?<br></v-card-text>
  <v-card-action>각종 버튼</v-card-action>
</v-card>

괜히 v-card안에 <div style=""> 같은 거 스타일 채워서 꾸역꾸역 맞춰봤자.. 지금 잘나와도 해상도나 특이 상황에 구겨질 수 있습니다..

v-card 안에 v-list를 넣을 수 있습니다.

예제에 잘 나와 있기 때문에 알고 있는 거죠~

공식 예제를 카피엔 페이스트하고 약간 주물럭 거린 결과 입니다.

  • sm
    alt comsm
  • xs
    alt comxs

src/renderer/components/testUI.vue

<template>
  <v-layout row wrap>
    <v-flex xs12 sm6>
      <!--<v-card height="400">-->
      <v-card style="height: 400px">
        <v-toolbar color="cyan" dark>
          <v-icon>mode_comment</v-icon>

          <v-toolbar-title>Inbox</v-toolbar-title>

          <v-spacer></v-spacer>

          <v-btn icon>
            <v-icon>search</v-icon>
          </v-btn>
        </v-toolbar>
        <v-list two-line>
          <template v-for="(item, index) in items">
            <v-subheader
                v-if="item.header"
                :key="item.header"
            >
              {{ item.header }}
            </v-subheader>

            <v-divider
                v-else-if="item.divider"
                :inset="item.inset"
                :key="index"
            ></v-divider>

            <v-list-tile
                v-else
                :key="item.title"
                avatar
                @click=""
            >
              <v-list-tile-avatar>
                <img :src="item.avatar">
              </v-list-tile-avatar>

              <v-list-tile-content>
                <v-list-tile-title v-html="item.title"></v-list-tile-title>
                <v-list-tile-sub-title v-html="item.subtitle"></v-list-tile-sub-title>
              </v-list-tile-content>
            </v-list-tile>
          </template>
        </v-list>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn color="primary">
            <v-icon left>add</v-icon>
            Add
          </v-btn>
          <v-btn color="error">
            <v-icon left>remove</v-icon>
            Remove
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-flex>

    <v-flex xs12 sm6>
      <v-card height="400">
        <v-toolbar color="orange" dark>
          <v-icon>home</v-icon>

          <v-toolbar-title>Todo list</v-toolbar-title>

          <v-spacer></v-spacer>

          <v-btn icon>
            <v-icon>search</v-icon>
          </v-btn>
        </v-toolbar>
        <v-card-text>
          <v-alert
              :value="true"
              type="info"
          >
            This is a info alert.
          </v-alert>
        </v-card-text>
        <v-list
            subheader
            two-line
        >
          <v-list-tile @click="">
            <v-list-tile-action>
              <v-checkbox v-model="notifications"></v-checkbox>
            </v-list-tile-action>

            <v-list-tile-content @click="notifications = !notifications">
              <v-list-tile-title>Notifications</v-list-tile-title>
              <v-list-tile-sub-title>Allow notifications</v-list-tile-sub-title>
            </v-list-tile-content>
          </v-list-tile>

          <v-list-tile @click="">
            <v-list-tile-action>
              <v-checkbox v-model="sound"></v-checkbox>
            </v-list-tile-action>

            <v-list-tile-content @click="sound = !sound">
              <v-list-tile-title>Sound</v-list-tile-title>
              <v-list-tile-sub-title>Hangouts message</v-list-tile-sub-title>
            </v-list-tile-content>
          </v-list-tile>

          <v-list-tile @click="">
            <v-list-tile-action>
              <v-checkbox v-model="video"></v-checkbox>
            </v-list-tile-action>

            <v-list-tile-content @click="video = !video">
              <v-list-tile-title>Video sounds</v-list-tile-title>
              <v-list-tile-sub-title>Hangouts video call</v-list-tile-sub-title>
            </v-list-tile-content>
          </v-list-tile>
        </v-list>
      </v-card>
    </v-flex>
  </v-layout>
</template>

<script>
  export default {
    name: 'testUI',
    data: () => ({
      items: [
        { header: 'Today' },
        {
          avatar: 'https://cdn.vuetifyjs.com/images/lists/1.jpg',
          title: 'Brunch this weekend?',
          subtitle: "<span class='text--primary'>Ali Connors</span> &mdash; <span class='hidden-xs-only'> I'll be in your neighborhood doing errands this weekend. Do you want to hang out?</span>"
        },
        { divider: true, inset: true },
        {
          avatar: 'https://cdn.vuetifyjs.com/images/lists/2.jpg',
          title: 'Summer BBQ <span class="grey--text text--lighten-1">4</span>',
          subtitle: "<span class='text--primary'>to Alex, Scott, Jennifer</span> &mdash; <span class='hidden-xs-only'>Wish I could come, but I'm out of town this weekend.</span>"
        },
        { divider: true, inset: true },
        {
          avatar: 'https://cdn.vuetifyjs.com/images/lists/3.jpg',
          title: 'Oui oui',
          subtitle: "<span class='text--primary'>Sandra Adams</span> &mdash; <span class='hidden-xs-only'>Do you have Paris recommendations? Have you ever been?</span>"
        }
      ],
      notifications: false,
      sound: false,
      video: false,
      invites: false
    }),
    mounted () {
    },
    methods: {
    }
  }
</script>

<style scoped>
</style>

코드가 장황해 보이긴 하지만 공식도큐에서 긁어온 것이며 실제 별것은 없습니다.

이중 디자인의 키포인트 몇가지만 짚어 보겠습니다.

  • v-card의 height: style 대신 해당 콤포넌트의 속성을 따르자..

왠만하면 style 건들지 말라고 하는 것은 이런 것 때문이죠..

<v-card height="400">
<v-card style="height: 400px">

둘의 효과는 같습니다.

하지만 동적으로 구현할때는 스타일보다 장점이 있습니다.

<v-card :height="hv">

hv라는 값이 상황에 따라 동적이어야한다면 정수형 변수 하나면 됩니다.

하지만 스타일일 경우 스타일 바인딩이라는 것을 해야하고 정수 + ‘px’를 추가해야하죠..

  • v-spacer: 공백의 묘미

그림에서 Inbox와 검색 버튼이 있는데 가운데 공백을 잘 넣어줄 때 쓰입니다.

실제 구현시 상단 혹은 하단 툴바에 좌우 끝에 붙히고 싶을 때 정말 많죠..(float-right같은 걸로 해결은 되지만..고려할 게 많죠..)

그림중 ADD REMOVE 버튼도 이걸 이용해서 우측에 잘 깔끔하게 붙어있는 것이죠..

  • v-card-action

버튼류는 이안에 채워 넣으면 좋습니다.

  • v-card-text

v-card안에 높이가 가변인 컨텐츠가 들어가면 됩니다.

  • v-cart-title

느낌처럼 제목 써주는 용도인데 높이가 정해져있어서 큰 컨텐츠는 찐따같이 나옵니다.

테마 동적으로 변경해보기

막상 화이트로 해봤는데 그냥 그렇네요..

업체와 얘기할때 구두로 설명하기보다는 보여주면서 설명하는게 어떨까요?

다른 콤포넌트가 다크 테마에서는 어찌 되는지 한번 확인해봅시다..

위에 설명했던 v-app의 dark가 동적이면 끝나겠죠?

src/renderer/App.vue

<template>
  <div id="app">
    <v-app :dark="dark">
      <v-navigation-drawer
        v-model="drawer"
        app
      >
        <v-list>
          <v-list-tile 
            router
            :to="item.to"
            :key="i"
            v-for="(item, i) in items"
            exact
          >
            <v-list-tile-action>
              <v-icon v-html="item.icon"></v-icon>
            </v-list-tile-action>
            <v-list-tile-content>
              <v-list-tile-title v-text="item.title"></v-list-tile-title>
            </v-list-tile-content>
          </v-list-tile>
        </v-list>
      </v-navigation-drawer>
      <v-toolbar app>
        <v-toolbar-side-icon @click.native.stop="drawer = !drawer"></v-toolbar-side-icon>

        <v-toolbar-title v-text="title"></v-toolbar-title>
        <v-spacer></v-spacer>
        <v-btn
          icon
          @click="dark = !dark"
        >
          <v-icon>loop</v-icon>
        </v-btn>
      </v-toolbar>
      <v-content>
        <v-container fluid fill-height grid-list-md>
          <v-slide-y-transition mode="out-in">
            <router-view></router-view>
          </v-slide-y-transition>
        </v-container>
      </v-content>

      <v-footer app>
        <v-spacer></v-spacer>
        <span>&copy; 2017 everybody go &nbsp; </span>
      </v-footer>
    </v-app>
  </div>
</template>

<script>
  export default {
    name: 'elecapp',
    data: () => ({
      drawer: true,
      items: [
        { icon: 'cloud', title: '메뉴바꿈!', to: '/' },
        { icon: 'bubble_chart', title: 'Inspire', to: '/inspire' },
        { icon: 'title', title: 'test', to: '/test' },
        { icon: 'attachment', title: 'testFile', to: '/testFile' },
        { icon: 'archive', title: 'testDB', to: '/testDB' },
        { icon: 'brightness_low', title: 'testUI', to: '/testUI' }
      ],
      title: 'Electron Test',
      dark: false
    })
  }
</script>

<style>
  @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons');
  /* Global CSS */
</style>

alt darkdyn

우측상단에 버튼을 누르니 테마가 토글되죠?

다크로 변하니 알아서 폰트등이 다크에서 화이트로 변경되네요..

실사용중인 앱 예

직접 만드는 중인 앱입니다.. (만들다 만 흔적이 역력하네요..)

alt anal

주로 이런식의 대시보드형에서 그리드 시스템이 쓰이죠..

위와 같은 차트 표등이 하나의 콤포넌트가 되서 이곳 저곳에서 퍼즐조각 맞추듯 사용이 가능합니다.

원본 소스

https://github.com/fkkmemi/elecapp

결론

공식도큐를 잘 살펴보고 가이드라인을 지키자!!

그리고 작업을 하실 때 가급적 레이아웃을 v-layout, v-flex로 전부 짜놓고 채워넣는 것이 좋습니다.

사실 디자이너 수준에서는 얘기가 하나 더 들어가는데..

바로 색상간 배치 문제가 있습니다.

각 콤포넌트들의 대비색도 마케팅 효과와 관련이 있기 때문에 제가 따라갈 수준은 안됩니다.

제가 도와드릴 수 있는 것은 극악의 공대감성 탈피 정도입니다.

다음번에는 디비를 연동해서 Todo 앱 비슷하게 만들어 보겠습니다.

ps

현재 솔루션으로 모바일 포팅도 성공해서 iOS, android 테스트 중입니다..

흉내내기 수준이긴 하지만.. 결국 한 플랫폼으로 서버, 데탑, 모바일로 가긴 가네요..

다 비슷한 사용자경험의 UI라 사용도 편리하고.. 개발하기도 좋네요~ 만들어둔 콤포넌트를 모바일에서 직접 씀..

해보니 back-end api가 제일 중요하네요.. 사용자 인증이니 푸쉬니 다 담당해야하니…

잡스러운 개발의 극에 달하고 있습니다..

일렉트론이 끝나면 모바일도 한번 강좌 진행해볼 예정입니다.

커뮤니티 공개

클리앙 팁과강좌

https://www.clien.net/service/board/lecture/12328153

댓글남기기