스토리 홈

인터뷰

피드

뉴스

조회수 2746

PHP CI 환경에서 완전한 Vue 사용하기

편집자 주Vue 또는 VUE로 혼용하나 공식 사이트의 표기에 맞춰 아래와 같이 통일함-Vue-Vuex-Vue-Router목차1.Controller2.VIEW3.webpack Vue 소스 진입점4.webpack 설정5.Package.json6.Vue-Router7.Vuex8.공통 처리 mixin9.요약10.마치며시작하며드디어 브랜디 관리자 서비스에 Vue를 도입하고자 떠났던 여정의 마지막 장입니다. 브랜디 관리자 서비스는 PHP Codeigniter와 jQuery로 구성되어 있습니다. 사실 잘 운영되고 있는 서비스에 리스크가 큰 신기술을 도입하는 것은 도박에 가깝습니다. 몇 시간만 운영이 정지되어도 회사에 엄청난 피해를 안겨줄 수도 있으니까요. 하지만 여러 번의 검증과 실험으로 도박에서 이길 확률을 100%에 가깝게 끌어올린다면 한번 도전해볼 만하지 않을까요?이전 글인 PHP Codeigniter 환경에서 VUE 사용해보기에서 기본적인 webpack + Vue + Codeigniter 환경 구축 방법을 알아봤는데요. 하지만 단순히 webpack과 Vue만 적용했다고 해서 “우리 시스템은 UI 프레임워크로 Vue를 사용하고 있습니다.”라고 말할 순 없습니다. 아주 중요한 숙제가 남았죠.Vue에는 활용도를 대폭 끌어올려주는 Vue-Router와 Vuex Store1)가 있는데 그중 Vue-Router를 이번 글에서 자세히 다루려고 합니다.2) Vue-Router는 Vue.js의 공식 라우터입니다. 공식 홈페이지의 소개는 아래와 같습니다.중첩된 라우트/뷰 매핑모듈화된, 컴포넌트 기반의 라우터 설정라우터 파라미터, 쿼리, 와일드카드Vue.js의 트랜지션 시스템을 이용한 트랜지션 효과세밀한 내비게이션 컨트롤active CSS 클래스를 자동으로 추가해주는 링크HTML5 히스토리 모드 또는 해시 모드(IE9에서 자동으로 폴백)사용자 정의 가능한 스크롤 동작한마디로 정리하면 입력된 URL에 반응해 부분에 해당 URL의 view를 보여주는 기능인 것입니다. 다시 말해 URL이 변경될 때 한 페이지에서 화면 전체를 갈아끼우거나, 화면의 일부분(부분)을 치환해주는 역할을 한다는 것이죠. 더 나아가 해당 화면이 로드되기 전후로 전처리, 후처리 기능까지 가능합니다.착안점Vue와 Vue-Rotuer를 알게 되었을 땐 PHP 기반 프로젝트에 Vue-Router를 적용할 수 없으니 처음부터 새로 만들어야 한다고 생각했습니다. 로그인 인증 문제, 메뉴의 권한 관리 등 모든 것이 Vue 아래에 있어야 한다고 생각했기 때문입니다.어느 날 관리자 서비스에 TDD를 구현해보려고 Python Flask + webpack + Vue 프로젝트를 구성하고 있었습니다. 그러던 중 우연히 Flask + Vue-Router에서 404 페이지를 처리하려면 Flask Fallback 페이지를 Vue-Router 메인 페이지(가 있는 페이지)로 보내고, Vue-Router에서 진짜 매핑된 URL이 없으면 404 처리를 하는 식으로 구성한다는 글을 읽고 문득 호기심이 생겼습니다.‘관리자 서비스에서도 컨트롤러로 여러 URL을 한 가지 페이지로 보낸다면?’PHP를 거쳐 페이지로 이동한 것이므로 권한 관리와 메뉴 트리까지는 PHP에서 처리되면서 URL이 변할 것이고, 실제로 화면을 보여주는 Contents 영역만 를 사용한다면 어떻게 될지 궁금해졌습니다. 바로 하던 일을 멈추고 관리자 소스에 Vue-Router를 활용한 테스트 소스코드를 작성해봤습니다.예상했던 대로 PHP의 로그인 인증 처리를 거치면서 실제로 보이는 부분에는 부분만 정상적으로 치환되었습니다. 이 간단한 실험을 바탕으로 통계 시스템의 일부를 구현하는 데에 Vue-Router와 Vuex Store, 공통 처리 Mixin을 추가해 제작했습니다.1.Controller4개의 페이지를 가진 통계 시스템의 Codeigniter 컨트롤러 모습입니다. 기존의 서비스 URL들이 존재하기 때문에 Fallback을 통으로 Vue-Router로 보낼 순 없었고, 라우터를 사용할 페이지들을 하나의 페이지로 보냈습니다.1-1) /application/controllers/[컨트롤러 경로]... 생략 /* [라우터 view]에서 태그를 포함하고 있습니다. */ public function salesAnalysisProduct() { $this->load->view('[라우터 view]'); } public function salesAnalysisSeller() { $this->load->view('[라우터 view]'); } public function trendAnalysisProduct() { $this->load->view('[라우터 view]'); } public function trendAnalysisSeller() { $this->load->view('[라우터 view]'); } ... 생략 2.VIEWCodeigniter 환경에 반영하는 것이므로 CI에서 인식시킬 PHP view와 webpack에서 빌드 결과를 자동으로 바인딩할 html 파일로 구성됩니다.2-1)/application/views/[Vue용 view 경로]/index.php// [index.php] Vue 를 매핑할 php파일 컨트롤러의 view로딩용 [라우터 view]입니다. ... header, menu 생략 ... //바인딩될 부분 //자동매핑 html 인클루드 <?php include('index.html'); ?> ... footer 생략 ... 2-2)/application/views/[Vue용 view 경로]/index.html webpack의 빌드 결과로 자동으로 생성되는 파일입니다. [removed][removed] 위는 webpack의 HtmlWebpackPlugin에 의해 자동으로 바인딩된 모습입니다. 빌드되기 전 index.html은 다음 항목에 있습니다.3.webpack Vue 소스 진입점관리자에서는 프로젝트 폴더 안에 webpack과 Vue 용 서브 폴더를 두고 webpack.config.js에서 output 옵션을 통해 빌드 결과를 삽입하는 구조입니다. webpack 루트 폴더는 application 폴더와 같은 레벨에 위치하며, 폴더 구조나 파일 위치는 어디에 둬도 상관없습니다. webpack.config.js에서 entry 속성으로 잡아주시면 됩니다.3-1)/[webpack루트]/index.html// HtmlWebpackPlugin으로 스크립트를 삽입하기 위한 빈 템플릿 파일 3-2)/[webpack루트]/index.js/** * 진입용 index.js */ import Vue from 'vue' import axios from 'axios' import router from './router' import App from './App.vue' Vue.prototype.$http = axios new Vue({ el : '#app', router, components : { App }, template : '' }); 3-3)/[webpack루트]/App.vue [removed] import mixin from './common/common-mixin.js' import store from './vuex/store' export default { store, name : 'App' } [removed] Vuex와 통신 모듈 axios, Vue-Router 등을 루트 Vue 객체에 추가해줍니다. 브랜디 관리자의 webpack은 babel을 사용하고 있기 때문에 위의 store처럼 축약해서 작성하면 빌드된 파일에는 store: store와 같이 입력됩니다.Vue-Router는 태그에 자동으로 매핑되며, 위와 같은 구조로 상위 컴포넌트에서 할당되어 있어야 합니다. Vuex와 Vue-Router 설정은 글 아래에서 다루겠습니다.4.Webpack 설정이번에 Vue-Router와 Vuex를 도입하면서 webpack의 설정도 실제 서비스용과 개발용으로 분리했습니다. 폴더는 편의상 추가하였으며, package.json에서 자신이 설정한 경로로 설정하면 됩니다.Webpack 설정 파일은 Webpack의 시작과 끝이라고도 할 수 있습니다. Webpack 설정 파일에서 빌드할 소스의 경로와 빌드 결과 파일의 명명 규칙을 정하고, html 파일에 스크립트파를 자동으로 주입시키거나, Babel 플러그인을 통해 최신 스크립트 작성법을 브라우저를 신경쓰지 않고 사용할 수도 있습니다.그중에서도 중요한 옵션이 있는데 바로 Code Splitting에 관련된 옵션입니다.관리자 초기 Vue 모델에는 Vue-Router가 없었기 때문에 js 번들 파일의 크기가 그렇게 크지 않았습니다. 하지만 Vue-Router를 사용해 싱글 페이지 어플리케이션이 되거나 화면의 UI가 복잡해 컴포넌트 수가 많아지면 번들 js 파일의 크기가 매우 커집니다. 즉, 캐시를 사용하지 않는 익스플로러라면 소스에서 한 글자만 바뀌더라도 모든 페이지에서 거대한 번들 js를 새로 로딩하게 되고, 상당한 서버 자원을 소모합니다.Code Splitting 적용 전위의 이미지는 Code Splitting을 적용하기 전의 번들 js 정보입니다. 실제로 완성된 Vue 프로젝트의 번들 js는 더욱 큽니다. 정말 단순한 페이지 하나를 띄우는데 매번 뚱뚱한 js를 로딩해야 하는 것은 서비스 제공자와 서비스 사용자를 모두 괴롭게 할 것입니다.Code Splitting 적용 후하지만 위처럼 작은 조각으로 나눠 필요한 시점마다 필요한 번들 js만 로드하면 매우 빠른 페이지를 제작할 수 있습니다. 따라서 Code Split 기능은 매우 중요한 이슈입니다.물론 개발을 진행하다 보면 역시 어느 것 하나 쉽게 넘어가지지 않습니다. 관리자의 웹팩은 4.x 버전대를 사용하고 있습니다. 예전에 TF에서는 Webpakc 3.x 버전대를 사용하였는데 당시에는 CommonChunkPlugin 설정을 통해 Code Splitting을 사용할 수 있었습니다. 그대로 관리자에 적용하려 했는데..Removed라고 쓰여 있습니다. 찾아보니 CommonChunkPlugin이 옵티마이즈 옵션 하위의 splitChunk 속성으로 들어가면서 설정 방법이 바뀌었더군요. 머리를 싸매고 설정을 잡습니다.4-1) /[webpack루트]/build/webpakc.config.js : 공통 설정파일'use strict' const HtmlWebpackPlugin = require('html-webpack-plugin'); const { VueLoaderPlugin } = require('vue-loader'); const path = require('path'); module.exports = { entry: { //string, object, array 가능 - 기본은 ./src app: path.join('[스크립트 파일 경로]', 'index.js') //진입점 스크립트 파일입니다. }, output: { path: '[빌드된 js 목적지 경로]', publicPath: '[이미지등의 웹상 리소스 경로]', filename: './[name].[chunkhash].js', // 엔트리 파일명명규칙 chunkFilename: '[id]_[chunkhash].js', // chunk파일 명명 규칙 // --mode development에서는 [id]에도 chunkName들어갑니다. }, //vue와 js, css 로드 규칙을 설정합니다. module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', include: [ /[Vue 소스 경로]/ ] }, { test: /\.js$/, use: { loader: 'babel-loader?cacheDirectory', }, include: [ /[Vue 소스 경로]/ ] }, { test: /\.css$/, oneOf: [ { use: [ 'vue-style-loader', 'css-loader' ] } ] } ] }, resolve: { alias: { '@': '[Vue소스 경로]', // 편의상 소스단축경로를 설정합니다. }, //파일 확장자 자동인식 임포트시 해당 확장자는 생략가능합니다. extensions: ['.js', '.vue', '.json'], }, plugins: [ // Vue 파일 로더 new VueLoaderPlugin(), // html 자동 바인딩 // 아래의 플러그인으로 인해 index.html에 해시네임으로 빌드된 index.js가 자동으로 매핑됩니다. new HtmlWebpackPlugin({ // index.php에서 include할 파일이 생성될 경로와 파일명 입니다. filename: path.join('[View경로]', 'index.html'), // 자동으로 매핑할 진입점파일을 지정합니다. template: path.join('[Vue소스 경로]', 'index.html'), inject: true, minify: { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true } }), ], optimization: { //웹팩 4.x 버전에서 옵티마이즈 속성으로 추가된 CodeSplitting 기능입니다. splitChunks: { //initial - static파일만 분리, async - 동적로딩파일만 분리, all - 모두 분리 chunks: 'async', minSize: 30000, minChunks: 1, maxAsyncRequests: 5, //병렬 요청 chunk수 maxInitialRequests: 3, //초기 페이지로드 병렬 요청 chunk수 automaticNameDelimiter: '_', //vendor, default등 prefix 구분문자 (default : '~') name: true, //development모드일때 파일에 청크이름 표시여부 cacheGroups: { default: { minChunks: 2, //2개 이상의 chunk priority: -20, reuseExistingChunk: true //minChunks이상에서 사용할경우 공통사용 }, //axios, vue 같은 공통 모듈은 vendor로 관리합니다. vendors: { test: /[\\/]node_modules[\\/]/, priority: -10 } } } } }; 4-2) /[webpack루트]/build/webpack.dev.config.js 개발용 설정 파일 (네이밍은 자유)'use strict' const merge = require('webpack-merge') const webpack = require('webpack') const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin const baseWebpackConfig = require('./webpack.config') const config = require('../config').dev //개발용설정 const devWebpackConfig = merge(baseWebpackConfig, { //mode는 chunk[id], 디버깅 코드 등에 영향을 줍니다. webpack 3.x 버전에서는 Env 속성을 통해 관리했다고 합니다. mode: 'development', plugins: [ new BundleAnalyzerPlugin(), //번들 무게 분석기 제대로 스플릿 되었는지 확인할 때 사용합니다. new webpack.DefinePlugin({ env : config.env }), ], watch: true, //코드의 변화를 감지해 자동으로 재빌드해주는 옵션입니다. cache: true, //캐시 사용을 활성화하면 변경사항이 있는 코드만 재빌드합니다. optimization: { //uglify 플러그인 코드 압축 여부를 설정합니다 압축 시 용량을 매우 줄일 수 있으나 빌드 속도가 크게 저하되므로 개발 시에는 꺼줍니다. minimize: false, } }) module.exports = new Promise((resolve, reject) => { resolve(devWebpackConfig); }) 4-3) /[webpack루트]/build/webpack.prod.config.js 서비스용 설정파일 (네이밍은 자유)'use strict' const merge = require('webpack-merge') // 설정파일 결합에 사용합니다. const webpack = require('webpack') const baseWebpackConfig = require('./webpack.config') //베이스 설정파일 const config = require('../config').prod //서비스용 설정 const prodWebpackConfig = merge(baseWebpackConfig, { mode: 'production', //chunk[id], 디버깅 코드등 영향 있음 plugins: [ new webpack.DefinePlugin({ env : config.env }), ], //개발용과 반대로 용량은 줄이고 필요 없는 기능은 꺼줍니다. watch: false, cache: false, optimization: { minimize: true, } }) module.exports = prodWebpackConfig 5.package.json웹팩 설정 파일이 분리되면서 package.json의 런 스크립트도 변경했습니다.... "scripts": { "build": "webpack --config build/webpack.prod.config.js --progress", "build-dev": "webpack --config build/webpack.dev.config.js --progress" }, ... 6.Vue-RouterVue-Router는 위에서 설명한 대로 Vue의 컴포넌트와 밀접하게 결합된 라우터입니다. 그런데 여기서 webpack의 Code Split을 사용하려면 컴포넌트 import 방법이 매우 중요한데요.import './testComponent' 위처럼 import 구문을 사용해 컴포넌트를 불러오면 코드가 쪼개지지 않고 한 덩어리로 빌드되므로 아래와 같은 형태로 사용해야 합니다.const testComponent = () => import('./testComponent') webpack 공식 문서에도 나와있듯이 위처럼 ES2015 Loader spec에 있는 import()를 사용하여 컴포넌트를 생성해야 번들 js가 제대로 분리되며, Dynamic Import가 가능해집니다.Vue-Router를 쓰는 순간 싱글 페이지 어플리케이션이 되기 때문에 이곳에서 설정을 잘못 잡아주는 순간 육중한 컴포넌트 한 덩어리가 튀어나오면서 Code Splitting은 물거품이 되어버립니다. 조심합시다!또한 import 함수 안쪽엔 아래와 같은 주석을 달아야 청크 이름이 적용됩니다.const testComponent = () => import( /* webpackChunkName: '[청크이름]'*/ './testComponent') 라우터 경로 속성인 path 와 Codeigniter의 컨트롤러 경로를 맞춰주는 것이 포인트입니다!6-1) /[webpack루트]/router/index.js - 경로와 파일명은 자유입니다!import Vue from 'vue' import Router from 'vue-router' // 주석의 webpackChunkName = 코드 스플릿 chunk Name으로 사용됩니다. // 꼭 컴포넌트와 청크 이름을 같게 설정할 필요는 없습니다. const SalesAnalysisProduct = () => import(/* webpackChunkName: "salesAnalysisProduct" */ '[컴포넌트 파일 경로]') const SalesAnalysisSeller = () => import(/* webpackChunkName: "salesAnalysisSeller" */ '[컴포넌트 파일 경로]') const TrendAnalysisProduct = () => import(/* webpackChunkName: "trendAnalysisProduct" */ '[컴포넌트 파일 경로]') const TrendAnalysisSeller = () => import(/* webpackChunkName: "trendAnalysisSeller" */ '[컴포넌트 파일 경로]') Vue.use(Router) const router = new Router({ mode: 'history', routes: [ /* 통계 */ { path: '[CI컨트롤러 url]/salesAnalysisProduct', name: 'salesAnalysisProduct', component: SalesAnalysisProduct }, { path: '[CI컨트롤러 url]/salesAnalysisSeller', name: 'salesAnalysisSeller', component: SalesAnalysisSeller }, { path: '[CI컨트롤러 url]/trendAnalysisProduct', name: 'trendAnalysisProduct', component: TrendAnalysisProduct }, { path: '[CI컨트롤러 url]/trendAnalysisSeller', name: 'trendAnalysisSeller', component: TrendAnalysisSeller }, ] }) // 아래의 함수로 전처리 후처리도 가능합니다! router.beforeEach((to, from, next) => { // ... }) router.afterEach((to, from) => { // ... }) export default router 7.Vuex앞서 Vue와 Vuex, 컴포넌트간 통신과 상태 관리에서 소개했던 상태 관리와 통신을 위한 Vuex도 추가합니다. Vuex는 하나의 Store만 쓸 경우 상태 변수의 과포화로 인해 유지 보수가 어려워질 수 있으므로 namespace: true 옵션을 통해 도메인별로 관리합니다.7-1) /[webpack루트]/vuex/store.js - Vuex 진입파일import Vue from 'vue' import Vuex from 'vuex' // 각 도메인별 store들이 들어있는 modules 를 임포트해줍니다. import * as modules from './modules/index' Vue.use(Vuex) export default new Vuex.Store({ state : { }, getter: { }, mutations : { }, actions : { }, modules : modules.default }) 7-2) /[webpack루트]/vuex/modules/index.js - 도메인별 Store 자동 바인딩 스크립트const files = require.context('.', false, /\.js$/) const modules = {} //자신(index.js)를 제외한 파일들을 파일이름을 Key로 modules에 담습니다. files.keys().forEach((key) => { if (key === './index.js') return modules[key.toLowerCase().replace(/(\.\/|\.js)/g, '')] = files(key).default }) export default modules 7-3) /[webpack루트]/vuex/modules/statistics.js(통계 store 파일) - 예시입니다.export default { namespaced : true, //해당 속성을 통해 파일명을 namespace로 사용합니다. state: { /* 상태값 및 데이터 */ ... }, getters: { }, mutations: { /* state 변경처리 */ ... }, actions: { /* 통신처리 */ ... } } namespace: true로 되어있으므로 파일명인 statistics를 namespace로 사용하게 됩니다. 따라서 store 각 항목에 대한 접근은 다음과 같이 이뤄지며 computed 속성에 state: this.$store.state.statistics 처럼 정의해두면 편리합니다.dispatch는 this.$store.dispatch(‘statistics/[action 이름]’)commit은 this.$store.commit(‘statistics/[mutation 이름]’)state 변수 접근은 this.$store.state.statistics.[state 이름]8.공통 처리 mixinapi 통신에 사용되는 통신 라이브러리와 그 라이브러리의 복잡한 설정 코드, 단순한 Toast 출력 함수, 로딩 이펙트를 보여주는 함수 등 모든 항목들이 매 페이지마다 있으면, 통일되지 못한 UI, 페이지마다 일관되지 못한 설정 등으로 휴먼 에러가 발생할 확률이 높아집니다. 유지 보수 측면에서도 비용이 높아집니다. 이러한 단순 반복 코드들은 한번만 정의하고 재사용하는 것이 바람직합니다. 나중에 수정할 때도 용이하죠.공통사항을 묶어 Vue 전역 믹스인으로 Vue 루트 객체에 추가합시다. 단, global 옵션인 만큼 조심해서 써야 합니다. 시스템에 영향을 줄 것 같으면 하위 컴포넌트 mixins 속성에 넣어 해당 스코프에서만 사용하는 것이 바람직합니다.8-1) /[webpack루트]/common/common-mixin.js (파일이름, 경로는 자유입니다!)import Vue from 'vue' import Vue from 'axios' import Cookies from 'js-cookie' const TIMEOUT = '[타임아웃 시간(ms)]' /* mixin의 기본 형태는 Vue 컴포넌트의 형태와 동일합니다. 주로 전역 통신과 상태 관리는 vuex store에서, 전역 data 속성과 전역 함수는 mixin에서 관리합니다. */ Vue.mixin({ /* 전역 사용 data속성 선언 */ data: () => { return { ... //이곳에 선언하는 data 속성은 전역에서 this로 접근 가능합니다. } }, created: function() { // 공용 axios 객체 생성 this.axios = axios.create({ timeout: TIMEOUT, withCredentials: true, //공통해더는 여기에 headers : { } }); //axios 의 success와 error를 mixin method에서 처리 하도록 등록 this.axios.interceptors.response.use(this.onSuccess, this.onError) }, /* 전역 사용 함수 선언 */ methods: { /* axios의 response handling 함수*/ onSuccess : response => { }, onError : function (error) { }, /*GET, POST 등의 통신 함수, Toast(alert) 표출함수, 에러핸들링함수 등 선언*/ /*... 내용이 너무 길어서 생략 ...*/ } }); 9.요약지금까지의 내용은 파일 경로를 토대로 요약하면 다음과 같습니다. 참고로 아래의 폴더 구조는 절대적인것은 아닙니다. 모든 폴더 구조는 자율이며, 폴더 구조에 맞게 webpack.config.js에서 조정해주면 됩니다.[프로젝트 루트] └ [웹팩 루트] └ package.json └ [Vue 소스 루트] └ [common] └ [router] └ index.js // 라우터 설정파일 - CI 컨트롤러와 url 맞춰줘야함 └ [vuex] └ index.js // 도메인별 store module export 스크립트 └ [modules] └ 도메인별 store.js └ [컴포넌트 폴더] //예시에서는 ststistics └ App.vue //진입점 vue파일 Vuex와 전역 mixin 세팅 └ index.html //index.js가 주입될 껍데기 └ index.js //진입점 js Vue-Router와 App.vue 세팅 └ [build] // 빌드파일경로 └ webpack.config.js //베이스 설정파일 └ webpack.dev.config.js //개발용 설정파일 └ webpack.prod.config.js //서비스용 설정파일 └ [application] //Codeigniter 루트 └ [controllers] └ [컨트롤러 경로] // 예시의 통계부분 └ [views] └ [웹팩빌드 결과 폴더] └ [index.php] // CI 에서 로드하는 view (index.html include) └ [index.html] // js 번들이 자동 주입된 빌드결과 파일 └ [include] └ [scripts] └ [빌드결과 js 경로] //public path 속성 경로 └ 빌드 결과 js chunk들 마치며관리자 서비스에서 완전한 Vue를 사용하기 위해 꽤 험난한 과정을 거쳤습니다. 지금도 잘 돌아가는 서비스에 리스크를 감수하면서도 새로운 것을 도입하려는 이유를 찾아야 했고, 한동안은 레거시와 Vue로 된 소스를 2중으로 개발해야 했습니다.게다가 이 글을 작성하기 시작했을 땐 Code Splitting 설정 방법이 바뀌어 적용하지도 못한 상황이었기 때문에 사실 Code Splitting 내용이 없었습니다. 그런데 글을 작성하면서 splitChunk옵션을 성공해버렸어요! 덕분에 이 글도 모두 수정해야 했죠. Vue의 도입을 고려하는 개발자분들에게 도움이 되길 바라는 마음으로 글을 마칩니다.참고1)Vuex Store는 Vue와 Vuex, 컴포넌트간 통신과 상태 관리에 자세히 정리해두었다.2) 브랜디 관리자 서비스는 jQuery로 작성되어 있다. 따라서 jQuery를 베재할 수만은 없는 상황이었다. 이에 따라 기존 jQuery 컴포넌트들에 대한 해결책은 천보성 팀장님이 작성한 JQuery 프로젝트에 VUE를 점진적으로 도입하기를 참고했다. props와 emit 기능을 이용해 jQuery로 제작한 컴포넌트를 깔끔하게 Wrapping 하는 방법에 대해 자세히 기술되어 있으며, 이를 활용하면 레거시 UI 플러그인을 마치 네이티브 Vue 플러그인처럼 사용할 수 있다.글강원우 과장 | R&D 개발2팀[email protected]브랜디, 오직 예쁜 옷만
조회수 1666

[Tech Blog] Compare Software Architectures: Monoliths, SOA and Microservices

요즘 Software architecture 라는 단어를 들으면 아마도 Client engineer 분들은 MVC, MVP, MVVM 이 먼저 떠오를 것이고, Server engineer 분들은 Microservice architecture 를 먼저 떠오를 것 같네요. Clean architecture 나 Event-driven architecture 등을 떠올리는 분들도 계실 것 같구요. Software architecture 를 어떻게 정의할 수 있을지에 대해서는 Software architecture: The important stuff 에 적어 봤으니 여기에선 넘어가도록 하죠. https://mherman.org/blog/developing-microservices-node-react-docker/ Microservice architecture 는 대세라고 말할 수 있습니다. Netflix, Amazon 등 굴지의 기업들이 성공적으로 적용해서 운영하고 있고, 국내 기술적으로 뛰어난 많은 기업들 역시 이미 적용했거나 시도하고 있습니다. “남들 다 하는데 이러다 도태 되는거 아냐?” 라는 생각이 들 정도로 말이죠. 그러나 이전 글에서 얘기했듯이 정답은 없으며, Microservice architecture 역시 예외는 아닙니다. 모든 선택에는 Tradeoff 가 있고, Microservices 는 다른 architecture 에 비해 어떤 장점이 있는지 살펴봐야 합니다. 이와 관련하여 정말 많은 좋은 글들이 이미 있으니, 이 글에서는 몇 가지 Software architecture 들을 가볍게 정리 및 비교해 보도록 하겠습니다. Monolithic Architecture Monolithic architecture 는 Microservice architecture 의 장점을 얘기할 때 반드시 언급될 정도로 대척점에 있는 architecture 입니다. Monolithic architecture 는 하나의 큰 덩어리로 구성되어 있고, 모든 기능이 하나의 프로젝트에 집중되어 있습니다. 쉽게 구성이 가능하고 초기에 기능을 빠르게 추가하기에 용이하나, 복잡도가 늘어날수록 기능 추가 속도가 느려지고 문제가 발생할 가능성이 높습니다. PoC(Proof of Concept)를 위한 가벼운 프로젝트나 아주 초기 프로젝트에 적용 가능합니다. Semi-Monolithic Architecture Monolithic architecture 보다는 작지만, 여전히 기능들이 몇 개의 프로젝트에 집중되어 있는 architecture 입니다. 예를 들어 frontend 와 backend 프로젝트를 나누었지만 각 프로젝트가 monoliths 인 경우 semi-monolithic architecture 라고 볼 수 있습니다. 다만 Semi-monoliths 의 경우 몇 군데에서 언급한 것을 볼 수 있지만, 일반적으로 사용되는 architecture 용어는 아닌 듯하고, Semi-monoliths 로 구분될 수 있는 경우 Monolithic architecture 라고 분류할 수 있을 듯합니다. 단순 frontend / backend 보다 좀 더 많은 수의 service 로 분할된 architecture 를 구성하더라도 각 service 가 monoliths 로 구분될 수 있다면 여전히 monolithic architecture 를 구성하고 있다고 할 수 있습니다. Service-Oriented Architecture 여러 조직이 다수의 application 사이에서 로직과 데이터를 공유하기 위해 제안된 architecture 입니다. Monolithic architecture 와 달리 기능을 나눠서 여러 개의 서비스로 구성하고, 서비스 사이는 API 를 통해서 통신합니다. Microservice architecture 와 Service-oriented architecture (SOA) 를 비교하기 위해 Enterprise Service Bus (ESB)가 많이 언급됩니다. ESB는 Enterprise Application Interface (EAI) 와 대조적으로 가볍고 흔한 통신을 위해 제안되었으나, 통제와 관리를 위해 점점 무거운 방향으로 진행되면서 최초의 의도와 달라졌습니다. SOA 가 무거워짐에 따라 최초의 의도였던 빠른 적용, 민첩한 개발 및 적은 통합 비용과 멀어지게 되면서 자연스럽게 도태되었습니다. 서비스 사이에 데이터베이스를 공유할 수 있느냐 아니냐로 Microservice 와 구분을 짓는 의견도 있습니다만, SOA의 정의가 넓어서 이 부분에 대해서는 이견들이 있습니다.   https://dzone.com/articles/microservices-vs-soa-2 SOA가 넓은 범위에서 정의됐기 때문에 ESB 나 DB 공유 여부로 SOA 를 규정 짓기는 어렵습니다. 정의 상으로 보면 Microservice architecture 역시 SOA 의 일종이라고도 볼 수 있습니다. Microservice 의 예시로 자주 등장하는 Netflix 와 Amazon 역시 Microservice 라는 단어가 사용되기 전에는 스스로의 시스템을 SOA 라고 지칭했습니다. Microservice Architecture: The O’Reilly Book 의 공동 저자 Matt McLarty 는 Learn from SOA: 5 lessons for the microservices era 라는 글에서 SOA 와 Microservice architecture 가 같은가 다른가는 그다지 중요한 것이 아니며, 우리가 SOA 로부터 어떤 것들을 배웠는가가 중요하다고 강조합니다. Microservice Architecture Microservice architecture는 규모가 빠르게 커져도 제품 생산 속도를 빠르게 유지하고 안정성을 가질 수 있는 architecture 입니다. 충분히 작은 서비스들이 서로 통신하면서 기능을 수행합니다. Microservice architecture 를 SOA의 잘 구현된 형태라고 보는 시각도 있지만, micro 라는 단어가 SOA 에서 정의하는 서비스보다 작은 크기의 서비스임을 명시적으로 표현하기 때문에 매우 다르다는 의견 역시 있습니다. Microservice architecture 는 각 서비스의 크기를 작고 가볍게 유지함으로써 더 깔끔하고 명확하게 서비스를 유지할 수 있습니다. 잘 구성될 경우 특정 서비스에 장애가 생겨도 다른 서비스에 영향을 적게 미치거나 유연하게 대응할 수 있기 때문에 전체 시스템 오류(e.g Single Point of Failure)를 방지할 수 있습니다. 각 서비스는 독립적으로 배포 및 확장 가능하기 때문에 기능 배포가 빠르고 많은 트래픽에 유연하게 대처할 수 있습니다. 한편 Microsoft architecture 는 구조적인 면에서 복잡도가 증가하며, 많아진 서비스 및 서비스 간 통신에 대한 유지 보수 비용이 추가됩니다. 이를 대응하기 위해서 충분히 자동화되고 잘 구성된 시스템이 필수적으로 필요합니다. Conclusion 판단과 결정은 근거를 필요로 합니다. 가끔 감을 믿고 밀어붙여야 할 때(e.g 오늘 점심은 해장국을 먹어야 한다던가)도 있다는 점은 인정합니다. 하지만 그 역시 설득력을 가지지 못하면 하나의 목표를 향해 모두가 미친듯이 달려가기는 어렵겠죠. Software architecture 를 결정하기 위해서는 추구하는 비전과 비지니스를 이해하고 그에 맞는 근거 하에 모든 팀원을 판단하고 설득해야 합니다. 버즈빌 에서는 더 빠르고 큰 성장을 위해 Architecture Task Force 팀을 구성하였습니다. ATF 팀은 버즈빌에 최적인 Software architecture 를 판단하고, 구성하고, 실행하기 위해 바쁘게 움직이고 있습니다. Buzzvil Services Characteristic:  제품이 다양하고 제품별로 제공해야 할 기능이 많다. 각 제품이 공통적으로 필요로 하는 기능이 많다. 서비스 혹은 기능별로 대응해야 하는 트래픽이 다르다. 전체 서비스 장애 발생 시 많은 후속 문제가 발생한다. 트래픽 변동이 특정 이벤트에 의해 크게 일어날 수 있다.  Buzzvil 의 제품과 비지니스는 위와 같은 성격을 가지고 있습니다. 이를 바탕으로 우리는 Microservice architecture 가 가장 적절하다고 판단하였고, 현재 microservices 의 장점을 살리면서 안정적이고 빠르게 우리가 원하는 목표에 도달할 수 있도록 다양한 방면에서 변화를 가져가고 있습니다. References  Learn from SOA: 5 lessons for the microservices era Microservices vs. SOA On monoliths, service-oriented architectures and microservices Microservices.io Microservices Resource Guide Design Microservice Architectures the Right Way Developing Microservices – Node, React, and Docker    *버즈빌에서 개발자를 채용 중입니다. (전문연구요원 포함)작가소개 Whale, Chief Architect “Keep calm and dream on.”
조회수 7343

HTTP 404 Status Code 에 대한 고찰

뭐가 문제였나필자는 현재 HMR(가정간편식) 커머스를 다루는 모 스타트업에서 백엔드 개발자로 재직 중이다. 말이 백엔드지 최근 변화되고 있는 트렌드에 맞춰 열심히 API 작성 셔틀을 하고 있다.API 개발에 주로 사용하는 HTTP 상태 코드는 주로 200 (정상), 400 (잘못된 요청), 401 (보안 토큰 에러), 403 (권한 없음), 404 (찾을 수 없음) 정도가 있었다.문제는 여기에서 발생했는데, API를 계속 개발해 나가다 보니 API 요청 시 데이터가 없을 때 200 상태 코드에 빈 배열을 돌려주어야 하는지, 404 상태 코드를 돌려주어야 되는지 상황에 따라 다를 수 있겠다는 생각이 들었다.만약 '데이터가 없을 수도 있는 상황'과 '데이터가 없으면 안 되는 상황'에서 404 Not Found 에러 코드로 같게 응답할 경우 다음과 같은 애매한 상황이 펼쳐질 수 있다.API를 사용하는 클라이언트가 404 에러에 대한 대응을 에러로 표시할지 데이터 없음으로 표시할지 상황에 따라 다르게 정의해줘야 한다. 결과적으로 클라이언트에서 API 요청에 대한 처리가 복잡해진다.// front-endimport fetch from 'node-fetch'; function fetchUserList() {  // 유저 목록을 가져오는 API를 사용한다고 가정  return fetch('https://api.exmaple.com/users')    .then((response) => {      if (response.statusCode === 404) {        // 이 404 Http 상태 코드를 에러로 처리할 것인가? 데이터 없음으로 처리할 것인가?        // 에러일 경우 : throw new Error('Not Found');        // 데이터 없음일 경우 : return [];      } else if (response.statusCode === 200) {        return response.json();      } else {        throw new Error('Unexpected Http Status Code');      }    })    .then(result => render(successPage, result))    .catch(error => render(failurePage, error));}결국, 어떤 식으로 표시해야 명확하게 표현할 수 있을까 하여 페이스북 존잘 개발자님들에게 의견을 물었다. # 굉장히 많은 분이 의견을 주셨고 나름대로 생각을 정리할 수 있었다.결론적으로는 '데이터 없음'과 '404 Not Found'를 같은 용도로 사용하면 안 된다.그렇다면 뭘 어째야 하나위에서 나온 결론을 조금 더 자세히 풀어보면 다음 내용이다.상황에 따라 데이터가 없는 것이 정상인 상황이 있고, 데이터가 없는 것이 에러인 상황이 있다. 이를 구분 해야 한다.데이터가 없는 것이 정상일 수 있는 상황// server-sideAPI.get('/orders/date/:date', async (request, response) => {  // 특정 날짜의 주문을 검색. 특정 날짜에 주문이 없을 수도 있다.  const { date } = request.params;  const orders = await Repository.Order.findByDate(date);  // 200: OK  // 204: No Contents  response.statusCode(orders.length > 0 ? 200 : 204).json(orders);});데이터가 없는 것이 에러인 상황API.get('/orders/:orderId', async (request, response) => {  // 특정 ID의 주문을 검색. 데이터가 없으면 에러다.  const { orderId } = request.params;  const order = await Repository.Order.find(orderId);  if (order.length > 0) {    response.statusCode(200).json(order);  } else {    // 404: Not Found    response.statusCode(404).json({      message: `${orderId} is Not Found`    });  };});그렇다면 요청한 API 리소스가 없는 경우에는 어떤 에러를 보여줘야 하는가? 일반적으로는 404 Not Found 가 통상적으로 사용되지만 우리는 이미 404를 다른 용도로 사용하고 있다. 다행히도 HTTP 상태 코드에는 501 Not Implemented 이라는 좋은 친구가 있다. 이 친구를 사용할 수 있다.import { Users, Orders } from './Routes'; app.route('/users', Users);app.route('/orders' Orders);app.all('*', (request, response) => {  // 501: Not Implemented (구현되지 않음)  response.statusCode(501).json({    message: 'This Method is Not Implemented',  });})대충 이 정도면 클라이언트는 Http 상태 코드를 보고 다음 로직을 처리할 수 있을 것이다.물론 일반적으로 사용되는 상태 코드들이지만 실제 개발 진행 시에는 클라이언트를 개발하는 개발자와 미리 어떤 상황에서 어떤 상태 코드를 보낼 것인지 정해야 할 것이다.마무리API 개발 시 사용할 법 직한 응답 코드를 정리해보았다.200: OK (정상, 데이터 있음)204: No Contents (정상, 데이터 없음)301: Moved Permanently (리다이렉션)400: Bad Request (실패, 클라이언트에서 넘어온 파라미터가 이상함)401: Unauthorized (실패, 클라이언트에서 넘어온 보안 토큰이 이상함)403: Forbidden (실패, 사용자의 권한으로 리소스를 사용할 수 없음)404: Not Found (실패, 데이터가 있어야 하나 없음)410: Gone (실패, 데이터가 있었으나 삭제됨. 이건 굳이...?)500: Internal Server Error (실패, 서버 로직 문제)501: Not Implemented (실패, 없는 리소스 요청)기타 304나 502, 503 등의 상태 코드의 경우 API Application을 작성하는 개발자의 역할보다는 Server 쪽의 역할에 가깝다고 생각하여 작성하지 않음.뭔가 어렵다고 느껴진다면 다음 짤을 참고해서 쉽게 이해할 수 있다. #플레이팅 #개발 #개발자 #인사이트 #경험공유 #조언 #꿀팁 #HTTP #버그 #버그수정 #문제해결
조회수 2089

음성 기반 인터페이스의 등장

필자가 재직 중인 일정 데이터 스타트업 히든트랙(린더)은 현재 SKT NUGU, Google Assistant에서 '아이돌 캘린더'라는 이름의 일정 검색/구독 서비스를 운영 중이며, 삼성 빅스비와 협업을 통해 내년 상반기 전시/공연 일정 검색/구독 서비스 상용화를 앞두고 있다.https://blog.naver.com/nuguai/221387861674세계적으로도 아직 음성 관련 서비스 사례가 많지 않은 상황에서 VUI 기반 서비스 개발에 도움이 될만한 자료를 국내에서 찾기는 더더욱 쉽지 않았고, 향후 음성 기반 서비스를 준비하는 다른 이들이 우리가 겪었던 시행착오를 줄일 수 있기를 바라는 마음으로 간단하게 5부작 형태의 글로 우리가 고민해온 과정을 준비해보았다.음성 서비스 시장의 확대해외 리서치 업체 닐슨에 따르면 2018년 2분기 기준 미국 가구 중 4분의 1에 해당하는 24%가 최소 1대 이상의 AI 스피커를 소유하고 있으며 미국 성인의 20%가 하루 1회 이상 음성 검색 서비스를 활용하고 있다. 국내 리서치 전문 기관인 컨슈머 인사이트에 따르면 국내 AI 스피커 사용 경험률은 11%에 달하며 올해 안으로 세계 5위 수준의 스피커 시장 점유율(3%)을 확보할 것으로 예상된다.아마존 에코는 시각 장애인들이 콘텐츠에 접근하는 속도를 최대 10배까지 빠르게 만들어주었으며 SKT 내비게이션 서비스 T-Map은 NUGU의 음성 인터페이스를 통해 터치 인터랙션을 26%까지 감소시켜 사고 위험을 줄였다.음성 서비스 시장이 확대되고 있다는 것과, 그 변화가 사람들의 삶에 많은 영향을 끼치고 있다는 것은 누구도 부정할 수 없는 자명한 사실이다.하지만 여전히 아쉬운 일상 속 음성 서비스 만족도그렇다면 과연 우리의 일상 속 음성 서비스 경험의 만족도는 어떨까?지난 4월 진행된 컨슈머인사이트의 조사에 따르면 국내 주요 음성 서비스에 대한 사용자 만족률은 49%로, 절반에 채 못 미치고 있는 상황이다."국내 음성 서비스 만족도 - 49%"주요 불만족 이유로는 ‘음성 명령이 잘되지 않는다’(50%), ‘자연스러운 대화가 곤란하다’(41%), ‘소음을 음성 명령으로 오인한다’(36%) 등이 꼽혔으며, 아직도 대다수의 사용자들에게 AI 스피커는 기업들의 서툰 시도로 인식되고 있다.국내 음성 기반 서비스 만족도는 타 스피커 상용화 국가들과 대비해서도 현저히 낮은 편인데, 유독 국내의 사용자들이 만족스러운 음성 서비스 경험을 누리지 못하고 있는 이유가 대체 무엇인지, 이번 글을 통해 잠시 논해보고자 한다.1. 과열된 AI 마케팅국내 'AI 스피커' 시장은 타 국가 대비 매우 치열한 점유율 경쟁이 벌어지고 있는 곳이다. 미국의 경우만 하더라도 구글 어시스턴트, 아마존 알렉사, 애플 시리의 삼파전이 벌어지고 있는 상황에서 국내는 KT 기가지니, SKT NUGU, 네이버 클로바, 카카오 i, 삼성 빅스비 등 5개가 넘는 다양한 플레이어들이 이 작은 시장을 차지하기 위해 혈투를 벌이고 있다.AI, 즉 인공지능은 사전적으로 '인간의 지능으로 할 수 있는 사고, 학습, 자기 개발 등을 컴퓨터가 할 수 있도록 하는 방법'을 뜻하는데, 현존하는 대다수의 속칭 'AI' 서비스들이 해당 수준에 다다르기에는 아직 많은 시간이 필요하다는것은 누구도 부정할 수는 없을듯 하다. 경쟁이 과열되다 보면 제품을 판매하기 위해 다소 공격적인 선택을 하는 경우가 있고, 현재 국내에서 이루어지고 있는 AI라는 용어의 지나친 남발이 바로 그 대표적인 예시라고 할 수 있다.멀리 갈 것 없이 각 나라에서 스피커를 부르는 호칭을 보면 잘 알 수 있는데, 우리가 흔히 'AI 스피커'라 부르는 구글 홈, 아마존 에코 등 대다수의 스피커는 미국 내에서 '스마트 스피커'라는 단어로 통용된다.(구글에 AI Speaker를 검색해보면 Smart Speaker로 자동 대체되는 것을 확인할 수 있다)구글 내 AI 스피커 검색 결과(첫 두 검색은 광고)즉, 아직은 '스마트'하다고 부를 수밖에 없는 수준의 기능에 대한 과장 된 'AI 마케팅'으로 인해 국내 사용자들은 시장 생성 초기부터 고도화된 인공지능을 기대하게 되고, 이는 결국 자연스레 낮은 사용자 만족도로 이어질 수밖에 없는 것이다.향후 AI가 음성 기반 서비스의 핵심 기술이 될것은 분명하지만 당장의 지나친 기대감은 되려 국내 음성 기반 서비스의 *캐즘 기간을 장기화시킬 수 있을것으로 우려된다.*캐즘: 첨단기술 제품이 선보이는 초기 시장에서 주류시장으로 넘어가는 과도기에 일시적으로 수요가 정체되거나 후퇴하는 단절 현상2. 조금 더 시간이 필요한 기술력앞서 언급한 컨슈머 인사이트의 조사에 따르면 사용자의 불만족 이유 중 TOP 3 모두가 '낮은 인식률' 바탕으로 하고 있는 것을 재차 확인할 수 있다.1. 음성 명령이 잘되지 않는다(50%)2. 자연스러운 대화가 곤란하다(41%)3. 소음을 음성 명령으로 오인한다(36%)  컨슈머인사트 AI 스피커 만족도 통계음성 서비스 경험은 사용자의 명확한 의사가 전달되지 않는다면 애초에 시작될 수 없다. 자연스러운 대화를 진행하기 위해서는 결국 사람의 언어, 즉 자연어를 분석하여 의도를 파악할 수 있어야 하며 이를 실현하기 위해서는 아래에 소개 된 ASR(음성 인식)과 NLU(자연어 처리)가 높은 수준으로 구현되어야 한다.T map X NUGU 디자인 사례로 알아보는 음성인터페이스 디자인 1강 - https://youtu.be/Dz-rxGV-dOAASR과 NLU 성능이 뒷받침되지 않는 음성 서비스는 아무리 고도화 된 서비스 로직이 준비된들 '대화'가 진행될 수 없으며 부족한 성능은 결국 국내 대다수 스피커들이 "죄송합니다. 무슨 말인지 이해 못했어요"를 출력하며 사용자 불만족도를 상승시키는 주요 요인으로 볼 수 있다.인식 정확도를 상승시키기 위해서는 결과적으로 더 많은 양의 학습 데이터가 필요하며 대다수의 업체가 아직 관련 기술력이 많이 부족한 상황에서도 공격적으로 스피커를 출시하는 이유 또한 결국 초기 점유율 높여 이 학습 데이터를 지속적으로 쌓기 위해서다.국내에서는 아직 높은 수준으로 두 단계를 구축한 메이저 업체가 없는 상황에서, 국내 기업들은 경쟁력을 확보하기 위해 관련 기술력을 가진 국내외 다양한 기업에 지속적으로 투자를 늘려나가고 있는 상황이다.http://www.zdnet.co.kr/view/?no=201702231628363. 더 많은 고민이 필요한 음성 사용자 경험(VUX) 디자인이번 협업 프로젝트를 진행하며 VUX를 공부하는 과정에서 우리의 사례를 포함한 몇 가지 재미있는 질문들을 발견할 수 있었다.질문1. 음악 앱이 재생되는 상황에서 사용자가 "앞으로 10초"라고 말했다면, 빨리 감기를 하는 게 맞을까 되감기를 하는 게 맞을까? - 네이버 클로바 사례질문2. 자정이 살짝 넘은 새벽 1시, 사용자가 "내일 일정 알려줘"라고 말했다면, 향후 23시간 동안의 일정을 알려주는 게 맞을까 23시간이 지난 그 다음날 일정을 알려주는 게 맞을까? - 히든트랙 린더(빅스비, SKT 파트너 스타트업) 사례질문3. '오늘'이라는 이름의 기업이 존재하는 상황에서 "오늘 기업 정보 알려줘"라고 말했다면, 오늘의 주요 기업 정보를 제공하는게 맞을까 주식회사 '오늘'의 정보를 제공하는게 좋을까? - 딥서치(빅스비 파트너 스타트업) 사례앞서 언급했던 1,2번의 사용자 만족도 문제가 이미 어쩔 수 없는 국내 시장의 지나친 경쟁과 더 시간이 필요한 기술력에 대한 아쉬움을 토로하는 내용이었다면, 3번의 VUI상의 새로운 경험에 대한 고민들이 이번 글을 쓰게 된 계기이자 목적이라고 볼 수 있다. 아직도 각 질문에 대한 뚜렷한 정답이 없는 상황에서 위와 같은 고민들을 함께 논의하며 최대한으로 정답에 가까운 선택을 내릴 수 있었으면 한다.클로바의 "앞으로 10초", 린더의 "내일 일정 알려줘", 딥서치의 "오늘 기업 정보 알려줘"에 대한 해답과 같이 '최선'이라고 부를 수 있는 가이드가 아직 존재하지 않는 현 VUX 시장은 더욱더 깊은 고민과 통찰이 필요한 시점이다. 단순히 해외 사례를 그대로 인용하여 국내 서비스에 적용하는 것이 아닌 정서와 문화, 그리고 각 콘텐츠에 대한 높은 이해도를 바탕으로 적절히 녹여낼 수 있어야 한다.올해 초 처음으로 챗봇을 디자인해보며 겪었던 애로사항들을 적은 부족한 글이 새로운 디자인을 시도하는 이들에게 조금이나마 도움이 되었다는 피드백을 받을 수 있었고,http://magazine.ditoday.com/ui-ux/일정-구독-서비스-린더의-탄생/이에 용기를 얻어 이번에는 다소 길지만 조금 더 많은 내용을 담고 있는 글을 준비하게 되었다.SKT NUGU, 삼성 빅스비와의 협업 과정에서 '음성 기반 인터페이스(VUI)'는 챗봇과는 확연히 다른 또 다른 형태의 디자인이라는 것을 알 수 있었고, 단순히 대화형 인터페이스(CI: Chatting Interface)를 음성의 형태로 재가공하는 것이 아닌, 서비스 기반부터 리디자인이 필요하다는것을 깨달았다.이미 구글, 아마존, 애플 등 메이저 업체들이 수년간의 경험과 데이터를 기반으로 다양한 VUX 가이드라인을 제시하고 있으며, 최근에는 SKT NUGU, 네이버 클로바 등 국내 업체들도 조금씩 VUX 서비스 제작에 대한 구체적인 로드맵을 제공하고 있는 상황이다.https://developers.nugu.co.kr/docs/voice-service-design-guideline/앞으로 약 다섯 달간 연재 진행 예정인 향후 4편의 내용들은 위 가이드 문서들에서 언급하는 다양한 해외와 국내 사례들을 바탕으로 주제를 선정하였으며, 각 편의 내용들은 VUI 서비스 제작 경험이 있는 다양한 국내 회사들의 고민 과정을 조금씩 담고 있다.1편: 음성 기반 인터페이스의 등장2편: 음성 기반 인터페이스와 TPO3편: 음성 기반 인터페이스와 페르소나4편: 음성 기반 인터페이스 vs GUI5편: 국내 음성 기반 인터페이스 현황음성 인터페이스는 정말 유용할까?음성 인터페이스는 먼 미래의 것이 아니다. 우리는 이미 수 년 전부터 다양한 종류의 음성 인터페이스를 접해왔으며, 그중 대표적인 예시가 바로 누구나 한 번쯤은 경험해보았을 ARS, 자동응답 시스템이다.각종 정보를 음성으로 저장 한 후, 사용자가 전화를 이용하여 시스템에 접속하면 음성으로 필요한 정보를 검색할 수 있도록 사용법을 알려주고, 필요한 정보를 찾으면 이를 음성으로 들려 주는 바로 그 시스템이 현 음성 인터페이스 경험의 모태라 할 수 있다.예약을 진행하는 과정에서 어떤 제품군을 수리 맡기고 싶은지, 냉장고인지, 컴퓨터인지, 노트북인지, 핸드폰인지 '말로 검색하고 말로 예약 확인을 받는' 바로 그 과정이 바로 수년 전부터 존재해온 음성 인터페이스이다. 우리가 말로, 음성으로 수리하고 싶은 제품을 말하고 응답을 받아온 이유는 간단하다.더 편했기 때문이다.다만 그렇다고 해서 음성 인터페이스가 모든 분야를 혁신시킬 변화의 축이 되기는 힘들다.음성 입출력의 한계는 매우 명확하며, 시각적 입출력이 반드시 필요한 산업과 분야(음식, 지도 등)는 꾸준히 기존과 같은 시각 기반의 인터페이스를 필요로 할 것이다.모든 분야에 적용될 수는 없는 음성 인터페이스이지만 한가지 확실한 것은 이제 시작이라는 것이다.다소 장황하고 부족한 이 글이 조금이나마 앞으로의 험난한 여정을 도울 기초적인 가이드가 될 수 있었으면 하는 마음으로 연재를 시작해본다.저도 아직 많이 낯선 분야인만큼 의아하시거나 틀린부분이 있다면 댓글로 많은 지적 및 피드백 부탁드립니다. 감사합니다 :)#히든트랙 #음성기반기술 #스타트업인사이트 #UX디자인 #음성기반디자인
조회수 1717

StyleShare Engineering Blog?!

변정훈님 강의 모습생각해보기한 번도 생각해본 적이 없다! 왜 글을 작성해야 하는가?! 왜냐하면, 우리는 글로 먹고사는 사람도 아니고, 수려한 글솜씨도 없기 때문에?! 하지만, 이미 우리 사회는 PR의 시대를 뛰어넘어 미디어의 홍수에서 살아가고 있고, 매우 쉽게 무의식적으로 많은 글을 읽고 있다.하지만, 우리가 글을 작성하기 위해서 얼마나 많은 준비가 되어 있을까? 이 글을 쓰고 있는 필자 역시 글을 써본 경험이 거의 없다. 특히 회사의 이름을 걸고 글을 쓴다는 것은 매우 부담스러운 일이다.그래서 우리는 변정훈[Outsider’s Dev Story]님을 초대하여, 그분이 생각하는 블로그 일상과 엔지니어링 블로그에 대한 생각 공유의 시간을 가져보았다.엔지니어링 블로그회사 블로그 운영을 해보았는가?아쉽게도 변정훈 님도 회사 블로그를 운영해본 적은 없다고 하신다. 그 원인을 다음과 같은 이유로 해석을 하였다.주제가 많지 않다.개발보다 우선순위가 떨어진다.누구나 처음부터 글을 잘 적을 수 있는 건 아니다.그렇다. 이 글을 쓰는 중에도 위와 공감할 수 있는 부분이 적지 않다.우선 글을 많이 써보지 못한 필자로서도 어떤 글을 적어야 할지 난감하게 느껴지고, 업무 중에서도 중요도가 떨어지는 것은 분명하다. 마지막으로 주제 선정부터 매우 어렵게 느껴진다.그런 이유로 글쓰기를 즐겨 하시는 변정훈 님 조차 회사 블로그 운영을 잘 이끌어 본 적이 없다고 하신다.변정훈 님의 블로그는 2007년 부터 총 1,300여개 글이 게시되어 있다고 한다.왜 우리는 블로그를 운영하려 하는가?여러 가지가 있겠지만, 필자가 가장 공감한 부분은 이 부분이었다.팀 내 지식/경험 공유잠재적 입사자에게 기술 스택 및 문화 공유팀 전체의 실력 향상그동안 개발일을 해오면서, 몇 년 동안 풀리지 않는 큰 숙제 중에 하나가 좋은 개발자를 찾는 것이었다. 항상 사람을 찾는 것이 어렵다. 좋은 사람의 기준이 높아서인지 더 좋은 기업이 많아서인지 알 수는 없지만, 일하면서 느낀 가장 어려운 문제 중에 하나이다. 결국, 내부의 인력을 더 좋은 사람으로 만들고, 외부의 좋은 사람과도 교류의 장을 만들 수 있다는 희망을 품을 수 있게 되는 것이다.글 작성의 문턱을 어떻게 낮춰야 할까?좋은 점은 쉽게 공감이 되지만, 언제나 가장 어려운 것은 실천이 아닐까 싶다. 특히 회사에서 업무로 이런 일이 발생된다면, 많은 사람들이 엄청난 부담감을 가질 것이고, 결국 회사 엔지니어링 블로그는 대문만 남은 유명무실한 블로그가 될 가능성이 높다.그래서 변정훈 님은 이렇게 제안하셨다.월 1개 보다 적어도 된다.주제를 계속해서 제안하고 만들어 내야 한다.돌아가면서 작성한다.챙겨주는 사람이 필요하다.글도 리뷰하는게 좋다.부담감은 의도적으로 줄여야 …특히 변정훈님은 부담감을 줄이는 방법에 대해서, 팀 공유를 해주셨는데 잠시 소개하면 다음과 같다.나보다 모르는 사람 — 나 — 나보다 잘하는 사람언제나 어떤 기술에 대하여, 나보다 잘 모르는 사람과 나보다 잘 하는 사람이 있다는 것을 인지하고, 나보다 못하는 사람을 위해서 글을 작성한다는 것이다. 그러다 나보다 잘하는 누군가가 어쩌다 피드백을 준다면 오히려 매우 감사하게 새로운 지식을 터득하게 된다는 것이다. 역시 모든일에는 긍정적인 마인드가 중요하다. 세상 어딘가에는 나의 작은 지식이라도 필요로하는 사람들이 분명히 있을테니, 작은 용기를 가지고 세상에 누군가를 위해서 작성한다면, 세상은 분명 아름다워질 것이다.좋은 글, 좋은 주제란 무엇일까?사실 가장 어려운 이야기일지도 모른다. 변정훈님은 이런 내용을 좋다고 표현하셨는데, 잠시 정리하면 다음과 같다.개발팀의 문화어떻게 일하는가?프로젝트 수행 회고실패기개인적으로 실패기가 가장 적기 어려운 글이라고 생각한다. 하지만, 누군가에게는 가장 소중한 경험이 될지도 모르니, 간접경험의 공유는 특히 소중한 것일 수 있다.만약 회사 블로그의 글이 회사 내/외부의 사람들에게 지식 공유와 전달이 목적이라면, 그리고 좋은 문화를 계속 가지고 유지하기 위해서라면, 실패기가 어쩌면 가장 소중한 경험의 공유가 아닐까 생각해본다.질문과 답변을 통한 소통의 시간마지막으로 이 날 소개 받은 좋은 글의 흐름이란 다음과 같았다.하고자 했던 일 (Context)경험한 문제 사황 정리(격리된 상황)시도해 본 방법(내가 아는 지식)왜 동작이 안되는가? 왜 동작하는가?(가설)문제 상황 재현예제 코드관련 링크개념 설명지금까지 적은 이 글을 위의 원칙대로 다시 한번 살펴본다. 부족한 부분이 있는지, 수정할 부분이 있는지…이제 부터 회사 블로그를 더욱더 적극적으로 운영해보자!!!#스타일쉐어 #개발팀 #조직문화 #블로그 #기업문화 #사내복지
조회수 3798

iOS에서 간결한 API 클라이언트 구현하기 (like Retrofit+GSON)

이 글은 안드로이드 개발에서 웹 서버 API 클라이언트를 간결하게 구현할 수 있도록 도와주는 강력한 오픈소스 라이브러리인 Retrofit과 GSON의 조합을 iOS 개발에서도 따라해보고 싶은 분들을 위해 작성되었습니다. Retrofit+GSON를 실제로 사용하는 좋은 예제는 다른 블로그 글에서도 찾아볼 수 있습니다.배경리디북스 서비스가 발전하면서 점점 복잡해지고, 자연히 앱의 기능도 다양해지기 시작했습니다. 기능이 다양해지면서 웹 서버와의 연동을 위한 API 종류도 늘어났고 앱 내에서 API 호출이 필요한 부분도 다양해지면서 관련된 중복 코드가 이곳 저곳에 산재하게 되었고 전체적인 코드 퀄리티 향상을 위해 이를 최소화하고 모듈화 할 필요성이 생겼습니다.안드로이드에서는 Pure Java로 작성되어 어노테이션을 통한 간결한 코드를 사용할 수 있게 해주는 Retrofit을 GSON과 연동하여 JSON 응답을 손쉽게 객체에 맵핑 하여 사용함으로써 이러한 문제를 성공적으로 해결할 수 있었습니다. 이후 iOS 개발을 진행하면서 비슷한 역할을 할 수 있는 도구가 있을까 찾아봤지만 마땅하지 않아 결국 사용 가능한 도구들을 이용해 비슷하게 따라해보기로 했습니다.목표Retrofit+GSON 조합을 최대한 따라해서 iOS 앱의 코드 퀄리티를 높이기 위한 작업을 진행하기는 하지만 모방하는 것 자체가 목적이 될 수는 없으므로, 구체적인 목적은 다음과 같은 것들로 상정해보았습니다.API 통신 부분을 모듈화하여 관련 중복 코드를 최소화하기NSArray, NSDictionary를 직접 사용하여 제어 했던 JSON 처리 부분을 추상화하여 모델 클래스를 정의, JSON 응답을 자동으로 객체에 맵핑 해서 사용할 수 있도록 하기필요한 것Retrofit과 GSON의 동작에 대한 이해AFNetworking비동기 HTTP 요청 처리에 용이하므로 기존에도 이미 API 호출을 위해서도 사용하고 있었습니다.이 글의 내용은 버전 2.6.3 기준입니다.Swift 언어와 그에 대한 이해사실 Objective-C를 사용해도 무방하지만, 작업 당시 Swift가 발표된 지 얼마 되지 않은 시점 이었기 때문에 시험 삼아 선택 되었으며 실제로 Swift가 Objective-C 대비 가진 장점들이 적지 않게 활용되었습니다.이 글의 내용은 버전 2.0 기준입니다.구조와 동작클래스 이름 앞에 붙어 있는 RB는 리디북스에서 사용하는 클래스 접두어 입니다.RBApiServiceAPI 통신을 담당하는 부분의 핵심은 중앙의 RBApiService 클래스를 포함한 상속 구조라고 할 수 있으며 상술하면 다음과 같습니다.AFNetworking에서, HTTP 요청 작업의 큐잉부터 시작과 종료까지 라이프 사이클 전반을 관리하는 역할을 하는 AFHTTPRequestOperationManager를 상속받는 RBApiService 클래스를 정의각 API들은 역할군에 따라 RBBookService(책 정보 관련 API), RBAccountService(사용자 계정/인증 관련 API) 등과 같은 RBApiService의 하위 클래스들의 메소드로 정의됨이 하위 클래스들이 AFHTTPRequestOperationManager의 역할을 그대로 이어받아 자신을 통해 이루어지는 API HTTP 요청 작업들을 관리이 설명에 따르면 웹 서버의 /api/foo/bar API를 요청하는 메소드는 RBFooService 클래스에 다음과 같이 정의될 것입니다.func bar(param1: String, param2: String, success: RBApiSuccessCallback, failure: RBApiFailureCallback) -> AFHTTPRequestOperation! { let paramters = ["param1": param1, "param2": param2] responseSerializer = RBJSONResponseSerializer(responseClass: RBFooBarResponse.class) return GET("/api/foo/bar", parameters: parameters, success: success, failure: failure) }RBApiSuccessCallback과 RBApiFailureCallback은 요청과 응답이 완료되고 각각 성공, 실패일 때 호출되는 람다 함수(Objective-C의 block에 대응되는 개념) 타입으로 다음과 같이 typealias를 통해 선언되어 있습니다. typealias RBApiSuccessCallback = ((operation: AFHTTPRequestOperation, responseObject: AnyObject) -> Void)? typealias RBApiFailureCallback = ((operation: AFHTTPRequestOperation?, error: NSError) -> Void)?GET 메소드는 AFHTTPRequestOperationManager의 메소드로 새로운 HTTP GET 요청 작업을 생성하고 큐에 넣은 뒤 그 인스턴스를 반환합니다. bar 메소드는 이렇게 반환된 인스턴스를 다시 그대로 반환하는데 API 호출을 의도한 측에서는 이 인스턴스를 통해 필요한 경우 요청 처리를 취소할 수 있습니다. API에 따라 GET 이외의 다른 방식의 요청이 필요하다면 POST, PUT, DELETE등의 메소드들 또한 사용할 수 있습니다.RBFooBarResponse 클래스는 이 API 호출의 JSON 응답을 맵핑하기 위한 모델 클래스입니다. 이 API 요청의 응답은 RBJSONResponseSerializer 클래스를 통해 사전에 정의된 규칙에 따라 적절히 RBFooBarResponse 인스턴스로 변환되고 이 모든 과정이 성공적으로 진행되면 RBApiSuccessCallback의 responseObject 인자로 전달됩니다.모델 클래스와 RBJSONResponseSerializer앞서 이야기했듯이 RBJSONResponseSerializer는 JSON 형태로 온 응답을 특정 모델 클래스의 인스턴스로 맵핑시키는 작업을 수행합니다(Retrofit+GSON 조합에서 GsonConverter의 역할에 대응한다고 볼 수 있습니다).iOS 개발에서 전통적으로 JSON을 다루는 방식은 Cocoa 프레임워크에서 기본적으로 제공하는 NSJSONSerialization 클래스를 이용하여 JSON Array->NSArray로, 그 외의 JSON Object는 NSDictionary로 변환하여 사용하는 방식입니다. 이러한 방식을 사용할 경우 별다른 가공이 필요 없다는 장점이 있는 대신 다음과 같은 문제들에 직면할 수 있습니다.데이터가 명시적으로 정의된 프로퍼티로 접근되지 않고 문자열 키 기반의 키-밸류 형태로만 접근되므로 데이터의 타입이 명시적이지 않아 타입 검사와 캐스팅이 난무하게 되어 가독성을 해침오타와 같은 개발자의 단순 실수로 인한 버그를 유발할 가능성도 커짐특히 오타로 인한 버그의 경우 명시적인 모델 클래스의 프로퍼티로 맵핑 해서 사용한다면 IDE가 에러를 검출해주거나 최소한 빌드 타임 에러가 발생할테니 미연에 방지할 수 있습니다. 이러한 문제는 사소한 실수로 인해 찾기 힘든 버그가 발생한다는 점과 코드 리뷰를 통해서도 발견하기가 힘들다는 점에서 지속적으로 개발자를 괴롭힐 수 있습니다.RBJSONResponseSerializer를 통한 인스턴스로의 변환은 이런 문제 의식에서 출발했고 Retrofit에 GSON을 연계하여 사용하기 위한 GsonConverter가 해결을 위한 힌트를 제공한 셈입니다.// AFJsonResponseSerializer는 NSJSONSerializer를 이용해 NSArray/NSDictionary로 변환하는 기본적인 작업을 해줌 class RBJSONResponseSerializer: AFJSONResponseSerializer { var responseClass: NSObject.Type! override init() { super.init() } required init(responseClass: NSObject.Type!) { self.responseClass = responseClass super.init() } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func responseObjectForResponse(response: NSURLResponse?, data: NSData?, error: NSErrorPointer) -> AnyObject? { // 파서를 직접 구현하는 건 노력이 많이 필요하므로 우선 AFJSONResponseSerializer를 이용해 NSArray/NSDictionary로 변환 let responseObject: AnyObject! = super.responseObjectForResponse(response, data: data, error: error) if let dictionary = responseObject as? NSDictionary where responseClass != nil { // 변환 결과가 NSDictionary이면서 responseClass가 정의되어 있다면 변환 작업 시작 return responseClass.fromDictionary(dictionary, keyTranslator: PropertyKeyTranslator) } // NSArray라면 JSON이 top level array로 이루어졌다는 뜻이므로 변환 불가로 보고 그대로 반환 // 혹은 responseClass가 정의되어 있지 않아도 그대로 반환 return responseObject } }Key translatorfromDictionary 메소드 호출 시 함께 인자로 전달되는 keyTraslator는 JSON에서 사용되는 키로부터 모델 클래스의 프로퍼티 이름으로의 변환을 나타내는 람다 함수로 개발자가 원하는 규칙에 따라 정의하면 됩니다. 위의 코드에서 사용 중인 PropertyKeyTranslator는 리디북스 API에서 사용 중인 규칙 및 Swift의 네이밍 컨벤션에 따라 다음과 같이 언더스코어(_) 케이스로 된 이름을 카멜 케이스로 바꾸는 형태로 정의되었으며 이는 GSON의 FieldNamingPolicy 중 LOWERCASE_WITH_UNDERSCORES와 유사합니다.let PropertyKeyTranslator = { (keyName: String) -> String in let words = keyName.characters.split { $0 == "_" }.map { String($0) } var translation: String = words[0] for i in 1..NSObject.fromDictionary 메소드fromDictionary 메소드는 NSDictionary로 표현된 데이터를 실제 모델 클래스의 인스턴스로 변환하는 작업을 수행하며 NSObject의 extension(Objective-C의 category 개념과 유사합니다)으로 정의하여 원하는 모델 클래스가 어떤 것이든지 간에 공통적인 방법을 사용할 수 있게끔 했습니다.extension NSObject { class func fromDictionary(dictionary: NSDictionary) -> Self { // keyTranslator가 주어지지 않으면 디폴트 translator 사용 return fromDictionary(dictionary, keyTranslator: { $0 }) } class func fromDictionary(dictionary: NSDictionary, keyTranslator: (String) -> String) -> Self { let object = self.init() (object as NSObject).loadDictionary(dictionary, keyTranslator: keyTranslator) return object } func loadDictionary(dictionary: NSDictionary, keyTranslator: (String) -> String) { // 주어진 dictionary에 포함된 모든 키-밸류 쌍에 대해 작업 수행 for (key, value) in (dictionary as? [String: AnyObject]) ?? [:] { // keyTranslator를 이용해 키를 프로퍼티 이름으로 변환 let keyName = keyTranslator(key) // 프로퍼티 이름을 사용할 수 있는지 검사 if respondsToSelector(NSSelectorFromString(keyName)) { if let dictionary = value as? NSDictionary { // 밸류가 NSDictionary면 해당 프로퍼티의 타입에 대해 fromDictionary 메소드 호출 if let ecls = object_getElementTypeOfProperty(self, propertyName: keyName) as? NSObject.Type { setValue(ecls.fromDictionary(dictionary, keyTranslator: keyTranslator), forKey: keyName) } else { NSLog("NSObject.loadDictionary error: not found element type of property. (key: \(keyName), value: \(dictionary))") } continue } else if let array = value as? NSArray { var newArray = [NSObject]() // 밸류가 배열이면 각 요소별로 작업 수행 for object in array { if let dictionary = object as? NSDictionary { // 배열 요소가 NSDictionary면 프로퍼티의 배열 요소 타입에 대해 fromDictionary 메소드 호출한 뒤 배열에 추가 if let ecls = object_getElementTypeOfProperty(self, propertyName: keyName) as? NSObject.Type { newArray.append(ecls.fromDictionary(dictionary, keyTranslator: keyTranslator)) } else { NSLog("NSObject.loadDictionary error: not found element type of property. (key: \(keyName), value: \(dictionary))") } } else if let object = object as? NSObject { // NSDictionary가 아니면 그대로 배열에 추가 newArray.append(object) } else { NSLog("NSObject.loadDictionary error: can't cast element. (key: \(keyName), value: \(object))") } } setValue(newArray, forKey: keyName) continue } else if value is NSNull { continue } // NSDictionary, NSArray가 아니면서 null도 아니면 그대로 사용 setValue(value, forKey: keyName) } } } }주어진 dictionary에 존재하는 모든 키-밸류 쌍에 대해 밸류가 가진 타입과 이에 대응하는 프로퍼티의 타입에 따라 적절히 프로퍼티에 대응될 객체를 구한 다음 Cocoa 프레임워크에서 제공하는 KVC를 이용해 채워넣습니다.프로퍼티 타입 정보 가져오기모델 클래스가 반드시 Int, String, Float과 같은 기본적인 타입들로만 이루어져 있을 필요는 없고 다른 모델 클래스의 인스턴스나 배열을 포함하고 있어도 타입 정보를 런타임에 가져와 재귀적으로 데이터를 채워나가는 것이 가능합니다. 프로퍼티의 타입을 알아내는 과정은 다음과 같이 Swift에서 제공하는 Mirror 구조체를 통해 이루어지는데 이는 마치 (이름에서도 느낄 수 있듯이) Java의 리플렉션을 떠올리게 합니다.// 타입 이름에서 특정 접두어("Optional", "Array", "Dictionary" 등)를 찾아 제거 func encodeType_getUnwrappingType(encodeType: String, keyword: String) -> String { if encodeType.hasPrefix(keyword) { let removeRange = Range(start: encodeType.startIndex.advancedBy(keyword.length + 1), end: encodeType.endIndex.advancedBy(-1)) return encodeType.substringWithRange(removeRange) } else { return encodeType } } // object의 타입에서 propertyName의 이름을 갖는 프로퍼티의 타입 이름을 반환 func object_getEncodeType(object: AnyObject, propertyName name: String) -> String? { let mirror = Mirror(reflecting: object) let mirrorChildrenCollection = AnyRandomAccessCollection(mirror.children)! // object의 타입 구조 children 중에서 propertyName을 찾음 for (label, value) in mirrorChildrenCollection { if label == name { // Optional 타입인 경우 "Optional" 접두어를 제외 return encodeType_getUnwrappingType("\(value.dynamicType)", keyword: "Optional") } } return nil } // object의 타입에서 propertyName의 이름을 갖는 프로퍼티의 타입 인스턴스를 반환 func object_getElementTypeOfProperty(object: AnyObject, propertyName name: String) -> AnyClass? { // 타입의 이름을 가져옴 if var encodeType = object_getEncodeType(object, propertyName: name) { let array = "Array" // "Array" 접두어로 시작할 경우 (배열인 경우) if encodeType.hasPrefix(array) { // "Array" 에서 "Array" 제외하고 T를 반환 return NSClassFromString(encodeType_getUnwrappingType(encodeType, keyword: array)) } let dictionary = "Dictionary" if encodeType.hasPrefix(dictionary) { // "Dictionary" 에서 "Dictionary", "K"를 제외하고 V를 반환 encodeType = encodeType_getUnwrappingType(encodeType, keyword: dictionary) encodeType = encodeType.substringWithRange(Range(start: encodeType.rangeOfString(", ")!.endIndex.advancedBy(1), end: encodeType.endIndex)) return NSClassFromString(encodeType) } // 커스텀 클래스 접두어를 가지고 있다면 그 타입 그대로 반환 if encodeType.hasPrefix(RidibooksClassPrefix) { return NSClassFromString(encodeType) } } return nil }RidibooksClassPrefix는 커스텀 클래스들의 접두어를 나타내는 상수이며(리디북스의 경우 앞서 이야기했듯 “RB”), 이 접두어가 붙어있는 경우에만 모델 클래스로 간주해 해당 타입 인스턴스가 반환됩니다.예시앞서 정의한 PropertyKeyTranslator를 사용했을 때, 위에 예시로 사용했던 /foo/bar API 요청의 JSON 응답과 모델 클래스 및 생성되는 인스턴스 형태의 예를 들면 다음과 같을 것입니다.(Int, Bool, Float과 같은 기존 NSNumber 기반의 타입을 가지는 프로퍼티들은 아직 정확한 원인은 알 수 없으나 nil 이외의 값으로 초기화 해주지 않으면 프로퍼티가 존재하는지 확인하기 위해 사용하는 respondsToSelector 메소드가 false를 뱉게 되어 사용할 수 없으므로 클래스 선언시 적절한 초기값을 주어야 합니다.{ "success": true, "int_value": 1, "string_value": "Hello!", "float_value": null, "baz_qux": { "array_value": [1, 2, 3] } }class RBFooBarResponse : NSObject { var success = false // true var intValue = 0 // 1 var stringValue: String! // "Hello!" var floatValue: Float! = 0.0 // nil var bazQux: RBBazQux! } class RBBazQux : NSObject { var arrayValue: [Int]! // [1, 2, 3] }맺음말이런 작업들을 통해 당초 목표했던 두 가지, API 통신 관련 중복 코드를 최소화 하면서 JSON 응답을 가독성이 더 좋고 실수할 확률이 적은 모델 클래스의 인스턴스로 자동 변환 하도록 하는 것 모두 달성하는 데에 성공했습니다.다만 모든 것이 뜻대로 될 수는 없었는데 Retrofit+GSON과 비교했을 때 플랫폼 혹은 언어의 특성에 기인하는 다음과 같은 한계들 또한 존재했습니다.Retrofit에서는 Java 어노테이션을 이용해 API 메소드의 인터페이스만 정의하면 됐지만 iOS 구현에서는 GET, POST 등의 실제 요청 생성 메소드를 호출 하는 것 까지는 직접 구현해줘야 함키->프로퍼티 이름 변환 규칙에 예외 사항이 필요할 때 GSON에서는 @SerializedName 어노테이션을 통해 손쉽게 지정할 수 있지만 iOS 구현에서는 예외 허용을 위한 깔끔한 방법을 찾기가 힘듬 (다만, 예외가 필요한 경우가 특별히 많지는 않기 때문에 큰 문제는 되지 않음)향후에는 HTTP 통신을 위해 사용 중인 AFNetworking(Objective-C로 작성됨)을 온전히 Swift로만 작성된 Alamofire로 교체하는 것을 검토 중이며 기존에 비해 좀 더 간결한 코드를 사용할 수 있을 것으로 기대하고 있습니다. 다만 Alamofire의 최신 버전이 iOS 8 이상을 지원하고 있어 iOS 7을 아직 지원 중인 리디북스인 관계로 언제 적용할 수 있을지는 아직 미지수입니다.#리디북스 #개발 #개발자 #iOS #iOS개발 #API #API클라이언트 #GSON #Retrofit #중복코드 #최소화 #API통신 #웹서버 
조회수 1350

스타트업 개발팀에서 일한다는 것

 대부분의 정보통신 분야, 특히 소프트웨어를 제작하는 "개발 조직"의 구성은개발,  디자인, 기획(또는 PM), QA가 팀으로서 분리되어있고, 규모 있는 회사들의 경우, 업무에 관련된 직군 간의 갈등 상황이나 문제가 생길 시, 파트장 또는 팀장님들(이하 중간관리자)이 중재를 하고 의사를 결정하는 정해져 있는 프로세스가 되어 있습니다. 그러나, 작은 규모의 스타트업이나, 업무에 책임을 지고 있는 인원이 단 한두명일 경우, 중간관리자들의 부재 때문에 개인 간 생기는 갈등 상황을 피할 수 없습니다. 그래서 오늘은 지금까지 제가 느낀 "정확하고 빠른 업무 진행을 위해 각 직군 간 인원들이 챙겨야 할 덕목들."을 말씀드리려 합니다. 그렇다면 무엇보다도, 왜 갈등 상황이 생기게 되는 걸까요?저는 "각 직군 간 종사자의 업무를 진행하는 과정과 목표가 다름에도, 이해보단 자신의 기준에서만 업무를 바라보는 경향"이 가장 큰 원인이라고 생각합니다. 저의 글(라고 적고 깨알 홍보라 읽는다...)에서 말씀드렸듯, 개발자, 디자이너, 기획자 들은 서로 일을 하는 방식도 다르고(심지어 개인차도 있지요), 각 직군마다 지향하는 부분들이 다르기 때문에 같은 방향을 보고 가더라도 서로가 서로 간에 집중하지 못하는 부분들이 분명히 존재합니다. 그리고 그런 부분들을 줄이기 위해선 업무 중 서로가 서로를 이해할 수 있도록 한 번씩 자신을 돌아보는 것들이 중요합니다. 그래서 아직 많이 부족하지만 각 직군에 종사하는 분들 그리고 공통적으로 업무 중에 한 번씩만 더 생각해 주셨으면 하는 것들과, 이유를 적어보려 합니다.기획 (또는 프로젝트 매니지먼트)1. 스펙 산정은 다 같이큰 회사라면 잘 모르겠지만, 작은 개발팀의 장점은 "많은 인원들이 비교적 짧은 시간 안에 많은 생각을 나누고 지향점을 찾아가는 과정을 같이 할 수 있는 것."이라고 생각합니다. 분명 기획자가 생각하고 만들어 내야 하는 스펙들이 있겠지만, 독단적으로 "이건 무조건 해야 하니 들어."라는 태도는 작은 팀일수록 업무의 동기를  꺾어버리는 일이 생길 수 있으니 항상 조심해야 합니다.2. 혼자서 "뭐... 개발이든 디자인이든 되겠지." 하는 추측은 절대 금물개발자 출신, 또는 디자이너 출신 또한 마찬가지라고 생각합니다. 몇 가지 예를 들자면, 1. 지금 개발자 또는 디자이너가 부딪힌 상황에 있지 않고, 2. 해당 직군에서 새롭게 화두 되는 트렌드에 덜 민감하고, 3. 각 개발자, 또는 디자이너가 생각하고 있는  스펙이나 디자인을 알지 못하고, 4. 어떤 라이브러리, 어떤 테마를 기반으로 작업할 지에 대한 기본적인 이해가 없으면서"이건 이렇게 되니깐 당연히 금방 될 거야."라는 생각은 절대 금물이라고 생각합니다.(디자이너나 개발자 분들이 그냥 "이거 간단하게 뭐 메뉴 만들어서 대충 어디 집어넣으면 되지 뭘 그리 어렵게 생각해?"라고 하면 피꺼솟 하는 거랑 마찬가지입니다ㅎㅎ) 3. 삼초 안에 정해진 내용도 항상 문서화 작은 개발팀일수록, 내용 저장과 공유, 그리고 의사 판단의 근거들이 약할 때도 있고, "우리 왜 이거 이렇게 가게 됐지?"라고 생각할 때가 많습니다. 적어도 "몇 월 며칠날 어떤 주제에 관해서 어떤 이유 때문에 어떤 방식으로 처리하기로 한다." 정도라도 항상 적어둘 필요가 있습니다. 디자이너1. 레퍼런스 자료 준비에 시간을 아끼지 말자 원하는 인터렉션, 원하는 디자인의 방향이 있다면, "왜 원하는지, 왜 이런 방향으로 개발을 해주었으면 하는지."에 대한 판단의 근거가 필요합니다. "이쁘잖아."는 상당히 설득력이 있지만 개발자 또는 기획자를 완전히 설득시킬 순 없어요... 특히 아직 개발이나 기획에 대한 로직을 잘 모르시는 디자이너 분들의 경우, 레퍼런스 자료를 찾을 때 드리블(Dribbble)이나 핀터레스트(Pinterest)도 좋지만, 스스로 프로토 타이핑 구현이 불가능하다면, 반드시 구동되고 있는 애플리케이션을 찾아보고 어떻게 작동하는지에 대해 면밀하게 파악해 주세요.2. 작은 부분이라도 시안에 변경이 있다면 반드시 공유하기 처음 팀 단위로 일을 하다 보면, "요거 내가 생각해보고 금방 슉 바꿔놔야지."라는 생각에 조용히 디자인을 바꿀 수 있습니다... 아? 아니에요 절대 안 됩니다.....  다 같이 협업하는 일을 하다 보면, 버전 관리와 변경내역 공유가 제작보다 더 중요한 상황이 올 수 있습니다. "내가 어디 부분을 변경했고, 변경한 이유는 이것 때문이다." 라는것 없이 홀로 조용히 변경한 디자인은 엄청나게 큰 갈등 상황을 부를 수 있어요!개발자1. 장애 발견 시 어디가 어떻게 안될 거 같은지에 대해서 설명하기 가장 힘든 줄 알지만, 할 수 있다면 가장 강점인 부분일 것 같아요. "개발하는 과정에서 이러한 부분은 지금 서비스에서는 이런 식으로 동작하는데, 원하시는 이런 부분은 이런 게 다르기 때문에 동작하는데 장애가 있을 수 있어요."를 설명해 줄 수 있는 개발자와 일한다는 것은 정말 같이 일하는 다른 직군들에게는 큰 축복이라고 할 수 있죠. "내가 백번 말해도 모르실 거예요."라고 말하는 건 결국, 무엇이 문제인지도 모르고, "다른데선 되는데 왜 우린 안돼?"라고 생각하는 다른 직군의 동료들에게 질타를 받을 수밖에 없습니다. 개발자는 소통이 잘 안 되는 사람이라는 고정관념을 깨고, 오래 걸려도 좋고, 다른 직군의 사람들이 당장 무슨 이야기를 하시는지 이해 못해도 좋아요. 같은 선상에서 고민한다는 것을 알려주는 것만으로도 큰 도움이 됩니다.전반적으로모든 작업의 종료는 내 결과물 발표가 아닌 다음 작업자의 업무 최적화입니다.기획자는 "문서 완료했을 때"디자이너는 "디자인 가이드 또는 산출물 나왔을 때"개발자는 "시킨 개발 다 했을 때"가 아니라,기획자는 "다음에 문서를 읽을 디자이너, 개발자가 스펙이 이해가 안돼서 업무를 진행하지 못하는 일이 없도록"디자이너는 "개발 중에 필요한 자료가 없어 업무를 진행하지 못하는 일이 없도록 "개발자는 "QA 중 개발에서 요구한 스펙에 미달되는 부분이 있어 업무를 진행하지 못하는 일이 없도록"하는 게 업무의 궁극적인 틀입니다. 결국, 일의 최종점은 결과물이 온전하게 나왔을 때 의미있는 것이기 때문에 최종 결과물이 나오는 과정에서 내 역할을 마지막까지 충실히 하는것이 업무의 종료라고 생각합니다. 분명, 모든 것들을 한 번에 모두 다 알고, 또는 모든 것들을 다 계산하면서 할 수는 없어요. 항상 실수는 할 수 있죠. 하지만, 실수가 아니라 이런 부분들을 알면서, 또는 이러한 고려를 하지 않고 업무를 지금까지 진행하셨다면, 말씀드린 부분은 분명히 한 번씩은 생각해 보아야 될 부분이라고 생각합니다. 굉장히 오랜만에 글을 쓰는 것 같네요! 다들 건강하게 잘 지내고 계시죠? 저는 마지막 글 이후 한 번의 이직과 다른 이런저런 일들에 치여 이제야 글을 쓰게 되네요. 이번글을 시작으로, 직군별로 하나하나 더 디테일하게 설명드리도록 할게요! 그리고 앞으로는 기획 업무 관련 뿐만이 아니라, 이번 글과 같이 각 직군 간의 이해관계나 업무를 진행하며 느끼는 것들에 대해 공유드리고, 서비스 기획 관련해서도 조금 더 자주 글 쓸 예정입니다. 앞으로도 자주자주 들러주세요, 감사합니다! :)#코인원 #블록체인 #기술기업 #암호화폐 #스타트업인사이트
조회수 1405

QA 끝! ADB 설치부터 사용까지

Overview안드로이드 개발자라면 모두 ADB(Android Debug Bridge)를 사용합니다. 안드로이드 SDK에 포함되어 있는 기능인데요. 쉽게 말하면 에뮬이나 안드로이드 단말과의 연결고리, 도구를 의미합니다. 특히나 QA(Quality Assurance)를 진행할 때 ADB를 사용하면 아주 유용하고, 있어 보입니다. 이번 글에서는 ADB를 잘 모르는 QA직군들을 위해 설치 방법과 간단한 사용법을 공유하려고 합니다. SDK, ADB 설치하기앞서 ADB는 SDK에 포함된 기능이라고 했죠? 우선 여기를 클릭해 SDK를 설치해주세요. 참, 안드로이드는 JAVA가 기본 언어! JAVA도 설치하고 환경 변수도 설정해주세요!SDK를 설치했다면 plalform-tools 폴더 안의 adb.exe파일을 찾아야 합니다. 저의 설치 경로는(C:\Users\brandi_171205_02\android-sdks\platform-tools) 네요.경로를 찾았다면 JAVA 환경 변수 설정하듯 ADB도 환경변수를 설정해야 합니다. ‘내 컴퓨터 마우스 오른쪽 > 속성 클릭’해주세요.고급 시스템 설정 클릭 (개인정보라 지웠습니다.)환경변수 클릭시스템변수 영역 path클릭 > 편집 클릭윈도우10은 앞뒤로 ;를 추가하지 않아도 됩니다. ADB 경로를 추가해주세요. (C:\Users\brandi_171205_02\android-sdks\platform-tools)cmd창을 열고 ADB를 입력하고, 엔터를 눌러주세요.아래와 같이 나오면 성공!잘 따라왔나요? 그 다음은 단말기입니다. 개발자 옵션 > usb디버깅 허용 후 단말을 pc와 연결해주세요. CRM창에서 adb devices 를 입력해주세요. 이 명령어는 에뮬이나 단말 연결을 확인하는 명령어 입니다.ADB 설치를 마쳤습니다. 참 쉽죠? 지금부턴 자주 쓰는 ADB 명령어를 알려드립니다. 한 번 사용해보세요. 한 번 써봤다는 사람은 봤어도, 한 번만 썼다는 사람은 못 봤습니다.자주 쓰는 ADB 명령어단말 재시작QA진행하시면 재시작 많이 하죠? 단말초기화..!adb rebootapk설치 내컴퓨터 > 단말 > 다운로드할 필요가 없어요. 바로 설치!!adb install -r [파일명].apkapk 삭제adb uninstall [패지지명]Android버전 확인adb shell getprop ro.build.version.releaseScreenshotadb shell /system/bin/screencap -p 장치내경로동영상 녹화 QA일하면서 필수입니다. 정말 유용해요.adb shell screenrecord /sdcard/[저장할파일명].mp4텍스트 입력 로그인, 텍스트 입력 테스트 진짜 좋습니다.adb shell input text “[입력할 텍스트]”마치며ADB엔 엄~청나게 많은 명령어가 있습니다. 더 많은 정보를 알고 싶다면 adb help를 입력해보세요. 명령어 도움말이 툭 나올 겁니다. ADB가 있다면 이슈 등록과 이슈 관리 정말 편해집니다. 우선 알려드린 7번까지만 사용해보세요. 당신의 QA가 편안해질 겁니다. 지금까지 브랜디 QA 문지기, 김치영이었습니다.글김치영 대리 | R&D PM팀[email protected]브랜디, 오직 예쁜 옷만#브랜디 #개발자 #개발팀 #인사이트 #경험공유
조회수 2517

BLSTM Tutorial

Summary:이 포스팅은 Bidirectional LSTM에 대한 기본 개념을 소개하고, tensorflow와 MNIST 데이터를 이용하여 구현해 봅니다.Bidirectional LSTM1. 개념 설명앞에서 RNN 과 LSTM 모델에 대해 소개했습니다.기본적인 LSTM 모델은 이전 시간의 step들이 다음 step에 영향을 줄 것이라는 가정을 했습니다.하지만 이후의 step 또한 앞의 step 에 영향을 줄 수 있다면 이 모델을 어떻게 적용시킬 수 있을까요?이후의 step 의 영향도 반영한 모델이 Bidirectional LSTM 모델입니다.위의 그림과 같이 BLSTM 은 두 개의 LSTM 모델을 Concatenate 하여 사용합니다.Time step 이 1부터 t 까지 있다고 가정할 때 forward lstm model 에서는 input 을 Time step 이 1 일때부터 t 까지 순차적으로 주고 학습합니다.반대로 backward lstm model 에서 input 을 T = t 일때부터 1까지 거꾸로 input 주고 학습을 하게 됩니다.time step 마다 두 모델에서 나온 2개의hidden vector은 학습된 가중치를 통해 하나의 hidden vector로 만들어지게 됩니다.2. 구현전체 코드는 Github page 를 참고해주세요.MNIST image 를 input 으로 넣었을 때 이 image 가 0 에서 9 중에 어떤 숫자인지 맞추는 BLSTM 모델을 만들어 보고자 합니다.MNIST 는 0 - 9 의 숫자 image data 이며 각 데이터는 28 x 28 의 matrix (data 는 28 x 28 길이의 array) 로 이루어져 있습니다.앞에서 봤듯이 LSTM 은 sequence 형태를 요구합니다.그래서 데이터 하나를 한 번에 넣는 것이 아니라 각 데이터의 matrix 를 row 만큼, 즉 28번의 time step 으로 나누어 넣어주게 됩니다.그래서 input_sequence 를 28 길이로 설정합니다. learning_rate = 0.001 training_epochs = 10 # 전체 데이터를 몇번 반복하여 학습 시킬 것인가 batch_size = 256 # 한 번에 받을 데이터 개수 # model # 입력되는 이미지 사이즈 28*28 input_size = 28 # input size(=input dimension)는 셀에 입력되는 리스트 길이 input_steps = 28 # input step(=sequence length)은 입력되는 리스트를 몇개의 time-step에 나누어 담을 것인가? n_hidden = 128 n_classes = 10 # classification label 개수 X = tf.placeholder(tf.float32,[None, input_steps, input_size]) Y = tf.placeholder(tf.float32,[None, n_classes]) W = tf.Variable(tf.random_normal([n_hidden * 2, n_classes])) b = tf.Variable(tf.random_normal([n_classes])) X 는 28 x 28 의 matrix 로 이루어진 데이터를 받고 Y 는 실제 class (0 - 9) 를 의미하는 length 10 의 vector 를 받습니다.그리고 각 forward lstm 모델과 backward lstm 모델에서 들어오는 weight 값을 받을 변수를 설정합니다.DropoutWrapper 는 모델에서 input 으로 주어진 data 에 대한 Overfitting 이 발생하지 않도록 만들어주는 모델입니다.각 state 를 랜덤하게 비활성화시켜서 데이터를 더 random 하게 만들어줍니다. keep_prob 변수를 통해서 dropoutWrapper 의 확률값을 조정합니다.keep_prob = tf.placeholder(tf.float32) forward lstm 과 backward lstm 에서 사용할 cell을 생성합니다# lstm cell 생성 lstm_fw_cell = tf.nn.rnn_cell.LSTMCell(num_units = n_hidden, state_is_tuple = True) lstm_fw_cell = tf.nn.rnn_cell.DropoutWrapper(lstm_fw_cell, output_keep_prob=keep_prob) lstm_bw_cell = tf.nn.rnn_cell.LSTMCell(num_units = n_hidden, state_is_tuple = True) lstm_bw_cell = tf.nn.rnn_cell.DropoutWrapper(lstm_bw_cell, output_keep_prob=keep_prob) 학습할 모델을 생성합니다outputs,_ = tf.nn.bidirectional_dynamic_rnn(lstm_fw_cell,lstm_bw_cell, X, dtype = tf.float32) 기존의 lstm 과 달리 output 이 2개의 LSTMStateTuple 로 이루어져 있습니다.각 output 에 가중치를 더해서 하나의 output 으로 만들어주는 과정이 필요합니다.여기서 가장 헷갈리는 부분이 transpose 입니다. 왜 output 에 대해서 transpose를 하는 것인지 의문이 들 수 있습니다.tf.nn.bidirectional_dynamic_rnn 문서를 보시면 output 의 default 는 [batch_size,max_time,depth] 라고 나와있습니다.각각 mini batch 의 크기 그리고 time step, hidden state 의 depth 를 의미합니다.우리는 각 데이터마다 마지막 time step 의 결과값을 output 으로 선택해야 합니다.그래야지 전체 step 이 반영된 output 을 얻을 수 있습니다.outputs_fw = tf.transpose(outputs[0], [1,0,2]) outputs_bw = tf.transpose(outputs[1], [1,0,2]) pred = tf.matmul(outputs_fw[-1],w_fw) +tf.matmul(outputs_bw[-1],w_bw) + biases matmul operation 연산 속도를 위해서 다음과 같이 하나의 output 으로 먼저 합치고 전체에 대한 가중치를 주는 것이 더 좋은 방법입니다.outputs_concat = tf.concat([outputs_fw[-1], outputs_bw[-1]], axis=1) pred = tf.matmul(outputs_concat,W) + b 이하 코드는 이전의 tutorial 과 동일합니다.
조회수 1290

[인공지능 in IT] 맥락인식, 말하지 않아도 알아요

오전 6시 30분. 휴대전화 알람이 울리기 시작한다. 부랴부랴 샤워를 끝내고, 나갈 채비를 하고 있으니, 5분 후 집 앞 버스 정류장에 회사로 향하는 100번 시내버스가 도착한다는 메세지가 나타났다. 버스에 몸을 싣고 사무실 근처 정류장에 내려서 걸어가는 도중, 필자가 즐겨 마시는 커피를 맛있게 내린다는 동네 카페에 대한 정보를 받았다. 어느새 다가온 점심시간에는 어제 이태원에서 과음한 것을 어떻게 알았는지, 휴대폰 잠금화면에 주변 해장국집 추천이 뜬다.그저 영화 속 이야기가 아니다. 실제 사용자의 취향과 행동 등을 분석하고, 시간, 날씨, 교통 등과 같은 외부적 환경요소를 정교하게 더한 시나리오다. 각 개인에게 필요하고, 일상을 윤택하게 만들 수 있는 유의미한 정보를 제공하는 것. ‘맥락인식’ 혹은 ‘상황인지 기술’이라고 불리는 ‘Context Recognition’의 궁극적인 목표 중 하나다.맥락인식 기술은 여러 센서로부터 수집한 데이터를 통해 사용자의 상황을 인지하고, 실시간으로 맥락을 이해하는 데 초점을 맞춘다. GPS, 와이파이, RFID, 모션 센서, 소리 등 여러 시그널을 수집해 분석하며, 사용자의 일정, 문자 메시지나 행동 정보 등을 가져와 ‘사용자가 어떤 사람인지’, 그리고 ‘현재 어떤 상황인지’ 등을 추론한다. 이와 같은 맥락인식 기술을 구현하기 위해 필요한 몇 가지 주요기술은 다음과 같다.1. 상황정보 수집사용자 인터페이스 또는 센서, 센서 네트워크 등를 통해 사용자의 위치, 활동, 생활 패턴 등 다양하고 복잡한 정보를 수집하는 기술.2. 상황정보 모델링상황정보를 가공, 저장 및 공유하는 모델링 기술.3. 상황정보 융합 및 추론사용자의 상황정보를 다른 기술과 융합해 높은 수준의 추론 기능을 제공하는 기술.4. 상황정보 교환센서, 장치 및 객체와의 상호작용을 지원하기 위해 이벤트 기반의 통신 메커니즘을 제공하는 기술.5. 지능형 에이전트사용자의 단순한 의도뿐만 아니라 감정이나 감성을 고려해 전체 상황을 자율적으로 판단, 사용자에게 적합한 서비스를 제공하는 기술.기업 입장에서 생각했을 때, 맥락인식 기술은 소비자 개인에게 특화된 서비스를 제공할 수 있는 날카로운 검이다. 간단한 예로 맥락인식을 활용한 맞춤형 광고에 대해 알아보자. 소비자 A와 소비자 B는 서울에 사는 30대 남성이고 스포츠를 좋아한다. 일반적인 검색이나 구매 히스토리에 기반한 광고와 달리 맥락인식 기술을 활용하면, 이들의 라이프스타일이나 행동패턴을 바탕으로 더 깊은 디멘션까지 분석해 세분화된 광고를 제공할 수 있다. 두 소비자 모두 스포츠를 좋아한다고 가정했을 때, A는 한강 근처에서 매일 저녁 7시 정도에 조깅하는 것을 좋아할 수 있고, B는 남산에서 새벽 6시부터 등산하는 것을 좋아할 수 있다. 미묘한 차이겠지만, 분명 다른 카테고리의 소비자로 정의할 수 있는 것이다.스켈터랩스에서 진행하고 있는 맥락인식 기술 프로젝트를 예로 들어보자. 앞서 언급한 것처럼 다양한 기기로부터 측정하는 저레벨 데이터를 수집하는 것으로 맥락인식 프로세스는 시작된다. 측정 데이터는 서버에 전송되어 시간 순으로 변경 및 취합되고, 기계학습을 통해 필터링 후 수집되며, 고레벨의 맥락으로 추상화된다. 시간, GPS, 와이파이, 모션센서, 소리, 문자메시지, 일정 등 여러 데이터를 처리해 사용자의 맥락을 이해한다. 이러한 일련의 과정 역시 맥락인식 기술의 한 부분인지라 메시지 스트림 프로세서를 기반으로 확장할 수 있는 인프라를 설계, 구축했다.실시간으로 데이터를 처리할 수 있는 파이프라인이다. 처리한 데이터는 좀 더 상위 레벨의 이벤트와 행동으로 인식되어 ‘의미‘를 지니는 데이터로 표현되는데, 예를 들어 GPS 정보를 와이파이 및 시간 등과 같은 다른 데이터와 결합하고, 방문 장소와 행동반경 등을 포함해 사용자의 장소를 식별하는 방식이다. 이러한 사용자의 행동 히스토리는 패턴인식 기술을 활용해 사용자 특정 행동을 학습하고, 이를 기반으로 ‘언제 집으로 돌아갈지‘, 혹은 ‘언제 식사를 하는지’ 등 행동을 예측할 수 있다. 결국, 맥락인식을 통해 사용자의 다음 활동을 예측할 수 있는 기술을 개발, 사용자에게 필요하고 유용한 정보와 서비스를 제공하는 것이 목표다.< 맥락인식 기술을 적용한 큐 앱 화면, 출처: 스켈터랩스 한지예, 이해연 디자이너 >얼마 전, 스켈터랩스의 맥락인식 기술 프로젝트 팀은 해당 기술을 활용해 사용자들이 일상 속에서 가볍게 사용할 수 있는 서비스가 무엇일까 고민하고, ‘큐(Cue)’라는 앱을 개발했다. 큐는 사용자가 직접 명령할 필요가 없다. 큐가 먼저 사용자의 생활을 돕기 때문이다. 날씨를 예를 들면, 사용자가 날씨를 알아보기 전에 비가 올 것 같으면 우산을 챙기라 알려주고, 덥거나 미세먼지가 많을 경우 도움 되는 정보를 알려준다. 사용자에게 전달하는 정보는 카드 메시지를 통해 잠금화면으로 표시된다.큐 프로젝트의 이민학 시니어 프로덕트 매니저는 큐를 통해 사용자가 ‘내'가 누구인지 파악할 수 있을 것이라고 말한다. 예를 들어, 나는 내가 운동을 좋아하는 액티브한 라이프스타일을 살고 있는 줄 알았는데, 실제로는 집에 누워서 영화보는 것을 더 좋아하는 사람에 가깝다는 것. 개인의 삶이 매우 중요해지는 시대이지만, 정작 내가 누구인지 확인하기 어렵기 때문에 맥락인식 기술은 다양한 용도로 사용될 수 있다.< 사용자 패턴을 분석한 유형 결과 예시, 출처: 스켈터랩스 한지예, 이해연 디자이너 >이민학 매니저는 맥락인식 기술에 대해 이렇게 말한다. 그는 “누가, 언제, 어디서, 무엇을, 어떻게, 왜로 구성된 사용자의 육하원칙을 파악하고, 더 나아가 ‘Next-육하원칙’을 파악하는 것이 진정한 맥락인식 기술입니다. 앞으로 기업 특히, 마케터들은 타겟 고객을 잡는데 굉장히 유용하게 사용할 것이라고 생각합니다”라며, “소비자 입장에서는 일상, 문화, 생활 등 세분화된 영역에서 자신의 삶을 더 윤택하게 영위할 수 있습니다. 맥락인식 기술이야말로 인간에게 정말 도움될 수 있는, ‘피부에 와닿는' 인공지능 기술이 아닐까 생각합니다”라고 설명했다.이호진, 스켈터랩스 마케팅 매니저조원규 전 구글코리아 R&D총괄 사장을 주축으로 구글, 삼성, 카이스트 AI 랩 출신들로 구성된 인공지능 기술 기업 스켈터랩스에서 마케팅을 담당하고 있다#스켈터랩스 #기업문화 #인사이트 #경험공유 #조직문화 #인공지능기업 #기술기업
조회수 370

금요일의 해커톤

안녕하세요. 엘리스입니다!지난 8월 말, 엘리스의 야심 찬 첫 해커톤이 있었습니다. 이번 해커톤은 매주 금요일 찾아가는 문제 ‘금요일에 코딩하는 토끼’에 대한 수강생 여러분의 성원에 힘입어 개최되었습니다.주제는 ‘코딩 문제의 A에서부터 Z까지 직접 설계하고 제작한다.’ 해커톤에서는 아이데이션 단계에서부터 문제 기획과 코딩, 채점을 위한 그레이더 제작까지 코딩 문제의 모든 것을 다루었습니다. 물론 실제 문제 동작을 위해 실행과 채점을 반복하며 디버깅하여 완벽한 실습 문제를 만드는 것 역시 이번 경연의 핵심이었는데요.이를 통해 모든 참가자 여러분들은 일일 엘리스 아카데미 실습 문제의 출제자가 되었습니다. 어떤 과정을 거친 어떤 결과물들이 있었을까요?해커톤 현장 스케치해커톤의 소개를 경청 중이신 참가자 여러분.지금까지 프로그래밍 문제를 많이 풀어보셨을 여러분이, 반대로 문제의 출제자가 되어 문제를 구성하는 관점에서 생각해보고 채점 방식까지 고민해본다면 프로그래밍에 대한 이해도를 더 높일 수 있을 것이라는 기대로 이와 같은 해커톤이 기획되었습니다. 교육자로서 엘리스 플랫폼의 다양한 기능을 직접 이용해볼 수 있는 것은 일석이조의 이점이었죠!경직된 분위기를 깨고 뇌를 말랑말랑하게 만들기 위한 아이스 브레이킹 시간은 팀 대항전으로 진행되었습니다.간단한 코딩 문제를 가장 먼저 맞히는 팀이 점수를 얻는 스피드 코딩 게임을 통해서 순발력을 높이고, 잠시 후 해커톤에서 본격적으로 사용하게 될 엘리스 플랫폼과 친해질 시간도 가질 수 있었습니다.'그림 그리기 게임'에서는 각 팀 디자이너들의 창의력이 폭발! 개발과 관련된 온갖 단어들을 1초 만에 그림으로 표현해야 하는 설명자의 재치와 크로키 실력(?)이 강조되었던 순간이었는데요. 승자는 '오즈'팀! 모두 오즈 팀 디자이너의 그림 실력에 입을 다물지 못했다고 합니다.게임을 하는 동안 어느새 어색했던 처음의 분위기가 파괴되었습니다. ^^ 1시간 동안 문제의 초안을 기획하는 시간이 주어지고, 이어 각 팀의 아이디어 발표 시간이 있었습니다.해커톤의 룰은 아래와 같았는데요.실행 가능한 프로그래밍 문제 1개 출제.동화를 모티브로 한 문제 스토리를 기획.채점 가능한 그레이더 제작.모든 팀들이 알고리즘 문제를 기획해주셨습니다. 동화의 서사구조를 논리적으로 단순화하거나 변형하여 알고리즘 문제에 녹여낸 과정이 인상적이었습니다.아이데이션 단계에서는 문제의 완성된 모습이 전부 그려지지는 않았지만 많은 고민의 흔적과 창의적인 생각들을 엿볼 수 있어 이로부터 탄생될 프로그래밍 실습을 기대할 수 있었습니다.밤샘 코딩 중...우승 문제 소개기획하고 코딩하고 디자인을 하다 보니(!) 어느새 날이 밝아왔습니다. 이제 남은 것은 팀별 결과물 발표와 우승팀 시상 뿐!'금코토'를 패러디하여 팀 명을 지어주신 어린 왕자 팀. /* prince */로고까지 깨알 섬세!모든 팀이 각기 다방면에서 강점을 부각하는 문제를 출제해주셨기 때문에 우열을 가리기 어려웠는데요. ‘금코토’배 해커톤이라는 이름에 걸맞게 금코토 과목의 취지와 가장 부합하는 문제를 출제한 팀에게 가산점을 주어 우승팀을 선발하였습니다. 그 결과 대망의 우승 문제는...거울나라의 앨리스팀의 ‘케이크와 병’ 단순한 명료한 문제 구성과 초등학생도 이해할 수 있는 쉽고 친절한 프레젠테이션으로 인상 깊었던 문제였습니다. 완성도, 문제 활용도 면에서 금코토 문제를 능가하며 단순하면서도 재미있게 풀 수 있는 문제라는 심사위원들의 평가가 있었습니다. 우승팀인 거울나라의 앨리스 팀 전원에게는 엘리스 굿즈를 선물로 보내드립니다. :)이밖에 겁쟁이 사자를 동물의 왕으로 만들기 위해 용기의 성을 짓는 알고리즘 문제를 낸 오즈의 마법사 팀의 문제는 스토리에 착안하여 자칫 복잡해질 수 있는 내용을 세세한 문제 설계로 극복하려 했던 점이 우수하게 평가받았습니다. 술주정뱅이 별에 사는 만취한 아저씨를 옮기는 알고리즘 문제를 낸 ‘목요일에 코딩하는 어린 왕자’ 팀은 참신성과 '넓이 우선 탐색', '깊이 우선 탐색', '다익스트라 알고리즘'을 모두 공부해볼 수 있도록 한 문제 구성 면에서 높은 평을 받았습니다.큰 상품도 내걸지 않았던 첫 해커톤이었는데도 참가자분들 모두가 열과 성을 다해 밤을 새워 문제를 만들어 주셨습니다. 모든 참가자 여러분들께 감사의 말씀 전합니다. :) 해커톤 이후 진행한 설문 조사에서 100%의 확률로 모든 분들이 다음 해커톤에 재참가 의사를 밝히셨는데요. 모두 첫 해커톤을 즐겨주셨던 것 같네요. 엘리스에서는 앞으로도 해커톤을 지속적으로 개최할 예정입니다. 코끝 시려질 때쯤 더욱 풍성하고 유익한 기획의 해커톤으로 찾아뵐 예정이니 많은 관심 가져주세요!*금코토 — ‘금요일에 코딩하는 토끼’라는 엘리스 아카데미 과목의 줄임말. 매주 금요일 저녁때쯤 업로드되는 문제로, 특정 루트로 토끼가 움직이도록 코딩해야 하는 콘셉트와 귀여운 휴보 래빗이 특징입니다. >>문제 풀어보기(무료)

기업문화 엿볼 때, 더팀스

로그인

/