대학생이라면 네이버 캠퍼스 핵데이에 참가해야하는 이유 + 지원 꿀팁

네이버 캠퍼스 핵데이란?

네이버 캠퍼스 핵데이(이하 핵데이)는 연 2회, 1박 2일로 진행되는 해커톤 행사이다. 다만 일반적으로 우리가 해커톤이라고 부르는 것과는 다르다. 일반적인 해커톤은 하나의 테마가 주어지고 그 테마에 맞는 소프트웨어를 개발, 마지막 날에 시상식으로 마무리한다. 반면 핵데이는 시상을 위해 경쟁하는 대회가 아니다. 참여 멘티와 멘토가 서로서로 배워가는 시간이다. 멘티는 네이버 현업 개발자에게 멘토링을 받고 멘토도 업무와 연관있거나 평소에 관심 있던 주제를 발제해 함께 도전하고 배우기 위해 참가한다.

핵데이에서는 현업 네이버 개발자 한 명과 대학생 멘티 2~3 명이 한 팀이 된다. 멘티는 수 십 개의 주제 중에 참여하고 싶은 주제를 3순위까지 선택해서 지원한다. 멘토는 미션을 가장 잘 수행할 것 같은 지원자를 선발해서 자신의 팀을 꾸린다. 합격 발표가 나면 멘티는 깃헙 리포와 메신저 방에 초대를 받게 된다. 그로부터 핵데이까지 2주 정도의 기간이 있는데 이 기간에 온라인으로 멘토링을 하기도 한다. 핵데이는 시작부터 끝까지 팀이 중심이 되며 매우 자율성이 높다. 그래서 멘토와 멘티의 팀워크가 중요하다. 적극적으로 임할수록 더 많은 것을 배워갈 수 있다.

커넥트원

핵데이는 물 좋고 공기 좋은 춘천 네이버 커넥트원에서 1박 2일 동안 진행된다. 오후 12시 ~ 1시쯤 그린팩토리에 모여서 오리엔테이션을 하고 버스에 탄다. 약 한 시간 반을 달려 도착하면 짧은 오리엔테이션을 마친 후 자리 세팅을 하고 본격적으로 해커톤이 시작된다. 무제한 제공되는 스낵과 커피와 음료수를 먹으면서, 다같이 개발 방향을 논의하기도 하고 막히는 부분을 서로서로 도우면서 문제를 해결한다. 깃헙에 코드를 푸시하면 멘토가 보고 리뷰를 해주기도 한다. 시작할 때만 해도 ‘이걸 어떻게 개발하지’ 라고 생각했던 문제들이 조금씩 풀린다. 자매품으론 ‘아니 벌써 10시야?’ 라는 말도 있다.

핵데이 참가자들은 평가 받기 위한 결과물을 만들지 않고 스스로 배우기 위해 개발을 한다. 그래서 이튿날 시상식이나 발표회 같은 건 없다. 대신 팀 별로 모여서 자유롭게 회고를 한다. 참가자 사이에 경쟁도 없다. 멘티들은 협업하는 관계이다. 협업해야 모두가 각자 더 많은걸 배워갈 수 있다.

핵데이에서 얻어 가는 것

이 행사의 VIP는 대학생 개발자다. 핵데이를 준비하시는 분들은 참가 멘티들이 더 배우고 성장할 수 있게 세심한 부분까지 신경쓰신다. 행사가 거듭될수록 멘티들의 만족도도 높아지고 있다고 한다. 나는 여태까지 멘티로 한 번, 멘토로 두 번 참가했다. 왜냐면 멘토로서 얻어 갈 수 있는 것도 정말 많다. 하물며 해커톤 + 멘토링 + 인턴십 3종 세트를 얻어가는 멘티들에게는 이 행사가 풀 패키지나 다름 없다.

좀 더 자세히 적어본다면,

  • 기술적 성장: 앞서 언급했듯이 캠퍼스 핵데이는 상을 받는 대회가 아니다. ‘어렵지만 해결할 수 있는’ 난이도의 문제와 직접 부딪히면서 나의 실력을 한 단계 업그레이드할 수 있는 자리이다. 뿐만 아니라 개발자 커리어나 평소 궁금했던 것들을 멘토에게 물어보고 조언을 얻을 수도 있다. 기술적 성장에는 코딩 실력 뿐 아니라 커뮤니케이션, 협업, 코드 리뷰, 토론, 문제 해결 능력이 포함된다. 핵데이에서는 이 모든걸 경험해볼 수 있다. 학교와 회사의 딱 중간이라는 느낌이 든다. 그래서 중요한 것이 자기 주도성이다.

  • 인턴십: 채용형 인턴십, 체험형 인턴십 두 가지가 있다. 주제별로 인턴십 종류가 정해져 있어서 지원할 때 유의하여 지원하면 된다. 4학년이 채용형 인턴십에 합격하면 인턴십이 끝난 후 최종 면접을 통해 채용으로 이어진다. 1 ~ 3학년은 채용 전환 면접이 없는 체험형 인턴십을 하게 된다.

  • 네트워크: 관심사가 비슷한 친구들을 만날 좋은 기회다. 보통은 주제가 iOS, 안드로이드, 웹프론트엔드, 백엔드, 머신러닝/딥러닝 등으로 분류되기 때문에 나와 같은 걸 개발하는 또래 친구들을 만나 새로운 자극을 받을 수 있다. 그 뿐 아니라 인턴까지 하게 되면 네이버 팀분들과 계속 알고 지낼 수 있다. 나도 체험형 동계 인턴십을 했었는데 인턴이 끝난 뒤에도 팀 회식에 불러주시고, 여름 방학이 다가올 즈음에 방학 때 할 일 없으면 인턴 또 하러 오라고 제안해주시는 등 좋은 기회를 많이 얻을 수 있었다.

지원서 작성 팁

멘토로 참여하면서 100여 명의 지원자를 검토해보고 느낀 점을 토대로 지원서 작성 팁을 몇 개 정리해봤다.

지원서는 네이버 공식 사이트에서 작성하고 제출한다. 핵데이에 등록된 주제를 볼 수 있는 깃헙 리포가 공유되는데 거기서 흥미를 끄는 주제를 고른다. 그리고 관심 있는 주제를 선호순으로 작성한다. 단순히 선택하고 끝이 아니라, 내가 왜 이 프로젝트를 잘 할 수 있는지 설명을 해야한다. 프로젝트마다 ‘요구사항’이 있는데 내가 이걸 잘 해낼 수 있다는걸 설득한다는 마음으로 써본다. 비슷한 주제의 프로젝트를 해본 경험이 있다면 꼭 밝힌다. 없더라도 왜 관심을 가지게 됐는지, 관심을 갖고 나서 어떤 활동을 했는지 등을 쓴다. 핵데이 지원 전에는 잘 몰랐더라도 주제를 보고나서 관심이 생겼다면 공고가 뜬 시점부터 지원 마감까지 몇 주의 기간이 있으니 이 때를 활용해서 튜토리얼을 찾아서 해보거나 프로토타입을 직접 만들어 보고나서 지원서를 작성하는 것도 좋은 방법이다.

주제별 멘토가 직접 멘티를 선발하기 때문에 통일된 기준은 없다. 어떤 멘토님은 깃헙 활동과 커밋 메시지를 보고 뽑았다고 한 경우도 있고, 해커톤 수상 경력을 좋게 보고 뽑았다고 하는 분들도 있다. 내가 멘토링하는 주제는 iOS 개발이다. 그래서 지원자들은 보통 본인이 개발한 앱을 적는다. 여태 지원자 중에 iOS 앱을 아예 개발해보지 않은 사람은 없었던 것 같다. 대부분의 지원자가 앱 개발 경험이 있기 때문에 결과물을 잘 어필하는게 중요한 것 같다. 앱 소개도 여러 방법으로 할 수 있다. 앱스토어에 출시되어 있다면 링크 하나만으로도 충분하고 직접 써볼 수 있기 때문에 제일 좋다. 공개되어 있지 않아도 소개 페이지나 시연 영상이 있다면 충분하다. 만약 깃헙에 프로젝트가 올라가 있다면 README를 활용해서 소개 글을 넣을 수도 있다. README에 스크린샷이나 gif가 있다면 어떤 앱인지 한 눈에 파악할 수 있어서 좋다. (혹시나 빌드가 안되서 앱을 설치해 볼 수 없다면 말짱 도루묵). 그리고 그 경험이 핵데이에서 어떻게 도움이 될지 간략하게 써주면 좋을 것 같다. 만약 여러 명이 같이 개발한 프로젝트라면 본인이 개발한 부분을 명시해주는 것이 좋다.

캠퍼스 핵데이는 채용과 연계된 행사이기도 해서 주제와 딱 맞는 경험이 없더라도 기본 프로그래밍 실력을 갖추고 있고 관심 분야가 팀과 맞는 지원자를 뽑기도 한다. 지원서에 대회 수상 경력, 대외 활동, 관심 분야, 기술 실력을 나타낼 수 있는 프로젝트를 적을수 있는 항목이 있다. 특히 채용을 염두해 둔 4학년을 뽑을 때는 기술 스택이 일치하는지를 보게 된다. 앱 개발과는 달리 백엔드 분야는 언어나 프레임워크가 훨씬 다양하다. 백엔드라 하더라도 노드를 쓰느냐 자바를 쓰느냐 파이썬을 쓰느냐 등등에 따라 많이 달라서 아무래도 팀이 쓰는 언어나 프레임워크를 써본 경험이 있는 후보를 선호할 것 같다.

사실 매번 뽑고 싶은 지원자들이 많아서 합격자를 선발하는 일이 정말 힘들다. 멘티당 최대 세 명까지 선정할 수 있는데 보통 뽑고 싶은 지원자가 9 ~ 10명 정도는 된다 😱. 흥미로운 프로젝트를 해봤거나 iOS 개발 외에도 머신러닝이나, 혹은 아예 개발과 무관한 다양한 분야를 경험해 본 멘티에게서 새로운 걸 배우고 자극 받기도 한다. 그래서 더 많은 대학생들이 캠퍼스 핵데이에 와서 성장하고 좋은 네트워크를 쌓고 무엇보다 새로운 경험을 해갔으면 하는 바람이 있다. 그리고 더 많은 회사들이 이런 핵데이를 열었으면 하는 바람도.

이 글의 초안을 읽어준 조소현에게 고마움을 전합니다.

Tags: NAVER CAMPUS HACKDAY, mentoring, application tips  

앱스토어 1위를 하면서 배운 것 (feat. 사이드 프로젝트)

2014년 12월 17일 보안카드 위젯을 출시했다. 며칠 뒤 금융 카테고리 1위, 한 달 뒤엔 유료앱 전체 2위까지 올랐다. 그 후 꾸준한 인기에 힘입어 2015년, 2016년, 2017년 3년 연속 올해를 빛낸 인기 앱에 선정됐고 현재 별점 4.7점(누적 리뷰 1,285개)을 달고 있다.

보안카드 위젯은 모바일 뱅킹을 할 때 필요한 은행별 ‘보안카드’를 아이폰에 암호화해서 저장해두고 알림센터 위젯을 통해 앱 전환 없이 간편하고 빠르게 조회할 수 있는 유틸리티성 금융 앱이다.

(2017년꺼는 스크린샷을 안 찍어놨다..)

수업 과제로 시작했던 사이드 프로젝트지만 많은 사람들이 좋아해준 덕분에 값진 경험을 할 수 있었다. 유저들의 칭찬에 들뜨기도 했고 부정적인 리뷰와 꾸짖는 메일로 인해 상처도 받고 감정이 상할때도 있었다. 하지만 그런 경험들로 인해 다양한 유저들을 이해하고 그들의 입장에서 생각해 볼 수 있었다. 무엇보다 사용자들과 함께 만드는 과정도 매우 즐거웠다.

토스나 카카오뱅크 같은 편리하고 혁신적인 앱들의 등장으로 이제는 내리막길을 가고 있지만 보안카드 위젯을 만들고 운영하면서 배운 것들은 또 새로운 프로젝트를 시작할 때 도움이 될 것 같아서 그 동안의 과정을 회고해보며 정리해봤다.

1. 일단 만든다

보안카드 위젯은 넥스트 재학 시절 iOS 수업의 기말 과제로 제출하기 위해 시작한 프로젝트였고, 엄청 독창적이고 새로운 걸 만들기보다는 내가 스마트폰으로 이미 쓰고 있던 앱 중 하나를 비슷하게 만들어보면서 개발 공부를 할 목적이었다. 다만 과제 제출로 끝내지 않고 사람들이 돈 주고 살 만한 완성도로 만들어서 실제 출시까지 해보자는 목표가 있었다. 그래서 여러 후보 중에 개인적으로 가장 오랫동안 유용하게 쓰고 있던 보안카드 앱을 만들어보기로 결정했다.

괜찮은 아이디어라는 생각이 들고 재밌을 것 같으면 일단 만들어 보자. 만들기 전 주변 사람들의 의견을 들어보는 것도 도움이 될 수는 있다. 하지만 부정적이거나 미적지근한 주위의 말에 너무 휘둘릴 필요 없다고 생각한다. 긍정적인 피드백이 오히려 빨간불일 수도 있다. 듣자마자 사람들이 ‘괜찮은데?’라고 하는 아이디어는 누구나 한번쯤은 떠올렸을만한, 괜찮아 보이는 아이디어에 그칠 가능성이 높다. 실리콘밸리의 스타텁 인큐베이터 Y Combinator에서는 이를 시트콤 아이디어라고 부른다고 한다. [1] 사업할 것도 아니고, 너무 거창하게 생각할 필요 없이 재밌으면 된 것이다.

2. 기능은 적게 출시는 빠르게

Minimum Viable Product(최소 기능 제품, MVP)를 첫 출시 목표로 삼는다. 처음부터 기능을 많이 넣으려다보면 사이드 프로젝트가 질질 끌리고 결국 끝을 보지 못하고 흐지부지될 수 있다. 일단 가장 중요한 기능 한 두 개만 만들어서 출시를 하고, 나머지 기능은 유저들의 의견을 받아 가면서 차차 추가하면 된다. 같은 일을 하더라도 실사용자들이 직접 요청한 것이라면 더 의욕이 생기고, 본업에 소진한 집중력과 체력도 다시 샘솟는다. 보안카드 위젯을 출시하고 얼마 후 입사했는데 유저들이 지속적으로 기능을 제안해주고 개선 사항을 남겨준 덕분에 퇴근 후에도 지치지 않고 앱을 발전시킬 수 있었다.

출시를 빠르게 하고 유저를 모아보자. 실사용자들의 피드백은 사이드 프로젝트의 가장 큰 원동력이다.

3. 차별화를 한다

보안카드 위젯의 제일 중요한 차별점은 위젯에서 바로 조회 할 수 있게 만든 것이다. 하지만 처음부터 차별점을 염두해두고 시작한 것은 아니었다. 개발을 한창 하던 와중에 당시 릴리즈된 iOS 8의 다양한 기능들을 살펴보다가 알림센터 위젯 기능을 결합해야겠다는 아이디어가 떠올랐다. 이 아이디어가 처음 떠오른 뒤 주위 사람들에게 설명을 해줬을 때 특별히 반응을 보인 사람은 없었다. 대부분 그냥 ‘아 그렇구나’ 정도였다. 하지만 개발을 해서 프로토타입을 보여주고나니 사람들이 ‘아이디어 좋다’ 이런 피드백을 주었다. 말로 듣는 것과 실물 사이의 간격은 꽤 크다는 걸 느꼈다.

🎯 보안카드 위젯의 차별점: Today Extension 기능

깔끔한 디자인과 심플한 UX만으로도 충분한 차별점을 만들 수 있다. 그리고 그런 차별점이 있다면 유저들은 비슷한 앱을 이미 쓰고 있더라도 새로 구매하는 경향이 분명 있는 것 같다. 특히 기존에 쓰던 앱이 오랫동안 업데이트가 되지 않는다면 유저들은 디자인이 좀 더 이쁘고 최신 iOS 기능을 탑재한 앱으로 갈아타기 때문에 기회는 항상 열려 있다.

🎯 앱 아이콘 개선

(좌) 1.0 버전 앱 아이콘 (우) 개선된 디자인

4. 한국 유료 앱스토어 공략

아래는 2015년 ~ 2017년 사이 판매량 그래프인데, 1년에 한번씩 판매량이 눈에 띄게 증가할 때가 있었다.

바로 매년 아이폰이 출시 되는 시기이다. 안드로이드에서 아이폰으로 넘어오는 사람들 뿐 아니라 새 아이폰으로 바꾼 사람들이 이때 앱스토어를 구경하면서 새 앱을 찾아보는게 아닌가 싶다. 그리고 실제로 노트, 캘린더, 카메라 앱 등은 정말 꾸준히 새로운 앱들이 나오고 있다. 그러니 이미 앱스토어에 비슷한 앱이 있다고 프로젝트를 접지 말고 차별점을 가지고 계속해서 관리하면 유저층을 쌓을 수 있다고 생각한다.

랭킹별 다운로드 규모

주위 사람들에게 판매량을 알려주면 다들 생각보다 적다면서 놀란다. 보안카드 위젯은 특성상 ‘국내용’ 앱이기 때문에 96%는 한국 앱스토어에서 판매됐다. 그래서 순수 국내 앱스토어 만의 규모를 파악해볼 수 있었다. (씁쓸..😢) [2]

유료앱 차트 랭킹 1일 평균 다운로드
5위 이내 200 ~ 300
10 ~ 30위 30 ~ 50
30 ~ 50위 10 ~ 20
50 ~ 100위 5 ~ 15

금융 카테고리 내에서 1위를 할 때는 하루 평균 150 ~ 200 정도 였다. 한국 앱스토어 규모가 작다고 볼 수도 있지만 뒤집어 생각하면 인기 차트에 올라가기 좀 더 쉽다는 뜻 아닐까. 그리고 순위에 오르면 마케팅 비용 없이도 사람들에게 지속적으로 노출되기 때문에 매우 좋다. 노출 👉 다운로드 발생 👉 순위 유지 👉 노출의 선순환이 발생하는 것 같다. 그래서 사이드 프로젝트를 띄우기에 괜찮은 환경이지 않나 생각도 든다.

UI 업데이트

4년 동안 약 20번의 업데이트를 해보면서 유저들은 UI 업데이트 자체를 꺼린다는 걸 느꼈다. 너무나 심플해서 바꿀 UI가 있나 싶은 정도의 앱인데도 버튼이나 글자 색, 모양, 크기, 배치 등을 바꾸면 한동안은 낮은 별점과 악플 같은 리뷰들을 많이 받는다. 거지같다, 업뎃하지 마라, 어이없다, 돌려놔라 등등.. 익숙함이 최고의 UI인 것이다. 그래도 바꿀건 바꿔야한다는 생각에, UI를 개선할때는 당분간 욕 먹을 각오하고 감행해야 한다.

결론

사이드 프로젝트를 하자!


✏️ Notes

[1] Paul Graham, How to Get Startup Ideas: 시트콤 작가들이 각본을 쓰기 위해 만들어낸 그럴싸한 아이디어라는 뜻

[2] 보안카드 위젯의 총 누적 다운로드 수는 5.5만

📍 Special

이바닥늬우스 : 찰지고 신나는 테크바닥 늬우스

이 글의 초안을 읽어준 조소현, 강한용, 김결, 김남훈, 김영, 이승원에게 고마움을 전합니다.

Tags: side project, Security Cards Widget, App Store, MVP  

Swift 개발자처럼 변수 이름 짓기

1979년에 발간됐지만 여전히 프로그래밍 입문서로 유명한 Structures and Interpretation of Computer Programs의 도입부에 이런 말이 있다.

Programs should be written for people to read, and only incidentally for machines to execute.

(내맘대로 의역) 프로그램은 사람들에게 읽히기 위한 목적으로 만들어져야 하고, 우연히 컴퓨터가 실행할 수 있다면 더욱 좋다.

신입으로 입사했던 첫 해, 협업을 해보니 코드를 쓰는 시간만큼이나 읽는 시간도 엄청 많다는 걸 알게 되었다. 동료(또는 나)의 코드가 잘 읽히면 내 작업 속도가 빨라졌고 흐름을 이해하기 어려운 코드에 부딪히면 느려졌다. 그래서 그때부터 독자의 입장도 고려해야겠다는 생각을 하게 됐다. 소프트웨어의 사용자독자 양 쪽 모두에 감정 이입하는게 프로그래머의 일인 것 같다.

영어와 Naming Conventions

영어가 외국어인 우리는 컨벤션도 지켜야하고 영어도 고민해야하는 이중고에 처해 있다고 생각할수도 있지만 딱히 억울해 할 것도 없다. 영어가 모국어인 개발자가 전세계에 얼마나 될까. 미국인은 전세계 인구의 5%도 안된다. 그렇기에 완벽한 영어를 할 필요도 없고 회화 실력과는 더더욱 무관하다(해외 취업할 때는 큰 도움이 되겠지만!). 그럼에도 프로그래머 업무의 절반 이상은 이름 짓기라고 하니 알아두면 유용한 영어 문법과 스위프트 컨벤션을 일곱 가지로 분류했다.

  1. 동사의 변형
  2. 단수와 복수
  3. 타입별 Naming Conventions
  4. Bool 변수
  5. 중복 제거
  6. 스위프트의 getter
  7. fetch, request, perform 비교

#1 동사의 변형

영어에서 동사는 세 가지 형태로 사용된다. 동사원형 - 과거형 - 과거분사.
동사원형은 ‘~한다’라는 행위의 의미, 과거형은 ‘~했다’라는 과거의 의미, 과거 분사는 수동형의 의미이다. 그런데 프로그래밍을 할 때는 과거의 의미를 쓸 일이 거의 없으므로 일단은 무시해도 좋다.

동사 원형 과거형 과거 분사
request(요청하다) requested(요청했다) requested(요청된)
make(만들다) made(만들었다) made(만들어진)
hide(숨다) hid(숨었다) hidden(숨겨진)

✏️ 동사 원형

동사 원형은 크게 세 군데에 사용된다.

  • 함수 및 메서드
  • Bool 변수 (조동사 + 동사원형)
    • canBecomeFirstResponder, shouldRefresh
  • Life Cycle 관련 delegate 메서드 (조동사 + 동사원형)
    • didFinish, willAppear, didComplete

✏️ 과거 분사

동사의 의미를 형용사로 쓰고 싶을 때는 과거 분사로 변형해서 사용해야 한다.

  • 명사 수식
    • requestedData, hiddenView
  • Bool 변수
    • isHidden, isSelected

⚠️ 동사와 명사가 똑같이 생긴 단어도 있다
request(요청하다 혹은 요청), start(시작하다 혹은 시작점), play(재생하다 혹은 재생) 등

#2 단수와 복수

보통 인스턴스 하나는 단수형으로 이름 짓고 어레이 타입은 복수형으로 이름 지어준다. 복수형으로 이름 지어주는 것만으로 배열이라는 걸 알 수 있으니 특수한 경우가 아니라면 ListArray를 뒤에 붙여줄 필요는 없지 않을까 싶다.

let album: Album
let albums: [Album]

let albumList: [Album]
let albumArray: [Album]

✏️ 불규칙 복수형

복수형을 쓸 때 고려해야할 건 단어마다 복수형이 조금씩 다르다는 것이다. 대다수의 경우 -s를 붙이면 되지만 -es일때도 있고 -ies일수도 있고 새로운 단어로 바뀔 수도 있다. 안 바뀔수도..

단수 복수
view views
box boxes
half halves
category categories
child children
person people
index indices 혹은 indexes
datum data
information information

#3 타입별 Naming Conventions

좀 전에 배열 타입의 변수에 굳이 ListArray라는 타입을 명시하지 않는다고 했는데 Cocoa Touch 프레임워크 전반에 걸쳐 타입을 변수명에 명시해 주는 경우가 있다. URL, UIImage, Date, Size, Data처럼 상대적으로 raw한 데이터 타입이라면 명시해 주는 것 같고, 새로 정의한 타입이라면 써줄 필요가 없을 것 같다.

var fullSizeImageURL: URL?     //PHContentEditingInput
var thumbnailURL: URL?         //MLMediaObject
var referenceURL: URL          //SCNReferenceNode

var referenceImage: ARReferenceImage?   //ARImageAnchor
var iconImage: NSImage?                 //MLMediaGroup

var physicalSize: CGSize               //ARReferenceImage
var startDate: Date 
var adjustmentData: PHAdjustmentData?  //PHContentEditingInput

✏️ id vs Id vs ID vs identifier

프레임워크 문서를 보다가 또 한가지 발견한 것. Cocoa Touch에서는 대부분 identifier를 쓰고 별도의 타입을 쓸 때는 ID를 쓴다. 근데 이것도 절대 규칙은 아닌것 같고.. “대체로” 그렇다.

var formatIdentifier: String { get }          //PHAdjustmentData  
var identifier: String { get }                //CLRegion          
var identifier: String { get }                //MLMediaGroup 
var objectID: NSManagedObjectID { get }       //NSManagedObject
var recordID: CKRecordID { get }              //CKRecord          
var uniqueID: String? { get }                 //AVMetadataGroup 

#4 Bool 변수

Bool 변수명을 지을 때 알아두면 좋은 문법 몇 가지는 Bool 변수 이름 제대로 짓기 위한 최소한의 영어 문법에서 자세히 다뤘었고, 컨벤션적인 측면에서도 고려할 것이 몇 가지 있다.

✏️ isSelected vs selected

문법적으로만 보면 isSelectedselected 둘 다 맞다고 할 수 있으니 각 언어나 개발 플랫폼마다의 컨벤션을 따르면 되겠다. 스위프트에서는 is를 써주는 것이 컨벤션이다보니 맞춰주면 좋을 것 같다.

✏️ isEnabled vs isDisabled

Bool 타입의 경우 반대어를 써서 동일한 기능을 하는 이름을 만들 수 있다. 둘 중 뭐가 나은지 정답은 없지만 고민해볼 만한 기준은 있다. 조건문에 not 연산자를 붙이는 것 보다는 없는 것이 가독성에 조금 더 좋다. 따라서 로직이 추가되는 경우가 true가 되도록 하는 단어를 선택하면 어떨까. 또 하나는 변수의 디폴트 값이 true가 되도록 하는 것이다. UIView의 경우 ‘보여지는’ 상태가 디폴트임에도 isHidden을 쓰고 있어서 종종 뇌에 혼란이 올 때가 있다.

if isEnabled { ... }
if !isEnabled { ... }

tableView.isVisible = !items.isEmpty  //"tableView가 보일 때는 items가 비어있지 않을 때"
tableView.isHidden = items.isEmpty    //"tableView가 숨어있을 때는 items가 비어있을 때"

#5 중복 제거

의미가 중복되는 단어가 많으면 읽기 좋은 글이 아니다. 코드도 마찬가지로 불필요하게 의미가 중복되는 것을 빼버리면 간결하고 읽기 좋은 코드가 될 것이다.

struct User {
  let userID: String
}

userID는 떼어놓고 보면 의미가 명확하고 잘 지은 변수명이지만 실제 쓰일 때는 User 인스턴스와 함께 쓰일 것이기 때문에 굳이 ‘user’라는 의미를 포함시키지 않아도 된다. identifier라고만 해줘도 충분하다.

let id = user.userID
let id = user.identifier

함수나 메서드 이름을 지을 때도 마찬가지다.

struct ImageDownloader {
  func downloadImage(from url: URL) { ... }
}

downloadImage(from:)도 잘 지은 것 처럼 보이지만 실제로 사용해보면 image와 download라는 단어가 불필요하게 중복된다.

let image = imageDownloader.downloadImage(from: url)

메서드가 사용될 때의 인스턴스 이름까지 고려해서 아래와 같이 중복을 제거해볼 수 있을 것이다.

let image = imageDownloader.fetch(from: url)
let image = imageManager.download(from: url)

등등.

#6 스위프트의 getter

스위프트에서 어떤 인스턴스를 리턴하는 함수나 메서드에 get을 쓰지 않는다. get 없이 바로 타입 이름(명사)으로 시작하면 된다.

func date(from string: String) -> Date?
func anchor(for node: SCNNode) -> ARAnchor?                          
func distance(from location: CLLocation) -> CLLocationDistance        
func track(withTrackID trackID: CMPersistentTrackID) -> AVAssetTrack? 

#7 fetch, request, perform

우리는 데이터를 어디선가 가져오는 함수를 자주 작성하게 된다. 디스크에 저장되어있는 이미지, 리모트 서버에 있는 유저 db, 메모리 캐싱되어 있는 데이터 등등. 그럴때 위와 비슷한 동사들을 사용한다. 가져오다, 요청하다, 수행하다 등 사전적인 의미는 크게 다르지 않지만 iOS 프레임워크들을 찬찬히 읽어보니 쓰이는 경우가 명확히 구분되어 있었다.

✏️ 결과를 바로 리턴하는 fetch

//PHAsset - Photos Framework
class func fetchAssets(withLocalIdentifiers identifiers: [String], options: PHFetchOptions?) -> PHFetchResult<PHAsset>

//PHAssetCollection - Photos Framework
class func fetchAssets(in assetCollection: PHAssetCollection, options: PHFetchOptions?) -> PHFetchResult<PHAsset>

//NSManagedObjectContext - Core Data
func fetch<T>(_ request: NSFetchRequest<T>) throws -> [T] where T : NSFetchRequestResult

fetch를 쓴 함수는 결과물을 바로 리턴해준다. 오래 걸리지 않는 동기적 작업이라는 것이다. 결과가 0개인 경우를 제외하곤 요청이 실패하지 않는 종류의 작업이다. 강아지가 공을 물어오는 놀이를 영어로 “play fetch”라고 하는데, 공을 무조건 물어오는 강아지의 모습을 연상하며 사용하면 되지 않을까


(playing fetch)

✏️ 유저에게 요청하거나 작업이 실패할 수 있을 때 request

//PHImageManager
func requestImage(for asset: PHAsset, targetSize: CGSize, contentMode: PHImageContentMode, options: PHImageRequestOptions?, resultHandler: @escaping (UIImage?, [AnyHashable : Any]?) -> Void) -> PHImageRequestID

//PHAssetResourceManager
func requestData(for resource: PHAssetResource, options: PHAssetResourceRequestOptions?, dataReceivedHandler handler: @escaping (Data) -> Void, completionHandler: @escaping (Error?) -> Void) -> PHAssetResourceDataRequestID

//CLLocationManager
func requestAlwaysAuthorization()
func requestLocation()

//MLMediaLibrary
class func requestAuthorization(_ handler: @escaping (MPMediaLibraryAuthorizationStatus) -> Void)

반면에 request를 쓰는 함수들은 비동기 작업이어서 handler를 받고 있거나 delegate 콜백으로 결과를 전달해준다. 그리고 결과 타입이 옵셔널이고 실패했을 경우 실패 이유를 알수 있는 Error 객체도 있는걸로 보아 요청이 실패할 수도 있다는 걸 알 수 있다. 또한 유저에게 특정 권한을 요청하는 메서드는 모두 request로 시작한다. 이것을 보면 request는 실패할 수 있는 작업이거나 누군가가 요청을 거절 할 수도 있을 때 사용하면 좋을 것 같다.

✏️ 작업의 단위가 클로져나 Request로 래핑 되어있으면 perform 혹은 execute

//VNImageRequestHandler
func perform(_ requests: [VNRequest]) throws

//PHAssetResourceManager
func performChanges(_ changeBlock: @escaping () -> Void, completionHandler: ((Bool, Error?) -> Void)? = nil)

//NSManagedObjectContext
func perform(_ block: @escaping () -> Void)

//CNContactStore
func execute(_ saveRequest: CNSaveRequest) throws

//NSFetchRequest
func execute() throws -> [ResultType]

perform이나 execute은 파라미터로 Request 객체나 클로져를 받는 경우이다.

덧붙이는 말

스위프트에 특화된 내용이기 때문에 다른 언어나 플랫폼은 또 다른 컨벤션을 가지고 있고 읽기 좋은 코드의 기준도 다르다. 예를 들어 자바에서는 get을 써서 getter를 만드는 것이 컨벤션이고, 또 안드로이드의 ViewisHidden이 아니라 VISIBLE, INVISIBLE, GONE을 쓴다. 이처럼 프로그래밍 언어에 따라, 플랫폼에 따라 달라진다.

마무리

최적의 알고리즘을 짜기 위해 고민하는 것 만큼이나 그 코드를 영어로 잘 표현하는 것도 중요한 일인 것 같다. 그러면 대체 어떤 단어를 쓰고 어떻게 이름 지어야 좋은 걸까? 먼저 동의어 사전을 활용해 다양한 단어를 찾아보고 뜻을 알아보자. 그리고, ‘읽기 좋은 코드가 좋은 코드다(한빛미디어)‘의 저자는 좋은 코드를 많이 읽어보라고 한다. 또 표준 라이브러리와 프레임워크 문서를 하루에 15분씩이라도 시간을 할애하여 읽어보면 좋다고 한다. 그 조언대로 스위프트와 iOS 공식 문서를 앞에서부터 순서대로 읽어봤는데 주위에도 강력 추천하고 싶다.

이 글의 초안을 읽어준 이승원, 김결, 강한용, 김영에게 고마움을 전합니다.

Tags: naming variables, iOS, english grammar, swift conventions  

google/promises를 활용한 스위프트 비동기 프로그래밍과 에러 핸들링

Background

올해 1~2월 즈음 우연히 google/promises를 알게 되었는데 소개 글과 샘플 코드 몇 줄을 보고 나서 ‘이건 꼭 써봐야겠다’는 생각이 들었다. 뭘 하는 프레임워크인지는 깃헙에 잘 소개되어 있어 상세한 설명은 생략하지만 한 줄 요약을 하자면 비동기 작업에 대한 결과를 completion handler로 처리하는 iOS의 특성에 기인하는 nested closures 문제를 해소할 수 있다. 여기에 덤으로 에러 처리까지 깔끔해진다.

Promises를 보자마자 써봐야겠다고 느꼈던 이유는 그 전부터 내 코드에서 시도하고자 했던 것들이 있었는데 이 라이브러리가 그걸 달성할 수 있게 해줄것 같았기 때문이다.

  • 사이드 이펙트 없는 함수들의 체이닝
  • 유저에게 의미있는 에러 핸들링
  • 최소한의 튜닝(디펜던시)

이런 작은 목표들은 작업의 흐름을 읽기 쉬운 코드를 짜고 유저에게 더 나은 경험을 주고자 하는 최종적인 목표에 도움이 될 것이라 생각했다.

Chaining functions w/o side effects

2016년 D2 iOS 오픈세미나에서 발표했던 걸 계기로 파파고와 웨일 브라우저를 개발한 팀의 리더님을 알게되어 그 팀에서 단기로 프로젝트를 했던 적이 있다. 멘토님에게서 배운 여러가지 중 가장 기억에 남고 지금도 코딩하면서 매일 실천하기 위해 노력하는 것은 함수는 10줄을 넘기지 않는다이다. 자극적으로 들릴 수도 있지만 본질적으로 함수는 하나의 작업만 하도록 짜라는 강령으로 이해했다. 10줄 이내로 짜려는 노력을 하다보면 계속해서 함수는 작은 기능 단위로 쪼개지고 나는 객체 간의 관계를 설계하는데 고민하는 시간이 늘게 된다. 더 나아가 side effect가 없는 함수들을 체이닝 하면 코드가 쉽게 읽히고 수정이 용이해진다는 장점이 있다. 특히 스위프트의 map, flatMap, compactMap, filter 등의 메서드와 함께 쓸 때 빛을 발한다. Promises에서는 then, always, validate 등으로 함수를 체이닝할 수 있다.

예시:

extension CNContact {
  var mainPhoneNumber: String? {
    return phoneNumbers.map { $0.value.stringValue }
                        .filter(prefixValidater)
                        .map(replaceKoreanCountryCode)
                        .map(removeNonNumerics)
                        .filter { $0.isPhoneNumber && $0.count == 11}
                        .first
  }

  private func prefixValidater(_ target: String) -> Bool { ... }
  private func replaceKoreanCountryCode(_ digits: String) -> String { ... }
  private func removeNonNumerics(_ digits: String) -> String { ... }
}

Conveying meaningful error messages to users

모바일 앱 개발을 몇 년 하다보니 어떻게 에러 핸들링을 잘 할 수 있을까 하는 갈증이 생겼다. 모바일 앱에서 가장 많이 일어나는 작업의 단계는 유저 인터랙션 👉 요청을 처리하기 위한 일련의 작업 👉 화면에 결과 보여주기 인데 일련의 작업을 처리하다보면 다양한 에러가 발생할 수 있다. 데이터가 변질됐거나, 네트워크가 불안정하거나, 서버가 다운됐거나, 권한이 없다거나 하는 등. 이때 단순히 “요청이 실패했습니다”라는 의미없는 메시지보다는 에러의 원인을 유저에게 알리는 것이 사용자 경험 측면에서 월등히 좋다. NSError의 localizedDescription을 활용하거나 스위프트에서는 Error 혹은 LocalizedError 프로토콜을 사용할 수 있다. Promises에서는 여러 연속된 비동기 작업 도중 발생한 에러를 최종 단계에서 통일성 있게 전달 받을 수 있고, 심지어 recover로 복구할 기회도 있다.

이런 에러 메시지를 보여줄 것인가?

아니면 실질적으로 유저에게 도움이 되는 메시지를 보여줄 것인가?

Minimizing dependencies

스탠다드 라이브러리를 감싼 무슨무슨 ~Kit을 쓰는 것에 대한 미묘한 거부감이 있다. 아이폰도 케이스 없이 보호 필름만 붙이고 다니는 성격이라 그런지 프로젝트 한 두군데에서만 쓰기 위해, 혹은 필요한 1만큼의 기능을 가져다 쓰기 위해 10 크기의 라이브러리를 코코아팟에 이것저것 추가하는 것이 무척 꺼려진다. 오토레이아웃도 별도 라이브러리 없이 쓰고 있고 한때 관심을 가졌던 Rx도 공부하다가 결국 과도한 튜닝을 하는 것 같아 도외시했다. 튜닝의 끝은 순정이라고, 순정을 선호하는 입장에서 Promises는 다른 유사 라이브러리보다 GCD를 더 가볍게 감쌌기 때문에 성능에서 앞서고 학습 비용도 적은 것 같다.

Adopting Promises to Your Project

Promises를 도입하겠다고 해서 당장 프로젝트 전체를 뜯어고치지 않아도 되기 때문에 실무에서 차근차근 적용해보기에도 좋다. 보통 작업의 특성별로 담당 클래스가 있을텐데(e.g. 로그인매니저, 이미지다운로더 클래스 등과 같은) 이런 클래스 한 두개만 우선적으로 적용하며 맛보기를 해봐도 충분하다. 또한 wrap을 쓰면 기존에 있던 코드를 손댈 필요도 없이 코드 몇 줄로 Promises식 비동기 함수를 만들 수도 있다.

기존에 completion handler를 파라미터로 받던 함수:

func data(from url: URL, completion: @escaping (Data?, Error?) -> Void)

Promise 객체를 리턴하도록 수정한 비동기 함수:

func data(from url: URL) -> Promise<Data>

그러면 함수 내부에서 Promise 객체를 생성하여 리턴해준 후, fulfill된/될 결과값을 가지고 추가적인 작업을 해주면 된다.

let url = ...
data(from: url).then { data in
  //data로 추가 작업
}.catch { error in
  //error 처리
}

더 다채로운 활용법은 문서에 간단명료하게 잘 설명되어 있다!

Use Cases

그동안 사용해보면서 시행착오를 거쳐 습득한 몇 가지 유즈케이스 및 주의사항을 정리했다.

Partial Application 기법

Promises 파이프라인의 가독성과 함수 재활용성을 높이기 위해 partial application 기법을 활용하는 방법을 소개한다. map, forEach 등의 higher order function들을 사용하고 있었다면 아마 써본적이 있을 확률이 높다.

API 서버에 로그인하여 access token을 받아오는 작업은 로그인 기반의 서비스에서 빠질 수 없는 작업이다. 가상의 로그인 단계는 다음과 같다. 회원가입 👉 로그인 👉 엑세스 토큰 획득. 하지만 이미 회원가입이 되어 있는 유저라면 회원가입에 실패하게 된다. Promises에서는 실패했을때 recover를 사용해서 실패를 복구할 기회가 있다. 그래서 만약 회원가입 실패의 원인이 duplicate user라면 로그인을 시도한다.

Promises 코드:

typealias MyAccessToken = String

func retrieveAccessToken(with naverToken: String) -> Promise<MyAccessToken> {
  return requestSignUp(with: naverToken)
         .then(signIn(with: naverToken))
         .recover(onError(with: naverToken))
}

//Async Server API calls
func requestSignUp(with naverToken: String) -> Promise<SignUpResponse> { ... }
func requestSignIn(with naverToken: String) -> Promise<MyAccessToken> { ... }

//partially applied functions
func signIn(with naverToken: String) -> (SignUpResponse) -> Promise<MyAccessToken> {
  return { _ in requestSignIn(with: naverToken) }
}

func onError(with naverToken:String) -> (Error) -> Promise<MyAccessToken> {
  return { error in
    switch error {
    case SignUpError.duplicateUser:
      return requestSignIn(with: naverToken)
    default:
      return Promise(error)
    }
  }
}

signIn(with:)onError(with:)는 각각 SignUpResponse와 Error 파라미터를 나중에 전달 받도록 짠 partially applied functions이다. 이런 식으로 체이닝을 할 때 클로져를 바로 쓰지 않고 partial application을 활용하여 기존의 API 관련 함수들을 재활용함과 동시에 비동기 작업 파이프라인을 훨씬 읽기 쉽게 만들었다.

단순 체이닝으로 불가능한 작업은 await으로

await은 여러 비동기 작업으로부터 얻은 결과들을 혼합해서 사용해야 할때 유용하다.

예를 들어, 썸네일 이미지에 대한 url을 가지고 UIImage와 해당 이미지의 대표 UIColor 추출해 화면에 그려야 하는 작업이 있다고 가정해본다. 정리하면 URL을 UIImage로 변환 👉 UIImage에서 UIColor 추출 👉 UIImage, UIColor를 가지고 화면에 썸네일 생성 해야했는데 이 작업은 then 체이닝만으로는 구현하기 어려웠고 이를 await으로 해결했다. 또한 이 방식은 비동기 작업을 동기적인 코드처럼 쓰고 싶을 때 사용해도 된다.

Promises 코드:

typealias ThumbnailData = (image: UIImage, color: UIColor)

func thumbnailData(from url: URL) -> Promise<ThumbnailData> {
  return Promise<ThumbnailData>(on: queue) { //queue는 백그라운드 DispatchQueue
    let image = try await(image(from: url))
    let color = try await(dominantColor(from: image))

    return (image: image, color: color)
  }
}

//Async functions
func image(from url: URL) -> Promise<UIImage> { ... }
func dominantColor(from image: UIImage) -> Promise<UIColor> { ... }

활용한 부분:

let cell: MyTableViewCell = ...
let myDatum = data[indexPath.row]
let url: URL = myDatum.imageURL

thumbnailData(from: url).then { result in
  cell.imageView.image = result.image
  cell.dominantColorView.backgroundColor = result.color
}

추가적으로 Promises 사용 전 꼭 알아두면 좋은 내용으로는,

정도가 있다.

Wrap Up

iOS 개발을 하면서 한번이라도 completion handler 방식의 비동기 프로그래밍에 아쉬움을 느껴봤거나 Rx는 나의 필요 이상으로 너무 방대하다는 생각이 든 적이 있다면 한번 시도해보길 추천한다. google/promises는 최소한의 코드 변형으로 비동기 코드를 유연하게 하고, 가독성을 높이고, 적은 학습 비용으로 여러가지 시도해볼 수 있는 확장성 있는 좋은 프레임워크인 것 같다.

Tags: swift, promises, async programming, error handling