스토리 홈

인터뷰

피드

뉴스

조회수 2185

레진 기술 블로그 - IntersectionObserver를 이용한 이미지 동적 로딩 기능 개선

구글 크롬 51 버전부터 DOM 엘리먼트의 노출 여부를 비동기로 처리하는 IntersectionObserver API를 사용할 수 있게 되었습니다. 이 기능을 이용하면 이미지의 동적 로딩이나 광고 배너의 노출 측정 등을 효율적으로 사용할 수 있다고 구글 개발자 블로그에서 소개하고 있습니다. 이 글에서는 기존의 이미지 동적 로딩에 대한 문제점을 짚어보고 여러 예제를 통해 IntersectionObserver의 사용 방법을 익혀 기존 기능을 개선할 수 있도록 합니다. 사용한 예제들은 브라우저의 호환성을 고려하지 않고 구글 크롬을 기준으로 작성하였습니다.기존의 이미지 동적 로딩 구현이미지의 개수가 많거나 용량이 큰 페이지를 불러올 경우 쓸데없는 네트워크 비용이 증가하고 이미지를 불러오는 과정에서 서비스 속도에 문제가 발생할 소지가 있어서 이미지가 사용자에게 보일 때만 불러오는 동적 로딩 기능이 필요합니다. IntersectionObserver를 소개하기 전에 먼저 기존 라이브러리들이 이미지 동적 로딩을 어떤 방법으로 구현하고 있는지 간단하게 알아보도록 합니다.엘리먼트 가시성 판단이미지 동적 로딩에서 가장 중요한 코드는 해당 엘리먼트가 현재 화면 내에 보이는지 알아내는 것입니다. 이를 위해 엘리먼트의 크기와 위치 값을 돌려주는 Element.getBoundingClientRect 함수를 사용하는 경우가 많습니다. 아래는 이 함수를 사용한 간단한 구현 예제입니다.function isInViewport(element) { const viewportHeight = document.documentElement.clientHeight; const viewportWidth = document.documentElement.clientWidth; const rect = element.getBoundingClientRect(); if (!rect.width || !rect.height) { return false; } var top = rect.top >= 0 && rect.top < viewportHeight; var bottom = rect.bottom >= 0 && rect.bottom < viewportHeight; var left = rect.left >= 0 && rect.left < viewportWidth; var right = rect.right >= 0 && rect.right < viewportWidth; return (top || bottom) && (left || right); } 이벤트 처리위에서 구현한 isInViewport 함수는 언제 호출해야 할까요? 먼저 문서를 처음 불러왔을 때 호출해야 합니다. 그 후에는 동적 로딩이라는 단어에 맞게 사용자의 동작에 따라 보이지 않던 엘리먼트가 보이게 되는 이벤트를 감지해야 합니다. 마우스나 터치로 스크롤을 통해 문서의 위치가 바뀌거나 브라우저의 크기가 바뀔 수도 있고 모바일 기기의 화면을 돌려서 볼 수도 있습니다. 데스크톱의 경우 scroll, resize 이벤트를, 모바일의 경우 orientationchange 이벤트의 처리를 생각해야 합니다.const images = Array.from(document.querySelectorAll('img')); document.addEventListener('scroll', () => { images.forEach(image => { if (isInViewport(image)) { image.onload = () => images.splice(images.indexOf(image), 1); image.src = 'original_image_path'; } }); }); 간단하게 위 코드와 같이 구현할 수 있습니다. 물론 실제 서비스를 위해서는 수정할 점이 몇 가지 있습니다. 동적 로딩의 대상이 되는 이미지를 구분하기 위해 해당 엘리먼트에만 특정 클래스를 부여하거나 HTML5에서 지원하는 data 속성을 이용하기도 합니다. 스크롤이나 리사이즈 이벤트가 과도하게 발생하는 경우가 많으므로 throttle 또는 debounce 등을 사용해 실행 빈도를 조절할 수도 있습니다. 일부 라이브러리에서는 requestAnimationFrame을 이용해 이벤트 핸들러를 처리하기도 합니다.TADA레진코믹스에서는 이미지 동적 로딩을 위해 서비스 초기에 Unveil 라이브러리를 사용했었습니다. 그러나 적용 후 몇 가지 아쉬움이 있어 따로 TADA 라이브러리를 제작했습니다. 먼저 마크업 구조상 이미지를 태그가 아닌 다른 태그에 배경 이미지로 사용하는 경우를 지원해야 했습니다. 그리고 모바일 웹에 주로 많이 적용하는 수평 스크롤 구역에 대한 처리도 필요했습니다. 문서의 스크롤 이벤트로는 처리가 되지 않기 때문에 특정 엘리먼트를 받아 그 엘리먼트의 스크롤 이벤트 핸들러를 등록할 수 있도록 해야 했습니다. 이를 해결하기 위해 만든 라이브러리를 현재 서비스에 적용 중이지만 이 라이브러리 역시 아래와 같은 문제점들을 가지고 있습니다.</> < id>기존 이미지 동적 로딩의 문제점getBoundingClientRect 함수의 문제점위 코드에서 특정 엘리먼트가 현재 화면 내에 보이는지 검사할 때 사용하던 Element.getBoundingClientRect 함수에는 치명적인 단점이 있습니다. 이 함수를 호출할 때마다 브라우저는 엘리먼트의 크기와 위치값을 최신 정보로 알아오기 위해 문서의 일부 혹은 전체를 다시 그리게 되는 리플로우(reflow) 현상이 발생한다는 점입니다. 호출 횟수가 적을 경우에는 부담이 되지 않지만, 이 함수는 위에서 구현한 것처럼 스크롤이나 리사이즈 이벤트가 발생할 때마다 등록한 모든 엘리먼트를 순환하면서 호출하게 됩니다. 이 코드들이 하나의 메인 스레드에서 실행되기 때문에 스크롤을 할 때마다 실행 속도가 눈에 띄게 느려질 수도 있습니다.외부 도메인 문서를 사용하는 iframe최신 브라우저들은 동일 도메인 정책에 따라 iframe 내의 외부 도메인 문서에서 현재 문서에 접근하지 못 하게 막고 있습니다. 이 제한은 서비스 개발에서 겪게 되는 문제는 아니고 외부 광고 플랫폼 개발자의 입장에서 발생하는 문제입니다. 광고 이미지의 표시나 클릭 이벤트의 처리 등은 iframe 내에서 처리할 수 있지만 광고 이미지를 지연 로딩한다거나 이 광고가 사용자에게 노출이 되었는지 기록하는 등의 기능은 iframe 내에서 불가능합니다. 그래서 광고를 적용하는 서비스 개발자에게 스크립트를 제공하고 서비스 문서 내에 삽입하는 방식으로 처리하고 있습니다. 이러한 외부 광고가 여러 개라면 삽입해야 하는 코드가 늘어날 뿐만 아니라 코드 내에서 스크롤이나 리사이즈 이벤트 등을 각각 사용하기 때문에 위에서 언급한 문제점들이 배가될 수 있습니다.기타 이벤트에 대한 처리대부분의 동적 로딩 라이브러리들은 적용할 엘리먼트에 특정 클래스 또는 data 속성을 부여하면 코드를 추가 작성하지 않더라도 쉽게 사용할 수 있습니다. 하지만 화면에서 보이지 않던 엘리먼트가 갑자기 나타나는 현상은 스크롤이나 리사이즈 이벤트에서만 발생하는 것이 아닙니다. 더보기 버튼을 눌렀을 때 숨겨져 있던 엘리먼트를 노출할 수도 있고 AJAX 호출 후 엘리먼트를 생성한 후 보여줄 수도 있습니다. 이런 경우 일괄적으로 처리하기가 어렵우므로 해당 이벤트가 발생할 때 수동으로 처리하는 수밖에 없습니다.IntersectionObserver위에 나열한 문제들을 효과적으로 처리하기 위해 크롬 51/엣지 15/파이어폭스 55 버전부터 IntersectionObserver를 지원하기 시작합니다. 우리말로 번역하면 교차 감시자 정도가 될 이 기능은 등록한 엘리먼트가 보이는 영역에 나타나거나 사라질 때(용어에 충실하자면 대상 엘리먼트의 영역이 루트 엘리먼트 영역과 교차하기 시작하거나 끝났을 때) 비동기로 이벤트를 발생시켜 줍니다. 기본적인 사용법은 아래와 같습니다.const intersectionObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { // do something observer.unobserve(entry.target); } }); }); intersectionObserver.observe(element); 먼저 IntersectionObserver 생성자에 콜백 함수를 인자로 넘겨주고 생성한 인스턴스의 observe 메소드를 통해 동적으로 처리할 엘리먼트를 등록합니다. 콜백 함수는 IntersectionObserverEntry 객체 목록을 전달받으므로 순환문을 통해 각 엘리먼트에 대한 처리를 완료하고 필요한 경우 unobserve 메소드를 이용해 감시 대상에서 해제할 수 있습니다.IntersectionObserver 예제아래 예제들은 기본적으로 아래 코드를 바탕으로 작성되었습니다. 각 예제들의 최상단에 엘리먼트에 짝을 이룬 표시등을 두었고 콜백 함수가 호출될 때마다 파동 효과를 주었으며 이 때 isIntersecting 속성을 기준으로 표시등을 켜지거나 꺼지게 되어 있습니다.const io = new IntersectionObserver(entries => { entries.forEach(entry => { // isIntersecting 속성으로 해당 엘리먼트가 보이는 지 표시 }); }); Array.from(document.querySelectorAll('.box')).forEach(box => { io.observe(box); }); 스크롤 이벤트를 다루지 않더라도 엘리먼트의 가시성 여부를 isIntersecting 속성을 통해 알 수 있습니다.<iframe class="demo" data-fr-src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/basic-verticalscroll.html" width="600" height="400" frameborder="0">수평 스크롤의 경우에도 overflow 속성이 적용된 컨테이너 엘리먼트의 스크롤 이벤트를 처리하지 않더라도 상관없습니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/basic-horizontalscroll.html" width="600" height="200" frameborder="0">더보기 버튼에 대한 클릭 이벤트를 따로 등록하지 않아도 동작합니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/basic-unfold.html" width="600" height="240" frameborder="0">리사이즈 이벤트 역시 따로 처리할 필요가 없습니다. 새 창을 띄운 후 창 크기를 조절하면서 확인할 수 있습니다.위의 모든 예제들은 <iframe> 태그 안에서 실행되고 있습니다. 이처럼 작성한 코드가 외부에서 실행이 되더라도 IntersectionObserver API는 문제없이 동작합니다.IntersectionObserver 생성자 옵션rootroot 옵션에는 가시성의 판단 기준이 될 HTML 엘리먼트를 지정합니다. observe 메소드로 등록하는 엘리먼트들은 반드시 이 루트 엘리먼트의 자식이어야 합니다. 이 옵션을 지정하지 않을 경우 브라우저 화면에서 현재 보이는 영역인 뷰포트가 기본이 됩니다. 아래 예제에서 알 수 있듯이 루트 엘리먼트를 지정하면 현재 화면과는 상관없이 루트 엘리먼트와 등록한 엘리먼트들의 영역이 교차하는 지 판단하게 됩니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/option-root.html" width="600" height="400" frameborder="0">rootMargin루트 엘리먼트의 마진값을 지정할 수 있습니다. CSS에서 사용하는 형식과 같기 때문에 “10px”, “10px 20px”, “10px 20px 30px 40px” 형태가 모두 가능하며 음수값으로 지정할 수도 있습니다. 기본값은 0이고 루트 엘리먼트를 지정한 상태라면 퍼센트값을 사용할 수도 있습니다. 이 옵션을 이용하면 이미지 동적 로딩에서 해당 엘리먼트가 화면에 나타나기 전에 이미지를 불러오기 시작해 이미지 공백을 줄이는데 유용하게 이용할 수 있겠습니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/option-rootmargin.html" width="600" height="400" frameborder="0">위 예제에서는 root 옵션은 지정하지 않고 rootMargin 옵션을 각각 0, 100px, -100px, 50%로 지정했습니다. 하지만 iframe 내에서 실행을 하게 되면 이 옵션이 정상적으로 동작하지 않습니다. 프로젝트 페이지의 이슈 댓글에는 동일 도메인의 프레임 안에서는 동작하는 것으로 논의되고 있지만 뷰포트에는 해당하지 않거나 버그 또는 아직 크롬에 반영이 되지 않아 보입니다.이 예제는 새 창을 띄워 프레임을 벗어나면 정상적으로 동작합니다. 스크롤을 내리다보면 엘리먼트마다 IntersectionObserver 콜백 함수가 다른 위치에서 호출됨을 알 수 있습니다.thresholdthreshold 옵션은 엘리먼트가 콜백 함수의 호출 시점을 정하는 옵션입니다. 0과 1을 포함한 그 사이의 숫자 또는 숫자 배열을 지정할 수 있는데 이 숫자는 엘리먼트의 전체 영역 중에 현재 보이는 영역의 비율입니다. 이 비율의 경계를 넘나들 때마다 콜백 함수가 호출됩니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/option-threshold-value.html" width="600" height="400" frameborder="0">위 예제에서는 threshold 옵션을 각각 0, 0.5, 1, [0, 1]로 지정했습니다. rootMargin 예제처럼 엘리먼트마다 콜백 함수가 다른 위치에서 호출됩니다. 두 번째와 세 번째 상자는 콜백 호출 시점에 isIntersecting 값이 항상 참이기 때문에 표시등이 정상적으로 표시되지 않습니다. isIntersecting 속성을 기준으로 처리해야 할 작업이 있다면 반드시 threshold 속성에 0을 포함시켜야 정상적으로 동작합니다. threshold 속성은 아래 예제처럼 콜백 함수의 인자로 받는 IntersectionObserverEntry 객체의 intersectionRatio 속성과 같이 사용하기에 유용합니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/option-threshold-ratio.html" width="600" height="400" frameborder="0">위 예제에서는 threshold 옵션을 각각 [0, 1], [0, 0.5, 1], [0, 0.25, 0.5, 0.75, 1]로 지정하고 콜백이 호출되면 intersectionRatio 값을 기준으로 표시등의 배경 투명도를 바꾸도록 했습니다.IntersectionObserver의 활용이미지 동적 로딩지금껏 알아본 IntersectionObserver를 이용해 이미지 동적 로딩을 간단하게 구현해봅니다. 이미지 엘리먼트를 구성할 데이터가 배열로 존재한다고 가정하고 ES6에서 지원하는 템플릿 문자열을 사용해 배열을 순환하면서 이미지 목록을 생성합니다. 그 후 IntersectionObserver를 초기화하고 만들어진 엘리먼트들을 등록합니다. 콜백 함수에서는 엘리먼트가 보이는 상태일 때 이미지를 로딩하고 해당 엘리먼트를 감시 해제합니다. id="comics"> const comics = [ { alias: 'eunsoo', id: 6080299074584576, title: '은수' }, ... ]; const template = comics => ` ${comics.map(comic => ` ${comic.id}"> ${comic.alias}" class="info">${comic.title} `).join('')} `; document.getElementById('comics').innerHTML = template(comics); const io = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (!entry.isIntersecting) { return; } const target = entry.target; const id = target.dataset.id; target.querySelector('.info').style.backgroundImage = `url(https://cdn.lezhin.com/v2/comics/${id}/images/wide?width=600)`; observer.unobserve(target); }); }); Array.from(document.querySelectorAll('.comic')).forEach(el => { io.observe(el); }); 아래 예제를 실행하면서 브라우저의 개발자 도구를 열고 네트워크 탭을 살펴보면 이미지 엘리먼트가 보이기 시작할 때 불러오기 시작하는 것을 확인할 수 있습니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/demo-lazyload.html" width="600" height="400" frameborder="0">무한 스크롤IntersectionObserver 기능을 이용하면 무한 스크롤 역시 쉽게 구현할 수 있습니다. 아래 예제에서는 스크롤의 끝부분에 감시를 할 엘리먼트를 두고 그 엘리먼트가 노출이 될 때마다 콘텐츠를 추가로 불러오도록 작성하였습니다. id="items"> id="sentinel"> const count = 20; let index = 0; function loadItems() { const fragment = document.createDocumentFragment(); for (let i = index + 1; i <= index + count; ++i) { const item = document.createElement('p'); item.classList.add('item'); item.textContent = `#${i}`; fragment.appendChild(item); } document.getElementById('items').appendChild(fragment); index += count; } const io = new IntersectionObserver(entries => { entries.forEach(entry => { if (!entry.isIntersecting) { return; } loadItems(); }); }); io.observe(document.getElementById('sentinel')); loadItems(); 실행 결과를 아래 예제에서 확인할 수 있습니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/demo-infinitescroll.html" width="600" height="400" frameborder="0">마무리IntersectionObserver API는 아직 몇몇 브라우저의 최신 버전에서만 사용할 수 있지만 지원하지 않는 브라우저를 위해 Polyfill을 제공하고 있습니다.IntersectionObserver API는 웹 광고 플랫폼 제공자와 사용자 모두에게도 좋은 소식이라 봅니다. 이 API가 정착된다면 광고 노출 여부를 측정하기가 쉬워지고 서비스에 더해졌던 리소스 낭비를 줄일 수 있을 것이라 생각합니다.레진코믹스는 크롬 브라우저 사용자의 비중이 높은 편이기 때문에 빠른 시일 안에 적용해 볼 예정입니다. 이미지 동적 로딩 기능의 개선과 정주행 기능과 같이 현재 스크롤 이벤트를 과하게 사용하고 있는 코드에 대한 부담을 덜 수 있기를 기대하고 있습니다.참고자료Intersection Observer API SpecificationIntersectionObserver’s Coming into ViewIntersection Observer API
조회수 1240

안드로이드 개발자의 고민 Fragment (2)

이전 글 보기: 안드로이드 개발자의 고민: Fragment이번 글에서는 Fragment stack 관리와 Fragment 데이터 Lifecycle 관리 이슈를 줄일 수 있는 해결 방법을 찾아보겠습니다. 이전 글에서는  Fragment를 하나의 View로 관리하는 오픈소스를 검토했었습니다.하지만 검토하는 중에 기존 오픈 소스의 변경과 버전업 관리 이슈의 문제를 그냥 넘어갈 수는 없었습니다. 상용 소스에 바로 적용하기에는 리스크가 크다고 판단해 좀 더 신뢰할 수 있는 방법을 선택하기로 했는데요.요즘은 작년 6월에 Google IO 에 발표한 AndroidX의 내용을 다시 검토하고 있습니다. Deeplink를 통한 목적 화면과 Fragment 스택 관리가 중요한데, 이 기능을 도와주는 것이 AndroidX Navigation이기 때문입니다. 화면 전환을 UI 기반으로 사용하여 화면 관리를 용이하게 만들었습니다. 물론 코드 기반에 익숙한 저는 적용하는데 시간이 걸렸죠.기존의 Fragment 관리는 FragmentManager를 통하여 개발자가 직접 코드 상에서 관리했습니다. 하지만 Navigation의 경우에는 아래와 같이 직관적으로 설정할 수 있습니다.firstFragment -> secondFragment -> thirdFragment 로 화면 간의 흐름을 설정합니다. 하나의 Navigation 파일은 하나 이상의 Activity 에서 사용할 수 있습니다.이 방식은 오히려 현재 사용하는 브랜디 소스와 비슷합니다. 하나의 Activity에 ActivityFragment를 만들어서 1:1 매핑으로 화면을 Fragment를 관리하는 방식과 유사합니다. Navigation 의 세부내용은 Google Developers에서 확인할 수 있습니다.Deeplink 를 통한 Fragment Stack 관리도 간단합니다.Notification 또는 Serice 등에서 PendingIntent를 사용하여 테스트할 수 있습니다. Navigation Fragment stack 순서대로 화면을 쌓은 다음 최종 destination Fragment 로 도착합니다. 이와 같은 방법으로 Push를 통한 화면 관리를 쉽게 할 수 있습니다. 이 내용은 여기에서 자세히 확인할 수 있습니다.신속한 마무리기존 Android 에서 화면 관리가 불편했다면 Navigation으로 직관적이고 쉽게 화면을 관리할 수 있을 겁니다. 브랜디는 아직 적용할 준비 중이지만, 꼭 kotlin과 Navigation을 적용해보려 합니다. 그럼 다시 개발의 숲으로 들어가보겠습니다.글고재성 과장 | R&D 개발1팀[email protected]브랜디, 오직 예쁜 옷만
조회수 2559

타다 시스템 아키텍처 - VCNC Engineering Blog

2018년에는 VCNC에 큰 변화가 있었습니다. 오랫동안 비트윈 기반의 서비스들을 개발하고 운영했지만 2018년 10월에 기사 포함 렌터카 서비스를 포함한 종합 모빌리티 플랫폼인 타다를 기획하고 출시하였습니다. 변화가 많은 모빌리티 시장에서 신규 서비스를 성공적으로 출시하기 위해 많은 고민을 하였습니다. 이번 글에서는 타다의 시스템 구성과 이를 위해 사용한 여러 기술을 소개하면서, 타다 개발팀의 기술적 결정을 공유해보고자 합니다.타다에서 사용하는 기술들의 로고. 왼쪽부터 Kotlin, Spring Boot, Kubernetes, Terraform, gRPC, Redis.기존과 다른 선택비트윈의 경우 Netty를 이용해 인하우스 네트워크 라이브러리를 만들기도 하였고, 메인 데이터베이스로 NoSQL인 HBase를 사용하는 등 남들이 통상적으로 사용하지 않는 기술 스택을 선택한 경우가 많았습니다. 그 배경에는 나름대로 이유가 있었지만, 서비스 초기에는 안정성에 어려움을 겪기도 하였고 서버 배포 과정이 느리고 복잡하여 쉬운 길은 아니었습니다. 여러 문제를 해결하기 위해 Haeinsa 등 라이브러리와 소프트웨어를 직접 만들기도 하였습니다.타다는 이슈가 많은 모빌리티 시장을 타겟으로 하고 있기 때문에 Time to Market이 특히 중요했습니다. 개발하는 기간 동안 시장 상황에 따라 기능의 우선순위가 변하기도 하였습니다. 이에 따라 서비스를 빨리 출시하고 외부의 변화에 유연하게 대처할 수 있도록, 완성도 있게 만들어져 있는 프레임워크나 라이브러리를 선택하였고, AWS에서 이미 잘 관리되고 있는 서비스를 적극적으로 활용하였습니다.사용 중인 기술들Kotlin: Java는 불편한 점이 많지만, JVM에 대한 경험을 무시할 수는 없어 비교적 새로운 JVM 기반 언어인 Kotlin을 사용하기로 하였습니다. 다른 여러 JVM 기반의 대안 언어들이 있지만, Spring Boot에 쉽게 적용할 수 있고 커뮤니티에서 적극적으로 권장하고 있는 점 등 여러 이유로 Kotlin을 선택하게 되었습니다.Spring Boot: 널리 쓰는 웹 프레임워크이며 이미 지원하는 기능 또한 많기 때문에 보일러 플레이트 코드 작성을 줄이고 서비스 개발에 집중할 수 있습니다. SQS 메시지 처리, HTTP 요청 및 응답으로 Protocol Buffers 메시지 사용 등 프레임워크에서 제공하는 기능을 많이 활용하고 있습니다.Kubernetes: 컨테이너 오케스트레이션 플랫폼으로 배포 자동화와 스케일링 등 여러 가지 운영적인 편의성을 제공합니다. 처음에는 kops를 이용해 클러스터를 직접 띄웠지만, 지금은 EKS를 이용하고 있으며 직접 object를 만들기보다 helm을 이용하고 있습니다.gRPC: 실시간성이 중요한 차량 위치나 운행 상태 변화 등은 Streaming을 이용하여 전달하고 있습니다. 직접 개발할 수도 있었지만, 서비스 개발에 집중하고 앞으로의 관리 오버헤드를 줄이기 위해 gRPC를 이용하기로 하였습니다.Redis: 서버 간 메시징을 위해 Redis의 Pub/Sub 기능을 사용하고 있습니다. 메시지 브로커 기능을 제공하는 RabbitMQ, ActiveMQ, Kafka 등 여러 옵션이 있었지만, 개발을 시작하던 당시에는 Redis만이 ElastiCache를 이용하여 쉽게 띄우고 관리할 수 있어 Redis를 선택하게 되었습니다.Protocol Buffers: gRPC 뿐만 아니라 HTTP/2로 주고받는 메시지를 정의할 때도 이용하고 있습니다. 덕분에 따로 문서화 하지 않고 proto파일을 공유하여 더욱 명확하고 편리하게 API 명세를 공유할 수 있었습니다.Terraform: HCL을 이용해 인프라스트럭처 프로비저닝 및 관리를 편하게 해주는 도구입니다. AWS 서비스의 생성 및 관리를 콘솔에서 직접 하지 않고 Terraform을 이용하고 있습니다.사용 중인 AWS 서비스들AWS는 개발팀이 오랜 기간 사용하여 가장 익숙한 클라우드 플랫폼이기 때문에 큰 고민 없이 선택할 수 있었습니다.EKS: Kubernetes 클러스터의 마스터 노드들을 쉽게 띄우고 관리해주는 서비스입니다. 서울 리전에 EKS가 출시된 후에는 관리 오버헤드를 줄이기 위해 EKS로 옮겼습니다.ECR: 타다 서버를 배포할 때는 Docker Gradle Plugin을 통해 docker 이미지를 만들고 ECR에 푸시합니다. 그 후 helm 명령을 통해 Kubernetes에 배포합니다.SQS: 배차 요청을 처리하기 위해 SQS를 이용합니다. 배차 요청을 구현하는 방법에는 다양한 옵션이 있었지만 AWS 서비스를 최대한 활용하여 빠르게 개발할 수 있었습니다.RDS: 타다의 대부분 데이터는 Aurora에 저장하고 있습니다. RDS를 이용하면 DB의 배포와 관리가 쉬우며, Aurora는 MySQL과 호환될 뿐만 아니라 같은 비용이면 성능이 더 좋습니다.Kinesis: 실시간 차량 위치 정보 및 로그를 수집하기 위해 사용하고 있습니다. 다른 오픈소스 소프트웨어를 직접 이용하기보다는 AWS에서 제공하는 서비스를 최대한 이용하고 있습니다.Firehose: 비트윈에서는 KCL를 활용해 Acheron이라는 프로그램을 직접 만들어 로그들을 S3에 저장하였지만, 이제는 서울 리전에서 Firehose를 사용할 수 있으므로 큰 고민 없이 사용하기로 하였습니다.시스템 구성타다에서는 필요에 따라 서비스를 여러 종류로 분리하여 운영하고 있습니다. 일반적인 모바일 앱 API와 실시간 차량의 위치 정보를 바탕으로 사용자의 요청에 대해 적합한 차량을 배차하는 기능이 필요했습니다. 핵심적인 역할을 하는 일부 서비스와 시스템 구성에 대해 간단하게 소개합니다.라이더 앱: 아이폰은 Swift, 안드로이드는 Kotlin으로 작성하였으며 여러 오픈소스 라이브러리를 적극적으로 활용하였습니다. 서비스 특성상 RIBs라는 아키텍처를 사용하여 개발하였습니다.드라이버 앱: 아이폰과 안드로이드를 모두 지원하려면 기술적, UX적으로 고려해야 할 점들이 많고 불특정 다수의 유저를 대상으로 하는 앱도 아니었기 때문에 안드로이드 버전으로만 개발하게 되었습니다.서버: 모바일 앱의 요청을 대부분 처리하며 Spring Boot로 작성된 HTTP/2 API 서버입니다. Protocol Buffers로 정의된 메시지를 JSON 형태로 주고받습니다.gRPC 서버: 서버에서 발생하는 이벤트를 실시간으로 전달하기 위한 서버입니다. Redis Pub/Sub을 통해 받은 이벤트 메시지들을 클라이언트들에게 전달합니다.Dispatcher: 배차 요청을 처리하는 서버입니다. 주변 차들의 ETA 계산을 위해 외부 API를 이용하는데, Reactor를 이용해 비동기적, 동시적으로 요청하여 쓰레드 점유 없이 효율적으로 처리되도록 구현하였습니다.Tracker: 차량 위치 정보 수집 서버입니다. KCL를 이용해 위치 정보 레코드를 읽어 들여 TrackerDB에 기록합니다.Redis: 서비스 초기에는 차량의 최신 위치 등을 저장하기도 했지만, 지금은 주로 서버 간 메시징을 위해 Pub/Sub 기능을 이용하고 있습니다.DB: 운행 기록, 사용자 데이터 등 대부분 데이터를 기록합니다. 비트윈에서는 HBase를 이용했지만 타다의 경우 아직 절대적인 트래픽이 많지 않기 때문에 트랜잭션 등 다양한 편의 기능을 제공하는 RDB를 이용하고 있습니다.TrackerDB: 차량 운행 정보 및 차량의 최신 위치 등을 저장합니다. Aurora를 이용하며 대부분의 요청이 차량 위치 정보 업데이트이므로 안정성을 위해 별도의 인스턴스를 띄워 사용하고 있습니다.Kinesis Log Stream: 타다의 여러 서비스에서 로깅을 위해 이용합니다. Firehose를 통해 S3에 기록됩니다.Kinesis Tracker Stream: 드라이버의 실시간 위치 정보는 Kinesis를 통해 Tracker로 전달됩니다.서비스 플로우차량 위치 업데이트차량 위치 업데이트는 요금 계산, 차량 위치 제공 등 서비스에서 가장 많이 일어나는 요청입니다. 드라이버 앱에서 안드로이드 Foreground 서비스를 이용해 GPS 정보를 수집하고 일정 주기마다 서버로 현재 위치를 전송합니다. 이렇게 전송받은 GPS 위치 정보는 데이터 크기를 최소화하기 위해 Protocol Buffers로 직렬화되어 Kinesis 레코드로 만들어지게 됩니다. Tracker에서는 전달된 Kinesis 레코드를 읽어 간단한 처리를 한 후에 TrackerDB에 삽입합니다.서비스 초기에는 차량의 마지막 위치에 대한 정보만 Redis에 적었습니다. 그러나 차량의 이동 경로를 효율적으로 조회해야 할 일이 생겼는데, 당시 차량 이동 경로는 로그로만 저장되고 있었습니다. S3 Select나 Athena를 이용해 조회하는 방안도 고려했지만, 일단은 Aurora에 저장하기로 하였습니다. 당분간은 Aurora로도 충분했고 RDB를 쓰는 것이 가장 쉽고 편한 방법이었기 때문입니다.차량 배차차량 배차는 서비스의 가장 기본적인 기능으로 배차 요청에 가장 적절한 주변 차량을 할당하는 플로우입니다. 라이더 앱에서 유저가 배차를 요청하면 서버가 배차 요청 정보를 DB에 기록하고 배차 요청 메시지를 SQS 대기열에 집어넣습니다. Dispatcher가 배차를 처리하는 로직을 수행하여 차량이 매칭되면 드라이버 앱으로 이벤트가 전달됩니다.드라이버가 배차를 수락하면 서버로 수락 요청이 전송되고 서버에서는 DB의 배차 요청 상태를 수락 상태로 변경합니다. 배차 요청이 수락되었다는 이벤트는 결과적으로 gRPC 서버를 통해 해당 이벤트를 구독하고 있던 유저에게 전달됩니다.Dispatcher에서 배차를 처리하는 로직은 여러 옵션이 있었지만 가장 간단하고 효율적으로 개발하기 위해 SQS의 기능을 최대한 활용하였습니다. Dispatcher 수를 늘리는 것만으로도 처리량 확장이 가능하며 Dispatcher가 갑자기 종료되어도 한 대라도 살아있다면 결국에는 잘 처리가 됩니다. Dispatcher가 배차 요청을 받으면 다음과 같은 로직을 수행합니다. 종료 조건을 만족하지 않았다면 일정 시간 후 동일한 로직을 다시 반복합니다.배차가 가능한 상태라면 배차 로직을 수행합니다. 이동 경로와 교통정보를 고려하여 적합한 주변 차량을 찾습니다.만약 적합한 차량이 있다면 배차 요청을 해당 드라이버에게 할당되었다는 정보를 DB에 적고 배차 할당 이벤트를 전파합니다. 드라이버의 수락을 기다리기 위해 일정 시간 후 로직을 재시도합니다.만약 적합한 차량이 없다면 일정 시간 후에 로직을 재시도합니다.배차 요청이 드라이버의 수락을 기다려야 하거나 타임아웃이 남아있는 상태라면 적절한 시간 후 재시도합니다.배차 요청이 수락되어 완료된 상태거나 취소되었거나 타임아웃이 지난 상태라면 SQS에서 메시지를 삭제합니다.못다 한 이야기타다를 런칭하는 날, 기사 간담회에서 쏘카의 VCNC 인수 이후 짧은 기간 동안 타다를 만들 수 있었을 리 없으니, 실제 개발 기간은 어느 정도냐는 질문이 있었습니다. 짧은 기간 내 서비스를 성공적으로 런칭할 수 있었던 것은 상황에 맞는 올바른 기술적 선택들뿐만 아니라 훌륭한 팀원들이 있었기에 가능했던 일이었습니다. 타다는 개선해야 할 부분도 많고 앞으로 새로운 기술적 도전들이 많이 있을 것입니다.네 그렇습니다. 결론은 기술적 난제들을 고민하면서 좋은 팀과 서비스를 함께 만들고 키워나갈 좋은 분들을 기다리고 있다는 것입니다.
조회수 2476

MySQL의 Transaction Isolation Level (Lock에 관하여)

편집자 주문맥에 따라 ‘Transaction’과 ‘트랜잭션’으로 영어와 한글을 혼용함.문맥에 따라 ‘LOCK’과 ‘lock’으로 대문자와 소문자를 혼용함.OverviewMySQL DB는 일반적인 운영환경에서 뛰어난 성능을 제공합니다. 특히 적은 양의 자료가 빈번하게 교류되는 환경에서는 더욱 빛을 발하죠. 국내에서는 주로 작은 규모의 웹사이트를 구축할 때 MySQL을 사용합니다. 그런데 문제는 사이트의 규모가 커지면서부터 생긴다는 것이죠. 조금씩 느려지는 Query가 생기면 원인도 파악하고, Query를 튜닝하고, 설계도 변경하지만 MySQL의 특징적인 문제를 곧 만나게 됩니다.테이블을 복제(CREATE SELECT)하거나 다른 테이블로 옮기면(INSERT SELECT) 작업을 하는 동안 SELECT 절에 있는 테이블들이 Lock이 걸립니다. 게다가 다른 Session에서 해당 테이블을 수정(UPDATE / DELETE)하면 복제와 이동을 마칠 때까지 대기 상태로 있어야 한다는 것입니다. 이러한 문제는 시스템을 구축하고 자료가 일정량 쌓이기 전까지는 알 수 없습니다. 또한 Oracle과 같은 DB를 사용하던 사용자가, MySQL을 사용하면 이와 같은 문제가 있을 것이라고 생각하기도 어렵습니다.이러한 특징을 가진 MySQL의 Transaction Isolation Level을 알아보고자 합니다. Transaction Isolation Level 은 Transaction의 경리 수준을 말합니다. 트랜잭션 처리 시 다른 트랜잭션에서 접근해 자료를 수정하거나 볼 수 있도록 하는 수준입니다.Transaction Isolation Level의 종류와 특성Transaction Isolation Level에는 READ UNCOMMITTED, READ COMMIITED, REPEATABLE READ, SERIALIZE 네 가지 종류가 있습니다. 1)READ UNCOMMITTED1) COMMIT 되지 않은 데이터에 다른 트랜잭션에서 접근할수 있다.2) INSERT, UPDATE, DELETE 후 COMMIT 이나 ROLLBACK에 상관없이 현재의 데이터를 읽어온다.3) ROLLBACK이 될 데이터도 읽어올 수 있으므로 주의가 필요하다.4) LOCK이 발생하지 않는다.READ COMMIITED1) COMMIT 된 데이터에 다른 트랜잭션에서 접근할 수 있다.2) 구현 방식이 차이 때문에 Query를 수행한 시점의 데이터와 정확하게 일치하지 않을 수 있다.3) LOCK이 발생하지 않는다.4) MySQL에서 많은 양의 데이터를 복제하거나 이동할 때 이 LEVEL을 추천한다.REPEATABLE READ1) Default LEVEL이다.2) SELECT시 현재 시점의 스냅샷을 만들고 스냅샷을 조회한다.3) 동일 트랜잭션 내에서 일관성을 보장한다.4) record lock과 gap lock이 발생한다.5) CREATE SELECT, INSERT SELECT시 lock이 발생한다.SERIALIZE1) 가장 강력한 LEVEL이다.2) SELECT 문에 사용하는 모든 테이블에 shared lock이 발생한다.LOCK과 테이블, 어떻게 해결할 수 있을까?지금부터는 관련된 내용을 확인해보겠습니다. 우선 현재의 경리 수준부터 알아보겠습니다.mysql> SHOW VARIABLES WHERE VARIABLE_NAME='tx_isolation'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | tx_isolation | REPEATABLE-READ | +---------------+-----------------+ 1 row in set (0.00 sec) 다음으로 TEST 테이블을 만듭니다. 이때 SELECT절의 테이블을 UPDATE할 경우, 대기 상태로 빠지는 것을 확인해보겠습니다. 테이블을 만들고 상태를 확인합니다.CREATE TABLE test.TB_PROD_BAS_TEST ( PRIMARY KEY (PROD_ID) ) SELECT T101.PROD_ID ,T101.PROD_NM ,T101.PROD_EN_NM ,T101.PROD_MEMO FROM test.TB_PROD_BAS T101 ; -- 생성시 INFORMATION_SCHEMA.PROCESSLIST 로 상태를 확인합니다. mysql> SELECT -> * -> FROM INFORMATION_SCHEMA.PROCESSLIST -> WHERE USER = 'hansj' -> AND COMMAND <> 'Sleep' -> \G *************************** 1. row *************************** ID: 11004 USER: hansj HOST: 192.168.1.150:50711 DB: test COMMAND: Query TIME: 5 STATE: Sending data INFO: CREATE TABLE test.TB_PROD_BAS_TEST ( PRIMARY KEY (PROD_ID) ) SELECT T101.PROD_ID ,T101.PROD_NM ,T101.PROD_EN_NM ,T101.PROD_MEMO FROM test.TB_PROD_BAS T101 1 row in set (0.00 sec) 다음으로 테이블 생성 시 UPDATE를 해 대기 상태로 빠지는지 확인해보겠습니다.UPDATE test.TB_PROD_BAS SET PROD_MEMO = 'TEST' WHERE PROD_ID = 1 ; mysql> SELECT -> * -> FROM INFORMATION_SCHEMA.PROCESSLIST -> WHERE USER = 'hansj' -> AND COMMAND <> 'Sleep' -> \G *************************** 1. row *************************** ID: 11004 USER: hansj HOST: 192.168.1.150:50711 DB: test COMMAND: Query TIME: 24 STATE: Sending data INFO: CREATE TABLE test.TB_PROD_BAS_TEST ( PRIMARY KEY (PROD_ID) ) SELECT T101.PROD_ID ,T101.PROD_NM ,T101.PROD_EN_NM ,T101.PROD_MEMO FROM test.TB_PROD_BAS T101 *************************** 2. row *************************** ID: 11006 USER: hansj HOST: 192.168.1.150:50719 DB: test COMMAND: Query TIME: 22 *****이부분 중요합니다.****** STATE: updating *****이부분 중요합니다.****** INFO: UPDATE test.TB_PROD_BAS SET PROD_MEMO = 'TEST' WHERE PROD_ID = 1 2 rows in set (0.00 sec) 위의 TIME을 보면 테이블이 생성될 때까지 대기하고, UPDATE 문의 상태가 updating 으로 표시됩니다. 하지만 이렇게 나올 경우 건수가 많으면 실제 UPDATE 중인지 대기상태인지 확인하기가 어렵습니다. LOCK이 걸린 테이블을 확인하려면 INNODB LOCK 테이블로 정확하게 알 수 있습니다. 아래 세 가지 테이블로 확인해보겠습니다. 보다 자세한 설명은 MySQL 홈페이지를 확인합니다.information_schema.INNODB_TRXLOCK을 걸고 있는 프로세스 정보information_schema.INNODB_LOCK_WAITS현재 LOCK이 걸려 대기중인 정보information_schema.INNODB_LOCKSLOCK을 건 정보위의 각 항목마다 테이블 생성 및 UPDATE 시 정보가 어떻게 나타나는지 확인해보겠습니다.1.information_schema.INNODB_TRXmysql> SELECT -> T101.TRX_ID -> ,T101.TRX_STATE -> ,T101.TRX_STARTED -> ,T101.TRX_REQUESTED_LOCK_ID -> ,T101.TRX_WAIT_STARTED -> ,T101.TRX_WEIGHT -> ,T101.TRX_MYSQL_THREAD_ID -> ,T101.TRX_ISOLATION_LEVEL -> ,SUBSTR(T101.TRX_QUERY,1,10)AS TRX_QUERY -> FROM information_schema.INNODB_TRX T101 -> ; +---------+-----------+---------------------+-----------------------+---------------------+------------+---------------------+---------------------+------------+ | TRX_ID | TRX_STATE | TRX_STARTED | TRX_REQUESTED_LOCK_ID | TRX_WAIT_STARTED | TRX_WEIGHT | TRX_MYSQL_THREAD_ID | TRX_ISOLATION_LEVEL | TRX_QUERY | +---------+-----------+---------------------+-----------------------+---------------------+------------+---------------------+---------------------+------------+ | 8771591 | LOCK WAIT | 2019-05-27 16:15:53 | 8771591:70031:4:306 | 2019-05-27 16:15:53 | 2 | 11006 | REPEATABLE READ | UPDATE tes | | 8771586 | RUNNING | 2019-05-27 16:15:51 | NULL | NULL | 1538969 | 11004 | REPEATABLE READ | CREATE TAB | +---------+-----------+---------------------+-----------------------+---------------------+------------+---------------------+---------------------+------------+ 2 rows in set (0.00 sec) TRX_ID_STATE트랜잭션의 상태를 나타냅니다. 실행 중인지 LOCK WAIT 상태인지 알 수 있습니다.TRX_MYSQL_THREAD_IDPROCESSLIST 의 ID를 나타냅니다.TRX_ISOLATION_LEVELISOLATION LEVEL을 나타냅니다.따라서 위의 내용을 보면 CREATE TABLE이 실행 중인 것과, UPDATE가 LOCK WAIT인 것, 그리고 관련된 PROCESSLIST의 ID까지도 알 수 있습니다2.information_schema.INNODB_LOCK_WAITSmysql> SELECT -> * -> FROM information_schema.INNODB_LOCK_WAITS T101 -> ; +-------------------+---------------------+-----------------+---------------------+ | requesting_trx_id | requested_lock_id | blocking_trx_id | blocking_lock_id | +-------------------+---------------------+-----------------+---------------------+ | 8771591 | 8771591:70031:4:306 | 8771586 | 8771586:70031:4:306 | +-------------------+---------------------+-----------------+---------------------+ 1 row in set (0.01 sec) requesting_trx_idLOCK WAIT 인 TRX_IDblocking_trx_idLOCK 을 건 TRX_ID현재 LOCK이 걸린 TRX_ID와 LOCK을 걸어둔 TRX_ID를 알 수 있습니다.3.information_schema.INNODB_LOCKSmysql> SELECT -> * -> FROM information_schema.INNODB_LOCKS -> ; +---------------------+-------------+-----------+-----------+----------------------+------------+------------+-----------+----------+-----------+ | lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data | +---------------------+-------------+-----------+-----------+----------------------+------------+------------+-----------+----------+-----------+ | 8771591:70031:4:306 | 8771591 | X | RECORD | `test`.`TB_PROD_BAS` | PRIMARY | 70031 | 4 | 306 | 1 | | 8771586:70031:4:306 | 8771586 | S | RECORD | `test`.`TB_PROD_BAS` | PRIMARY | 70031 | 4 | 306 | 1 | +---------------------+-------------+-----------+-----------+----------------------+------------+------------+-----------+----------+-----------+ 2 rows in set (0.01 sec) lock_trx_idLOCK 과 관련된 TRX_IDlock_modeX 쓰기, S 읽기 2)어떤 테이블이 LOCK을 걸고 있는지 알 수 있습니다.위의 내용들을 통해 REPEATABLE READ에서 CREATE SELECT시 SELECT 테이블에 LOCK이 걸려 UPDATE가 대기하게 되는 것을 알 수 있습니다. 이번에는 Transaction Isolation Level 을 READ COMMIITED로 변경하고 CREATE SELECT 및 UPDATE를 진행해보겠습니다.SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; SHOW VARIABLES WHERE VARIABLE_NAME='tx_isolation'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | tx_isolation | READ-COMMITTED | +---------------+-----------------+ 1 row in set (0.00 sec) UPDATE 문은 다음과 같이 수행됩니다. mysql> UPDATE test.TB_PROD_BAS -> SET PROD_MEMO = 'TEST' -> WHERE PROD_ID = 1 -> ; Query OK, 0 rows affected (0.04 sec) Rows matched: 1 Changed: 0 Warnings: 0 기존에 대기했던 것과 다르게 0.04초가 걸렸습니다.mysql> SELECT -> * -> FROM INFORMATION_SCHEMA.PROCESSLIST -> WHERE USER = 'hansj' -> AND COMMAND <> 'Sleep' -> \G *************************** 1. row *************************** ID: 11004 USER: hansj HOST: 192.168.1.150:50711 DB: test COMMAND: Query TIME: 9 STATE: Sending data INFO: CREATE TABLE test.TB_PROD_BAS_TEST ( PRIMARY KEY (PROD_ID) ) SELECT T101.PROD_ID ,T101.PROD_NM ,T101.PROD_EN_NM ,T101.PROD_MEMO FROM test.TB_PROD_BAS T101 1 row in set (0.00 sec) -- 프로세스 정보도 CREATE TABLE 만 진행중임을 알수 있습니다. mysql> SELECT -> T101.TRX_ID -> ,T101.TRX_STATE -> ,T101.TRX_STARTED -> ,T101.TRX_REQUESTED_LOCK_ID -> ,T101.TRX_WAIT_STARTED -> ,T101.TRX_WEIGHT -> ,T101.TRX_MYSQL_THREAD_ID -> ,T101.TRX_ISOLATION_LEVEL -> ,T101.TRX_QUERY -> FROM information_schema.INNODB_TRX T101 -> ; +---------+-----------+---------------------+-----------------------+------------------+------------+---------------------+---------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------+ | TRX_ID | TRX_STATE | TRX_STARTED | TRX_REQUESTED_LOCK_ID | TRX_WAIT_STARTED | TRX_WEIGHT | TRX_MYSQL_THREAD_ID | TRX_ISOLATION_LEVEL | TRX_QUERY | +---------+-----------+---------------------+-----------------------+------------------+------------+---------------------+---------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------+ | 8771856 | RUNNING | 2019-05-27 17:17:45 | NULL | NULL | 4594347 | 11004 | READ COMMITTED | CREATE TABLE test.TB_PROD_BAS_TEST ( PRIMARY KEY (PROD_ID) ) SELECT T101.PROD_ID ,T101.PROD_NM ,T101.PROD_EN_NM ,T101.PROD_MEMO FROM test.TB_PROD_BAS T101 | +---------+-----------+---------------------+-----------------------+------------------+------------+---------------------+---------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec) READ COMMITTED LEVEL로 CREATE만 수행 중인 것을 알 수 있습니다.mysql> SELECT -> * -> FROM information_schema.INNODB_LOCK_WAITS T101 -> ; Empty set (0.00 sec) mysql> SELECT -> * -> FROM information_schema.INNODB_LOCKS -> ; Empty set (0.00 sec) LOCK을 걸고 걸린 것이 없어 내용도 없습니다.Conclusion지금까지 Transaction Isolation Level 을 기준으로 CREATE SELECT 시 SELECT 에 사용되는 테이블도 LOCK이 걸릴 수 있는 것을 확인했고, 그에 따른 해결 방법까지 알아봤습니다.INSERT INTO SELECT에서도 같은 현상이 나타납니다. 그렇기 때문에 운영 중인 테이블을 복제(CREATE SELECT)하거나 다른 테이블로 옮길 경우(INSERT SELECT) Transaction Isolation Level을 READ COMMITTED 변경하고 작업하기를 권장합니다.그렇지 않으면 관련된 TABLE은 LOCK이 걸리고, 관련 Query들이 대기 상태로 빠지면서 시스템 장애가 발생할지도 모릅니다.참고1)MySQL :: MySQL 5.6 Reference Manual :: 14.7.2.1 Transaction Isolation Levels2)MySQL :: MySQL 5.6 Reference Manual :: 14.7.1 InnoDB Locking글한석종 부장 | R&D 데이터팀[email protected]브랜디, 오직 예쁜 옷만
조회수 3694

포스트맨 200% 활용하기

편집자 주 MAC OS 기준으로 작성했으며, 본문 내용 중 Proxy(또는 프록시)는 영문으로 통일하여 표기함. OverviewPOSTMAN은 API 테스트에 큰 도움을 주는 도구입니다. 강력한데다가 무료입니다. 안 쓸 이유가 없군요. POSTMAN은 사용하는 방법도 쉽습니다. 그래서 이번 글에서는 최근에 나온 POSTMAN native 버전 패킷캡쳐 방법을 공유하겠습니다.native App은 기존 크롬 플러그인 버전보다 깔끔하고 버그도 많이 줄었습니다. 하지만 원래부터 강력했던 postman interceptor가 아직 지원하지 않습니다.1)공식 블로그 답변입니다.이미 interceptor를 사용하고 있어서 native App에 대한 니즈는 없었는데요. 한글 패킷 캡쳐를 시도하고 생각이 완전히 바뀌었습니다. intetceptor로 캡쳐된 패킷테스트 중이던 공지사항 제목이 이상하게 변경됐습니다.Postman Proxy를 써보자!어쩔 수 없이 native App 을 써야겠다고 생각했습니다. 가장 먼저 postman interceptor에 연결할 방법이 필요했는데 위의 공식 블로그 답변처럼 지금은 안 된다고 합니다. 구글링을 했더니 아래와 같은 글이 보였습니다.스마트폰이나, 기타 기기들의 패킷을 캡쳐할 수 있기 때문에 매력적인 방법입니다. 웹을 사용할 땐 브라우저를 Proxy 태우면 결과는 비슷하게 나올 겁니다.native Appnative App은 여기에서 다운로드 받을 수 있습니다. nativeApp을 켜면 오른쪽 위의 메뉴에 interceptor 아이콘은 없고 위성안테나 모양의 아이콘이 있습니다. 이것은 Proxy Server 기능입니다. Proxy Server를 postman native가 구동해주고 사용하는 방식이죠.Proxy 설정 화면이 뜨는 기본 포트는 5555번입니다. 따로 할 건 없고, 캡쳐 위치는 기본 값인 History로 지정합니다. 만약 다른 컬렉션에 내용을 모으고 싶다면 그곳으로 지정하세요. Connect 버튼을 클릭하면Proxy가 구동됩니다.요청 내용을 긁어 모을 때다!Proxy 세팅을 마쳤으니 브라우저를 연결해야겠죠? 일반적인 방법으로는 연결되지 않습니다. 여기선 크롬 확장 프로그램인 Proxy SwitchyOmega의 도움을 받았습니다. 다운로드는 여기를 클릭하세요.이것은 Proxy 스위칭 프로그램입니다. 도메인 단위로 설정이 가능하기 때문에 on 또는 off 따로 하지 않고도 사용이 가능할 겁니다. 플러그인 설치를 마쳤다면 설정을 유도합시다.Server에는 localhost, Port에는 5555를 적어주세요.캡쳐하고 싶은 사이트에 들어가 Direct 옵션을 켭니다.Proxy를 활성시킵니다.브랜디 주요 도메인인 brandi.co.kr을 클릭해 Proxy를 활성시키면 ***.brandi.co.kr 도메인은 Local Proxy를 타고 넘어가는데요. 이제 받기만 하면 됩니다. (빵끗)진짜 긁어 모아보자!캡쳐하려고 했던 사이트에 접속해 요청을 발생시킵니다.내부 테스트 서버postman native App 캡쳐 내용와우! 발생한 요청 내용이 캡쳐되어서 들어오기 시작합니다.속이 뻥!!!속을 썩이던 한글도 깔끔하게 캡쳐되었군요. 이제 행복한 테스트만 남았습니다. 즐거운 시간 되시길 바랍니다.소소하지만 알찬 팁1: 필터 기능proxy 설정도구에서 필터 기능을 사용하면 원하는 것만 캡쳐할 수 있습니다.소소하지만 알찬 팁2: 테스트 기능스마트폰의 native App은 위와 같이 설정하면 테스트할 수 있습니다. 이제 휴대폰 테스트 결과를 PC로 수집할 수 있을 겁니다. 앱 테스트에 대한 상세 설명은 여기를 클릭하세요.소소하지만 알찬 팁3: 안 쓸 때는..proxy를 안 쓸 때는 System Proxy를 클릭해 끄도록 합시다.1) interceptor는 브라우저 요청을 postman에서 패킷을 캡쳐해주는 도구다.참고Capturing HTTP requests글천보성 팀장 | R&D 개발2팀[email protected]브랜디, 오직 예쁜 옷만#브랜디 #개발자 #개발팀 #인사이트 #경험공유 #Postman
조회수 2742

Next.js 튜토리얼 4편: 동적 페이지

* 이 글은 Next.js의 공식 튜토리얼을 번역한 글입니다.** 오역 및 오탈자가 있을 수 있습니다. 발견하시면 제보해주세요!목차1편: 시작하기 2편: 페이지 이동 3편: 공유 컴포넌트4편: 동적 페이지  - 현재 글5편: 라우트 마스킹6편: 서버 사이드7편: 데이터 가져오기8편: 컴포넌트 스타일링9편: 배포하기개요여러 페이지가 있는 Next.js 애플리케이션을 만드는 방법을 배웠습니다. 페이지를 만들기 위해 한 개의 실제 파일을 디스크에 만들어야 합니다.그러나 진짜 애플리케이션에서는 동적 컨텐츠를 표시하기 위해 동적으로 페이지를 생성해야 합니다. Next.js를 사용해 이를 수행하는 여러 방법들이 있습니다.쿼리 문자열을 사용하여 동적 페이지를 생성해봅시다.간단한 블로그 애플리케이션을 만들 예정입니다. 이 애플리케이션은 home (index) 페이지에 전체 포스트 목록을 가지고 있습니다.포스트 제목을 클릭하면 뷰에서 각 포스트를 볼 수 있어야 합니다.설치이번 장에서는 간단한 Next.js 애플리케이션이 필요합니다. 다음의 샘플 애플리케이션을 다운받아주세요:아래의 명령어로 실행시킬 수 있습니다:이제 http://localhost:3000로 이동하여 애플리케이션에 접근할 수 있습니다.포스트 목록 추가하기먼저 home 페이지 안에 포스트 제목 목록을 추가해봅시다.pages/index.js에 다음과 같은 내용을 추가해주세요.위의 내용을 추가하면 다음과 같은 페이지가 보입니다:첫 번째 링크를 클릭하면 404 페이지가 나지만 괜찮습니다.페이지의 URL은 무엇인가요?- /?id=Hello Next.js- /post?title=Hello Next.js- /post?title=Hello Next.js- /post쿼리 문자열을 통해 데이터 전달하기쿼리 문자열(쿼리 파라미터)를 통해 데이터를 전달했습니다. 우리의 경우에는 "title" 쿼리 파라미터입니다. 다음에서 보이는 것처럼 PostLink 컴포넌트를 이용해 구현해봅시다:(Link 컴포넌트의 href prop를 확인해주세요.)이처럼 쿼리 문자열을 이용하여 원하는 모든 종류의 데이터를 전달할 수 있습니다."post" 페이지 생성이제 블로그 포스트를 보여줄 post 페이지를 생성해야 합니다. 이를 구현하기 위해 쿼리 문자열로부터 제목을 가져와야 합니다. 어떻게 구현하는지 살펴봅시다:pages/post.js 파일을 추가하고 다음과 같이 내용을 작성해주세요:다음과 같이 보입니다:위의 코드에서 무슨 일이 일어났는지 살펴봅시다.- 모든 페이지에서 현재 URL과 관련된 내용들을 가진 "URL" prop를 가져옵니다.- 이 경우 쿼리 문자열을 가진 "query" 객체를 사용하고 있습니다.- props.url.query.title를 사용해 제목을 가져왔습니다.애플리케이션에서 몇 가지를 수정해봅시다. "pages/post.js"를 다음과 같이 변경해주세요: http://localhost:3000/post?title=Hello Next.js 페이지로 이동하면 무슨 일이 일어날까요?- 예상대로 동작할 것이다.- 아무 것도 랜더링하지 않을 것이다.- 해더만 랜더링할 것이다.- 에러를 발생시킬 것이다.특별한 prop "url"보다시피 위의 코드는 이와 같은 에러를 발생시킵니다:url prop는 페이지의 메인 컴포넌트에만 전달되기 때문입니다. 페이지에서 사용되는 다른 컴포넌트에는 전달되지 않습니다. 필요하다면 다음과 같이 전달할 수 있습니다:마치며쿼리 문자열을 사용하여 동적 페이지를 생성하는 방법을 배웠습니다. 이제 시작일 뿐입니다.동적 페이지를 렌더링하기 위해 더 많은 정보가 필요합니다. 그리고 쿼리 문자열을 통해 모든 것을 전달할 수는 없을 것입니다. 또는 http://localhost:3000/blog/hello-nextjs와 같은 깔끔한 URL을 원할 것입니다.다음 편에서 이것들에 대해 모두 배울 수 있습니다. 이번 편은 모든 것의 기초입니다.#트레바리 #개발자 #안드로이드 #앱개발 #Next.js #백엔드 #인사이트 #경험공유
조회수 1541

레진 기술 블로그 - SVG를 이용해 간단한 웹 게임 만들어보기

근래 소규모로 게임 프로그래밍 스터디를 시작했습니다. 서비스 UI를 개발하는 프론트엔드개발자에게 있어 게임 프로그래밍은 언제나 커튼 뒤에 비친 풍경처럼 흐릿하고 형체를 쉽게 알 수 없는 신비한 존재입니다. 이번에 미약하게나마 커튼을 걷어 창문 너머 펼쳐진 풍경을 감상해 보자는 게 이번 스터디의 개인적인 목표입니다.왜 SVG를 선택했나게임을 만드는 데 어떤 기술을 사용할지 고민했습니다. 일반적인 DOM은 쉽게 객체를 조작할 수 있지만, 문서의 엘리먼트를 추상화한 것에 불과하므로 다양한 도형을 만들거나 좌표계에 사상(寫像, Mapping)1하기 쉽지 않습니다.캔버스는 그래픽 처리에 환상적인 성능을 보여주고 원, 다각형 등 다양한 도형을 그리기 쉽지만 일일이 객체화해야 하고 이를 관리하기 쉽지 않습니다. 여기에 필자가 캔버스를 좀 처럼 써 본 경험이 없어서 무턱대고 사용하기에도 부담을 느꼈습니다.하지만 SVG는 이 두 장점을 모두 갖고 있습니다. 확장 가능한 벡터 그래픽(Scalable Vector Graphics)이라는 이름을 통해서 알 수 있듯이 그래픽 요소를 그리는데 적합한 포멧이며 DOM처럼 추상화된 객체도 지원합니다.어떤 게임을 만들었나필자가 만든 게임은 크롬에 내장된 Running T-Rex와 비슷한 것으로 JUMPING CAR라고 이름을 붙였습니다. 플레이해보고 싶은 분은 uyeong.github.io/jumping-car를 방문하시기 바랍니다.규칙은 단순합니다. 게임을 시작하면 자동차가 달려나가고 이윽고 장애물을 만나게 됩니다. 장애물을 뛰어넘으면 점수가 1씩 증가하지만 부딪히면 게임이 종료됩니다.이 글에서는 게임을 만드는 과정을 소개하기보다 SVG를 이용하면서 알게 된 몇 가지 주요한 내용을 다룹니다.Pattern을 사용한 요소는 느리다이미지를 반복해서 출력할 때 HTML에서는 CSS의 background-url 속성으로 간단히 해결할 수 있습니다. 하지만 SVG에서는 Pattern 요소를 이용해야 합니다.아래 그림처럼 pattern#pat-land 요소를 만들고 이를 rect.parallax에서 사용하여 그림을 반복 출력되도록 합니다. 그리고 rect.parallax를 조금씩 Transform 하여 앞으로 이동하도록 구현합니다.코드는 다음과 같습니다(예제: svg-parallax-test/parallax1).<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="..."> <defs> <pattern id="pat-land" x="0" y="0" width="..." height="100%" patternUnits="userSpaceOnUse"> <image x="0" y="0" xlink:href="../images/land.png" width="..." height="100%"></image> </pattern> </defs> <g> <rect class="parallax" x="0" y="0" width="..." height="100%" fill="url(#pat-land)" transform="translate(0,0)"></rect> </g> </svg> 표면상으론 전혀 문제가 없는 코드지만 크롬 브라우저에서 이 코드를 실행하면 프레임이 50 이하로 떨어지는 경우도 발생합니다. 이 정도면 육안으로도 화면의 움직임이 매끄럽지 않게 느껴지는 수치입니다.따라서 성능에 영향을 주는 pattern을 제거하고 image 요소로 대체합니다. image 요소는 자동으로 반복할 수 없으므로 두 개의 요소를 이어 붙여 사용합니다(예제: svg-parallax-test/parallax2).<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="..."> <g> <image x="0" y="0" xlink:href="../images/land.png" width="..." height="100%"></image> <image x="..." y="0" xlink:href="../images/land.png" width="..." height="100%"></image> </g> </svg> 실행 결과 프레임이 안정적이고 육안으로도 이질감을 느낄 수 없습니다. 이처럼 Pattern을 이용한 SVG 요소를 애니메이션 처리할 때에는 주의가 필요합니다.일부 안드로이드 기종에서의 성능 문제pattern을 제거하고 image로 대체하면서 Parallax 처리 시 발생한 문제를 해결할 수 있습니다. 하지만 image로 대체하더라도 일부 안드로이드 기종에서는 여전히 성능 문제가 발생합니다.아래 영상처럼 image 요소를 Transform 할 경우 프레임이 급격하게 떨어집니다. 이는 크롬 개발자 도구에서도 쉽게 발견하기 힘든데 CPU 성능을 10배 줄여 테스트해도 수치상으로는 크게 차이 나지 않기 때문입니다.<style>.video-container { position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden; } .video-container iframe, .video-container object, .video-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style><iframe width="560" height="315" src="https://www.youtube.com/embed/F_-zXf1jb8I?rel=0" frameborder="0" allowfullscreen="">이 처리를 DOM으로 바꿔보면 어떻게 될까. 놀랍게도 큰 차이를 보여줍니다(예제: svg-parallax-test/parallax3).<iframe width="560" height="315" src="https://www.youtube.com/embed/VXQ1aT79D2s?rel=0" frameborder="0" allowfullscreen="">SVG에 대한 최적화 상황은 브라우저마다 조금씩 다릅니다. DOM은 과거부터 최적화 노력이 많이 이뤄졌지만, SVG는 pattern 요소나 다음 절에서 이야기할 리페인팅 문제 등 성능 문제를 일으키는 부분이 아직 남아있습니다.따라서 충돌 계산처럼 특별히 좌표계 연산이 필요 없는 배경은 DOM으로 옮기고 자동차, 장애물만 SVG로 구현했습니다(예제: svg-parallax-test/parallax4).SVG는 항상 페인트를 발생시킨다SVG는 이상하게도 svg 요소의 크기를 고정하더라도 자식 요소를 변경하면 페인팅이 발생합니다. 아래는 svg 요소의 자식 요소인 rect의 좌표를 수정하는 예제 코드입니다.<svg"http://www.w3.org/2000/svg" width="500px" height="500px" viewBox="0 0 500 500"> width="500" height="500" x="0" y="0"> </svg> [removed] setTimeout(() => { rect.setAttribute('x', '100'); }, 3000); [removed] svg는 viewBox로 설정한 사이즈 만큼 내부에 그림을 그립니다. 즉, 내부의 어떠한 그래픽적 변화가 문서에 변화를 일으킬 가능성이 없습니다. 그래서 개인적으로 쉽게 이해가 되지 않는 렌더링 흐름입니다.그러면 SVG 요소의 크기나 좌표를 바꾸지 않고 색상 또는 투명도를 변경하면 어떨까요. 이번에는 rect 요소의 좌표가 아니라 색상을 바꿔봅니다.<svg"http://www.w3.org/2000/svg" width="500px" height="500px" viewBox="0 0 500 500"> width="500" height="500" x="0" y="0"> </svg> setTimeout(() => { rect.setAttribute('fill', '#ebebeb'); }, 3000); 그래도 페인트가 발생합니다. 하지만 앞서 진행한 테스트의 페인팅 시간은 수십 마이크로세컨드로 크게 의미가 없어 보입니다. 그래서 현재 서비스 중인 레진코믹스의 메인페이지에 SVG를 넣고 테스트했습니다.페인팅에 0.51ms가 소요됐습니다. 작다고 느낄 수 있지만 페이지 전반적으로 영향을 줄 수 있으며, 애니메이션 처리 중인 SVG라면 성능적 문제를 발생시킬 수 있는 부분입니다.그래서 svg 요소에 null transforms 핵을 선언해 문서 상위 레벨까지 페인팅이 전파되지 않도록 합니다.<svg"http://www.w3.org/2000/svg" width="500px" height="500px" viewBox="0 0 500 500" style="transform:translate3d(0,0,0)"> width="500" height="500" x="0" y="0"> </svg> 또는 아예 svg 내부의 요소를 개별로 분리하는 방법도 있습니다(참고: Doubling SVG FPS Rates at Khan Academy).<svg> fill="red" transform="translate(2px, 3px)"> fill="blue" transform="scale(2)"> </svg> style="transform:translate(2px, 3px)"> <svg> fill="red"> </svg> style="transform:scale(2)"> <svg> fill="blue"> </svg> 끝으로여기까지 SVG를 이용해 게임을 개발하면서 만나게 된 이슈와 해결 방법을 간단히 정리했습니다.필자는 간단한 게임은 SVG로 만들 수 있고 괜찮은 성능을 보장할 것이라고 기대했습니다. 하지만 현실은 달랐습니다. 이 글에서 다룬 문제 외에도 사파리와 크롬 브라우저의 성능 차이, 자동차를 움직일 때 버벅이는 현상 등 다양한 문제를 해결해야 했습니다. 객체의 개수도 적고 애니메이션도 복잡하지 않은 단순한 게임이었는데 말이죠.다음 게임은 캔버스로 시작하고자 합니다.공간(空間)의 한 점에 대(對)하여, 다른 공간(空間) 또는 동일(同一)한 공간(空間)의 한 점(點)을 어떤 일정(一定)한 법칙(法則)에 의(依)하여 대응(對應)시키는 일 ↩
조회수 1111

EOS Proxy Voting이란?

우선 EOS BP 투료를 한 번쯤 해보신 분들은 매번 새롭게 등장하는 BP 후보들은 넘쳐나고 그들의 이름과 공약을 확인하는 것이 귀찮다고 느끼셨을 수 있습니다.또한 어렵게 공약을 확인하고 정말 이 팀이 EOS를 위해 무엇을 할 수 있는지 다른 팀들과 어떤점이 다른지 꼼꼼하게 비교하여 선거한 여러분의 소중한 투표권 파워는 시간이 지날수록 가치가 줄어들게 됩니다.그렇다면 나 대신에 꾸준히 선거를 대신해줄 사람이 있다면 얼마나 좋을까요?사실 이런 문제에 대해 EOS도 알고 있었으며, 어떤 해결 방법이 있을지 생각해왔습니다.그래서 바로 만들어진 것이 EOS Proxy Voting입니다.Proxy란 ‘대리인’이란 의미를 갖고 있습니다.따라서 EOS Proxy Voting은 EOS BP 대리 투표 시스템을 뜻합니다.이 대리인 투표권을 신청하게 되면 여러분은 더 이상 투표에 대해 고민하실 필요가 없게 되는 거예요!이제 이 Proxy 시스템을 어떻게 이용하는지 방법을 소개하고자 합니다.1. 어떻게 Cleos를 통해 다른 사람에게 나의 투표 권한을 넘길 수 있나요?나의 투표 권한을 Cleos를 통해 다른 사람에게 넘기기 위해선 다음과 같은 명령어를 입력해야합니다.간단하지요? 이 명령어는 eosaccount12가 자신의 투표 권한을 proxyvoter34에게 넘기겠다는 의미를 갖고 있습니다.2. 어떻게 툴킷을 통해 다른 사람에게 나의 권한을 넘길 수 있는 건가요?대표적으로 https://eostoolkit.io/vote/setproxy에서 Proxy를 설정하는 방법을 안내해드릴게요! (참고로 https://www.myeoskit.com/#/tools/proxy/https://eosvoter.eosphere.io 에서도 가능합니다. )나의 proxy를 툴킷을 통해 다른 사람에게 넘기기 위해선 먼저 Scatter 구글 확장 프로그램을 설치해야 합니다.Scatter 설치 후 EOS 계정 및 접속 정보를 Scatter에 등록하셔야 합니다. (Scatter에 정보를 등록하는 방법은 곧 업데이트 하도록 하겠습니다.)그렇다면 등록을 다 하셨을 테니 다음으로 넘어가겠습니다.우선 EOStoolkit에 접속하셔서 스캐터 계정으로 로그인하셔야 합니다.로그인 하셨다면 이제 왼쪽 카테고리에서 [Manage Voting] 항목을 보실 수 있을거에요![Manage Voting]를 클릭하시면 Voting에 관한 여러 항목이 촤르르 나오게 되는데 그 중에 [Set Proxy]를 눌러주세요!자 그럼 아래 화면에 나온 대로 그대로 따라하신 후 저장만 해주시면 됩니다.드디어 투표 권한을 지정 Proxy에게 넘기게 되었습니다.3. 어떻게 내가 설정한 Proxy를 해제할 수 있나요?Proxy 지정을 하고 며칠동안 투표에 신경을 쓰지 않았다가 오랜만에 들어간 투표 사이트에서 내가 지정한 대리인이 행사하는 나의 투표권이 마음에 들지 않을 땐 어떻게 해야할까요?해제를 해야겠지요!그렇다면 지금 내가 지정한 Proxy가 마음에 안들어서 해제하고 싶을 때는 어떻게 할지도 알아보겠습니다.Proxy 설정을 했다면, 저 네모박스에 체크되어 있을겁니다. 그 체크를 해지 하면 간단하게 내가 설정한 Proxy를 해제하게 되는 것입니다.아주 간단하네요.그럼 이제 다음은 내가 직접 Proxy가 되기 위해선 어떻게 할 수 있을지 알아보겠습니다.그 방법도 마찬가지로 Cleos 또는 Toolkit 과 Scatter를 통해 할 수 있습니다.4. Cleos를 통해서 내가 직접 Proxy가 될 수 있는 방법은 어떤게 있나요?내가 직접 Cleos를 통해 Proxy가 되기 위해선 다음과 같은 명령어를 입력해야합니다.이 명령어는 proxyvoter34는 Proxy로 지정되었는 의미를 갖고 있습니다.5. 어떻게 툴킷을 통해 내가 직접 Proxy가 될 수 있는 건가요?우선 툴킷을 통해 Proxy로 등록하기 위해선 가장 먼저https://eostoolkit.io/vote/setproxy 에 나의 Scatter 계정으로 로그인해야 합니다.(참고로 https://www.myeoskit.com/#/tools/proxy/https://eosvoter.eosphere.io 에서도 가능합니다. )로그인 하셨다면 왼쪽 카테고리에서 [Manage Voting]을 찾아주세요!찾으셨다면 해당 항목의 아래 항목에서 [Create Proxy] 를 클릭해주세요. 그럼다음과 같은 화면이 나오게 됩니다.아래 나와있는 설명 그대로 적어주시고 저장해주시면 됩니다. 다 완료하셨으면 드디어 Proxy가 되셨어요!6. 더이상 Proxy로 활동하고 싶지 않으면 어떻게 해야 하나요?더 이상 Proxy로서 활동을 하고 싶지 않다면 마찬가지로 [Manage Voting]를 통해 Proxy 철회를 할 수 있습니다.[Manage Voting]를 클릭 후 아래 항목에서 [Resign Proxy]을 누르시면 됩니다. 첫 번째 Resign 버튼은 Proxy 등록을 해지하는 것이고 두 번째 Unregister 버튼은 등록한 정보를 삭제하는 버튼입니다.각각의 버튼을 눌러 그대로 진행하시면 Proxy 철회가 완료될 거예요!자 여기까지 이제 EOS Proxy Voting을 하기 위해Proxy 설정하는 방법을 알아보았습니다. 어렵게 보이지만 Scatter 연동만 하면 Proxy를 설정하거나 내가 직접 Proxy가 되는 것은 어렵지 않습니다!아 참고로, 현재 등록된 모든 Proxy 리스트를 Aloha EOS Proxy Research Portal에서 확인할 수 있습니다.또한 해당 사이트에서 Proxy들이 자신들이 Proxy로 활동하면서 어떻게 투표를 행사할 것인지에 대한 공약도 자세히 나와있으니 한 번쯤 들어가서 보시면 Proxy를지정하는 데에 있어서도, 내가 직접 Proxy가 됨에 있어서도 도움이 될 거예요!#헥슬란트 #HEXLANT #블록체인 #개발자 #개발팀 #기술기업 #기술중심
조회수 2333

AWS S3를 이용하여 Vue 배포하기

Vue를 처음 만났을 때, 이것으로 무엇을 할 수 있을지 궁금했다. 하지만 Vue로 데모 앱과 개발 가이드를 따라하면서 의문은 점점 풀렸다. 알다시피 Vue는 front-end 로 활용이 된다. 빌드가 없어도 되고, 빌드를 해서 배포할 수도 있다. Vue는 일반 CDN을 이용하여 페이지를 만드는 방법과 여러 프레임워크를 활용하여 배포하는 방법 외에 다양한 방법이 존재하는데, 무슨 방법을 쓰든 결과물은 html과 js, css 같은 static 파일로만 이루어져 있다.처음에는 일반적인 방법으로 테스트하면서 다양한 디렉티브와 손쉽게 DOM 처리를 하는 방법을 익혔다. 나중엔 프로젝트에 참여하면서 webpack 으로 빌드해 배포하도록 프로젝트를 구성했다. webpack을 이용한 배포방법은 여기 를 참고하면 된다. 참고로 webpack은 nodeJS로 실행되기 때문에 기본적인 환경을 세팅해야 한다.webpack build.js 일부위처럼 직접 스크립트를 만들어서 사용해도 되지만 Vue에서 제공하는 템플릿으로 프로젝트를 생성할 수도 있다. 단 Vue-CLI가 미리 설치되어 있어야 한다.터미널에서 vue init webpack 프로젝트명만 치면 세팅된 템플릿으로 폴더 및 스크립트들이 구성된다. 아래와 같이 프로젝트의 기본 속성들을 입력하자.프로젝트를 만들면 기본적인 파일들로 이루어진 폴더가 생성된다. 현재는 관련 라이브러리들이 없는 상태이므로 npm install 을 통해 설치한다. 설치 후 nom run dev 로 개발모드를 실행하면 브라우저로 화면을 볼 수 있다. 만약 설치하고 빌드 설정을 수정하지 않았다면 기본 8080 포트로 가동된다. 브라우저를 실행해 http://localhost:8080 으로 접속하면 아래와 같은 화면이 나온다.여기까지 하면 webpack 으로 배포할 수 있는 상태가 되었다. 이제 AWS로 가서 회원가입을 하고 S3를 생성한다. 생성 방법은 여기를 참고하면 된다. 버킷까지 생성되었다면 이제 빌드 후 업로드하자.위와 같이 nom run build 를 하면 빌드가 시작된다.빌드가 완료되면 해당 프로젝트 폴더에 dist 폴더가 생성된다. dist 폴더에는 index.html 과 js, css 와 같은 리소스들이 들어간다. 이제 S3로 가서 올리려는 버킷을 클릭하자.업로드 버튼을 클릭하고, dist 폴더에 있는 index.html 과 static 폴더를 업로드한다. 폴더가 업로드되면 아래와 같이 파일과 폴더들이 보인다.업로드가 완료되었다고 지금 바로 웹사이트처럼 접근할 수는 없다. 정적 웹사이트 호스팅 설정을 활성화해야 비로소 가능하다. 속성 탭을 클릭해 정적 웹사이트 호스팅을 활성화 상태로 만든다.위와 같이 활성화하고 인덱스 문서에만 index.html 을 입력한 후 저장 버튼을 클릭한다. 현재 보이는 엔드포인트 주소가 외부에서 접근할 수 있는 사이트 도메인이다. 그 후 엔드포인트 주소로 접속하면 아래와 같이 오류 페이지를 볼 수 있다.이게 무슨 오류란 말인가… index.html 파일도 있는데 403 오류라니..자세한 http 응답코드는 여기를 참고하면 된다. 위의 오류는 권한이 없어서 파일에 액세스할 수 없다는 페이지다. S3는 기본적으로 모두에게 공개하진 않는다. 그래서 특정 파일이나 특정 버킷만 공개형으로 변경해줘야 한다.이 문제를 해결하려면 권한 탭으로 이동해 버킷 정책을 설정해야 한다. 아래와 같이 설정해주면 누구에게나 공개되어 접근할 수 있다.위 내용을 아래와 같이 버킷 정책으로 설정한다.설정을 저장한 후 다시 엔드포인트로 접속하면 아래와 같이 로컬에서 보였던 페이지가 보인다.이렇게 보이면 성공!다음엔 Vue가 어떤식으로 동작을 하는지 알아보도록 하겠다.마치며Vue는 간결하면서도 강력한 기능을 가지고 있는 front-end 프레임워크다. 개념과 디렉티브, 이벤트 핸들링, 보안 등 궁금한 게 많았지만 신통방통한 놈인 건 확실하다. 아직 큰 프로젝트에 사용하는 건 힘들 수도 있으나 아래와 같이 장점이 많아 서버단과 클라이언트단 분리 개발, 외부 라이브러리와 사용하면 훌륭한 프레임워크가 될 거라는 생각이 든다.재사용 가능한 기능별 컴포넌트 개발훌륭한 라우터 탑재서버와 통신 가능한 ajax 모듈이 다양함 ( jQuery Ajax, Axios )다양한 호환 라이브러리를 활용하면 분명 훌륭한 프레임워크가 될 것!편집자 주) 함께 보면 좋아요!Vue, 어디까지 설치해봤니?PHP Codeigniter 환경에서 VUE 사용해보기JQuery 프로젝트에 VUE를 점진적으로 도입하기Vue와 Vuex, 컴포넌트간 통신과 상태 관리글장현준 팀장 | R&D 개발3팀[email protected]브랜디, 오직 예쁜 옷만
조회수 10025

Estimator: BLE를 사용한 Planning Poker 애플리케이션

1. Planning PokerStyleShare 개발팀에서는 스크럼을 활용하여 일을 진행하고 있습니다.1 스크럼에는 일감의 크기를 추정estimate하는 과정이 있는데요. 구성원들 모두가 일감에 대해 이해하고 일감의 크기가 어느정도인지 함께 논의하여 합의에 이르는 과정입니다. 스프린트 회의에서 일감을 등록한 사람(리포터)이 일감에 대해 설명하고 나서 전체 구성원들이 일감의 크기를 추정하는데, 이 때 사용하는 것이 바로 Planning Poker입니다.Planning Poker는 0.5부터 시작해서 1, 3, 5, 8, 13, 20, … 100과 같이 피보나치 수열로 증가하는 숫자를 가진 카드 덱입니다. 리포터의 설명이 끝난 뒤 스크럼 마스터가 하나, 둘, 셋을 외치면 각자 생각한 일감의 크기에 맞는 카드를 꺼내고, 스크럼 마스터는 구성원들의 추정치가 최대한 가까워지도록 부가설명이나 질문을 유도합니다.2▲ Planning Poker는 이렇게 생겼다. (출처: Control Group 블로그)하지만 개발팀이 커지면서 불편함이 생기기 시작했습니다. 회의에 참여하는 인원이 7-8명씩 되다 보니, 각자가 어떤 카드를 들고있는지 한눈에 보기가 어려워진 것입니다. StyleShare에서 자칭 아이디어 뱅크 역할을 담당하고 있는 저는 획기적인 방법이 필요하다고 생각했고, 굳이 카드를 꺼내들지 않아도 각자가 무슨 카드를 선택했는지를 쉽게 볼 수 있는 애플리케이션을 만들기로 결심했습니다.2. BLE (Bluetooth Low Energy)불편함을 덜기 위한 애플리케이션이므로, 사용자 경험이 굉장히 직관적이고 단순해야 했습니다. N:N 통신이 가능해야하고, 사용자를 귀찮게 하는 페어링Pairing이나 네트워크 접속 과정이 없어야 했습니다. 한마디로, 카드를 꺼내들고 눈으로 확인하는 것보다 더 편한 무언가를 만들어야 했습니다!처음에는 근거리 무선 통신을 위한 기술로 스타벅스에서 사이렌 오더 개발에 사용한 고주파 인식 기술을 생각했습니다.3 각자의 기기에서 선택한 카드에 맞는 소리를 내보내고, 다른 기기에서는 고주파를 읽겠다는 것이었는데요. Soundlly(구 aircast.me)와 같은 상업용 SDK를 쓰지 않는 이상, 사운드 프로그래밍을 한 번도 해본 적 없는 저에게는 데이터가 실린 고주파를 만드는 것부터 소리를 인식해서 데이터를 읽어내는 과정이 마치 화성에서 감자 키우는 이야기처럼 들렸습니다.그러다 문득 생각난 것이 바로 비콘Beacon입니다. 언젠가 소비자가 오프라인 매장에 방문하면 BLE를 이용해서 매장 위치를 파악하는 기술이 있다는 이야기를 들은 적이 있었습니다. 찾아보니 시중에 나와있는 대부분의 모바일 기기에서는 BLE를 위한 최소 조건인 블루투스 4.0을 지원했고, 페어링이나 네트워크 접속 과정도 불필요했습니다. 무엇보다, 화성에서 감자 키우는 것보다는 쉬워보였습니다.3. Swift로 BLE 개발하기그래서 BLE를 사용해서 개발하기로 했습니다. 컨셉은 간단했습니다. 내가 선택한 카드를 브로드캐스팅하고, 다른 사람들이 선택한 카드를 내 모바일 기기에 보여주면 되는 것이었습니다. BLE를 사용하면 정보를 브로드캐스팅할 수 있고, 다른 기기에서 브로드캐스팅하는 정보를 읽을 수 있습니다.BLE에서 데이터를 브로드캐스팅하는 것을 Advertising이라고 합니다. 정보를 advertising하는 주체는 Peripheral이고, advertising되는 정보를 스캔하여 데이터를 읽어들이는 주체는 Central이라고 합니다. Peripheral에서 정보를 advertising할 때에는 특정한 정보를 실어나를 수 있는데요. 이를 Advertising Data Payload라고 합니다. 이 정보에 카드 숫자와 이름을 실어서 전송하면 될 것 같습니다.BLE를 구현하기 위해서, iOS에서는 SDK에 기본적으로 포함돼있는 CoreBluetooth 프레임워크를 사용하면 손쉽게 개발이 가능합니다. CBPeripheralManager 클래스와 CBCentralManager 클래스를 쓰면 되는데요. BLE를 이용하여 제 이름 석자를 advertising하는 코드는 다음과 같습니다.Peripheralimport CoreBluetooth let serviceUUID = CBUUID(string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") let service = CBMutableService(type: serviceUUID, primary: true) /// 1. `CBPeripheralManager`를 초기화하고, self.peripheral = CBPeripheralManager(delegate: self, queue: nil) /// 2. 사용가능한 상태가 되면 특정 UUID를 가진 서비스를 추가한 뒤에 func peripheralManagerDidUpdateState(peripheral: CBPeripheralManager) { if peripheral.state == .PoweredOn { self.peripheral.addService(service) } } /// 3. 원하는 정보를 advertising합니다. func peripheralManager(peripheral: CBPeripheralManager, didAddService service: CBService, error: NSError?) { self.peripheral.startAdvertising([ CBAdvertisementDataLocalNameKey: "전수열", CBAdvertisementDataServiceUUIDsKey: [serviceUUID], ]) } 참고로, UUID는 커맨드라인 명령어를 통해 쉽게 만들 수 있습니다.$ uuidgen XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 마찬가지로, Peripheral에서 advertising하는 정보를 스캔하는 Central 코드는 다음과 같이 작성할 수 있습니다. UUID는 Peripheral에서 advertising에 사용한 UUID와 동일해야합니다.Centralimport CoreBluetooth let serviceUUID = CBUUID(string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") let service = CBMutableService(type: serviceUUID, primary: true) /// 1. `CBCentralManager`를 초기화하고, self.central = CBCentralManager(delegate: self, queue: nil) /// 2. 사용가능한 상태가 되면 특정 UUID를 가진 서비스를 스캔합니다. func centralManagerDidUpdateState(central: CBCentralManager) { if central.state == .PoweredOn { // 이미 한 번 스캔된 정보라도 계속 스캔합니다. let options = [CBCentralManagerScanOptionAllowDuplicatesKey: true] self.central.scanForPeripheralsWithServices([serviceUUID], options: options) } } /// 3. Peripheral이 스캔되면 이 메서드가 호출됩니다. func centralManager(central: CBCentralManager, didDiscoverPeripheral peripheral: CBPeripheral, advertisementData: [String : AnyObject], RSSI: NSNumber) { print(advertisementData[CBAdvertisementDataLocalNameKey]) // "전수열" } 스캔을 시작할 때 CBCentralManagerScanOptionAllowDuplicatesKey 옵션을 true로 설정해서 한 번 스캔된 정보라도 중복으로 계속 스캔하도록 합니다.4. 원하는 정보를 실어나르기CBPeripheralManager을 사용하여 advertising을 할 때에는 Advertising Data Payload를 포함시킬 수 있는데, 이 정보 중 개발자가 원하는 값을 넣을 수 있는 곳은 CBAdvertisementDataLocalNameKey밖에 없습니다. 그마저도 길이가 제한돼있기 때문에, 패킷을 효율적으로 사용하기 위해서는 정보를 저장하는 프로토콜을 직접 정의해야 합니다.우선, 카드에 대한 정의는 enum을 사용해서 작성했습니다. 0부터 0xFF까지의 숫자를 가지도록 정의했습니다.public enum Card: Int { case Zero = 0 case Half = 127 case One = 1 case Two = 2 case Three = 3 case Five = 5 case Eight = 8 case Thirteen = 13 case Twenty = 20 case Fourty = 40 case Hundred = 100 case QuestionMark = 0xFD case Coffee = 0xFE case None = 0xFF } 그리고 제가 정의한 패킷의 프로토콜은 다음과 같습니다.영역길이예시설명Version200프로토콜 버전 (00~FF)Channel201BLE 커버리지 내에서 회의하는 팀이 여럿일 수 있으니, 채널로 구분합니다. (00~FF)Card2FE카드의 16진수 값 (00~FF)Name12전수열사용자 이름 (UTF-8 기준 한글 4글자)이렇게 하면 총 18바이트 내에서 필요한 정보를 모두 전송할 수 있습니다. 이제 이 "00", "01", "FE", "전수열" 값을 직렬화해서 CBAdvertisementDataLocalNameKey로 advertising하면 됩니다.Peripheralself.peripheral.startAdvertising([ CBAdvertisementDataLocalNameKey: "0001FE전수열", CBAdvertisementDataServiceUUIDsKey: [serviceUUID], ]) 그리고, Central에서 정보를 스캔할 때에는 이 값을 각 영역의 길이에 맞게 끊어서 읽을 수 있습니다.5. 마치며비록 적은 양의 정보지만, BLE를 사용해서 실시간으로 근거리 통신을 할 수 있게 되었습니다. 이제 남은 것은 카드를 선택할 수 있는 화면과, 다른 사용자가 선택한 카드를 화면에 보여주는 인터페이스입니다. UI 개발은 본 포스트에서 중점적으로 다루고자 하는 주제와는 조금 벗어난 이야기가 될 것 같아, 오픈소스로 공개된 코드로 대신하려고 합니다. 소스코드는 GitHub에서 볼 수 있으며, Estimator는 앱스토어에서 받아보실 수 있습니다.6. 참고 자료BLE(BLUETOOTH LOW ENERGY) 이해하기 - Hard Copy World스타일쉐어의 스크럼이 지나온 길 포스트에 보다 자세히 설명되어 있습니다. ↩구성원들의 추정치에 차이가 난다는 것은 해당 일감에 대해 서로가 이해하고 있는 정도가 다르기 때문입니다. 스크럼 마스터는 구성원들이 일감에 대해 모두 비슷한 생각을 가지도록 커뮤니케이션을 유도해야합니다. ↩http://www.bloter.net/archives/226643 ↩#스타일쉐어 #개발 #개발팀 #개발자 #인사이트
조회수 2374

안드로이드와 자동화 툴

모바일은 플랫폼의 생태계와 규모에 비해 개발자들이 처리해야 할 것이 매우 많습니다.서버나 타 플랫폼들 또한 개발자들의 영역이 많지만 그 영역들이 세분화되고 전문화되어 가고 있습니다. 데이터베이스, 백엔드, 프론트웨어, 인프라, DevOps 와 같이 점점 분야별로 심화되고 독립성을 갖추어 가고 있습니다.하지만 모바일은 각 플랫폼의 개발자들이 전체적인 아키텍쳐, 프론트, 내부용 데이터베이스, 리소스 관리, 배포 등이 해당 플랫폼의 소수의 개발자들에게 광범위하게 공존합니다. 다양한 분야가 전문화되기엔 변화가 잦고 규모가 점 형태로 구성이 된 경우가 많기 때문입니다.그렇기 때문에 반복적이고 불필요하게 비용이 소모되는 작업일수록 자동화 해서 최대한 코드 작성 본연에 업무에 집중할 수 있도록 환경을 구성하는 것이 중요합니다.토스랩 안드로이드 팀은 2015년 초부터 조금씩 자동화 환경을 구성하여 현재는 아래와 같습니다.다국어 문자 관리 자동화이미지 관리 자동화CI다국어 문자 리소스 자동화1. 다국어 글로벌 담당자의 원본 문서토스랩은 다국어 지원을 위해 글로벌 번역 문서를 관리하고 있습니다. 문서는 Google Drive 를 통해서 관리되고 있으며 기획/개발 파트에서 다국어 지원을 위한 리소스를 기입하면 각 언어의 담당자들이 해당 언어를 번역하고 있습니다.구성은 아래와 같습니다ABCDEFGH영어한국어일본어중국어-간체중국어-번체웹키ios 키안드로이드 키2. 기존 작업기존에는 해당 언어의 번역 데이터를 추가하기 위해 개발 파트에서 수동으로 각 언어의 리소스 파일에 추가하는 형태로 진행하였습니다.이러한 작업의 단점은 언어별 리소스 파일에 키-값 형태의 문자 리소스를 추가하는 작업을 반복적으로 해야 한다는 것입니다. 또한 반영이 된 후에 수정된 문자에 대해서 반영하기가 매우 어렵고 실수도 빈번하게 발생합니다.이러한 가능성을 최소화 하기 위해 자동으로 문자 리소스를 갱신하는 작업을 진행하였습니다.3. 안드로이드 파트를 위한 별도 필터 파일 추가|A|B|C|D|E|F| |—-|—-|—-|—-|—-|—-| |영어|한국어|일본어|중국어-간체|중국어-번체|안드로이드 키|가급적 원본 파일에 대한 조작을 피하기 위해 안드로이드용으로 Read-Only SpreadSheet 를 별도로 생성하였습니다.해당 작업을 위해 Google SpreadSheet Script 를 사용하였습니다.4. 자동화 툴 작업자동화 툴의 역할은 크게 3가지였습니다.안드로이드용 필터 파일을 다운로드한다.Spread-sheet를 분석해서 다국어용 자료구조로 변환한다.다국어용 자료구조를 XML 파일로 변경한다.툴은 Python 스크립트로 작업하였습니다.5. Gradle Task 로 추가별도의 Python 파일을 실행해도 되지만 Gradle Task 로 추가하여 Android Studio 에서도 Task 를 실행할 수 있도록 하였습니다.개발팀에서 안드로이드 키를 원본 문서에 추가한 후 Gradle Task 실행하면 바로 반영되도록 하였습니다. 기존의 방식과 가장 큰 차이점은 Merge 시 충돌 이슈에 대해서 더이상 관여하지 않아도 된다는 것입니다. 가장 최근 시점을 기준으로 자동화 Task 를 실행하면 모든 리소스가 최신화되기 때문에 충돌이 난다하더라도 무시하고 새로 Task 를 실행함으로써 충돌에 의한 이슈를 완전히 배제하고 작업할 수 있다는 장점이 생겼습니다.더 나아가 현재는 Android 용 리소스 Key를 기획 팀에서 기획시 적용하도록 하기로 현재 논의되고 있습니다. 이러한 논의가 반영된다면 더이상 리소스 관리에 있어서 개발파트에서 관리 할 필요가 없어지므로 다국어 리소스에 반영해야할 리소스 또한 최소화 될 것이라 기대하고 있습니다.이미지 리소스 자동화1. 기존 작업앱에 사용되는 디자인 리소스는 이슈 트래커와 JANDI 의 디자인 토픽을 통해서 전달 받아 작업을 하였습니다.이런 작업 형태는 이미지 관리가 분산 될 뿐만 아니라 일관성 있는 전달 방식이 아니기 때문에 누락건이 언제든지 존재할 수 있습니다.그래서 디자인 리소스에 대한 관리를 디자인 팀이 주도적으로 하며 개발팀에서는 빠르고 편하게 이미지를 전달 받을 수 있도록 하기 위해 자동화 툴을 만들었습니다.2. 개선 작업토스랩의 디자인 팀에서 사용하는 저장소는 권한에 따라 접근이 가능하도록 API 를 제공하고 있습니다. Read-Only 권한을 부여받은 후 API 를 통하여 이미지를 다운로드하도록 툴을 구성하였습니다.툴은 Python 스크립트로 구성하였습니다.3. Gradle Task 로 추가문자 리소스와 마찬가지로 별도로 Gradle 로 툴을 이용할 수 있도록 하기 위해 별도의 Task 를 정의하여 사용하도록 하였습니다.자동화된 리소스의 관리문자와 이미지를 자동화로 관리한다 하더라도 개발자가 필요에 따라 임의로 추가/수정하는 리소스가 존재 할 수 있습니다.이를테면 다운로드한 이미지 리소스를 활용한 Selector-Drawable 과 같은 것들입니다.이에 따라 자동화 처리된 리소스들은 별도의 관리를 위해 추가적으로 ResourceSet 을 만들었습니다. android { // ...중략 sourceSets { main.res.srcDirs += ${별도의_리소스_경로} } } 이러한 방식을 통해서 자동화된 리소스와 추가적한 리소스를 분리하여 발생할 수 있는 문제를 최소화 하였습니다.지속적 통합 (Continuous Integration, CI)자동화와 관련되어서 결코 빠질 수 없는 내용입니다. 빌드, 테스트, 배포, 리포팅에 이르기까지 이 모든 과정에 있어서 자동화 되지 않았다면 상상하기 어려운 작업들입니다.토스랩에서는 Jenkins 를 활용하여 빌드-테스트-리포팅을 하고 있습니다.1. 빌드 대상빌드의 의미는 최소한 컴파일 오류가 발생하지 않는 코드들이 최종 상태로 관리되고 있음을 의미합니다. 그러기 때문에 언제나 중앙 저장소에 반영되었거나 반영될 예정의 소스들은 항상 빌드 대상이라고 볼 수 있습니다.안드로이드 팀은 내부적으로 빌드 대상이 되는 브랜치를 아래와 같이 정의하였습니다.개발된 이슈가 최종적으로 반영된 브랜치 (develop)Github 에서 코드에 변경이 발생하면 이를 Jenkins 로 통보하여 해당 브랜치를 빌드합니다.개발 브랜치에 반영을 위해 코드리뷰 중인 브랜치 (features, fixes)Github 에 새로운 Pull-Request 가 발생하면 Jenkins 로 통보하여 해당 브랜치를 빌드합니다.테스트와 리포팅은 이 시점부터 발생한다고 볼 수 있습니다.2. 빌드빌드를 하는 과정에 기본적인 정적 분석을 사용하고 있습니다. 코드의 Convention 이나 복잡도 등을 측정하고 이를 분석하여 수정할 부분을 파악하기 위해서입니다.3. 테스트안드로이드팀은 작년 중순까지 Robolectric 이라는 Test Framework 을 사용하였으나 여러가지 이슈로 인하여 현재는 Android Test Support Library 를 사용하고 있습니다. ATSL 은 에뮬레이터를 필요로 하기 때문에 Jenkins 서버에 에뮬레이터를 구동하여 Test-Bed 를 구성하였습니다.빌드 과정에서 정적 분석이 완료되면 테스트 코드를 동작 시킵니다.테스트 된 결과는 JUnit Test Report 와 Jacoco Coverage Report 를 받고 있습니다.4. 결과 리포트빌드, 테스트 결과는 Jenkins 에서 별도로 관리되고 있지만 모든 동작들은 자동화 되어 관리되기 때문에 별도의 장치가 없다면 알아채기 어렵습니다.좀 더 빠른 피드백을 받기 위해 JANDI-Webhook 기능을 이용하여 결과 리포팅을 바로 받아 확인 할 수 있도록 하였습니다. 또한 Github Pull-Request 화면에서 Build-Status 연동하여 코드리뷰 하는 과정에서 잠재적 오류를 찾을 수 있도록 하였습니다.※ 빌드된 결과물의 배포는 내부적인 정책으로 현재는 하지 않고 있습니다만, 현재 가용 가능한 리소스 안에서 해결 방안을 찾고 있습니다.총평자동화의 가장 큰 목적은 반복적이지만 시간을 소요하기엔 가치가 떨어지는 작업을 단순화 하기 위함이었습니다. 여기서 오는 가장 큰 의미는 관리에 소요되는 시간을 최소화함으로써 생산성을 향상 시켰다는 데에 있습니다.특히 다국어 리소스와 이미지 리소스를 자동화 하기 위한 작업은 소요된 시간이 극히 미미하지만 그 효과는 매우 긍정적이라 할 수 있습니다.CI 는 초기 설정뿐만 아니라 관리가 매우 어려운 작업입니다. 해당 시스템을 총체적으로 알고 있다는 가정에서 해야 하며 정책적으로 규정해야 하는 것들도 있습니다. 하지만 결과물 그 자체에 대한 관리를 위해서는 없어서는 안되는 도구이며 정적분석과 자동화 테스트 등 다양한 효과를 얻을 수 있기 때문에 많은 개발자들에게 권장하고 싶습니다.#토스랩 #잔디 #JANDI #개발 #효율 #자동화툴 #업무환경
조회수 1417

Code without Limits

WWDC18 Review (1): Bring the Func! 보기 Introduction지난 글 Bring the Func! 에서 WWDC를 소개했습니다. Keynote와 Platforms State of the Union에서 인상 깊었던 경험도 소개했고요. WWDC 첫째 날은 애플에서 큰 이벤트를 진행했고, 둘째 날부터 마지막날까지는 세션과 랩스, 스페셜 이벤트를 진행했습니다. 이번엔 지난 글에서 미처 쓰지 못했던 것을 소개하겠습니다.SessionWWDC 하면 가장 먼저 떠오르는 건 대개 Keynote입니다. 하지만 다른 세션이나 랩스부터 생각나는 애플 개발자도 있을 겁니다. 저도 처음엔 Keynote만 기대했지만, 행사에 참여하면서 세션과 랩스의 매력(?)에 빠졌습니다.Apple Developer 웹사이트에서 수많은 기술 관련 영상을 볼 수 있다.애플 관련 애플리케이션 개발자는 문제에 부딪히면 Apple Developer 웹사이트에서 도움을 얻는데요. 특히 Development Videos 사이트에 들어가면 그해 발표한 WWDC 세션부터 시작해서 그 동안의 세션들을 모두 볼 수 있습니다. Topics에서는 주제별로 카테고리를 만들어, 해당 주제에 관한 동영상들을 모아서 볼 수 있고, Library에서는 찾고자 하는 내용에 대한 키워드를 검색해서 찾을 수 있습니다.Development Videos - Apple Developer 첫 화면Topics 에서는 주제별 동영상들을 볼 수 있다.Library 에서는 검색하는 키워드에 해당하는 동영상들을 볼 수 있다.WWDC 행사장은 Hall 1 ~ Hall 3, 그리고 Executive Ballroom까지 4개의 방으로 구성되어 있었습니다. 이곳에서 각각의 세션을 들을 수 있었는데요. 시간대별로 3~4개의 세션을 동시에 진행합니다. 듣고 싶은 세션은 해당하는 방에 들어가서 들으면 됩니다. 만약 같은 시간에 듣고 싶은 세션이 두 개 이상이라면 하나만 현장에서 듣고, 다른 세션은 developer 웹사이트 또는 WWDC 앱에서 업로드되길 기다려야겠죠. 물론 24시간이 지나면 세션 영상이 WWDC앱에 업로드됩니다. WWDC 앱에서 제공하는 행사장 지도세션이 진행되는 곳의 내부수많은 개발자의 똑똑한 머리와 지미집세션이 시작되자 개발자들은 무릎 위에 올려 놓은 맥북을 열심히 쳤습니다. 하나라도 놓치기 싫어서 열심히 타자를 치는 개발자들의 모습이 멋있었습니다. 마치 대학 영어 강의를 듣는 기분이었죠.아쉬운 점이 있다면, 에어컨을 너무 강하게 틀어 세션 행사장이 매우 추웠다는 겁니다. 며칠을 견디다 마지막 날엔 결국 행사장 밖에서 라이브로 시청했습니다. 그리고 세션을 진행하는 동안 빠르게 코딩을 하다 보니, 소스 코드를 다 작성하기도 전에 다음 장면으로 넘어가는 부분이 많았습니다. 실시간으로 같이 작업할 예제 소스 코드를 제공하거나 조금 더 효율적으로 세션을 들을 수 있게 해줬으면 좋겠다는 생각이 들었습니다.행사장에서 제공하는 아침 식사와 함께 맥북 프로에서 라이브로 세션 시청What’s new in ARTKit 2지금부터는 인상 깊었던 세션 세 가지를 소개하겠습니다. 첫 번째는 What’s new in ARTKit 2였습니다. 이 세션이 가장 인상 깊었던 이유는 애플이 AR에 중점을 두고 있다는 생각이 들었기 때문입니다. 실제로 Keynote 발표 중에 장난감용 블럭을 만드는 회사 관계자 두명이 AR을 활용한 앱을 실행해 노는 모습을 보여주기도 했습니다.Keynote 발표 중 한 장면. 크레이그 페더리기가 AR 파트에서 Shared experiences에 대해 발표하고 있다.가장 재미있었던 건 현실 공간을 저장해 다른 유저들과 공유할 수 있는 기능이었습니다. ARWorldMap Object를 이용해 사용자가 기기를 움직이면서 현실 공간의 모습을 저장합니다. 나중에 앱을 다시 실행하면 저장했던 현실 공간 맵이 그대로 유지되고, 이전의 모습도 나타나죠. 예를 들어, 노란 테이블 위에 가상의 물건을 올려 놓았다면, 나중에 테이블을 향해 기기를 움직였을 때, 그 자리에 놓여있던 가상의 물건이 다시 나타납니다. 또한, 저장한 맵을 근처의 다른 유저의 기기로 전송할 수 있습니다. 이렇게 하면 서로 다른 기기에서 같은 맵을 보면서, 같은 경험을 할 수 있게 됩니다. 개념을 확장하면 하나의 AR앱으로 다중 유저들이 게임을 함께 즐기거나 멀리 떨어져 있어도 같은 교육을 받을 수 있죠.SwiftShot AR게임을 즐기려고 기다리는 개발자들WWDC18 Keynote에서 잠깐 소개되었던 SwiftShot AR 게임이 이런 특징을 잘 나타난 앱입니다. 실제로 행사장 1층 안쪽에 이 게임을 즐길 수 있는 공간이 따로 마련되어 있었습니다. 개발자들이 직접 게임을 즐길 수 있었고, 마지막 날엔 개인전과 팀전을 진행해 1등에게 선물(AR뱃지)을 주었습니다. 옆에서 구경했는데 재밌었습니다. 아이패드가 있다면 여기를 클릭해 샘플 코드를 다운 받을 수 있습니다. 빌드해서 재미있는 AR 게임을 친구들과 함께 즐겨보세요. A Tour of UICollectionView브랜디 앱은 90% 이상 UICollectionView를 이용해 앱 화면을 만들었습니다. 많은 UICollectionViewCell을 다시 사용할 수 있고, 커스텀 레이아웃도 만들 수 있기 때문입니다. 이전에 포스팅한 ‘테이블이냐, 컬렉션이냐, 그것이 문제로다!’에서 UICollectionView를 공부했지만 더 배우고 싶어서 A Tour of UICollectionView를 들었습니다.이 세션은 UICollectionView에 대해 좀 더 깊은 내용을 다뤘습니다. UICollectionView와 UITableView의 가장 큰 차이점인 레이아웃에 초점을 두었는데요. 단순히 UICollectionView에서 선형 레이아웃 말고 그리드 형식의 레이아웃을 만들 수 있다는 것, 커스텀 레이아웃을 만들 때 고려할 것, 구현에 대한 가이드라인까지 제시했습니다. 애플에서 제공하는 레이아웃 중 하나는 UICollectionViewFlowLayout입니다. UICollectionViewFlowLayout은 line-based 레이아웃 시스템입니다. 일직선 상에서 최대한 많은 아이템들을 채운 후, 다음 행 또는 열로 넘어가 아이템을 채우는 형식으로 컨텐츠들을 배치합니다. 가장 흔한 레이아웃 모습이 바로 그리드 레이아웃입니다.그리드 레이아웃, 또는 UICollectionViewFlowLayout으로 구현할 수 있는 레이아웃Line-based 레이아웃이 아닌 다른 모습의 레이아웃이라면 어떤게 있을까요? 세션에서 예를 든 레이아웃이 바로 모자이크 레이아웃이였습니다. 브랜디 앱, 또는 다른 앱에서 볼 수 있는 모자이크 레이아웃은 일직선상에서 일렬로 정렬하지 않고, 그리드 레이아웃과 조금 다른 모습입니다. 아래의 스크린샷을 보면 어떤 레이아웃인지 감이 잡힐 겁니다.브랜디 앱, 인스타그램 앱, 세션 예제 앱의 모자이크 레이아웃모자이크 레이아웃은 line-based 레이아웃이 아니기 때문에 일반적인 UICollectionViewFlowLayout을 사용하지 않고, UICollectionViewLayout을 상속하여 커스텀합니다. 총 4개의 기본 메소드와 추가적으로 고려해야하는 메소드 하나를 이용하여 커스텀 UICollectionViewLayout을 만들 수 있습니다. 모든 컨텐츠를 담는 뷰의 크기, 레이아웃의 속성 2개, 그리고 레이아웃을 준비하는 기본 메소드들을 구현하고, 레이아웃이 변경해야하는 상황(기기를 가로로 눕히거나 레이아웃의 위치가 변경될 때 등)을 고려하여 메소드를 구현하면 됩니다.open var collectionViewContentSize: CGSize { get } func layoutAttributesForElements(in rect: CGRect) → [UICollectionViewLayoutAttributes]? func layoutAttributesForItem(at indexPath: IndexPath) → UICollectionViewLayoutAttributes? func prepare() func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) → Bool 세션 강연자가 직접 소스를 작성하면서 메소드 구현과 퍼포먼스를 위한 팁을 설명했습니다. 이 세션을 통해서 UICollectionView의 핵심인 레이아웃에 대해 더 깊이 배울 수 있었죠. 레이아웃 말고도 멋진 애니메이션 효과 구현 방법을 설명해주었는데요, 여기를 클릭해 직접 동영상을 보는 걸 추천합니다! 영상을 보고 나면 분명 멋진 UICollectionView를 구현할 수 있을 겁니다.Build Faster in XcodeBuild Faster in Xcode 는 가장 인기 있었던 세션 중 하나였습니다. 한국 개발자들 사이에서도 추천할 세션 중 하나로 꼽혔죠. 물론 혁신적으로 빌드 타임을 줄일 수는 없지만, Xcode의 기능과 빌드 타임이 어떻게 연결되는지 알 수 있었습니다. 프로젝트 세팅과 가독성 있는 코드 작성, 이 두 가지가 빌드 타임과 관련되어 있었습니다. Xcode는 프로젝트를 구성(configure)할 때, 빌드할 targets(iOS App, Framework, Unit Tests 등)와 targets 사이의 종속 관계(dependency)를 따릅니다. Dependency에 따라서 target을 빌드하는 순서도 정해지는데, 순서대로 빌드하지 않고 최소한의 연결을 유지하면서 병렬적으로 빌드하게 됩니다.빌드 시간을 아름답게 줄일 수 있다.이것은 Xcode 10에서 Scheme Editor에서 설정할 수 있습니다. 프로젝트의 Target → Edit Scheme → Build → Build Options에서 Parallelize Build를 체크하면 됩니다.Xcode 10의 Parallelize Build또한 Xcode 10에는 빌드 타임을 계산하는 기능도 있습니다. 빌드할 때 어떤 부분에서 얼마나 걸렸는지 요약해서 보여주는 기능도 있습니다. Product → Perform Action → Build With Timing Summary를 선택하면 빌드 후 요약해서 Xcode에 나타납니다.Build With Timing Summary를 선택하여 빌드하면위 스크린샷처럼 요약해서 보여준다.Xcode 프로그램을 이용해서 빌드 타임을 관리하는 방법도 있고, Swift으로 작성한 소스 코드를 가독성 높은 코드로 바꾸는 방법도 알려줍니다. 또한 Bridging Header로 Objective-C와 Swift를 동시에 개발할 때 도움이 되는 방법도 설명해줍니다. 빌드 타임에 대해 관심을 가질 수 있는 계기가 될 겁니다. 한 번씩 영상을 보길 추천합니다!Labs세션을 듣고 궁금한 점이 생겼다면 Labs(랩스)에서 질문할 수 있습니다. 각 세부 분야별 애플 기술자들이 시간대별로 모여서 개발자의 질문을 받거나 문제점을 해결할 수 있도록 도움을 줍니다.Technology Labstechnology Labs 간판Labs 입구에 있는 부스별 주제짙은 남색 Engineer 티셔츠를 입은 애플 기술자들이 질문을 받고 있다.가장 인기가 많았던 랩스는 Auto Layout and Interface Builder, UIKit and Collection View, Building Your App with Xcode 10 등등이었습니다. 사람이 많아서 줄 서서 기다릴 정도였습니다. 내년에는 랩스 시간이 조금 더 길게 진행됐으면 좋겠다는 생각이 들었습니다.WWDC 기간 중에 랩스에서 시간 보낸 적이 있었습니다. iOS 프로그래밍을 시작한 지 1년도 되지 않아 궁금했던 것들과 새로운 Xcode 10에 대해서 질문했습니다. 아래는 질문했던 내용을 문답형식으로 작성했습니다.애플 기술자와의 문답문: iOS 프로그래밍을 개발한지 얼마 안 된 신입 개발자입니다. 어떻게 하면 프로그래밍 실력을 높일 수 있나요? 답: 앱 하나를 처음부터 끝까지 개발해보면 실력을 늘릴 수 있다. 또한, 애플에서 만든 스위프트 책 보는 걸 추천한다.문: WWDC 기간 동안에 테스팅(testing)에 관심을 가지게 되었습니다. 앞으로 상용하는 앱을 테스트하면서 개발하고 싶은데, 테스트는 어떻게 시작하면 좋을까요?답: 이것에 대한 세션 동영상 을 보는 걸 추천한다. 테스트는 중요한 것이기 때문에 이 동영상을 보면서 테스트에 대해 배우고 난 뒤, 직접 앱을 테스트해보길 권장한다.문: 새로운 Xcode 10에서 앱을 빌드해봤는데 에러가 났습니다. 이런 에러가 나타난 이유는 무엇인가요?답: Xcode 10에 있는 컴파일러 문제다. 소스를 수정하면 앱이 빌드될 것이다. 컴파일러에 대해서 Xcode 팀에게 전달하겠다. (Range 관련된 컴파일러 문제였습니다.)문: 빌드 시간을 줄일 수 있는 방법은 무엇인가요?답: 컴파일하는 소스 코드를 줄이거나 프레임워크를 만들어서 빌드할 때 마다 계속 빌드하지 않도록 하면 시간을 줄일 수 있다. 이와 관련된 세션을 들으면 조금 더 자세한 내용을 확인할 수 있다.Consultation Labs애플 기술자와 일대일 면담식으로 진행하는 랩스도 있었습니다. 예전에는 선착순으로 진행되었는데 올해는 신청을 받고 당첨된 개발자에게만 기회를 주었습니다. 당첨되면 30분 동안 신청한 분야(디자인, 앱 스토어, 마케팅 등)의 전문가와 질의응답을 할 수 있습니다. 가장 인기가 많았던 User Interface Design 랩스를 신청하고 당첨이 되었습니다. 디자인 전문가들과 시간을 보낼 수 있었는데요. 애플 디자이너들이 생각하는 최선의 디자인 가이드라인을 배울 수 있었고, 함께 앱을 관찰하면서 개선되었으면 하는 디자인 요소 등의 팁을 얻었습니다. 아쉽게도 촬영 및 녹음은 불가능했습니다. 시간도 짧게 느껴져서 아쉬웠습니다.Special EventsWWDC 기간 동안에는 세션과 랩스 위주로 진행되지만 중간에 가끔 스페셜 이벤트들도 진행합니다. 점심 시간에 유명 인사들을 초청해서 하는 짧은 강연, 아침 일찍부터 모여서 같이 달리면서 즐길 수 있는 이벤트(WWDC Run with Nike Run Club), 맥주와 함께 음악을 즐기는 이벤트 등 개발 외적인 이벤트들을 많이 진행했습니다. 저는 그 중에서 Bash 이벤트를 소개하고 싶군요.BashBash는 목요일에 진행한 뒤풀이 파티였습니다. WWDC 행사장 근처에 공원을 빌려서 맛있는 음식과 주류를 무료로 제공하고, 초청 가수의 공연도 볼 수 있었습니다. 초청 가수가 공연하기 전에 소개할 때 크레이그 페더리기가 무대에 나왔습니다. 개발로 지친 몸과 머리를 식히고 다른 개발자들과 어울려 놀 수 있는 공간이였습니다. 뒤풀이 파티가 끝나갈 때쯤 진짜로 WWDC가 끝나간다는 느낌이 들어서 괜히 아쉽기도 했었습니다.무대와, 맥주와, bash 입장권한국인 개발자들과 함께 즐긴 뒤풀이 파티초청 가수를 소개하러 무대에 올라온 크레이그 페더러기아름다운 노을!마치며이번 글에서는 WWDC의 세션, 랩스, 스페셜 이벤트를 설명했습니다. WWDC가 한 달 전에 끝났지만 지금 다시 생각하면 두근두근 설레고 또 가고 싶어집니다. 내년 WWDC에 또 갈 수 있을까요? 지금까지 애플 개발자들의 축제였던 WWDC의 Review를 마치겠습니다. 긴 글을 읽어주셔서 감사합니다!글김주희 사원 | R&D 개발1팀[email protected]브랜디, 오직 예쁜 옷만#브랜디 #개발문화 #개발팀 #업무환경 #인사이트 #경험공유 #이벤트참여 #이벤트후기 #미국

기업문화 엿볼 때, 더팀스

로그인

/