Software Workshop

Disclaimer

frontend 기술의 역사

vue.js

소개

(library) react.js << (hybrid) vue.js << (framework) angular.js

특징

2-way binding

vue cli로 프로젝트 구성하기

핵심 요소

components

예제 (실습)

<template>
  <div id="app">
    <book/>
  </div>
</template>

<script>
import Book from './components/Book.vue'

export default {
  name: 'App',
  components: {
    Book,
  }
}
</script>

data

예제 (실습)

Book.vue

<template>
  <div id="book">
    <p>title: {{ title }}</p>
    <p>number: {{ number }}</p>
    <p>book data: {{ bookData }}</p>
  </div>
</template>

<script>
export default {
  name: 'Book',  
  data: function () {
    return {
      title: 'Books',      
      number: 30,
      bookData: [
        {'author': '헤밍웨이', 'title': '노인과 바다'}, 
        {'author': '피츠제럴드', 'title': '위대한 개츠비'},
      ],
      status: true,
      errorMessage: '',
    }
  },
}
</script>

.eslintrc.js

module.exports = {
  root: true,
  parserOptions: {
    parser: '@babel/eslint-parser'
  },
  env: {
    browser: true,
    node: true,
  },
  extends: [
    'plugin:vue/essential',
    'eslint:recommended'
  ],
  // required to lint *.vue files
  plugins: [
    'vue'
  ],
  // add your custom rules here
  rules: {
    'vue/multi-word-component-names': 'off'
  }
}

computed

예제 (실습)

    <p>{{ '실행 ' + (status ? '중단' : '재개' }}</p>

-->

    <p>status: {{ status }}</p>
    <p>status message: {{ statusMessage }}</p>
  data: function () {
    return {
      status: true,
    }
  },
  computed: {
    statusMessage: function () {
      return '실행 ' + (this.status ? '중단' : '재개'); 
    }
  },

watch

예제 (실습)

    <p>error message: {{ errorMessage }}</p>

    <b-input-group class="mt-3">
      제목 입력: <b-form-input v-model="title"></b-form-input>
    </b-input-group>
  data: function () {
    return {
      errorMessage: '',
    }
  },
  watch: {
    title: function () {
      this.errorMessage = '제목 변경됨';
    }
  },

main.js

import { BootstrapVue } from 'bootstrap-vue';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';

Vue.use(BootstrapVue);

methods

예제 (실습)

  methods: {
    getApiUrlPath: function () {
      let pathPrefix = 'https://api.mydomain.com/myapp';
      if (process.env.NODE_ENV === 'development') {
        pathPrefix = 'http://127.0.0.1:5000';
      }
      return pathPrefix;
    },

mounted, created, ...

  created: function () {
    console.log("component created");
  },
  mounted: function () {
    console.log("component mounted");
  },

Lifecycle

vue_lifecycle.png

렌더링 기법

v-text와 v-html

v-if를 이용한 요소 삭제

<div v-if="status">
  <h1>작품 목록</h1>
  <ul v-if="bookData">
  </ul>
</div>

v-for를 이용한 목록 출력

예제 (실습)

<div v-if="status">
  <h1>작품 목록</h1>
  <ul v-if="bookData">
    <li v-for="item in bookData" :key="item.title">{{ item.title }} by {{ item.author }}</li>
  </ul>
</div>
      bookData: [
        {'author': '헤밍웨이', 'title': '노인과 바다'},
        {'author': '피츠제럴드', 'title': '위대한 개츠비'},
      ],

v-on을 이용한 이벤트 핸들링

<a v-on:click="doSomething"> ... </a>

@이벤트명으로 줄여서 사용할 것

<button @click="run()">실행</button>

클래스 적용

<div class="memo" v-bind:class="{ active: isActive, 'text-danger': hasError }">
</div>
isActive: true,
hasError: false,
<dlv class="memo active">
</div>

스타일 적용

<div class="memo" v-bind:style="{ color: activeColor }">
  메모
</div>
activeColor: 'red',

바인딩

v-model

2_way_binding.png

예제 (revisited)

    <b-input-group class="mt-3">
      제목 입력:
      <b-form-input v-model="title"></b-form-input>
    </b-input-group>
  data: function () {
    return {
      title: 'Books',
    }
  }

v-bind

예제 (실습 x)

<div v-bind:id="dynamicId"></div>
<my-button v-bind:class="dynamicClass"></my-button>  

API를 이용한 동적 바인딩

backend

예제 (실습)

app.py

#!/usr/bin/env python

from flask import Flask, jsonify, request, render_template
from flask_cors import CORS

app = Flask(__name__, static_folder="../frontend/dist", template_folder="../frontend/dist")
CORS(app, resources={r"/*": {"origins": "*"}})

@app.route("/books")
def books():
    data = {"status": False, "message": "실패", "books": []}

    data["status"] = True
    data["message"] = "성공"
    data["books"] = [{"author": "필립 K. 딕", "title": "안드로이드는 전기양의 꿈을 꾸는가"},
                     {"author": "아서 클라크", "title": "유년기의 끝"},
                     {"author": "H.G. 웰즈", "title": "우주 전쟁"}]
    print(data)
    return data

if __name__ == "__main__":
    app.run(host="0.0.0.0")

frontend

예제 (실습)

import axios from 'axios';

  methods: {
    getBooks() {
      const url = this.getApiUrlPath() + "/books";
      axios.get(url)
          .then((res) => {
            if (!res.data.status) {
              this.errorMessage = res.data.message;
            } else {
              this.bookData = res.data['books'];
            }
          })
          .catch((error) => {
            this.errorMessage = error;
          })
    },
  }
  mounted: function() {
    this.getBooks();
  },
<button @click="bookButtonClicked">책 불러오기</button>
  methods: {
    bookButtonClicked() {
      return this.getBooks();
    },
}

주의사항

@app.route("/groups/<group_name>/feeds/<feed_name>", methods=["DELETE", "POST", "GET"])
def get_feed_info(group_name, feed_name):
    response_object = {"status": "failure"}
    if request.method == "GET":
        ...
    elif request.method == "DELETE":
        ...
  axios
      .get(`/groups/${groupName}/feeds/${feedName}`)
      ...

  axios
      .delete(`/groups/${groupName}/feeds/${feedName}`)
      ...

개발 팁

컴퍼넌트

props

예제 (실습 x)

export default {
  name: 'MySubComponent',
  props: {
    variant: {
      type: String,
      default: 'info'
    },
    initialIcon: {
      type: Array,
      default: function () {
        return [];
      }
    },
  },
}
<my-sub-component
  :initial-icon="['fas', 'search']"
  variant="dark"/>

$emit

예제 (실습)

두 개의 버튼에서 발생하는 클릭 수를 합해서 보여주는 예제

ButtonCounter.vue

  <button v-on:click="incrementCounter">
    {{ counter }}
  </button>
export default {
  name: "ButtonCounter",
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    incrementCounter: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
}

TotalCounter.vue

<div id="counter-event-example">
    <p>클릭 수 합계: {{ total }}</p>
    버튼1: <button-counter v-on:increment="incrementTotal"></button-counter>
    버튼2: <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
import ButtonCounter from './ButtonCounter.vue';
 
export default {
  name: "TotalCounter",
  components: {
    ButtonCounter
  },
  data: function () {
    return {
      total: 0
    }
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
}

Book.vue

<template>
  <div id="app">
    <total-counter></total-counter>
  </div>
</template>

<script>
import TotalCounter from './TotalCounter.vue'

export default {
  name: 'Book',
  components: {
    TotalCounter,
  }
}
</script>

참고사항

this.$emit('decrement', e.target.value)

<parent-component @decrement="decrementWithValue"/>

decrementWithValue(value) {
    this.total -= value;
}

$refs

예제 (실습 x)

<button ref="saveFeedButton1" @click="saveFeedButtonClicked">Save</button>
methods: {
  saveFeedButtonClicked: function() {
    this.$refs.saveFeedButton1.innerText = "Saved";
  }
}

심화 주제

vue-router

예제 (실습)

Result.vue

<template>
  <div id="result">
    결과 내용
  </div>
</template>

<script>
export default {
  name: "Result"
}
</script>

Management.vue

<template>
  <div id="mgmt">
    관리 요소들
  </div>
</template>

<script>
export default {
  name: "Management"
}
</script>

App.vue

<template>
  <div id="app">
    <router-link to="/"><h1>홈</h1></router-link>
    <router-link to="/result"><h1>결과</h1></router-link>
    <router-link to="/management"><h1>관리</h1></router-link>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'App',
  components: {
  }
}
</script>

UI 개선 버전

    <b-navbar>
      <b-navbar-nav>
        <b-nav-item>
          <router-link to="/">
            <b-nav-text>홈</b-nav-text>
          </router-link>
        </b-nav-item>
        <b-nav-item>
          <router-link to="/result">
            <b-nav-text>결과</b-nav-text>
          </router-link>
        </b-nav-item>
        <b-nav-item>
          <router-link to="/management">
            <b-nav-text>관리</b-nav-text>
          </router-link>
        </b-nav-item>
      </b-navbar-nav>
    </b-navbar>
    <router-view></router-view>

router/index.js

import Vue from 'vue';
import Router from 'vue-router';
import Book from '../components/Book.vue';
import Result from '../components/Result.vue';
import Management from '../components/Management.vue';

Vue.use(Router);

export default new Router({
    mode: 'history',
    base: process.env.BASE_URL,
    routes: [
        {
            path: '/',
            name: 'Home',
            component: Book
        },
        {
            path: '/result',
            name: 'Result',
            component: Result
        },
        {
            path: '/management',
            name: 'Management',
            component: Management,
            children: [
                {
                    path: ':group/:feed',
                    component: Management
                }
            ]
        }
    ]
})

main.js

import router from './router';

new Vue({
  render: h => h(App),
  router,
}).$mount('#app')

this

예제 (실습 x)

axios
  .get(url)
  .then(function (res) {
    this.feeds = res.data;
  })
  .catch(function (res) {
    console.error(err);
  });

-->

axios
  .get(url)
  .then((res) => {
    this.feeds = res.data;
  })
  .catch((err) => {
    console.error(err);
  });

functional programming

예제 (실습 x)

newData = _.filter(data, (o) => {
  return o['sum'] > 100;
}).map(function (o) {
  o['extra'] = {'name': 'John', 'age': 14};
  o['job'] = 'Typist';
  return o;
});

예제 (실습)

Book.vue

import _ from 'lodash';
  
    getBooks() {
      const url = this.getApiUrlPath() + "/books";
      axios.get(url)
          .then((res) => {
            if (!res.data.status) {
              this.errorMessage = res.data.message;
            } else {
              this.bookData = res.data['books'].filter((o) => {
                return o['author'] !== '아서 클라크';
              }).map((o) => {
                o['date'] = new Date();
                return o;
              });
            }
          })
          .catch((error) => {
            this.errorMessage = error;
          })
    },

탐구 과제

Slot

Vuex

github

https://github.com/terzeron/VuejsTest/tree/master/tutorial