스토리 홈

인터뷰

피드

뉴스

조회수 17466

Nodejs 기반의 개발 환경 클린하게 재 구성하기

다양한 언어 기반으로 개발 환경을 구축하여 만들다보면, 소프트웨어 버전관리 해야할 일이 흔히 생기곤 한다. 특히, 종종 대격변이 있는 버전의 판올림으로인해 충돌이 나거나 심볼릭 링크가 유실되는 경우들이 간혹 있는데 이번에도 그런 케이스였다.최근 node.js 기반으로 다양한 프로젝트 (vue.js, react.js등)를 진행하다가 이것저것 환경을 만지고 고치다보니 결국 node.js 를 완전히 클린하게 삭제해야 할 일이 생겼다.아마 이 환경에 결정타를 먹인 것이 OSX 환경에서 El Capitan에서 작업하던 Node.js를 그대로 high sierra로 OSX를 판올림 하면서 퍼미션 권한의 문제가 생긴건지, 노드 패키지 관리나 npm이 정상적으로 동작하지 않으면서 개발환경을 재 설정 할 수 밖에 없게 되었는데, 그 과정에 기름을 부어버리듯 당시에 brew로 설치한 노드가 brew로 삭제가 되지 않는 문제가 발생해버렸다.결국 환경을 처음부터 재 설치 해야하는 과정을 겪어야했는데 기존에 설치된 다양한 패키지 모듈의 찌꺼기들이 남아서 한방에 클린 설치를 할 수 있는 방법이 없을까 싶어 구글링을 해본 결과 앞서서 수많은 시행착오를 겪은 선배님들의 아주 좋은 작업 방식이 있어서 아래에 방법을 공유해본다.요세미티에서 nodejs 정리하는 법 [1]Uninstall nodejs from OSX Yosemite# 첫번째:lsbom -f -l -s -pf /var/db/receipts/org.nodejs.pkg.bom | while read f; do  sudo rm /usr/local/${f}; donesudo rm -rf /usr/local/lib/node /usr/local/lib/node_modules /var/db/receipts/org.nodejs.*# 완전히 nodejs + npm 을 날려버리는 방법 :# /usr/local/lib 경로로 가서 node 와 관련된 노드 모듈을 전부 삭제cd /usr/local/libsudo rm -rf node*# /usr/local/include 경로로 가서 node 와 관련된 노드 모듈 전부 삭제cd /usr/local/includesudo rm -rf node*# 만약 brew 로 인스톨을 했다면 아래와 같은 방법으로 삭제도 가능함. (저는 아래는 brew자체가 망가졌었는지 판올림으로 인한 권한 문제인지 brew로는 삭제 불가능했음.)brew uninstall node# home 디렉토리나 local, lib, include등의 폴더와 관련된 모든 파일은 아래의 경로에 있으니 찾아 들어가서 삭제cd /usr/local/binsudo rm -rf /usr/local/bin/npmsudo rm -rf /usr/local/bin/nodels -las# 아마 혹시 모르니까 클린하게 아래의 명령어도 한번 돌려주자sudo rm -rf /usr/local/share/man/man1/node.1sudo rm -rf /usr/local/lib/dtrace/node.dsudo rm -rf ~/.npmhomebrew를 사용하는 유저들 중에 npm이 제대로 동작하지 않으면 아래와 같은 방법으로도 처방이 가능하다. [2]rm -rf /usr/local/lib/node_modulesbrew uninstall nodebrew install node --without-npmecho prefix=~/.npm-packages >> ~/.npmrccurl -L https://www.npmjs.com/install.sh | sh클린하게 설치를 끝나고 react-native를 컴파일하는 과정에서 깃에 관련된 오류가 발생한다면 아래의 방법을 사용해보자. [3]오류메세지 :xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun솔루션 :xcode-select --install엘케피탄에서 하이시에라로 osx를 업데이트 하면서 homebrew의 링크가 깨졌다면 아래의 방법으로 다시 붙여준다. [4]sudo chown -R "$USER":admin /usr/localsudo chown -R "$USER":admin /Library/Caches/Homebrewbrew link libpng참고 출처 :[1] : https://gist.github.com/TonyMtz/d75101d9bdf764c890ef[2] : https://stackoverflow.com/questions/32893412/command-line-tools-not-working-os-x-el-capitan-macos-sierra-macos-high-sierra[3] : https://stackoverflow.com/questions/39778607/error-running-react-native-app-from-terminal-ios[4] : https://github.com/mikepurvis/ros-install-osx/issues/28 #더팀스 #THETEAMS #풀스택개발자 #Node.js #백엔드 #인사이트 #꿀팁
조회수 1048

컴공생의 AI 스쿨 필기 노트 ⑥인공신경망

인공지능, 머신러닝, 딥러닝이번 6주차 AI 스쿨에서는 딥러닝의 가장 기초적인 부분을 배웠어요. 인공지능과 머신러닝, 그리고 딥러닝을 많이 들어보긴 했는데 이 셋의 차이는 무엇일까요?인공지능이라는 개념은 1956년 미국 다트머스 대학에 있던 존 매카시 교수가 개최한 다트머스 회의에서 처음 등장했고 최근 몇 년 사이 폭발적으로 성장하고 있는 중이에요. 1956년 당시 인공지능의 선구자들이 꿈꾼 것은 최종적으로 '인간의 지능과 유사한 특성을 가진 복잡한 컴퓨터'를 제작하는 것이었죠. 이렇듯 인간의 감각, 사고력을 지닌 채 인간처럼 생각하는 것을 인공지능이라고 해요.인공지능은 위 세 개념 중 가장 큰 개념이에요. 머신러닝은 일반적으로 사람들이 이야기하는 인공지능, 즉 머신러닝에 기반한 인공지능을 말하는데요. 인공지능을 구현하는 구체적인 접근 방식이라고 할 수 있어요.머신러닝에는 linear regression, logistic regression 등의 여러 알고리즘이 있는데요.  그중 학습에 사용되는 모델을 딥러닝이라고 해요. 즉 딥러닝은 완전한 머신러닝을 실현하는 기능이라고 볼 수 있어요. 이러한 딥러닝의 등장으로 인해 머신러닝의 실용성은 강화됐고 인공지능의 영역은 확장됐다고 해요.인공 신경망(Neural Network)오늘 수업의 핵심인 인공 신경망(Neural Network)은 어떻게 만들어졌을까요?뉴런의 구조이것은 우리 몸에 존재하는 신경세포인 뉴런이에요. 뉴런은 전기적인 신호를 전달하는 특이한 세포인데 뇌는 뉴런의 집합체라고 할 수 있어요. 뉴런은 수상 돌기(dendrites, input)에서 신호를 받아들이고 축색 돌기(axon terminals, output)에서 신호를 전송해요. 신호가 전달되기 위해서는 일정 기준(임곗값 : threshold) 이상의 전기 신호가 존재해야 해요. 이 신호들의 전달을 통해서 정보를 전송하고 저장해요.이런 신경세포로 이뤄진 신경망 시스템을 위의 그림처럼 표현할 수 있어요. 이처럼 인공신경망은 사람 몸속의 신경들을 모방해서 만든 시스템이에요.위의 식처럼 뉴런을 수학적으로 표현할 수 있는데요. 입력 값들(X)에 가중치를 두어(W) 값 (f(x))을 구하고 그 값과 임계치와의 관계를 활성함수(active function)*로 판단하여 결괏값을 출력하게 돼요.( * 활성함수는 인공신경망의 개별 뉴런에 들어오는 입력신호의 총합을 출력 신호로 변환하는 함수로 비선형 함수(non-linear function)를 씁니다.**)이때 활성함수는 뉴런에서 임곗값을 넘었을 때만 출력하는 부분을 표현한 것으로 sigmoid 함수, Relu 함수 등 여러 방식이 있어요.인공 신경망의 구조인공 신경망 구조는 위의 그림처럼 나타낼 수 있어요. 인공 신경망 구조는 입력층(input layer), 은닉층(hidden layer), 출력층(output layer)으로 이루어져 있어요. 위의 그림은 그 구조에 의해 3-layer Neural Network 또는 2-hidden-layer Neural Network라 부를 수 있는데요. 3-layer Neural Network는 3개의 층을 가지는 인공신경망이라는 뜻이고, 위 그림에서는 은닉층1, 은닉층2, 출력층이 해당되겠죠. 인공 신경망에 입력층과 출력층은 항상 존재하기 때문에 은닉층의 개수만을 고려하여 부르기도 해요. 위 그림에서는 은닉층이 2개 있기 때문에 2-hidden-layer Neural Network라고 부를 수 있어요. 전파(Propagation)이번에는 실제로 학습하는 과정인 인공신경망의 알고리즘에 대해 알아볼게요. 순전파(Forward Propagation)와 역전파(Backward Propagation)가 있어요.순전파는 입력값에서 출력값으로 가중치를 업데이트를 하고 활성화 함수를 통해서 결괏값을 가져오는 것을 말해요. 인공신경망이 설계된 정방향(input → hidden → output)으로 데이터가 흘러가기 때문에 순전파라고 해요. 말 그대로 입력값을 앞쪽으로 보낸다고 생각하면 돼요.역전파는 출력값을 통해서 역으로 입력값 방향으로 오차를 다시 보내며 가중치를 재 업데이트하는 것이에요. 출력값에서 계산된 오차에 가중치를 사용해 바로 이전 층의 뉴런들이 얼마나 오차에 영향을 미쳤는지 계산해요. 결과에 영향을 많이 미친 뉴런일수록 더 많은 오차를 돌려줘요.개념을 코드에 적용하기NumPy로 구현된 Neural Network(이하 NN)의 작동 방법을 살펴볼게요. NN은 총 2개의 레이어로 이루어져 있어요. 이번 과제에서는 입력 x가 들어왔을 때, 레이블에 따라 예측치가 1로 수렴하는지 알 수 있는 인공신경망을 구현하는 것이 목적이에요.Neural Network다음 코드는 simpleNueralNet() 클래스를 나타내는 코드예요. simpleNueralNet()은 두 개의 레이어로 구성된 NN이에요.N, D_in, H, D_out = 64, 1000, 100, 10- N은 batch size, 즉 한 번에 처리할 수 있는 데이터 사이즈를 말해요. - D_in은 입력값 차원에 쓰이는 값으로 1000을 할당해요.- H는 은닉층 차원에 쓰이는 값으로 100을 할당해요.- D_out은 출력값 차원에 쓰이는 값으로 10을 할당해요.아래 코드를 통해서 랜덤 입력과 출력 데이터를 만들어요.x = np.zeros((N, D_in))     #1  x.fill(0.025)                         #2y = np.ones((N, D_out))   #31. np.zeros() 함수를 사용하여 (64, 1000)의 차원을 갖는 0인 행렬을 만들어요.2. fill() 함수를 통해 x 안의 모든 0을 0.025로 바꿔요.3. np.zeros() 함수를 사용해 (64, 10)의 차원을 갖는 0인 행렬을 만들어요.아래는 랜덤 값을 갖는 가중치(weight)들을 초기화하는 코드예요. w1은 1000, 100 차원의 랜덤 값을 갖는 행렬로, w2는 100, 10차원의 랜덤 값을 갖는 행렬로 만들어요.w1 = np.random.randn(D_in, H)   w2 = np.random.randn(H, D_out)learning_rate는 학습 속도를 의미해요. 아래는 단계별로 움직이는 학습 속도를 1e-6으로 정의하는 코드예요.learning_rate = 1e-6이제 5000번의 순전파를 할 거예요.h = x.dot(w1)     h_relu = relu(h)  y_pred = h_relu.dot(w2)h는 은닉층에 전달할 값이에요. x와 w1을 행렬곱한 값을 가져요.활성 함수 relu에 h를 넣어서 계산해요.y_pred는 예상되는 출력값이에요. relu로 계산된 h_relu와 가중치 w2를 행렬곱한 값이에요.아래는 순전파로 얻은 y_pred에서 진짜 y를 뺀 값을 제곱한 것의 합을 구해 손실 값(loss)을 구하는 코드예요. print(loss) 코드로 손실을 확인할 수 있어요.loss = np.square(y_pred - y).sum()순전파 후 역전파를 이용해 손실에 대한 가중치 w1과 w2의 gradients를 계산하여 update 할 거예요.grad_y_pred = 2.0 * (y_pred - y)              #1grad_w2 = h_relu.T.dot(grad_y_pred)    #2grad_h_relu = grad_y_pred.dot(w2.T)    #3grad_h = grad_h_relu.copy()                    #4grad_h[h < 0>grad_w1 = x.T.dot(grad_h)                         #61. 순전파로 얻은 y_pred에서 진짜 y값을 뺀 값에 2.0을 곱하여 grad_y_pred를 구해요.2. grad_w2는 순전파에서 y_pred = h_relu.dot(w2) 식을 사용했으므로  h_relu.T.dot(grad_y_pred) 로 구해요. h_relu가 반대로 곱해지기 때문에 T를 이용하여 shape을 바꿔줘야 해요.3. grad_h_relu는 방금 위에서 사용한 y_pred = h_relu.dot(w2)을 이용하여 grad_y_pred.dot(w2.T) 로 구해요. 이번에는 w2 shape의 반대를 grad_y_pred에 곱해줘야 해요.4. 순전파에서 h_relu = relu(h)였는데요. 역전파에선 grad_h와 grad_h_relu가 같기 때문에 copy() 함수로 그대로 복사해요!5. 0보다 작은 h는 0으로 만들어요.6. 가중치 w1의 값인 grad_w1은 순전파의 h = x.dot(w1)와 반대로 x.T.doT(grad_h) 곱해요. 역전파는 순전파의 식에서 이항한다고 생각하면 조금 더 쉽게 이해할 수 있을 것 같아요. 이항한 값은 .T를 붙여서 표현한다고 생각하면 될 것 같아요.아래는 가중치를 재업데이트하는 코드예요.w1 -= learning_rate * grad_w1 w2 -= learning_rate * grad_w2 과제1을 통하여 NN을 알아보았는데요. 복잡하지만 순전파와 역전파를 알고 있다면 많이 어렵지는 않은 것 같아요. 과제 2는 정확도를 95% 이상으로 만들어보는 과제인데 여러 가지 방법을 동원해서 풀어보는데 생각보다 쉽지가 않아요. ^^;이번 수업시간에 배운 딥러닝의 기초인 신경망은 굉장히 중요한 개념이라고 해요. 신경망을 기반으로 한 딥러닝을 강화하여 안면인식을 가능하게 하거나 저장된 데이터를 정확하게 인식하고 분류할 수 있는 기기들도 만들어지고 있어요. 이처럼 AI는 점진적으로 활용 범위가 넓어지고 있기 때문에 이 수업을 통해 쌓은 AI 지식을 마음껏 뽐낼 수 있는 날이 왔으면 좋겠어요!** 왜 활성함수로 비선형 함수를 쓸까요?선형함수인 h(x)=cx를 활성함수로 사용한 3-layer 네트워크를 생각해봐요. 이를 식으로 나타내면 y(x) = h(h(h(x)))가 되는데요.  이는 y(x) = c3x와 같습니다.  이렇게 활성함수로 선형함수를 사용하면 은닉층을 사용하는 이점이 없어요.* 이 글은 AI스쿨 - 인공지능 R&D 실무자 양성과정 6주차 수업에 대해 수강생 최유진님이 작성하신 수업 후기입니다.
조회수 2710

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]브랜디, 오직 예쁜 옷만
조회수 2616

8퍼센트 Test case 작성 가이드

8퍼센트에서 Python Django 코드에 대한 Test case 작성시 사용하는 가이드를 공유해보려고 합니다.클래스명일반적으로 TestCase 를 상속 받는 클래스일 경우 class 명의 마지막에 TestCase 를 붙입니다.예제: SimpleTestCase(TestCase)함수명테스트 함수명의 경우 test_ 로만 시작하면 동작하는데 문제가 없고 테스트 코드에까지 주석을 다는 것은 번거로우므로 함수명의 test_ 뒷부분을 한글로 하여 설명을 대신하도록 합니다.class IUPaginationMethodTestCase(TestCase): @classmethod def setUpTestData(cls): cls.request = Mock() cls.request.GET = {'page': 1, 'items_per_page': 1} cls.pagination = IUPagination(cls.request) def test_page_url_기본(self): expected = '?{}=1'.format(self.pagination.page_key) self.assertEqual(self.pagination.page_url(), expected) def test_page_url_쿼리스트링_없는경우_물음표_붙인다(self): expected = '/?{}=1'.format(self.pagination.page_key) self.pagination.url_prefix = '/' self.assertEqual(self.pagination.page_url(), expected) def test_page_url_쿼리스트링_있는경우_엠퍼센드로_붙인다(self): expected = '{}&{}=1'.format( self.pagination.url_prefix, self.pagination.page_key )) self.pagination.url_prefix = '?utm=source' self.assertEqual(self.pagination.page_url(), expected) factory_boyfixture 를 대신해서 가급적 factory_boy 를 사용합니다.signals 끄기factory boy로 모델 객체 생성시 signal 이 호출되는데 signal에 대한 테스트가 아니라면 대부분 실행할 필요가 없습니다.이 때 factory.django.mute_signals를 사용해서 끄면 됩니다.decorator, context manager 둘 다 사용 가능합니다.decorator@mute_signals(signals.post_save) def test_some_code(self): some = SomeFactory() context managerwith mute_signals(signals.post_save): some = SomeFactory() 참고 링크factory_boyDisabling signalssetUpTestData vs setUpfixture를 사용하면 fixture로 정의한 모델 객체가 모든 테스트 시작 전에 생성이 되는데 유사하게 setUp 에서 factory 생성을 하게 되면 매번 객체 생성을 하게 되므로 느립니다.테스트에서 read only 로만 사용하는 객체의 경우 class method인 setUpTestData 에서 생성하면 1번만 생성이 되므로 빨라집니다.가급적 setUp 에서 매번 객체를 생성하는 것을 지양하고 테스트 함수 내에서 필요한 객체만 생성하는 것이 효율적이고 빠릅니다.method mock메소드를 mock 하는 경우 unittest.mock.patch() 를 사용합니다.decorator보통 테스트 메소드에 대한 decorator 로 사용합니다.직접 호출class 내의 여러 테스트 메소드 혹은 모든 테스트 메소드에서 동일한 함수를 mock 하는 경우에는 start, stop 을 활용하면 편합니다.예제 코드from unittest import mock class MyTest(TestCase): def setUp(self): self.mock_method1 = mock.patch('package.module.method1').start() self.mock_method1 = mock.patch('package.module.method2').start() def tearDown(self): mock.patch.stopall() def test_something(self): something() self.assertTrue(self.mock_method1.called) 참고 링크: patch methods start and stoptimezonedatetime.datetime.now() datetime.datetime.strptime() 등을 사용해서 naive datetime 객체를 django 모델의 DateTimeField 에 할당할 필요가 있는 경우 반드시 django.utils.timezone.make_aware() 를 사용해서 time-zone-aware datetime 객체로 변환한 후에 합니다.참고 링크: Django timezone 문제 파헤치기freezegun특정 시점에서의 테스트가 필요한 경우 freezegun 을 사용해서 현재 시간값을 고정합니다.가급적 decorator 나 context manager 를 사용해서 특정 클래스나 메소드, 혹은 코드 블럭에만 적용하도록 하는 것이 좋습니다.decorator 예제from freezegun import freeze_time import datetime import unittest @freeze_time("2012-01-14") def test(): assert datetime.datetime.now() == datetime.datetime(2012, 1, 14) context manager 예제from freezegun import freeze_time def test(): assert datetime.datetime.now() != datetime.datetime(2012, 1, 14) with freeze_time("2012-01-14"): assert datetime.datetime.now() == datetime.datetime(2012, 1, 14) assert datetime.datetime.now() != datetime.datetime(2012, 1, 14) 특정 테스트 케이스 전체에 적용을 하기 위해 start(), stop() 메소드를 사용하기도 하는데 이 경우 반드시 stop() 을 해주어야 다른 테스트 케이스의 시간 값에 영향을 주지 않습니다.예제from django.test import TestCase from freezegun import freeze_time class SomeTestCase(TestCase): def setUp(self): self.freezer = freeze_time("2016-01-05 00:00:00") self.freezer.start() def tearDown(self): self.freezer.stop() 참고 링크: freezegun맺음말Python Django 개발시 Test case 작성을 잘 하기 위한 8퍼센트 개발팀의 가이드를 공유해 보았습니다. Python Django 개발자들이 Test case 작성을 효율적으로 잘 해서 서비스의 안정성을 높이는데 도움이 되기를 기대해 봅니다.#8퍼센트 #에잇퍼센트 #Django #Python #장고 #파이썬 #개발 #개발자 #가이드 #꿀팁 #인사이트
조회수 1789

성장하는 PHP와 환대받지 못하는 개발자

https://kinsta.com/blog/php-7-2/ PHP v7.2 릴리즈최근(2017년 11월 30일)에 PHP  7.2 버전이 릴리즈 되었습니다.(다운로드 바로가기) PHP는 1995년에 만들어진 오래된 언어지만 여전히 많은 웹사이트들이 PHP로 만들어지고 있습니다. 특히 버전7로 넘어오면서 퍼포먼스가 비약적으로 좋아졌다는 평을 듣고 있습니다. 이번 7.2 버전에서는 아래와 같이 보안성강화와 프로그래밍 기능 향상을 제공하고 있습니다. (개선목록 바로가기)PHP 7.2.0 comes with numerous improvements and new features such as  Convert numeric keys in object/array castsCounting of non-countable objectsObject typehintHashContext as ObjectArgon2 in password hashImprove TLS constants to sane valuesMcrypt extension removedNew sodium extensionPHP로 만들어진 많은 사이트2017년 GitHub 통계를 보면 PHP는 GitHub에서 사용되는 337개의 언어들중에서 Top 5에 들어가는 매우 대중적인 언어입니다.https://octoverse.github.com/ WordPress, Drupal, Zoomla 와 같은 웹 기반의 오픈소스 컨텐츠 관리 시스템은 모두 PHP로 만들어 졌습니다. 그리고테크크런치(TechCrunch), 펩시 리프레시(Pepsi Refresh), 코메디닷컴(Comedy.com) 같은 기업들은 WordPress로 만들어진 사이트를 적극 활용하고 있기도 합니다. 다만 아쉬운 점은 아직도 5버전을 사용하여 개발한 사이트들이 많이 있다는 점입니다.https://kinsta.com/blog/php-7-2/환대받지 못하는 PHP 개발자PHP는 탁월한 접근성으로 인해 생각지도 못한 문제가 발생합니다. PHP가 누구나 사용할 수 있을 정도로 쉬운 구조이다보니 우리나라의 갑-을-병-정 으로 내려가는 SI 구조에서 저렴한 인력으로 구분되기 시작합니다. PHP 고급 개발자가 고급 대우를 못받게 되는 상황이 발생하는 것입니다. 또한 엔터프라이즈 개발에서 제외되다 보니 PHP 개발자는 점점 대규모 시스템 설계 경험이 적어지고 결국 중소규모의 서비스 개발에만 참여하게 되었습니다. 하지만 PHP도 충분히 대규모 서비스 개발이 가능한 언어이며 PHP The Right Way 와 같이 PHP를 잘 사용할 수 있는 방법들을 정리한 사이트를 보면 PHP의 저력을 확인할 수 있습니다.PHP 개발자를 위한 서비스 관리 도구PHP 개발에 있어서 아쉬운 부분이 있다면 개발 이후 운영에 관련된 부분입니다. 많은 국내 PHP 사이트들이 개발 이후 성능 분석이 되지 않은 상태에서 운영되고 있습니다. Java로 만들어진 엔터프라이즈 서비스들은 오픈 시점과 운영 과정에서많은 노력을 들여서 서비스 최적화 작업을 진행하는데 반해서, PHP로 개발된 서비스들은 사용자가 많아지더라도 튜닝 작업을 진행하는 경우가 거의 없습니다. 아쉬운 점은 이로 인해 PHP의 성능이 떨어진다는 오해가 발생하기도 한다는 것입니다.일반적으로 평균 응답시간을 계산하여 서비스의 상태를 파악하기도 하지만 하루 1만명이 들어오는 사이트에 100명이 10초 이상의 응답시간을 경험하더라도 나머지 인원이 0.1초의 응답시간을 갖는다면 서비스의 평균 응답시간은 0.2초 이내로 나오게 됩니다. 이런 고객의 장애를 해결하기 위해서는 사용하는 성능 분석 서비스가 이전까지는 솔루션으로만 제공되었기 때문에 고가이며 설치도 어려웠지만 최근에 서비스로 제공되기 시작하면서 비용도 저렴해지고 설치도 매우 쉬워졌습니다. 해외에서는 몇 년전부터 많은 PHP 개발자들이 모니터링 서비스인 뉴렐릭(https://newrellic.com)이나 앱다이나믹스(https://appdynamics.com)의 서비스를 통해 PHP 분석/모니터링 서비스를 사용하고 있습니다. 이런 서비스들은 당연히 한국에서도 사용이 가능합니다.https://newrelic.com/php국내 모니터링 서비스 중에서는 와탭(https://whatap.io)이 최근 PHP를 지원하고 있습니다. 어플리케이션의 성능을 분석하고 튜닝한 사이트와 안한 사이트의 성능 차이가 날수 있기 때문에 PHP로 만들어진 서비스의 운영 및 업데이트 작업을 진행하는 개발자 분들은 뉴렐릭이나 앱다이나믹스 또는 와탭을 사용하여 운영중인 서비스의 성능을 확인해 보시길 권하고 싶습니다. 대부분의 PHP 성능 모니터링 서비스는 트라이얼 기간을 제공해 주기 때문에 일정기간 무료로 서비스 사용이 가능합니다. 몇일간 성능을 분석하고 모니터링 한다면 서비스 운영 방식에 대한 인사이트도 얻을 수 있습니다. https://coderseye.com/best-php-frameworks-for-web-developers/PHP 성능 모니터링 서비스로 할수 있는 것들PHP 성능 모니터링 서비스는 정확히 표현하면 고객의 트랜잭션을 추적하는 서비스입니다. 서비스를 사용하는 모든 고객의 트랜잭션을 추적하여 서비스의 성능을 알아내는 방식입니다. 이런 어플리케이션 성능 모니터링 서비스는 대규모 서비스를 체계적으로 운영하는 위한 필수 도구입니다. 최근 서비스 형태로 제공되는 성능 모니터링 서비스들은 기존 운영자 위주의 기능에서 벗어나서 개발자와 운영자가 함께 참여하는 DevOps 환경에 맞는 기능을 제공하고 있습니다. 서비스를 운영하는 과정에서 응답시간의 상황을 실시간으로 확인할 수 있으며 문제가 발생한 쿼리를 빠르게 찾을 수 있도록 도와줍니다. 트랜젝션의 에러도 당연히 알수 있으며 문제가 발생한 메소드도 알수 있습니다. 코드상의 서비스 구조뿐만 아니라 실제 트랜잭션의 흐름을 알수 있기 때문에 서비스의 동작 구조도 함께 공유해가며 서비스를 발전시킬 수 있도록 도와줍니다. 결론PHP는 정말 빠르게 발전하고 있는 언어중에 하나입니다. 우리가 정보를 주고 받는 많은 서비스들이 PHP로 만들어 지고 있으며 언어의 구조도 모던하게 변화하고 있습니다. 특히 빠르게 변화하는 스타트업에서 사랑받는 언어이며 세계적으로도 많은 이들의 사랑을 받고 있는 언어입니다. 한편 PHP는 소규모에서만 적용한다는 인식과 함께 PHP로 시작했음에도 규모가 커지면서 서비스를 Java로 변경하는 경우에는 아쉬움이 남습니다. 하지만 PHP가 지속적으로 발전하고 있고 더 좋은 방향으로 나아가는 과정에서 더 좋은 PHP 개발자들이 나오기 시작할 거라 생각합니다. 그리고 뉴렐릭(https://newrelic.com)이나 앱다이나믹스(https://appdynamics.com) 아니면 와탭(https://whatap.io)과 같은 성능 분석 도구를 사용하여 PHP로 만든 서비스의 효율을 높이고 운영 관리를 체계화해 나간다면 국내에서도 페이스북과 같이 PHP로 개발하여 대규모로 서비스볼수 있을거라 생각합니다. http://php.net/archive/2017.php#와탭랩스 #개발자 #개발팀 #인사이트 #경험공유 #일지 #PHP
조회수 1359

AWS Rekognition + PHP를 이용한 이미지 분석 예제 (1/2)

OverviewAWS Rekognition은 딥 러닝 기반의 이미지, 동영상 분석 서비스입니다. Rekognition API를 사용하면 서비스에서 객체, 사람, 텍스트, 장면 및 동작을 식별하고 부적절한 콘텐츠를 탐지할 수 있습니다. Rekognition은 딥 러닝 기술을 기반으로 하고 있기 때문에 지금 이 순간에도 새로운 데이터를 통해 끊임없이 학습하고 있고, AWS에서도 새로운 레이블과 얼굴 인식 기능을 추가하고 관리합니다. 이번에는 AWS S3 Bucket에 업로드한 이미지로 이미지 분석 결과를 볼 수 있는 예제 사이트를 통해, Rekognition과 친해지는 시간을 갖도록 하겠습니다. 저는 예제 사이트를 개발하기 위해 PHP 프레임워크인 CodeIgniter 3, MAMP, Bootstrap을 사용했습니다.1. AWS Rekognition SDK 설치하기1-1) AWS Rekognition 사이트에 접속해 Download SDKs 를 클릭합니다.1-2) AWS 에서 제공하는 다양한 언어의 SDK를 확인할 수 있습니다. 저는 PHP를 사용할 것이므로 PHP 의 Install을 클릭하겠습니다.1-3) AWS SDK 를 설치할 수 있는 방법은 여러가지가 있습니다. 이 중에서 저는 Composer를 이용해 설치했습니다.curl -sS https://getcomposer.org/installer | php php -d memory_limit=-1 composer.phar require aws/aws-sdk-php 1-4) 짠! 짧은 명령어 2줄로 SDK 설치가 완료되었습니다 :)2. AWS S3 Bucket 에 업로드된 이미지를 분석하기2-1) 여기에 임의로 만든 예제 사이트가 있습니다. [이미지 선택] 과 [S3에 이미지 업로드하기] 를 통해 이미지 파일을 등록하면, 백단(Back-end) 에서는 해당 파일을 특정 S3 Bucket 에 업로드 한 후 Rekognition 에게 이미지 분석을 요청하도록 짜여있습니다. 관련 코드는 아래와 같습니다.{     "Image": {         "S3Object": {             "Bucket": "bucket",             "Name": "input.jpg"         }     },     "MaxLabels": 10,     "MinConfidence": 80 } 위의 코드 블록은 AWS Rekognition 개발자 안내서에 나와있는 예제 포맷이고, 아래의 코드는 예제 포맷을 PHP 에서 요청할 수 있는 방식으로 코딩한 것입니다.detectLabels 메소드 를 이용해 분석할 이미지가 저장되어 있는 S3 Bucket 과 이미지의 Name 을 전달해줍니다. 1) MaxLabels : 응답 받을 최대 Label 수 2) MinConfidence : Label 에 대한 최소 신뢰성 여기서 Label 이란 ‘이미지에서 발견되는 객체, 장면 또는 개념’ 이라고 생각하면 됩니다. 예를 들어 해변에 있는 사람들을 촬영한 사진에는 ‘사람’, ‘물’, ‘모래’ (객체) 및 ‘해변’ (장면) 그리고 ‘야외’ (개념) 등이 Label 이 될 수 있습니다. 자, 우주 사진을 한 번 분석해볼까요? array(3) {     ["Labels"]=>     array(8) {       [0]=>       array(2) {         ["Name"]=>         string(9) "Astronomy"         ["Confidence"]=>         float(96.8987350464)       }       [1]=>       array(2) {         ["Name"]=>         string(5) "Earth"         ["Confidence"]=>         float(96.8987350464)       }       [2]=>       array(2) {         ["Name"]=>         string(5) "Globe"         ["Confidence"]=>         float(96.8987350464)       }       [3]=>       array(2) {         ["Name"]=>         string(11) "Outer Space"         ["Confidence"]=>         float(96.8987350464)       } ...     } Rekognition이 업로드한 우주 사진을 분석하여 정확히 연관된 Label들만 반환한 것을 확인할 수 있습니다. 이 Label을 가지고 이미지 태그를 간단하게 구현했습니다.참 쉽죠 ?Conclusion이번 시간에는 AWS Rekognition 을 이용하여 기본적인 이미지 분석을 해보는 시간을 가져봤습니다. 다음 시간에는 ‘얼굴 감지 및 분석’ 기능을 응용하여 Collection 을 생성해보고, 얼굴 검색을 해보는 시간을 갖겠습니다. 참고놀라운 무료 이미지 · Pixabay핀터레스트 스타일 레이아웃 만들기 (masonry) - 생활코딩이미지에서 레이블 감지 - Amazon Rekognition글김우경 대리 | R&D 개발1팀[email protected]#브랜디 #개발문화 #개발팀 #업무환경 #인사이트 #경험공유
조회수 1054

잔디 iOS 개발자 Chris, 그가 처음으로 공개한 '잔디 1호 사원' 스토리

편집자 주: 잔디와 함께 하고 있는 멤버는 총 50여 명. 국적, 학력, 경험이 모두 다른 이들이 어떤 스토리를 갖고 잔디에 합류했는지, 무슨 일을 하고 있는지 궁금해하는 분들이 많습니다. 잔디 블로그에서는 이 궁금증을 해결해 드리고자 ‘맛있는 인터뷰’를 통해 ‘잔디’ 멤버들의 이야기를 다루고 있습니다.◇ 우리가 앉아 있는 이 공간이 어떤 곳인지 소개해 달라Chris: 설마 했다. 내가 맛있는 인터뷰 대상자가 될지는.. 머리가 멍해 고통받던 중 당신이 추천한 그릴 타이로 오늘 장소를 선정했다. 이름만 들었을 땐 ‘거기 뭥미?’ 이랬는데, 와보니 알겠다. 예전에 와 본 적이 있다.◇ 자기소개 좀 해달라C: 반갑다. 잔디에서 iOS 개발 파트를 담당하고 있는 1호 사원 Chris라고 한다. 아주 오랜 기간 동안 원래 이름인 ‘봉규’라고 불렸다가 얼마 전 회사 내 호칭에 변화가 생겼다. 아직 Chris로 불리는 게 어색하다.◇ 어떤 일을 하며 월급을 받고 있는지?C: 앞서 소개했듯 난 iOS 개발자다. 이 글을 읽는 독자분들 중 아이폰으로 ‘잔디’를 사용 중이라면, 필시 내가 개발한 잔디를 이용하고 있는 거다. 마음이 조금 아프지만 기획에 대한 관심으로 지난 겨울 잠시 PM 팀으로 외도했었다. 하지만 결국 내 마음의 고향, iOS 개발로 돌아왔다.◇ PM팀으로 외도를 했던 이유가 궁금하다C: 기획이라는 업무에 관심이 많았다. 개발을 하다 보니 자연스레 기획에도 관심을 갖게 되었다. 한 번쯤 해보고 싶었던 일이었기 때문에 롤이 주어졌을 때 정말 재미있게 일했다.하지만 PM 일을 직접 해보니 마냥 재미있기만 하지는 않더라. 비즈니스는 물론이고 개발자와 디자이너의 의견을 수렴해 조율까지 해야 하는데 모두의 의견을 100% 반영할 수 없으니 여간 괴로운 일이 아니더라. 기획자의 길이 쉽지 않다는 것을 깨닫게 되었다.그리고 PM의 업무라는 게 쉽게 눈에 띄지 않는 일이다. 제품이 아무리 잘 나와도 기획자에게 ‘기획 참 잘나왔어요’ 라고 말하는 경우를 많이 접하진 않았을 거다. 여러분 주위에 기획자를 만나게 되면 ‘고생이 많으십니다’라고 응원 한마디 해줬으면 좋겠다.◇ iOS 개발자로 컴백한 이유는 무엇인가? 향간의 소문엔 코딩이 그리워 개발자로 돌아갔는 소문이 있다C: 회사 측에서 기획보다는 iOS 개발을 다시 맡아주면 좋겠다는 이야기를 들었다. 사실 별다른 고민 없이 제안을 받아들였다. 아무래도 초기부터 개발한 자식 같은 iOS가 늘 머리 한 구석에 있었다. 물론, 잔디를 사랑하는 마음도 크게 한 몫 했다. 결코 어필하고 싶어 이런 멘트를 남기는 게 아니다.◇ 보여주기 멘트인 것 같지만 감동 받았다. 그렇다면 Chris에게 잔디 iOS란 무엇인지 조금 더 말해달라C: 나의 분신이다.  iOS는 곧 Chris다. 아무것도 없는 백지상태에서 지금에 이르기까지 수많은 과정이 있었고, 그 과정의 중심엔 언제나 내가 있었다. iOS는 분신이라는 단어 외엔 표현할 방법이 없다. 오바가 아니라 사무실 어딘가에서 누군가 ‘iOS’ 라고 속삭이면 몸이 반응한다. iOS에 대한 이야기는 곧 나에 대한 이야기와 마찬가지이니까.내가 곧 잔디 iOS이자, 잔디 iOS가 곧 나이다.그만큼 애착을 갖고 개발 업무에 임하고 있다.◇ 멘트가 찰지다. 듣기론 PM 팀의 데니스와 특별한 인연이 있다고 하는데?C: 동아리 이야기를 하는 것 같다. 사회에 나오기 전 연합 동아리 활동을 한 적이 있는데, 데니스가 그 동아리 후배다. 기수 차이가 많이 나 직접적으로 알던 사이는 아니었다. 내가 동아리에 잔디 채용 공고를 공유해 데니스가 합류하게 되었다. 특별한 인연이라면 특별하다고 볼 수 있다.◇ 어떤 동아리인지 궁금하다SOPT라는 연합 동아리로 선배들이 후배들에게 개발/디자인 등에 대해 강의하는  동아리다. 당시 나는 학년 차가 조금 되어 수업을 듣기보단 가르치는 역할을 맡았어야 했는데, 매주 시간을 내어 수업을 준비할 자신이 없어 디자인 수업을 들었다.◇ 잔디 1호 사원은 역시 남다른 것 같다. 디자인 수업은 어땠는지?C: 그 수업을 통해 내가 디자인에 소질이 없다는 사실을 깨닫게 되었다. 그림을 그리면 늘 내가 생각한 것과는 다른 결과물이 나오더라.◇ 그런데 정말 잔디 1호 사원인가?C: 말 그대로 1호 사원이다. 회사가 법인으로 등록하기 이전부터 함께 했다. 얼마 전 잔디 2주년 파티가 있었다. 나는 입사한 지 2년이 넘었다. 격세지감을 느낀다. 처음 잔디에 들어왔을 때, 나를 제외한 모든 사람이 C-Level이었다. 그리고 나서 개발자, 디자이너가 순차적으로 들어왔던 걸로 기억한다.◇ 법인 설립도 전에 잔디를 어떻게 알고 지원했나?C: 제대를 3개월 앞둔 군인 시절, 아이폰 개발자를 찾는 연락을 받았다. 그렇지 않아도 제대하고 바로 개발 경험을 쌓을 수 있었으면 좋겠다고 생각했다. 솔직히 말하면 그 당시엔 잔디가 어떤 회사인지 탐색이나 해보자는 생각에 멤버들을 만났다.◇ 그럼 사람들을 만나고 입사를 결심한 건가?C: 당시에는 아무것도 없었다. 잔디라고 말은 해도 유형적인 형태의 무언가가 존재하지 않았다. 멋진 사람들과 함께하며 일을 배울 수 있을 것 같다는 생각에 합류했다.◇ 마음가짐이 남다를 것 같다C: 내 스스로 창립 멤버라 생각하고 있다. 어찌 되었든 잔디가 지금의 모습을 갖추기 전부터 함께 해서인지 애착이 남다르다. 첫 직장이라는 사실도 한 몫하고 있고.◇ 그때로 다시 돌아가면 똑같은 결정을 할 것인가?C: 물론이다. 솔직히 좋은 결정이었다고 생각한다. 잔디가 이렇게 잘 성장하고 있고, 지금은 누구보다도 잔디의 성공을 확신한다.◇ 마지막 질문이다. 여름 휴가 계획은?C: 스타트업인이 휴가라니? 하하. 농담이다. 아쉽지만 아직 여름 휴가 계획이 없다. 생기면 알려주겠다.◇ 맛있는 인터뷰의 공식 마무리! 다음 인터뷰이에게 묻고 싶은 질문이 있다면?C: 꼭 물어봐 주셨으면 한다. “잔디에서 이루고 싶은 꿈이 있다면?”을 물어봐 달라.#토스랩 #잔디 #JANDI #iOS #개발자 #모바일개발자 #앱개발자 #팀원소개 #팀원인터뷰 #팀원자랑 #기업문화 #조직문화 #사내문화
조회수 1070

[Buzzvil People] Jen Yoon, Technical Account Manager

 Buzzvil People에서는 다양한 배경과 성격 그리고 생각을 지닌 버즈빌리언들을 한 분 한 분 소개하는 시간을 갖습니다. 어떻게 버즈빌에 최고의 동료들이 모여 최고의 팀을 만들어가고 있는 지 궁금하시다면, 색색깔 다양한 버즈빌리언들 한분 한분의 이야기가 궁금하시다면, Buzzvil People을 주목해주세요.1. 간단한 자기 소개 부탁드립니다. 안녕하세요, 버즈빌에서 기술지원 업무를 담당하고 있는 Jen입니다. 본명은 윤진 (Yoon Jeen) 인데요, 입사 당시 ‘본명이 매일 불리면 일상과 회사가 구분되지 않지 않을까’라는 생각에 이름에서 한 자를 줄여 Jen으로 결정했습니다. 그런데 얼마 지나지 않아 프로덕트 팀의 윤진한 매니저가 Jin 이라는 이름으로 입사했지요. 덕분에 매일 제 이름을 제 입으로 부르고 있습니다. 버즈빌에 입사하기 전에 소셜 스타트업, 푸드 스타트업, 인테리어 O2O 스타트업 총 세 곳에서 인턴으로 근무한 경험이 있습니다. 그리고 2017년 중순 버즈빌에 입사하여만 1년 3개월 동안 BD(Business Development)팀의 전략 매니저로 일했으며, 올해 초부터 BD팀 업무와 TAM(Technical Account Manager) 업무를 겸하다가 올 10월에 정식으로 TAM 업무를 전담하게 됐습니다. 2. 어떻게 버즈빌에 오시게 되셨나요? 지인의 추천으로 버즈빌에 대해 알게 됐고, 미래에 제가 사업을 할 때 도움이 될 좋은 회사라고 판단해 입사하고자 마음먹게 됐습니다. 몇 번의 인턴 경험을 통해 세운 ‘회사를 고르는 기준’ 중 1순위는 나중에 사업을 할 때 도움이 될 만한 곳에 가자는 것이었습니다. 세부적으로는 1) 리더가 능력 있고 2) 사람들에게서 많은 것을 배울 수 있고 3) 서로의 의견을 경청하는 곳이어야 한다는 것이었어요. 지인이 버즈빌을 추천하면서 해준 말들이 이 모든 것에 너무나 아름답게 부합했답니다. 더해서 CEO인 John이 대학교 특강에 연사로 와서 해준 설명을 듣고 입사에 대한 더욱 강한 열망을 품게 됐습니다. 그래서 학기 중에 면접을 보고, 여러 우여곡절 끝에 감사하게도 합격하게 됐어요. 면접을 보는 기간 과제 때문에 엄청나게 힘들어하기도 했고, 합격하지 못할까 봐 얼마나 노심초사했는지 모르겠습니다. 뽑아주신 덕에 8학기에 교환학생을 가려 했던 계획을 내던지고 지난 8월에 졸업해 완전한 직장인이 됐답니다. 3. 버즈빌에서 어떤 업무를 담당하고 계신가요? Technical Account Manager 의 업무는 크게 3가지입니다.  연동문서 관리 – 버즈빌에서 보유하고 있는 모든 프로덕트 및 기능에 대한 문서를 관리하고 있습니다. 문서를 작성하고 적절히 연결 짓고 업데이트하고 이에 대해 내외부에 설명하는 부분까지 포함됩니다. 파트너사와 개발 미팅이 있으면 문서를 설명하기 위해 참석하기도 하고요. 내외부 이슈 해결 – 업무시간의 가장 큰 비중을 차지하는 부분은 외부 파트너의 기술 관련 이슈 해결입니다. 버즈스크린 등의 SDK를 연동한 퍼블리셔(Supply 단) 및 버즈빌이 제공하는 광고 트래킹 기능을 연동한 광고주(Demand 단)까지가 모두 파트너에 포함됩니다. 우선 연동하면서 이상이 없도록 잘 안내하고, 이후 연동이 마무리되어 출시된 이후의 이슈를 처리하며, 추가 작업이 필요할 경우 개발팀에 전달하는 것까지 모두 이에 해당합니다. 내부 프로세스 개선을 위한 요청을 관리하는 것 또한 이 업무에 포함됩니다. 프로세스 세팅 및 개선 – 위 모든 것이 수많은 커뮤니케이션을 통해 이루어지기 때문에, 효율적으로 진행될 수 있도록 유관 팀과 협력하여 프로세스를 세팅하고, 현재 비효율적인 부분을 개선해나갑니다. 그리고 세팅하는 것보다 더 중요하게 이러한 프로세스가 잘 지켜지도록 안내하는 것도 업무의 일부입니다.  얼마 전까지는 하루 대부분을 문의와 이슈를 받아 이를 해결 or 전달하고, 있었던 커뮤니케이션에 대해 기록을 남기고 유관 팀에 공유하는 것에 할애해왔어요. 버즈빌의 사업 규모가 상당해서 모든 이슈를 관리하고 프로세스를 만들고 기록까지 하다 보면 하루가 늘 빠듯하더라고요. 감사하게도 최근에 좋은 분들이 팀에 참여해주셔서, 연동문서 체계화 및 문서화 쪽에 보다 집중할 수 있는 시간이 생기고 있습니다. 업무를 하면서 가장 곤란한 순간은 ‘이게 왜 이렇게 되어있지?’ 하게 되는 때입니다. 주로 개별 적용 되어있는 부분에 대해 히스토리가 남아있지 있거나, 기능 등이 실제로 어떻게 작동하는지 또는 사용되는지 몰라서 나오는 의문들입니다. 다행히 BD팀에서 파트너사를 운영하면서 알고 있었던 히스토리와 경험이 있어서 많은 도움이 되는 것 같아요. 이를 저뿐 아니라 다른 분들이 궁금하면 언제든지 아실 수 있도록 기록을 남기는 일에 힘쓰고 있습니다. 4. 스타트업에서 혹은 광고업계에서 일하는 느낌이 어떠세요? 스타트업으로서는 모든 것을 내 손으로 빚어나가는 느낌이에요. 이건 이전 회사들에서도 많이 느꼈던 부분이네요. 물론 버즈빌은 제가 입사할 당시부터 규모가 꽤 큰 상태였고 갖춰져 있는 시스템이 많아 매우 놀랐지만, 그런데도 많은 것들을 직접 만들어나가야 하는 ‘스타트업’이에요. 그래서 할 것이 너무 많아 버겁고 힘들 때도 있지만, 결국 제가 원하는 그림을 그려나가며 최대한 완벽하게 만들어나갈 수 있어서 매일 도전적이고 스릴 넘치는 하루를 보냅니다. 저뿐 아니라 버즈빌리언 모두 각자의 자리에서 본인에게 주어진 도전들을 클리어해나가는 것 같아요. 광고업계의 스타트업으로서는 변화와 적응의 결정체랄까요. 제가 모든 업계에 종사해본 것은 아니지만, 광고업계 특성 상 변화의 속도가 더 빠른 것 같아요. 수요와 공급 쪽의 요구가 계속해서 넘쳐나고, 그 안의 플레이어들도 계속해서 나타나서 변화와 혁신이 끊이지 않는 것 같습니다. 그 와중에 플랫폼 별 정책도 변화하게 되니 그야말로 내일을 점칠 수 없는 업계인 것 같아요. 그래서 저의 업무도 매일 변화하고 새로운 상품이나 퍼블리셔에 적응하는 과정의 일부입니다. 저는 이런 역동성이 좋아서 버즈빌에서 만족하며 일하고 있어요. 버즈빌에서 일하면서 받는 느낌은, 일이 제 삶 대부분을 차지해버렸다는 점이겠네요. 하루 대부분을 회사에서 보내고 그 외의 시간에도 회사나 일 생각을 하는 것 같아요. 이것의 장단점이 있다보니 이제 워라밸도 생각하려 하고는 있지만, 여전히 제 생각의 대부분을 차지하고 있는 것이 바로 버즈빌이랍니다. 5. 이것만큼은 버즈빌이 참 좋다! 어떤 게 있으실까요? 일단 사람들이 본인 일과 다른 사람에 대한 애정을 품고 있어요. 그 덕에 무엇 하나 대충 넘기는 일 없이 다 같이 힘을 모아 일을 해낼 수 있습니다. 다들 사소한 질문에도 답을 찾기 위해 적극적으로 도와주시고, 짜증 내지 않고 웃으며 대해주세요. 그래서 크고 작은 문제를 맞닥뜨렸을 때 혼자 끙끙대는 일 없이 헤쳐나갈 수 있고, 저도 질문을 받았을 때 더 신나게 도와드릴 수 있는 것 같습니다. 두 번째로는 회사가 커뮤니케이션을 굉장히 중시한다는 점이에요. 솔직히 저는 우리나라에서 나이에 따른 꼰대질이 없는 회사는 정말 희귀할 거로 생각하는데요, 버즈빌은 그 희귀한 곳 중 하나입니다. 제가 언제 어디서든 적극적으로 의견을 낼 수 있고, 아닌 것에 대해 아니라고 말할 수 있는 회사가 얼마나 있을까요. 말만 번지르르하게 하는 것이 아니라 실제로 모두의 의견을 존중하려는 버즈빌의 문화는 버즈빌의 큰 동력 중 하나라고 생각해요. 마지막으로 사람들이 끊임없이 자기발전을 위해 노력합니다. 기본적으로 사람들의 성향이 그런 것도 있고 회사도 적극적으로 지원해줘서 더 시너지가 나는 것 같아요. 그 덕에 저도 많은 자극을 받고 더 실력 있는 사람이 될 방법은 무엇일까 고민하게 돼요. 매일매일 기분 좋은 압박감 같은 것을 느끼고 있답니다. 6. 개인적인 목표나 꿈이 있으신가요? 있다면, 버즈빌에서의 경험이 어떻게 도움이 된다고 생각하시나요? 저는 ‘기댈 수 있는 존재’가 되고 싶습니다. 누군가 힘들 때 제게 말하거나 의지해줄 때 제 존재의 의미를 느끼거든요. 개인적으로는 누군가 힘들 때 찾아올 수 있는 사람이 되고 싶어요. 한때 ‘따뜻한 사람이 되고 싶다’ 정도로 마음에 품고 살았는데, 최근에 마냥 따뜻한 것을 넘어서 누군가 찾아올 수 있는 사람이 되고 싶다고 생각이 바뀌었어요. 그리고 삶의 큰 목표로서도 마찬가지로 즐거울 때든 힘들 때든 기댈 수 있는 상품이나 서비스로 사업을 하고 싶습니다. 사실 삶에서 힘이 되는 것들이 엄청 대단한 건 아닌 것 같아요. 아침에 눈을 떠서 오늘 좋아하는 사람들과 점심 약속이 있다는 사실에 벌떡 일어나기도 하고, 아무리 새벽까지 힘들게 일하고 집에 가도 고양이가 품에 안겨있으면 피로가 사르르 녹는 것처럼요. 그래서 일확천금의 기회를 준다거나 인생을 반전시킬 만큼의 서비스는 아니라도, 잔잔한 일상 속에서나 마음이 벼랑 끝에 몰리고 지친 순간이나 머릿속에 떠오를 만한 그런 사업을 하고 싶습니다. 아무래도 관심사가 음식, 동물, 심리 이런 분야이다 보니 그 교차점에 있는 무언가를 꼭 하고 싶어요. 아침마다 고양이들에게 둘러싸여 오늘의 요리를 준비한다면 더할 나위 없이 행복할 것 같네요. 일단 버즈빌에서의 직무가 저의 이런 꿈과 관련하여 참 잘 맞는 것 같아요. 누군가가 저를 필요로 하고 불러주는 게 정말 진심으로 너무 좋답니다. 그걸 잘 처리해드리지 못할 때는 매우 힘들지만, 제 이름을 불러주실 때마다 “넵!!” 하고 대답하는 게 항상 행복하고 감사해요. 그런 점에서 현 상태만으로도 제가 되고 싶은 제 모습을 만들어나가는 데에 도움이 많이 됩니다. 두 번째로는 나중에 사업을 할 때 도움이 될 인사이트를 많이 얻어가고 있어요. 특히 사업의 확장성에 대한 것인데요, 어릴 때 단순 자영업을 꿈꾸다가 더 많은 사람에게 도움이 되면 좋지 않을까 생각해서 기업가로 꿈을 바꾸었어요. 그와 유사하게, 버즈빌에 들어와 B2B 사업을 경험하면서 파트너사가 지닌 경험과 사용자를 leveraging하여 투자하여 더 큰 확장성을 가질 수 있다는 것을 절절히 느꼈습니다. 해서 나중에 사업을 하면서 다른 주체와 협업하고 소통하는 과정에서 지금 버즈빌에서 BD매니저, 그리고 현재 TAM으로서의 경험이 아주 큰 도움이 될 거라고 확신합니다. 이에 더해서 좋은 회사란 무엇인가 고민하고, 그 고민을 통한 액션 아이템이 도출되고 실행되는 과정에 함께하는 것 또한 나중에 제 일을 할 때 큰 도움이 될 것 같습니다. 마지막으로 모든 것을 다 떠나서 너무나 좋은 사람들과 함께 웃고 이야기하고 일할 수 있다는 것 자체가 제 인생을 풍요롭게 해줍니다. 단 하루도 쉬이 넘어간 적이 없지만, 동시에 즐겁게 웃지 않았던 날도 없어요. 앞으로 버즈빌에서 제 삶이 어떻게 될지 모르지만, 여기에서 시간과 경험과 인연은 제 삶에 보석 같은 존재로 남을 것 같습니다. 
조회수 4854

“디자인과 기술을 이어주는 존재, 마크업 개발자를 함께 알아볼까요?” - 유저플로우셀 오혜진

'마크업 개발자', 아직은 우리들에게 다소 생소한 직군이죠. '마크업 개발자'는 디자이너와 개발자 사이에서 '오작교' 같은 역할을 하는 아주 중요한 포지션이에요. 오늘은 코인원의 마크업 개발자로 활약 중인 혜진님과 이야기를 나눠보려 해요. 자신의 위치에서 묵묵히 유저 친화적인 웹 환경을 만들어나가고 있는 혜진님을 만나러 가보시죠!사실 이미 혜진님은 지난 4월 13일(토), 테크 업계 여성들의 목소리에 집중하는 소중한 행사 ‘Women Techmakers Seoul 2019’에서 ‘스타트업에서 마크업 개발자로 살아남기’를 주제로 자신의 이야기를 널리 알리고 왔답니다. 스타트업 그리고 코인원에서 마크업 개발자로 살아남는 혜진님만의 방법은 무엇일까요? :-)Q. 혜진님 안녕하세요, 자기소개 부탁드립니다.안녕하세요, 코인원 유저플로우셀에서 마크업 개발자로 일하고 있는 오혜진입니다. 유저플로우셀은 암호화폐 거래와 프로차트와 같은 트레이딩 영역을 제외한 전반적인 서비스 영역을 담당하고 있어요. 특히 ‘셀'이라는 목적조직으로 개편된 이후 PM, 디자이너, 개발자가 한곳에 모여 누구나 코인원에서 거래를 하고 싶은 마음이 들도록 매력적인 곳으로 탄생시키고 있답니다. 저는 셀안에서 마크업 개발자로 일하며 디자이너와 프론트엔드 개발자를 이어주는 다리 역할을 하고 있습니다.Q. 지난 ‘Women Techmakers Seoul 2019’에서 마크업 개발자를 널리 알리는 발표를 했다고 들었어요. 어떤 내용인지 소개해주세요!감사하게도 ‘스타트업에서 마크업 개발자로 살아남기' 라는 주제로 300명이 넘는 관중들 앞에서 발표를 하고 왔습니다. (사실, 많은 분들이 와주셔서 땀이 좀 나기도 했고요;) 마크업 개발자는 스타트업에서 발견하기 힘든 직군이기도 해요. 보통은 웹 에이전시에 많이 속해 있거든요. 제가 마크업 개발자로 일한지 6년이라는 시간 동안 스타트업에서 어떤 방식으로 일해왔는지 알리고 싶었어요. 그래서 지금까지 이런 일들을 해왔고, 앞으로도 더 활발하게 할 것이라고 속시원하게 말하고 왔습니다.Q. 마크업 개발자는 구체적으로 어떤일들을 하나요?마크업 개발자는 한마디로 디자인(Design)과 기술(Tech)의 오작교 같은 존재입니다. 디자인의 의도가 개발과 충돌하는 부분은 없는지 파악하고, 개발에 잘 녹아들 수 있도록 프론트엔드의 앞단을 맡고 있어요. 코인원 웹 서비스에서 제공하는 신규 기능의 마크업 개발을 담당하고, 운영하면서 생긴 이슈들을 처리합니다. 또한 마크업 레거시에 대한 유지보수 작업도 병행하죠.예를 들어, 코인원의 회원가입 페이지를 제작할 때 디자인 작업을 먼저 들어갑니다. 그럼 디자인 작업을 바탕으로 개발자들이 기능을 만들어 넣게 돼요. 이 때, 기능적인 개발을 제외하고 UI(User Interface)적인 부분을 제가 담당합니다. 회원가입 페이지에는 이메일 인증, 휴대폰 인증 등 여러가지 개발요소들이 많아요. 그래서 개발하기 전에 기능이 들어가는 기본적인 레이아웃을 만들어 개발자에게 전달합니다. 마크업 작업이 바탕이 되어 그 위에 기능 개발이 이뤄진다고 보시면 돼요.디자이너가 레시피를 만드는 사람이라면, 마크업 개발자는 레시피 재료를 세팅해 주는 사람이에요. 개발자들은 세팅된 레시피를 끓이고 버무려 요리를 완성시키고요. 저는 좋은 요리가 탄생할 수 있도록 중간과정을 도와주는 역할인거죠. ▲ 'Women Tachmakers 2019'에서 발표에 열중한 혜진님!Q. 디자인과 기술의 중간 역할을 담당하고 계시군요, 사실 중간자의 역할이라고 하면 이어주는 과정에서 고충(?)이 생길 것 같아요.아무래도 디자이너와 개발자, 양쪽과 다 소통해야하는 부분입니다. 디자이너 입장에서는 ‘왜 프론트엔드에서 이 디자인이 안되는걸까?’ 라는 불만이 생길때도 있고, 프론트엔드에서는 ‘왜 디자인이 이렇게 들어가야 하는걸까?’ 라고 이해를 못할 때도 있어요. 서로의 이해관계를 잘 전달해야 한다는 점이 나름의 고충이죠. 코인원에서는 ‘디자이너 - 마크업 개발자 - 프론트 개발자’의 협업 프로세스를 정립해서 각자가 맡은 분야에 집중 할 수 있는 초석을 다졌어요. 무엇보다도 배경이 다른 세 개의 직군이 원활하게 소통할 수 있는 체계가 잡혀 고충이 해결되고 있습니다 :) Q. 그렇다면 마크업 개발자는 어떤 부분을 기여한다고 볼 수 있나요?코인원 메인 화면에 기능 개발을 추가하지 않고도 마크업단에서의 처리만으로도 쉽게 변화를 줄 수 있습니다. 메인화면의 배너 이미지는 유저들이 코인원에 접속해 제일 먼저 마주하는 부분이죠. 그래서 유저들이 코인원의 시각화된 정보를 빠르게 접할 수 있도록 이미지를 교체합니다. 웹 페이지의 운영 측면에서 비주얼 개편을 빠르게 할 수 있는 환경을 만들어 놓고 대응하는거에요.곧 코인원 마이페이지 화면이 개편될겁니다. 웹 페이지를 새로 만든다는 것은 무에서 유를 창조하는 과정과 같아요. 제가 마크업 개발을 잘 해놓으면 다른 직군에게도 도움이 됩니다. 개발 속도도 더 잘 붙고, 디자인에서도 빈공간이 없는 페이지가 탄생하는거죠. 최대한 밑바탕을 꼼꼼하게 만들어 모두가 일에 더 집중할 수 있는 환경을 만든다고 보시면 돼요.Q. 코인원 마이페이지에서 새롭게 바뀌는 부분은?기존의 마이페이지는 유저들이 보기에 정리가 잘 안되어있다는 느낌이 있었어요. 어떤 인증과정을 끝마쳐야 하는지 한눈에 들어오지 않는 부분이 있었거든요. 이번에 개편될 마이페이지는 좀 더 명확해졌습니다. 이전의 인증페이지가 도돌이표의 느낌이었다면, 이번에는 UX(User experience)를 생각해서 flow 개선도 많이 이뤄졌습니다. 편리한 암호화폐 거래 경험을 코인원에서 느낄 수 있어요. (새롭게 바뀔 마이페이지 많은 기대 부탁드립니다! 물론 편리한 암호화폐 거래도 언제나 코인원!)Q. 유저들에게 편리한 거래경험을 선사하기 위해 어떤 가치를 가장 중요시 여기나요? 저는 중간자이므로 유저들 뿐만 아니라 개발까지 두 가지 측면을 모두 고려합니다. 유저의 입장에서 사용성과 접근성이 용이한 마크업을 짜려고 하고, 개발측면에서는 유지보수가 편리한 마크업을 최대한 짜려고 해요. 개발하기 편한것과 사용하기 편한 것은 다른 맥락이거든요. 요새는 코인원 디자인시스템을 적용하고 있어요. 디자이너 분들이 정리해주신 디자인 시스템을 잘 적용시켜서 코드적으로도 재사용성이 용이하게 관리가 되도록 하고, UI도 정돈이 되어가는 과정을 진행 중입니다. 이런 과정을 계속 거치면 유저들에게 편리한 거래 경험을 선사하는 부분은 놓치지 않을 것 같아요.▲ 마크업에 열중하고 있는 혜진님 (약간의 설정샷 +_+)Q. 코인원 크루로 일하면서 장점을 뽑자면?유저플로우셀은 코인원이 셀이라는 목적조직으로 개편되고나서 만족도가 높은 셀이라고 알고 있어요. 업무도 많은 편인데, 톱니바퀴처럼 잘 맞물린다는 느낌이거든요. 특히 일에 대해서 선긋지 않고, 이슈가 발생했을 때 해결할 수 있는 부분들을 빠르게 파악해주는 부분들이 정말 좋아요. 속도랑 효율성 측면에서 이만큼 해낼 수 있는 팀은 앞으로 만나지 못할 것 같아요. 항상 원활한 업무 소통을 위해 힘써주시는 셀원들에게 감사 드립니다!Q. 앞으로 이루고 싶은 목표가 무엇인가요?회사 안 뿐만 아니라, 바깥에서의 활동도 꿈꾸고 있어요. 마크업 개발자들이 모두 모여 이야기할 수 있는 CSS 컨퍼런스를 열어 좀 더 커뮤니티를 활성화 시키고 영향력을 높이고 싶습니다. 아직 마크업 개발자들만이 모여서 이야기 할 수 있는 곳이 부족하거든요. 저의 이야기도 차곡차곡 쌓아서 여러 창구를 통해 들려드리고 싶고요.코인원에서는 지금 하는 것 이상으로 마크업 개발도 열심히 할거에요. 우선 단기적인 목표로, 프론트엔드에서 사용하고 있는 angular에 대한 이해력을 높일 겁니다. 마크업 컴포넌트 단위에 최적화 된 CSS로 개편해서 사용하지 않는 스타일 리소스가 최소화가 되도록 만들거에요.▲ 마크업 개발자에 많은 관심 부탁드려요 :)디자이너가 디자인에 집중할 수 있게, 개발자가 개발에 집중할 수 있게 ‘일잘러’로 통한다는 혜진님. 혜진님의 인터뷰를 통해 ‘마크업 개발자’에 좀 더 친해지는 시간이길 바라봅니다. 그리고 이렇게 멋진 코인원 크루와 함께 성장하고 싶지 않으세요?  현재 코인원은 멋진 크루들과 함께 크립토갤럭시를 헤쳐나갈 분들을 기다리고 있습니다 :-)
조회수 2298

Angular Lazy Loading 모듈 사용하기

Angular는 비동기식 라우팅이 가능합니다. 나중에 사용할 기능들을 NgModule로 분리하여 사용자의 요청이 들어왔을 때 모듈을 불러와 기능을 사용할 수 있고, 이러한 기술을 지연 로딩이라 합니다.프로젝트가 진행되고 기능이 추가될수록 어플리케이션 번들 크기가 커지고, 결국엔 초기 로딩 시간도 길어지게 됩니다. 지연 로딩을 사용하면 초기 로딩 시간을 줄일 수 있습니다. 컴파일 단계에서 나중에 사용할 모듈들을 메인 모듈에서 분리하여 번들을 생성합니다. 그리고 사용자가 기능을 요청할 때 비동기로 스크립트를 불러와 실행합니다. 지연 로딩에 대한 소개와 사용법은 Angular 공식 문서의 Routing & Navigation — Milestom 6: Asynchronous routing 을 참고하시길 바랍니다하지만 지연 로딩을 사용할 때 유의해야할 점이 몇 가지 있습니다.지연 로딩 모듈과 인젝터(Injector)지연 로딩이 완료되었을 때 Angular는 지연 로딩된 모듈을 루트 인젝터(Root Injector)의 자식이 되는 자식 인젝터를 이용하여 초기화하고, 서비스들을 자식 인젝터에 추가합니다. 즉, 인젝터가 분리되기에 지연 로딩된 모듈의 클래스들은 자식 인젝터로의 서비스 주입이 가능하지만 루트 인젝터로 만들어진 클래스들은 불가능합니다.이는 Angular의 독특한 의존성 주입 시스템 때문입니다. Angular의 인젝터는 처음 애플리케이션이 시작되었을 때, 컴포넌트나 다른 서비스에 주입되기 전에 포함된 모든 모듈들의 서비스 제공자들을 블러와 루트 인젝터를 생성합니다. 애플리케이션이 시작되고 나면 인젝터는 서비스들을 생성하고 주입을 시작하고, 새로운 서비스들을 제공자로 추가가 불가능합니다.그러므로 지연 로딩된 서비스들은 이미 생성이 완료된 루트 인젝터로 추가가 불가능합니다. 따라서 Angular는 지연 로딩된 모듈에 대해서 새로운 자식 인젝터를 만들는 전략을 취하게 된 것입니다.자식 인젝터가 새로 만들어지기 때문에 공통된 모듈을 사용할 때 주의하여야 합니다. 예를 들어 다음과 같이 SharedModule 에 CounterService 를 서비스로 추가하고 루트 모듈인 AppModule 과 지연 로딩 모듈인 LazyModule 에 각각 SharedModule 을 import 하였습니다.import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { SharedModule } from './shared/shared.module'; import { AppShellComponent } from './app-shell.component'; const APP_ROUTES = [ { path: 'lazy', loadChildren: 'app/lazy/lazy.module#LazyModule' } ]; @NgModule({ imports: [ BrowserModule, SharedModule, RouterModule.forRoot(APP_ROUTES) ], declarations: [ AppShellComponent ], bootstrap: [AppShellComponent] }) export class AppModule { }import { Injectable } from '@angular/core'; @Injectable() export class CounterService { count = 0; increase(): void { this.count++; } decrease(): void { this.count--; } }import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { SharedModule } from '../shared/shared.module'; import { SomeLazyComponent } from './some-lazy.component'; const LAZY_ROUTES = [ { path: '', component: SomeLazyComponent } ]; @NgModule({ imports: [ SharedModule, RouterModule.forChild(LAZY_ROUTES) ] }) export class LazyModule { }import { NgModule } from '@angular/core'; @NgModule({ providers: [ CounterService ] }) export class SharedModule { }그리고 루트 모듈의 컴포넌트와 지연 로딩 모듈의 컴포넌트에서 각각 CounterService 를 사용하여 숫자 값을 바꿔봅니다.서로 다른 인젝터에 CounterService 인스턴스가 만들어졌기 때문에 두 컴포넌트에 표시되는 숫자값은 다릅니다. 앞에서 말했듯이 지연 로딩 모듈은 루트 인젝터가 아닌 자식 인젝터를 이용하여 초기화하기 때문입니다.만약, 지연 로딩 모듈에서 제공되는 서비스를 다른 모듈에서 사용하려면 루트 모듈에 포함시켜 줘야 합니다. 다음과 같이 루트 모듈에게만 노출시킬 서비스 제공자들을 따로 빼내어 줄 수 있습니다.import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AccountLoginPageComponent } from './login-page.component'; const ACCOUNT_ROUTES: Routes = [ { path: 'login', component: AccountLoginPageComponent } ]; @NgModule({ imports: [ ... RouterModule.forChild(ACCOUNT_ROUTES) ], decalartions: [ AccountLoginPageComponent ] }) export class AccountLazyModule { }import { ModuleWithProviders, NgModule } from '@angular/core'; import { AccountAuthService } from './auth.service'; @NgModule({ imports: [...] }) export class AccountModule { static forRoot(): ModuleWithProviders { return { ngModule: AccountModule, providers: [ AccountAuthService ] }; } }AccountModule.forRoot() 를 루트 모듈에 import하면 다른 모듈에서도 AccountAuthService 를 사용할 수 있게 됩니다. 물론 이 경우 AccountModule를 지연로딩 모듈로 만들면 루트 모듈에 포함되기 때문에 번들을 나누는 의미가 없어질 수 있으니 AccountLazyModule 을 따로 두어 코드를 분리하였습니다.#타운컴퍼니 #개발 #개발자 #인사이트 #꿀팁
조회수 1980

다양한 형태를 지원하는 리스트 UI, 잘 그리고 계신가요?

대략 1년 반 전, 5.0 롤리팝과 함께 나타난 RecyclerView. ListView 를 이용할 때 아주 기초적이고 정석적인 개념으로 사용되던 ViewHolder pattern 을 반 강제화? 하면서 동시에 성능까지 개선한 ListView 의 개량버전.앱 시장이 활성화되면서 한 가지 타입의 뷰만 반복적으로 보여주는 단순한 구성보다는 다양한 타입의 뷰를 보여주는 앱들이 많아지고 보편화 된 시점에 이것을 구현하기 위한 Adapter.getView 메소드는 혼돈.chaos 가 되었지요. 가독성을 높일만한 나름대로의 시도를 해보고 있을 때, RecyclerView 가 갑툭튀 했고 이걸 이용하면 원하는 만큼의 많은 타입의 뷰를 “가독성 좋게 만들어 볼 수 있겠다” 라는 생각이 들었습니다.그래서 RecyclerView.Adapter 를 상속 받아 다양한 타입의 뷰를 바인딩 할 수 있게 도와주는 헬퍼 클래스, MultiItemAdapter 라는 것을 만들어 보게 됐습니다. 구 회사 프로덕트에 적용해보기도 하고, 개인 프로젝트에 넣어보기도 하고, 토스랩에서 서비스하고 있는 “잔디”에 녹여내보기도 했는데 나쁘지 않은 느낌이들어 그 과정을 공유하고 많은 분들께 피드백도 받고 싶습니다. 또, 어떻게 더 잘 활용하고 계신지 여쭙고 싶습니다.RecyclerView.Adapter 의 이해를 위해 단순단순하게 만들어보자public class BasicAdapter extends RecyclerView.Adapter { private List mItems = new ArrayList<>(); @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()) .inflate(android.R.layout.simple_list_item_1, parent, false); return new MyViewHolder(itemView); } @Override public void onBindViewHolder(MyViewHolder holder, int position) { holder.mTextView.setText(mItems.get(position)); } class MyViewHolder extends RecyclerView.ViewHolder { private TextView mTextView; public MyViewHolder(View itemView) { super(itemView); mTextView = (TextView) itemView.findViewById(android.R.id.text1); } } ... 이런 식으로 구현하면 되는군, 하지만 내가 최종적으로 원하는 건 다양한 ViewHolder 를 다뤄야 되는 건데 ViewHolder 가 많아지는 경우 inner class 는 쓰면 안되겠다! ViewHolder 들은 따로 패키지 만들어서 관리하자. 음 근데 ViewHolder 를 구성하고 난 다음 어떻게 그려지는 지에 대해 궁금하면 다시 어댑터를 찾아가야 되고, 반대로 어댑터에서 ViewHolder 내 구성요소가 어떻게 생겼는지 궁금하면 다시 ViewHolder 찾아가서 뒤져봐야되는 군. 이건 비효율 적인 것 같다. ViewHolder에 뷰를 그리는 메소드를 하나 만들자. 아 기왕이면 추상화된 클래스를 만들어 돌려돌려 쓰자. 하나 더 Generic 을 사용하자.public abstract class BaseViewHolder extends RecyclerView.ViewHolder { public BaseViewHolder(View itemView) { super(itemView); } public abstract void onBindView(ITEM item); } 뷰를 그리는데 쓰이는 객체는 Generic 을 이용하면 ViewHolder 안에서 그리는 작업 또한 해결이 가능하겠군! 이걸 이용해서 다시 만들어보자.public class MyViewHolder extends BaseViewHolder { private TextView mTextView; public MyViewHolder(View itemView) { super(itemView); mTextView = (TextView) itemView.findViewById(android.R.id.text1); } @Override public void onBindView(String item) { mTextView.setText(item); } } ... public class BaseAdapter extends RecyclerView.Adapter { private List mItems = new ArrayList<>(); @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()) .inflate(android.R.layout.simple_list_item_1, parent, false); return new MyViewHolder(itemView); } @Override public void onBindViewHolder(MyViewHolder holder, int position) { holder.onBindView(mItems.get(position)); } public void setItems(List items) { mItems.clear(); mItems.addAll(items); } @Override public int getItemCount() { return mItems.size(); } } 음 원하는 모양새다. 근데 이제 Adapter 에선 ViewHolder 에 들어갈 layout 이 어떤 건지 관심꺼도 되겠네. 게다가 ViewHolder 에서 layout 궁금하면 다시 또 찾아와야 되는게 문제다. 좀 더 명시적인 방법으로 Factory method 로 생성자를 제한해보자. RecyclerView.ViewHolder 는 View 를 가지는 생성자가 강제되니 이렇게 바꾸자.public static MyViewHolder newInstance(ViewGroup parent) { View itemView = LayoutInflater.from(parent.getContext()) .inflate(android.R.layout.simple_list_item_1, parent, false); return new MyViewHolder(itemView); } private MyViewHolder(View itemView) { super(itemView); mTextView = (TextView) itemView.findViewById(android.R.id.text1); } 이렇게 하면 어떤 layout 을 다루고 있는지도 금방 알 수 있겠다. 이 정도만 되도 구색을 다 갖춘듯하니 이 느낌으로 다양한 타입의 뷰들을 다뤄보자.public class BasicMultiTypeAdapter extends RecyclerView.Adapter { public static final int VIEW_TYPE_A = 0; public static final int VIEW_TYPE_B = 1; private List mItems = new ArrayList<>(); @Override public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_A) { return AViewHolder.newInstance(parent); } else { return BViewHolder.newInstance(parent); } } @Override public void onBindViewHolder(BaseViewHolder holder, int position) { holder.onBindView(mItems.get(position)); } public void setItems(List items) { mItems.clear(); mItems.addAll(items); } @Override public int getItemCount() { return mItems.size(); } @Override public int getItemViewType(int position) { if (position % 2 == 0) { return VIEW_TYPE_A; } else { return VIEW_TYPE_B; } } } 음 깔끔하긴 하다. 근데 getItemViewType 이 스크롤 할 때마다 불릴 텐데, 분기도 많고 연산이 생겼을 때 스크롤 속도에 괜한 영향을 줄 듯? view type 을 차라리 미리 가지고 있게 만들자. 또! 가만보니 한 타입의 객체를 이용해서 다른 스타일로 뷰를 보여줄 뿐이었네. 이것도 여러가지 객체를 담을 수 있게 만들어야지.뷰를 그릴 대상이 될 객체랑 타입을 가지는 Wrapper class 를 만들어서 해결하자. 이러면 Adapter.onBindViewHolder 랑 Adapter.getItemViewType 도 해결이 되겠군.public abstract class MultiItemAdapter extends RecyclerView.Adapter { private List mRows = new ArrayList<>(); @SuppressWarnings("unchecked") @Override public void onBindViewHolder(BaseViewHolder holder, int position) { holder.onBindView(getItem(position)); } @SuppressWarnings("unchecked") public ITEM getItem(int position) { return (ITEM) mRows.get(position).getItem(); } public void setRows(List mRows) { mRows.clear(); mRows.addAll(mRows); } @Override public int getItemCount() { return mRows.size(); } @Override public int getItemViewType(int position) { return mRows.get(position).getItemViewType(); } public static class Row { private ITEM item; private int itemViewType; private Row(ITEM item, int itemViewType) { this.item = item; this.itemViewType = itemViewType; } public static Row create(T item, int itemViewType) { return new Row<>(item, itemViewType); } public ITEM getItem() { return item; } public int getItemViewType() { return itemViewType; } } } MultiItemAdapter 완성.네, 저는 이렇게 만들어서 1년 반 정도 필요한 부분(복잡해 질만한 부분)에 이 클래스를 상속받아 구현했습니다. 사용방법을 예로들어 데이터베이스나 서버로부터 긁어온 아이템들을 타입에 따라 A, B로 나눠서 보워줘야 한다면,// MutiItemAdapter 구현 public class AdvancedItemAdapter extends MultiItemAdapter { public static final int VIEW_TYPE_A = 0; public static final int VIEW_TYPE_B = 1; @Override public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_A) { return AViewHolder.newInstance(parent); } else { return BViewHolder.newInstance(parent); } } } // Activity 나 Fragment 등 view 요소에서 ListAdapter item setting. public void setItems(List items) { List rows = new ArrayList<>(); for (int i = 0; i < items xss=removed>이렇게 해주면 됩니다. 그런데 위 사용방법을 보면 추가적인 새로운 타입(Row)의 List 와 반복문을 돌려야 된다는 것이 단점으로 보이는데요. 그럼 이 클래스를 사용하지 않고 직접 구현한 결과를 좀 볼까요?public class NormalItemAdapter extends RecyclerView.Adapter { public static final int VIEW_TYPE_A = 0; public static final int VIEW_TYPE_B = 1; private List mItems = new ArrayList<>(); @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_A) { View itemView = LayoutInflater.from(parent.getContext()) .inflate(android.R.layout.simple_list_item_1, parent, false); return new AViewHolder(itemView); } else { View itemView = LayoutInflater.from(parent.getContext()) .inflate(android.R.layout.simple_list_item_1, parent, false); return new BViewHolder(itemView); } } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (holder instanceof AViewHolder) { Item item = getItem(position); ((AViewHolder) holder).getTextView().setText(item.getName()); } else { ((BViewHolder) holder).getTextView().setText("I am B."); } } private Item getItem(int position) { return mItems.get(position); } public void setItems(List items) { mItems.clear(); mItems.addAll(items); } @Override public int getItemViewType(int position) { if (getItem(position).getType().equals(Item.ITEM_TYPE_A)) { return VIEW_TYPE_A; } else { return VIEW_TYPE_B; } } @Override public int getItemCount() { return mItems.size(); } } 뭐, 나쁘진 않습니다. 이 정도 수준으로 개발이 끝나도 되고 추가적인 확장이 필요하지 않아보인다면 굳이 MultiItemAdapter 를 쓸 필요가 없습니다.중요성을 가지는 리스트 위주의 화면에서 위와 같이 개발된다면 당장 보이는 제 불만은 onCreateViewHolder, onBindViewHolder 계속해서 분기가 들어가게 되고 getItemViewType 에서는 계속 해서 List 데이터에 접근해야 한다는 것입니다. 접근 자체가 큰 문제, 큰 영향을 끼치지 않을 정도 규모의 자료구조라면 논외로 치더라도, 뷰 타입이 조금만 늘어나도 onCreateViewHolder, onBindViewHolder 의 덩치는 엄청 커질 겁니다.예를들면 맨 마지막 아이템 타입이 B 이고 현재 추가 될 아이템 타입이 A인 경우에는 다른 형태의 디바이더를 넣어야 한다던지 하는 추가적인 확장이 이루어져야 한다면 골치가 꽤 아플겁니다. 특히 저는 위 예와 비슷하게 뷰 타입에 따라 각기 다른 아래 위 마진값을 요구받을 때, ViewHolder 마다 이전 데이터를 참고하게 만들고 동적으로 Visibility 처리를 하거나 MarginLayoutParams 를 고치는 것이 비효율적으로 느껴져서 height를 주입받는 DividerViewHolder 를 하나 만들어 사용하곤 했습니다. 이렇게 하니 각각의 ViewHolder 들이 데이터들에 의존적이지 않게 코딩이 가능했었습니다. 한 가지 더 예를들어 리스트 중간 중간 광고가 보여지게 되고 이 광고 클래스는 완전히 다른 객체로부터 보여줘야 한다 라고 했을 때 MultiItemAdapter 를 이용하면 쉽게 해결이 가능합니다.정작 근 1년간 “잔디”를 만들면서는 자주 쓰진 않았는데, 작년부터 각광받기 시작한 MVP 패턴을 사용할 때 View 에서의 로직을 최소화 하려고 한다면 써먹을 수 있는 모델로 적합하지 않나 생각이 들면서 다시 사용하기 시작했습니다. Presenter 에서 Row 를 만들어 던져주면 View 는 그것을 그대로 사용하게 만들 수 있다는 생각이 들었거든요.(아직까지는 비교적 크지 않은 부분에서만 사용하게 되서 View(MainThread)에서 Row 를 만들게 코딩해 놓은 컴퍼넌트가 더 많네요 흑흑) 더 복잡한 구조를 갖는 컴퍼넌트를 만들어야 할 때는 비동기 스레드에서 Row 까지 만들어 내보내는 것도 해볼까 하는 생각도 듭니다.제 눈에만 괜찮은 구조인지, 생각지도 못한 치명적인 단점이 있진 않은지, 구조나 설계 측면에서 안 좋은 점은 있지 않은지, 논리없이 Generic 으로 “퉁” 치고 있는 코드는 아닌지, 여러가지가 많이 궁금합니다 ^^ MultiItemAdapter 를 쓴 것과 안 쓴것의 정말 심플한 비교 소스를 열어놓았습니다 MultiItemAdapter 또, 여러분들은 어떻게 구현하고 계신지요? 여러분의 관심이 필요합니다 ! :)#토스랩 #잔디 #JANDI #개발 #개발자 #인사이트 #경험공유
조회수 4013

Eclipse Memory Analyzer 소개

안드로이드 개발을 하다보면 종종 OutOfMemory(OOM)에러를 만나게 됩니다. 이전에 올렸던 포스팅에서도 이 문제로 고생을 했는데요, 메모리 누수 관련 문제는 로직 에러와는 달라서 찾기가 매우 난감한 경우가 많습니다. 이러한 메모리 누수 관련 문제를 해결하기 위한 검사 기능을 제공하는 무료 툴이 있습니다. 바로 Eclipse MAT(Memory Analyzer)(MAT)입니다.Eclipse MATMAT은 사용자로 하여금 힙 메모리의 상황을 파악하게 해주어 메모리 누수 현상과 필요없는 메모리 할당을 감지할 수 있도록 도와줍니다. 또한 자동으로 보고서를 작성하여 어떤 객체들이 메모리 누수를 일으키는지에 대한 추측을 해주는 기능을 제공합니다. MAT은 Eclipse 플러그인이기 때문에 사용하려면 Eclipse가 깔려 있어야 합니다. MAT을 설치하려면 MAT 다운로드 페이지에서 자신의 Eclipse버전에 맞는 파일을 받으시면 됩니다.How to use MATMAT을 설치하였다면 Eclipse화면에서 MAT관련 탭이 뜹니다. 탭을 클릭 하고File -> Open Heap Dump 를 누르면 힙 상황이 기록 된 hprof파일을 읽어올 수 있습니다.탭이 뜨지 않는다면Window -> Open Perspective -> Other에서 Memory Analysis 를 누르면 탭이 뜨는 것을 볼 수 있습니다.hprof 파일을 읽어오면 분석을 시작하고 결과를 Overview 화면에 보여줍니다.파이 차트의 각 부분에 마우스를 갖다 대면 옆의 Inspector 화면에 해당 객체의 정보를 보여주는 것을 볼 수 있습니다.InspectorInspector 창에서는 선택된 객체의 내용을 볼 수 있습니다. 해당 객체의 클래스명과 패키지 명 그리고 해당 객체가 가지고 있었던 변수의 내용을 살펴볼 수 있습니다.유용한 기능들MAT에서 가장 중요하게 살펴볼 기능이라고 한다면 Leak suspoect report와 Dominator tree라고 볼 수 있습니다. Leak suspect와 Dominator tree 둘 다 가장 메모리를 많이 차지하고 있는 객체에 대한 정보를 제공합니다.Leak suspectLeak suspect는 가장 큰 용량을 차지하고 있는 객체들을 좀 더 세분된 파이 도표로 보여줍니다. Problem suspect 1을 보면 현재 이 스레드 객체의 크기가 전체 힙 메모리의 크기 중 19.73%를 점유하고 있다는 것을 알 수 있습니다. 전체의 20% 가까이 차지하고 있다는 것은 이 객체를 OOM의 범인(?)이라고 생각할 근거가 됩니다. 해당 객체에 대한 더 자세한 정보를 얻고 싶다면 Details을 클릭하면 됩니다.Dominator treeDominator tree를 띄우면 현재 덤프 된 매모리 스냅 샷 중 가장 큰 용량을 차지하고 있는 객체 순으로 정렬하여 보여줍니다. Leak suspect와 비슷해 보이지만 더 구체적인 정보를 제공한다는 점이 다릅니다. 따라서 Leak suspect로 현 상황에 대한 힌트를 얻은 후 Dominator tree에서 디테일하게 살펴보는 것이 시간을 절약하는 방법입니다.상위에 있는 몇몇 객체들이 가장 의심 되는 객체들이라고 볼 수 있겠습니다. 왼쪽의 화살표를 클릭함으로써 그 객체가 참조하고 있는 다른 객체들에 대한 정보들을 볼 수 있습니다. 각 객체를 클릭하면 옆에 Inspect창의 내용이 달라지는 것을 볼 수 있습니다.실제 이 스냅 샷은 이전 포스팅의 문제를 해결하려고 떠놓은 스냅 샷인데요, 이 결과를 보고 많은 메모리가 네트워크를 통해 받아오는 스트림을 처리하고 문자열로 가공하는데에 낭비되고 있다는 생각이 들어 다른 방법으로 우회하는 방법을 썼고 결과적으로 문제를 해결 할 수 있었습니다.Android에서 MAT사용법먼저 안드로이드 기기에서 힙 덤프를 수행하여 hprof파일을 생성해야 합니다. hprof파일을 생성하기 위해서 간단하게 취할 수 있는 2가지 방법이 있습니다.1. DDMS를 이용한 추출Eclipse의 DDMS를 이용하여 힙 덤프를 추출할 수 있습니다. 아 방법을 쓰려면 앱의 메니페스트 파일에 WRITE_EXTERNAL_STORAGE 권한을 부여해야 하며, sdcard에 쓸 수 있는 권한이 있어야 합니다. 이 방법을 통해 sdcard경로에 앱 패키지명의 hprof파일이 생성됩니다.2. Heap dump method안드로이드 API에서 제공하는 메서드 중에 hprof파일을 생성하는 메서드인 dumpHprofData가 있습니다. 이 메서드는 Debug 클래스의 메서드인 것을 알 수 있는데, 이 Debug 클래스에는 앱의 상태를 점검할 수 있는 여러 유용한 메서드가 있으므로 나중에 필요하면 사용할 수 있도록 익혀두면 좋습니다.Android hporf 파일 변환앞서 설명한 방법을 적용하여 hprof파일을 추출하였어도 안드로이드에서 추출한 hprof파일은 MAT에서 받아들이는 일반적인 hprof포맷과 다르기 때문에 먼저 변환하는 과정이 필요합니다. 이러한 기능을 제공하는 것이 기본 SDK에 포함된 hprof-conv유틸입니다. 이 유틸은 SDK폴더 내의 tools폴더 안에 있는데 사용하려면 콘솔에서$ hprof-conv <안드로이드용 hprof 파일> <변환할 hprof 파일> 를 치시면 됩니다. 이제 변환된 파일을 MAT에서 열면 분석을 하실 수 있습니다.More tipEclipse Memory Analyser (MAT) - TutorialMemory Analyzer BlogJava Performance blog상기의 사이트들은 MAT과 Java의 메모리 처리에 관련된 내용을 포스팅한 사이트들입니다. 한 번 들러보면 좋은 정보를 얻을 수 있을것입니다.#스포카 #꿀팁 #개발 #개발자 #스킬스택 #스택소개 #인사이트

기업문화 엿볼 때, 더팀스

로그인

/