스토리 홈

인터뷰

피드

뉴스

조회수 1666

[인터뷰]미미박스의 TECHNOLOGY를 이끄는 CTO KAY를 만나다

안녕하세요. Ava입니다.여러분에게 더 건강하고, 아름다운 라이프스타일을 제공하기 위해 노력하는 미미박스 뒤에는여러분의 니즈를 만족시키고 안정된 서비스를 제공하기 위한 개발이끊임없이 진행되고 있습니다.오늘은 미미박스 TECHNOLOGY UNIT를이끌고 계신 김종광 CTO(이하 KAY) 님을 소개해드리겠습니다.KAY는 대기업과 IT기업에서 앱 개발과 웹 개발을 진행했던 커리어를 갖고 계신데요. 경력과 전문성 뿐만 아니라 개발자들이 상상할 수 있는 문화를 강조하며 만들어나가고 있습니다.항상 푸근한 아빠 미소로 미미박서의 질문과 제안을 받아주고,열린 리더의 모습을 보여주시는 KAY를 소개합니다.김종광 (Kay) 한국기술교육대학교 전기전자공학 석사전) NC소프트전) SK communicationsUNIT1. KAY를 소개해주세요.Q. 안녕하세요. 항상 아빠 미소를 짓고 계신 KAY~KAY를 소개해주세요.A. 처음에 미미박스에 모바일 앱 개발 총괄로 입사했어요. 지금은 개발 UNIT 전체를 맡고 있고요. 세 가지의 주 업무가 있는데요. 개발 전체 프로젝트를 leading 하고 다른 팀과 연관된 업무에 대해서 지원하는 일과, 새로운 개발자를 충원하는 업무를 하고 있습니다.Q. 미미박스에 입사하시게 된 계기는 무엇인가요? A. 처음에는 지인이 추천해서 미미박스를 알게 되었어요. 그 후에 미미박스에 대해 조사를 해봤죠. 비즈니스 모델, 성장 가능성을 봤을 때 '될 것 같다'는 생각을 가졌어요. 그리고 몇 번 찾아갔었는데 회사 분위기가 활기차고 재밌었어요. 그리고 일하는 사람들 중에 아는 사람들이 4~5명 정도 더 있었어요. 이분들과 다른 구성원들을 보면서 '이 친구들이랑 같이 일하면 즐겁게 일할 수 있겠다'라는 생각이 들었죠.Q. 여러 조직에서 있으셨던 만큼 개발 업무 자체에 대한 이유도 있을 것 같아요. A. 보통 기업에서는 개발자의 역할이 상당히 제한되어있어요. 업무에 대한 의사결정 권한이 거의 없죠. TOP-DOWN 방식으로 내려온 것들을 그냥 해야 하는 경우가 많거든요. 개발자들은 다들 알 거예요. 만들면서 '이거 안될 것 같다.'라는 감이 있는데, 느낌상으로 안될 것 같은 것을 만드니까 의욕이 생기지 않는 경우가 있었어요. 효과성보다는 어떤 서비스를 오픈했다는 것 자체가 실적이 되는 경우가 많거든요. Q. 그런 경우가 있군요. 그렇다면 미미박스 내에서는 개발 업무에 대해 어떤 식으로 의사결정이 이루어지나요?A. 업무에 대한 의사결정은 반반인 것 같아요. 우선 TECHNOLOGY UNIT 내부에서 프로젝트를 진행하는 것이 있어요. '이렇게 하면 회사와 서비스에 도움이 되겠다. 매출도 좋아질 것 같다. 사람들도 좋아할 것 같다'이런 의견을 내고 직접 만들 수 있고요. 서비스를 같이 진행하는 마케팅팀이나 플랫폼 운영팀에서 요청이 들어오면 그 요청에 대해 저희가 납득하고 하면 좋겠다고 생각하는 업무에 대해서 일정을 짜고 진행해요. 각 요소 별로 개발 측면의 논의도 많이 하고 실제 만드는 사람의 의견이 많이 반영됩니다. Q. 직접 만들고 구축하는 사람들의 의견은 정말 중요한 것 같아요.TECHNOLOGY UNIT을 이끌고 있는 UNIT 장님으로서 KAY의 하루 스케줄은 어떻게 되나요?A. 출근 후, 오전에는 집중 개발 업무를 하고 있어요. 제가 플랫폼 개발 팀장도 겸임하고 있거든요. 오후부터는 대부분 팀미팅이나 프로젝트 미팅을 많이 합니다. 프로젝트의 진행사항을 체크하고, 개발이 어려운 부분과 개발하면서 중요하게 생각하는 부분에 대해서 토론하죠. 그리고 새로운 개발 인력들 채용을 위해 면접을 많이 봅니다.Q. TECHNOLOGY UNIT 내부에서 소통과 역량 강화를 위해 주기적으로 여는 세션이 있다고 들었는데 소개해주세요!A. 일주일에 한 번씩 주니어 개발자를 대상으로 스터디를 진행하고 있습니다. 저는 스터디를 leading 하고 멘토 역할을 하고 있고요.이런 시간을 만들게 된 이유는 소통의 장을 만들고 개발자들의 역량을 키우기 위해서입니다.주니어 개발자들이 시니어 개발자들 앞에서 의견을 내는 것에 대해서 소극적인 면이 있어요. 틀릴까 봐 의견을 쉽게 못 내죠. 그래서 주니어 개발자들끼리 모여서 얘기할 공간을 만들어 주는 것이 중요하다고 생각했어요. 서로 의견을 내고 토론하면서 개발에 대한 역량도 쌓고 의견을 내는 훈련도 할 수 있죠.그리고 DATA UNIT의 협조를 받아서 빅데이터 관련 스터디를 진행하고 있어요. 그래서 개발자들 중 관심 있는 사람이들 모여서 빅데이터 관련 LOGIC을 만들어보고, 아이디어를 실현시켜보는 작은 프로젝트를 그룹별로 진행하고 있어요.앞으로는 이런 세션들을 발전시켜 세미나를 열 예정이에요. 그래서 각 개발자들이 적어도 1년에 2번 이상은 주제 발표할 수 있도록 환경을 만들려고 합니다. 개발 업무는 집중도가 높아서 건조해질 위험이 있어요. 집중하다 보면 일에 치여서 자기계발이 어려워질 수 있기 때문에 계속 자기계발하는 분위기를 만들어가려고 합니다. 전사적으로도 그런 분위기가 계속 만들어지면 좋겠어요.Q. 건조하긴요! 제가 보기엔 개발팀들이 가장 활발하고 참여도도 높은 것 같은데요! 열려있는 분들도 많고요.A. 개발팀이 아닌 팀들이랑 많이 소통하라고 조언을 많이 해요. 미미투게더(2개 이상 팀이 함께 회식하면 회식비를 지원해주는 기업문화 제도)를 할 때도 개발팀 내부에서만 하지 말고 무조건 다른 팀들과 함께하라고 하고 있어요. 새로운 아이디어를 나눌 수 있고 인간관계가 힘이 될 때가 많기 때문에 다른 팀들이랑 얘기를 많이 나누는 게 필요하죠. UNIT2. TECHNOLOGY UNIT을 소개해주세요.Q. TECHNOLOGY UNIT을 소개해주세요.A. TECHNOLOGY UNIT에서는 지금 미미박스에서 서비스하는 모든 PRODUCT, 플랫폼, 모바일 앱, PC 웹, 내부 직원들이 쓰는 모든 것들을 개발하고 있습니다. 대부분의 기업에서는 계약직이나 파견직의 고용형태로 진행하는 경우도 있는데, 저희는 모든 구성원이 정직원으로 개발 업무를 하고 있습니다.Q. TECHNOLOGY UNIT의 분위기는 어떤가요?A. 개발자라는 직무를 하는 사람들은 생각 자체가 자유로워야 합니다. 경직되어있으면 좋은 아이디어가 떠오르지 않죠. 그래서 TECHNOLOGY UNIT은 최소한의 규제나 룰을 두고 자유롭게 활동하게 하고 있어요. 특별한 일이 아니면 회의 소집도 지양하고 있어요.다양하게 상상하려면 경직되지 않고, 룰에 집착하지 않는 문화를 만들어야 하기 때문이에요. 그래야 본인의 의견도 편하게 이야기할 수 있죠. 구성원들을 보면 시니어 개발자들은 적응을 잘해요. 주니어 개발자들이 아직 조금 경직되어있긴 해요.지금 신입 공채 2기를 뽑고 있는데요. 보통은 스타트업에서 입사 후 바로 투입될 수 있는 사람을 뽑아요. 하지만 저는 확신이 있어서 저희 미미박스의 DNA를 가지고 처음부터 함께할 수 있는 신입을 뽑고 싶어요. 미미박스의 DNA를 가지고 더 성장하게 되면 저희 개발 조직에 기둥이 될 수 있을 거라고 생각합니다. <채용공고 보러 가기 클릭>Q. KAY와 함께 하는 구성원들이 점점 부러워지네요. 정말 구성원들의 성장에 많은 비중을 두고 여러 계획을 실천하는 것 같아요. 미미박스에는 여성 개발자도 점점 많아지는 것 같은데 재미있는 에피소드 있나요?A. 먼저 여성 개발자들의 비중이 점점 늘어나고 있어요. 우리가 여성 고객을 위한 서비스를 많이 하고 있잖아요. 그 감성을 같이 공유할 수 있는 사람이 많아지면 기술적인 부분뿐 아니라 감성적인 부분에서도 큰 시너지가 나죠. 실제로 웹페이지에 제품 가격이 잘못 올라간 적이 있어요. 저희 남성 개발자들이 그 데이터를 먼저 보는데 '이게 맞는 가격인가' 의심하는 사람이 아무도 없었어요. 그때 여성 개발자분이 '이 제품이 이 가격이 아닐 텐데? 문제를 제기했고 다행히 수정할 수 있었죠. Q. 그래도 남성 개발자들의 화장품 가격에 대한 감이 점점 정확해질 것 같아요. 호호KAY 님이 UNIT을 운영하시면서 가장 보람을 느끼신 적은 언제인가요?A. 고객들이 많이 와서 저희 서비스를 이용해 주실 때 보람을 느낍니다. 저희 UNIT 자체에서도 무언가를 만들어가고 있다는 것을 느끼고, 실제로 좋은 반응을 얻었을 때 기분이 정말 좋아요. Q. 점점 더 많은 분들이 미미박스를 찾아주신다는 게 느껴져요! 앞으로의 목표는 무엇인가요?A. 첫 번째는 글로벌로 플랫폼을 옮기는 것입니다. 저희 내부에서 개발한 플랫폼과 서비스가 점점 확대돼서 미미박스가 해외에 진출할 때마다 플랫폼을 그대로 이동시켜 글로벌화하는 것이 첫 번째 목표고요. 두 번째는 앞으로 온라인을 넘어 오프라인에 대한 서비스도 진행할 예정이에요. 현재 미미박스 플랫폼과 오프라인 요소의 연계성을 찾고 최고의 고객 경험을 만드는 것이죠. 일반적인 O2O 서비스를 넘어 대부분의 고객이 여성이기 때문에 IT 기술 자체가 숨어있고, 알아서 돌아가게 만드는 서비스를 만들 것입니다.미미박스는 뷰티에 대해서 많은 강점과 다양성을 가지고 있기 때문에 이런 것들을 통해 저희만 할 수 있는 서비스를 만들고 싶어요. 마지막은 Data-driven 방식을 더욱 견고히 가져가는 것이에요. 축적되어있는 경험과 데이터를 통해서 고객 맞춤형 서비스에 대한 역량을 강화하는 것이죠. Q. 글로벌 플랫폼, O2O 서비스, Data-driven 앞으로의 TECHNOLOGY UNIT이 만들어낼 것들이 기대돼요. 두근두근. 마지막으로 KAY가 TECHNOLOGY UNIT을 리드하면서 가장 집중하는 3가지가 무엇인지 궁금합니다. A. 가장 중요한 것은 우리 개발자들의 커리어를 관리해주는 것이에요. 이분들이 미미박스에 와서 자기의 역량이 발전하지 않고 정체되다면 제가 역할을 제대로 못했다는 뜻이거든요.그래서 구성원들이 고생을 하든 뭘 하든 해가 갈수록 성장할 수 있도록 관리하는 것에 집중하고 있어요. 두 번째는 우리가 TECH 조직이기 때문에 서비스가 아주 정상적으로 운영되는 것이 목표에요. 단순한 장애를 없애는 것뿐만 아니라 계속 플랫폼이 발전하면서 문제가 없게 만들어야 하죠. 매출, 데이터가 계속 쌓이면서 안정적인 서비스를 만드는 것, 기본적인 것 같지만 가장 중요한 것 같아요. 마지막으로는 Align이에요. 개발팀이 성장할 수 있는 서비스, 개발 역량을 강화시키다며 보면 회사의 목표에 Align 되는 것을 놓칠 수 있어요. 그렇기 때문에 개발자들이 관심 있는 것들과 회사의 목표를 Align시켜서 시너지 효과를 낼 수 있도록 집중하고 있습니다.UNIT2. TECHNOLOGY UNIT으로서 어떤 사람과 일하고 싶나요?Q. TECHNOLOGY UNIT에서 일하기 위하여 갖추어야 할 역량은 어떤 것이 있나요?A. 첫 번째로 성장 가능성을 봅니다. 성장 가능성에는 여러 가지 의미가 있지만 적극적이고, 새로운 지식에 대한 욕구가 항상 강한 사람이어야 합니다. 배우고 싶은 열망, 해보고 싶다는 열망을 가지고 실제 구체적으로 실행해본 경험이 있고, 뭔가를 해본 사람이 성장 가능성이 있는 사람이라고 생각합니다. 제가 면접을 볼 때마다 항상 물어보는 것이 '5년 후 계획, 5년 후 모습은 어떨 것 같아요?'에요. 면접자가 적극적으로 대답하면 '그것을 위해 어떤 실행계획이 있는지' 물어보죠.두 번째로는 스타트업 마인드 FIT이 맞는 것이에요. 저도 미미박스에 처음 왔을 때 힘들었어요. 갖춰져 있는 게 없었거든요. 하나부터 열까지 하려면 뭔가 어디서 걸리는 거예요. 큰 회사는 세팅이 다 되어있는데 말이죠. 그래서 뭔가를 하려면 그 업무뿐 아니라 처음부터 다 찾고 만들어야 해요. 이렇게 만들어가는 걸 좋아하는 사람이 있어요. 준비가 안되어있다고 불평하는 것이 아니라 부족한 환경에서 할 거리가 많은 것을 반기는 사람들. 이런 사람들은 '이것저것 해봐야지~' 신나있어요. 이런 마인드 FIT을 많이 봅니다.Q. 스타트업 마인드 FIT 정말 공간되는 말인 것 같아요. 저도 갖춰져있는 틀에서 무언가를 하는 것보다 이것저것 찾아서 만드는 걸 좋아하거든요! 그런 분들이 많이 오시면 재밌는 일이 많이 벌어질 것 같아요. 우리 미미박스의 비전은 'Beautify the people'인데요. 혹시 취업이나 이직을 준비하는 분들께 이것만은 아름답게 관리하라고 조언하고 싶은 게 있나요?A. 이력서와 경력기술서를 아름답게 해야 해요. 개발자들 중에 '내 역량만 좋으면 되지'라고 생각하시는 분들이 있는데 자신의 커리어 패스를 만드는 것도 중요하거든요. 회사에서 처음에 서류전형을 진행하는 게 많은 내용을 내포하고 있어요. 경력기술서의 내용이 부실하면 회사도 본인도 FIT이 맞는 곳을 찾기가 어려워지죠. 어디서든 인정받는 사람이 되려면 자신의 업무와 역할을 충실하게 표현한 경력기술서를 작성하라고 말씀드리고 싶네요.Q. 정말 실질적인 조언이네요. 누구보다 깊게 고민하고 집중한 일일수록 경력기술서와 이력서를 잘 쓸 수 있고 자신의 경력도 잘 전달할 수 있을 것 같아요.마지막으로 함께 일하고 있는 미미박서분들께도 한마디 해주세요!A. 제가 여기 처음 와서 한 이야기가 있어요. "여기가 제 마지막 회사입니다."그렇게 이야기한 이유는 미미박스의 성장 가능성, 발전 가능성을 보았고 믿음이 있기 때문이죠. 모두가 같이 노력한다면 원하는 것을 이룰 수 있을 거라 생각해요. 다 같이 파이팅!
조회수 6957

안드로이드 앱의 Persistent data를 제대로 암호화해 보자! (1/2)

들어가기오늘 소개해드릴 글은 안드로이드에서 좀 더 안전하게 파일 시스템에 데이터를 저장하는 방식에 관한 내용입니다. 이 글은 중급 이상, 상급 이하 안드로이드 개발자를 대상으로 작성했으며 완독하는데 약 20분 정도가 필요합니다. 최대한 쉽게 쓰려고 노력했습니다만 이 글이 잘 이해되지 않는 독자분들은 이 문서 말미의 더 보기 섹션에 링크된 외부 문서들을 읽어보시는 편이 좋습니다.1부에서는 Shared preferences 에 저장하는 데이터를 암호화 하는 방식에 대해 다루고 있으며, 2부에서는 데이터베이스를 암호화 하는 방식에 대해 다루겠습니다.내 앱의 데이터, 과연 유출로부터 안전할까?안드로이드 공식 사이트의 저장소 개발 가이드 문서는 데이터를 저장하는 여러 가지 방법을 소개하고 있습니다. 그 중 ‘내부 저장소’ 의 다음 특징은 눈여겨볼 만 합니다.기기의 내부 저장소에 파일을 직접 저장할 수 있습니다. 기본적으로, 내부 저장소에 저장된 파일은 해당 애플리케이션의 전용 파일이며 다른 애플리케이션(및 사용자)은 해당 파일에 액세스할 수 없습니다. 사용자가 애플리케이션을 제거하면 해당 캐시 파일은 제거됩니다.즉, 다른 애플리케이션에 노출하면 곤란한 중요한 정보들은 내부 저장소에 담아두면 안전하다고 할 수 있습니다. 하지만, 정말일까요? 다음 예제를 이용해 내부 저장소에 저장한 사용자의 중요한 정보를 어떻게 탈취하는지 알아보겠습니다. 예제 앱은 충성 사용자에게 보상하기 위해 사용자가 앱을 몇 번 실행시켰는지를 기록합니다.class AppRanTimesRecordingActivity : AppCompatActivity() {    privateval sharedPrefs by lazy {        // Shared preferences 는 Internal storage 에 저장된다.        getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)    }    private var appRanCount = 0    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        accessToSharedPrefs()        appRanCount++        Toast.makeText(applicationContext, "App has ran $appRanCount times!!", Toast.LENGTH_LONG).show()        finish()    }    override fun onDestroy() {        saveSharedPrefs()        super.onDestroy()    }    private fun accessToSharedPrefs() {         sharedPrefs.run { appRanCount = getInt(KEY_APP_RAN_COUNT, 0) }    }    private fun saveSharedPrefs() {        sharedPrefs.edit().run({            putInt(KEY_APP_RAN_COUNT, appRanCount)            apply()        })    }    companion object {        private const val SHARED_PREF_NAME = "MySecureSettings"        private const val KEY_APP_RAN_COUNT = "appRanCount"    } } [리스트 1] MODE_PRIVATE 로 보호하는 SharedPreferences 사용앱의 데이터는 /data/data/com.securecompany.secureapp 에 저장되어 있습니다만, 앱을 release 모드로 빌드하면 adb 명령으로도 볼 수 없으니 안전하다고 할 수 있을 겁니다. 실제로 adb 명령을 이용해 저장한 파일을 보려고 시도하면 아래와 같은 오류가 발생합니다.$ adb shell "run-as com.securecompany.secureapp ls -al /data/data/com.securecompany.secureapp" run-as: Package 'com.securecompany.secureapp' is not debuggable 그렇다면 디버거로도 볼 수 없으니 내부 저장소에 저장한 데이터가 안전하다고 말 할 수 있을까요?그렇지 않습니다! 안드로이드는 루팅이 매우 손쉬운 운영체제기 때문에 설령 release 모드로 빌드한 앱이라 하더라도 adb 명령을 이용해 모두 접근할 수 있습니다. 루팅한 기기에서 우리가 제작한 SecureApp의 내부 저장소 구조를 아래와 같이 확인할 수 있습니다.$ adb shell "sudo ls -al /data/data/com.securecompany.secureapp" drwxrwx--x u0_a431 u0_a431 2018-06-04 14:15 cache drwxrwx--x u0_a431  u0_a431           2018-06-04 14:15 code_cache drwxrwx--x u0_a431  u0_a431           2018-06-04 14:15 shared_prefs $ adb shell "sudo ls -al /data/data/com.securecompany.secureapp/shared_prefs" -rw-rw---- u0_a431 u0_a431 111 2018-06-04 14:15 MySecureSettings.xml $ adb shell "sudo cat /data/data/com.securecompany.secureapp/shared_prefs/MySecureSettings.xml" <?xml version='1.0' encoding='utf-8' standalone='yes' ?>     별다른 테크닉이 없더라도 인터넷에 널린 수많은 루팅 방법으로 기기를 루팅하면 제아무리 내부 저장소에 저장한 데이터라도 이렇게 손 쉽게 유출이 가능하다는 것을 확인할 수 있습니다. 이런 방식의 보안 기법은 불투명성에 의지한 보안이라고 하여, 방법을 전혀 모르는 공격자에게는 유효한 방식입니다만 이 글을 읽는 독자 수준의 개발자라면 취약점을 금세 파악할 수 있다는 단점이 있습니다.그렇다면 암호화를 적용하면 되지 않을까?맞습니다. 어차피 유출을 피할 수 없다면, 데이터를 암호화하면 됩니다. 그래서 암호화 로직으로 데이터를 암호화해 보도록 하겠습니다. 이 코드는 AES / CBC / PKCS5Padding 방식을 사용해 주어진 데이터를 암호화합니다. 각 용어를 간략하게 설명하자면 다음과 같습니다.AES: 미국에서 개발된 블럭 암호화 방식으로 좀 더 나은 보안성을 가진다. 데이터를 일정 크기(블럭)로 나눠 암호화하며 보통 128비트, 192비트, 256비트 단위로 암호화한다. 키의 길이는 암호화 방식에서 사용할 블럭 크기와 완전히 같아야 하는 특징이 있다.CBC: 블럭을 회전시키는 방식을 말한다. 최초로 소개된 블럭 회전 알고리즘인 ECB(Electronic Code Book) 의 보안 취약점을 해결하기 위한 방식으로 같은 데이터 입력에 대해 완전히 다른 결과를 내므로 보안성이 좀 더 높다. 하지만 CBC 방식을 위해서는 초기화 벡터(Initialisation Vector, IV)를 반드시 사용해야 한다.IV : CBC 블럭 회전방식에 사용하는 초기화 값. 암호화할 데이터와 키가 변하지 않더라도 이 값만 바뀌면 결과가 크게 달라진다. 암호화 key 와는 전혀 무관한 값이기 때문에 외부에 노출되더라도 보안 위협은 적은 편이며 암호화 요청마다 다른 IV 를 사용해 보안성을 높일 수 있다. 다만, 키 길이와 일치하는 길이의 IV 가 필요하다.PKCS5Padding: 블럭 암호화 방식은 입력 데이터의 길이가 블럭의 길이 혹은 그 배수와 일치해야 하는 문제점이 있다. 입력 데이터가 블럭 길이보다 짧을 경우 원칙적으로 암호화가 불가능하다. 이런 어이없는 단점을 보완하기 위한 방식으로, 입력 데이터를 강제로 블럭 크기만큼 맞춰주는 알고리즘의 일종이다.object AESHelper {    /** 키를 외부에 저장할 경우 유출 위험이 있으니까 소스 코드 내에 숨겨둔다. 길이는 16자여야 한다. */    private const val SECRET_KEY = "HelloWorld!!@#$%"    private const val CIPHER_TRANSFORMATION = "AES/CBC/PKCS5PADDING"    fun encrypt(plainText: String, initVector: String): String {        val cipherText = try {            with(Cipher.getInstance(CIPHER_TRANSFORMATION), {                init(Cipher.ENCRYPT_MODE,                         SecretKeySpec(SECRET_KEY.toByteArray(), "AES"),                         IvParameterSpec(initVector.toByteArray()))                return@with doFinal(plainText.toByteArray())            })        } catch (e: GeneralSecurityException) {            // 특정 국가 혹은 저사양 기기에서는 알고리즘 지원하지 않을 수 있음. 특히 중국/인도 대상 기기            e.printStackTrace()            ""        }        return Base64.encodeToString(cipherText, Base64.DEFAULT)    }    fun decrypt(base64CipherText: String, initVector: String): String {        val plainTextBytes = try {            with(Cipher.getInstance(CIPHER_TRANSFORMATION), {                init(Cipher.DECRYPT_MODE,                        SecretKeySpec(SECRET_KEY.toByteArray(), "AES"),                        IvParameterSpec(initVector.toByteArray()))                val cipherText = Base64.decode(base64CipherText, Base64.DEFAULT)                return@with doFinal(cipherText)            })        } catch (e: GeneralSecurityException) {            // 특정 국가 혹은 저사양 기기에서는 알고리즘 지원하지 않을 수 있음. 특히 중국/인도 대상 기기            e.printStackTrace()            ByteArray(0, { i -> 0 })        }        return String(plainTextBytes)    } } [리스트 2] 간단히 구현한 AES128 암호 및 해독 로직그리고 위의 AESHelper 를 이용해 SharedPreference 에 들어갈 자료를 암호화해 봅시다.class MainActivity : AppCompatActivity() {    privateval iv by lazy { lazyInitIv() }    privateval sharedPrefs by lazy {        getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)    }    private var appRanCount = 0    override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main) // Shared preferences 는 Internal storage 에 저장된다. accessToSharedPrefs()        appRanCount++ Toast.makeText(applicationContext, "App has ran $appRanCount times!!", Toast.LENGTH_LONG).show()    }    override fun onDestroy() {        saveSharedPrefs()        super.onDestroy()    }    private fun accessToSharedPrefs() {        sharedPrefs.run({            val appRanCntEncrypted = getString(KEY_APP_RAN_COUNT, "")            if (appRanCntEncrypted.isEmpty()) {                return@run            }            appRanCount = AESHelper.decrypt(appRanCntEncrypted, iv).toInt()        })    }    private fun saveSharedPrefs() {        sharedPrefs.edit().run({            putString(KEY_APP_RAN_COUNT, AESHelper.encrypt(appRanCount.toString(), iv))             apply()        })    }    private fun lazyInitIv(): String {        return sharedPrefs.run({            var iv = getString(KEY_SESSION_IV, "")            if (iv.isEmpty()) {                // 2001년 - 2286년 동안에는 항상 13자리로 나타난다. 그러므로 16자리 IV가 보장된다.                iv = "${System.currentTimeMillis()}000"                edit()                    .putString(KEY_SESSION_IV, iv)                    .apply()            }            return@run iv        })    }    companion object {        private const val SHARED_PREF_NAME = "MySecureSettings"        private const val KEY_APP_RAN_COUNT = "appRanCount"        private const val KEY_SESSION_IV    = "ivForSession"    } } [리스트 3] 리스트 2를 활용해 데이터를 암호화해 저장.저장한 SharedPreferences 를 확인해 보면 다음과 같은 결과를 얻을 수 있습니다.$ adb shell "sudo cat /data/data/com.securecompany.secureapp/shared_prefs/MySecureSettings.xml" <?xml version='1.0' encoding='utf-8' standalone='yes' ?>    1528095873216000    F9dq8ezypMPeUsHpPIUcnQ==     역시 기대대로 암호화되었네요. IV 는 노출돼도 상관없는 정보라고 했으니 괜찮겠죠. 이제 우리 앱의 사용자는 설령 기기를 잃어버리더라도 소중한 정보가 암호화되어 있으니 문제없을 겁니다.라고 생각한다면 오산입니다! 불행히도 안드로이드는 디컴파일이 매우 쉬운 플랫폼이기 때문에 이런 식의 암호화는 사실 그다지 효과가 있지 않습니다. 심지어 IV 가 그대로 노출되어 있기 때문에 공격자에게 큰 힌트가 되었습니다. IV 는 키와는 다른 값이므로 유출되어도 상관없다곤 하지만, 어쨌든 암호화 과정에서 중요하게 다뤄지는 정보임에는 매한가지이므로 사용자에게 굳이 노출할 필요는 없습니다.다소 극단적인 예를 들어 설명했습니다만 요지는 이렇습니다. 어떤 식으로든 우리의 로직 내에서 키를 관리하는 방식으로는 완벽하게 암호화했다고 말할 수 없습니다. AESHelper 소스의 첫 줄에 있는 내용을 다시 한번 살펴봅시다. /** 키를 외부에 저장할 경우 유출 위험이 있으니까 소스 코드내에 숨겨둔다. 길이는 16자여야 한다. */    private const val SECRET_KEY = "HelloWorld!!@#$%" 불행히도 이 소스에 적힌 코멘트는 틀렸습니다. jadx 나 bytecode-viewer 로 획득한 우리 앱의 APK 파일을 디컴파일 해 봅시다.@Metadata(   mv = {1, 1, 10},   bv = {1, 0, 2},   k = 1,   d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0002\b\u0007\bÆ\u0002\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u0016\u0010\u0006\u001a\u00020\u00042\u0006\u0010\u0007\u001a\u00020\u00042\u0006\u0010\b\u001a\u00020\u0004J\u0016\u0010\t\u001a\u00020\u00042\u0006\u0010\n\u001a\u00020\u00042\u0006\u0010\b\u001a\u00020\u0004R\u000e\u0010\u0003\u001a\u00020\u0004X\u0082T¢\u0006\u0002\n\u0000R\u000e\u0010\u0005\u001a\u00020\u0004X\u0082T¢\u0006\u0002\n\u0000¨\u0006\u000b"},   d2 = {"Lcom/securecompany/secureapp/AESHelper;", "", "()V", "CIPHER_TRANSFORMATION", "", "SECRET_KEY", "decrypt", "base64CipherText", "initVector", "encrypt", "plainText", "production sources for module app"} ) public final class zzw {   private static final String A = "HelloWorld!!@#$%";   private static final String B = "AES/CBC/PKCS5PADDING";   public static final zzw INSTANCE; // ... } [리스트 4] 리스트 2를 디컴파일한 결과. 키가 그대로 노출됨을 확인할 수 있다.이름은 난독화했지만 문자열이 그대로 노출된 상태이므로 공격자가 단서를 찾기란 매우 쉬울 겁니다. 더군다나 원본 소스의 내용이 짧으니 아무리 난독화 했더라도 내용을 파악하기란 그리 어렵지도 않을 것이고요.여기서 일부 독자분들은 ‘그럼 이 로직을 JNI 로 만들면 되지 않냐?’ 라고 반문하실 수도 있습니다. 하지만 JNI 로 컴파일한 .so 파일조차 objdump 같은 명령으로 내용을 다 들춰볼 수 있습니다. 특히 Kotlin 구현처럼 static const 형태로 소스코드에 적어두면 공격자 입장에서는 .data 세그먼트 만 확인하면 되죠. 그렇다면 .data 세그먼트를 회피하기 위해 로직으로 키를 생성하도록 작성했다고 해 봅시다. 좀 더 난이도가 올라가긴 하겠지만 숙련된 공격자라면 .text 세그먼트를 이 잡듯이 뒤져 실마리를 찾을 수 있을 겁니다. 물론 이 정도 수준의 역공학을 할 수 있는 사람의 수는 적지만, 아예 없지는 않으니 문제는 여전히 남아 있습니다. 한번 확인해 볼까요?static const char* SECRET_KEY = "HelloWorld!!@#$%" void encrypt(char* plainText, char* initVector, char[] result) {    char* now = malloc(sizeof(char) * 13);    itoa(&time(NULL), now, 10);    char* iv = malloc(sizeof(char) * 16);    strcpy(iv, *now);    strncpy(iv, "000", 3);   const struct AES_ctx aesCtx = { .RoundKey = 16, .Iv = *iv }    AES_init_ctx(aesCtx, SECRET_KEY);    // ... } [리스트 5] C 로 작성한 AESHelper 로직(일부).$ objdump "mySecureApp/build/obj/local/armeabi-v7a/libAESHelper.so" section .data    # 의미 불명의 문자열 발견! 혹시 key 는 아닐까???    00000200 db "HelloWorld!!@#$%", 16    00000210 equ $ - 00000200 section .text    global _start    _start:    # ...        mov rsi, 00000200  # 이 명령 앞뒤로 조사해보면 저 문자열의 용도를 파악할 수 있다.        mov rdx, 00000210        syscall    # ... [리스트 6] ARM EABI V7용으로 컴파일한 바이너리를 디스어셈블 한 결과.즉, 어떤 방식으로 구현하건 암호화에 쓸 키를 소스 코드에 박아두는 것은 그다지 현명한 선택이 아니란 것입니다. 더군다나 안드로이드에서 앱을 만든다는 것은 내 로직이 공격자에게 낱낱이 까발려져 있다는 것을 의미합니다. 중요한 데이터를 .text 에 들어가도록 숨기는 것도 가능하긴 하지만, 그런 방식은 나중에 유지보수하는 사람에게도 골치 아플 겁니다. 소스 코드가 그만큼 어려워질 테니까요. 그리고 그런 방식으로 정보를 숨긴다 하더라도 최정예 크래커 집단, 예를 들어 국정원 같은 수준이라면 그 정도는 큰 어려움 없이 파훼 가능합니다.꿈도 희망도 없는 상황처럼 보입니다만 다행히도 안드로이드는 이런 문제를 해결해 주는 KeyStore API 를 제공하고 있습니다.KeyStore 를 도입하자Android KeyStore 시스템 문서의 첫 머리에 적혀있는 글은 다음과 같습니다.The Android Keystore system lets you store cryptographic keys in a container to make it more difficult to extract from the device. Once keys are in the keystore, they can be used for cryptographic operations with the key material remaining non-exportable. Moreover, it offers facilities to restrict when and how keys can be used, such as requiring user authentication for key use or restricting keys to be used only in certain cryptographic modes.Android Keystore 시스템은 암호화 키를 ‘컨테이너’ 에 저장하도록 해 기기에서 키를 추출하기 더욱 어렵게 해 줍니다. 일단 키를 Keystore 에 저장하면 키를 추출 불가능한 상태로 암호화에 사용할 수 있습니다. 또한 Keystore 는 키 사용 시기와 방법(예: 사용자 인증 등의 상황)을 통제하고, 특정 암호화에서만 키를 사용하도록 허용하는 기능도 제공합니다.좀더 쉽게 다시 설명하자면, 암호화에 쓸 키를 소스코드 내부 어딘가가 아니라, 시스템만이 접근 가능한 어딘가(컨테이너)에 저장해 문제를 해결해 준다는 뜻입니다. 여기서 키가 저장되는 ‘컨테이너’ 는 기기별로 구현이 다를 수 있습니다만 핵심은 사용자 어플리케이션이 그 영역에 접근할 수 없다는 점입니다. 이 때문에 KeyStore 를 사용해서 키를 안전하게 저장할 수 있습니다.또한 앱에서 등록한 KeyStore 는 앱 삭제 시 함께 제거되므로, 똑같은 package name 으로 앱을 덮어씌우는 등의 공격으로 키를 유출할 수도 없습니다. 이는 여러 앱에서 공유하는 KeyChain 과는 다른 특성이며 기능 활성화를 위한 별도의 입력이 필요 없다는 장점이 있습니다.[그림 1] KeyChain API 사용시 나타나는 시스템 다이얼로그. 어려운 용어가 난무하는 등 사용자 경험이 그다지 좋다고 말할 수 없다.반면 Android M 이상에서는, KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean) API 로 시스템 다이얼로그의 표시 유무를 제어할 수 있습니다.Secure SharedPreferences 구현하기앞서 설명드렸던 KeyStore 를 사용해 SharedPreferences 의 내용을 암호화하는 로직입니다. 소스 코드의 길이가 꽤 길기에, github gist 링크로 대신합니다. 독자 여러분들을 위해 최대한 쉽고 간단한 형태로 구현했으므로 필요에 맞춰 커스터마이징 하는게 좋습니다.AndroidCipherHelper.kt - KeyStore 에서 생성한 랜덤 패스워드를 이용해 입력받은 문자열을 암호화 하는 로직. IV 설정 등 귀찮은 작업을 피하기 위해 비대칭 암호화 알고리즘을 사용했다. 또한 암호화 및 복호화 과정에서 비대칭키의 Public key 로 암호화하고, Private key 로 해독하도록 구현했다. TEE 를 올바르게 구현한 기기(안드로이드 23 이상 + 메이저 하드웨어 제조사)에서 동작하는 한, 이 데이터의 내용이 유출되더라도 복호화는 오직 이 로직 내부에서만 할 수 있다.SecureSharedPreferences.kt - AndroidCipherHelper 가 문자열 위주로 암호화하므로, 모든 입력값을 문자 형태로 변환 후 입출력한다.결과 확인Secure SharedPreferences 를 실제로 구현한 뒤, 앱의 shared preferences 를 열어보면 아래와 같은 결과가 나타납니다.$ adb shell "run-as com.securecompany.secureapp cat /data/data/com.securecompany.secureapp/shared_prefs/MySecureSettings.xml" <?xml version='1.0' encoding='utf-8' standalone='yes' ?>     oh+XL/vQqAdxNzFEkKVOfcZAkP7jh92tcKpxzM6bbv9iGUk2lR7ayJsR6FZXt3rAKC+4sLVTP1cy e+NpgZ67wjoeBM4maMjXjSkovc8cO8rVVsQLqedJtW3gGOItTTCkjIQGh+TsBDjz8C3IdmNSKqGE GmBwQBoV0QuO+uO6cdPI/Gx816P0kcLmr5xsAy9XUwJeTE9947sYydiztJsgkKxuiGFLJK435pAb UhatjSFse4MpBCugHcLUVg5UXGwQcfbJuuQ/CBcmQmYb3MldNzLfOWtsQiwQJpz0J12fsYlQOBnO UnLVcND+DU17cP+Q4Cjah8VwmiY1a0shMn09Rw==         ozh8dKH+yCRSWoiW0HQtF/bWD7Aw6rfjzklT302AlTOpYmVdEiIfVoTK97bsyK1mXbwN5Qpas82Q dYgnnZl9sfY8pzyXHM0dtm88euB5vgmzljb04LClF3oRZ7Qi5ZRyK90kQ/HN/6EgYvf6zEwR7Ydg 08kJ/bde4Z5lSz+kJ79dHEpE+QAV48U0F0/yp12+xKFRNbaBLBaaWclUNF10jONPKjC3HS/aQozT 1ngQWSKzPq87B0OFExraSPDoLT8zx8ElhTgEtpBRcUwtzmSnhGvgtIUhziFpZBbdvuqAGZ+L5El1 T7H9ipEosN3Aivh/5rz9dntJe3mJvfCFdFITlA==     (Android L 이상이라고 가정할 경우)앱의 개발자조차 키를 알 수 없기 때문에, 파일을 유출하더라도 이를 깨는것은 현재로선 매우 어렵습니다. 즉, 우리 앱은 사용자 데이터를 안전하게 보호하고 있다고 자신 있게 말할 수 있습니다.AndroidKeyStore 파헤쳐보기그렇다면 어떤 방식으로 AndroidKeyStore 가 동작하고, 왜 안전한지 좀더 상세히 살펴보겠습니다.“AndroidKeyStore” 문자열의 중요성Android Keystore system 문서에 따르면 Android Keystore Service 에 접근하기 위해서는 아래와 같이 코드를 작성해야 한다고 합니다. val keyStore = java.security.KeyStore.getInstance("AndroidKeyStore") [리스트 7] Android Keystore 인스턴스 획득 방법여기서 주의할 점은 AndroidKeyStore 라는 문자열입니다. 반드시 정확한 문자열로 입력해야 합니다. 왜냐면 이는 Google 이 안드로이드의 보안 시스템을 Java Cryptography Architecture(JCA) 표준에 맞춰 구현했기 때문에 그렇습니다. 그리고 JCA 표준을 구현하면 JVM 인스턴스(안드로이드도 변형 JVM 의 일종입니다) 내에서 동작하는 모든 로직이 Security 클래스에 등록된 암호화 구현체를 사용할 수 있게 됩니다. 즉, Google 이 컨트롤 할 수 없는 서드파티 로직(우리의 앱 혹은 각종 안드로이드 오픈 소스들)에서도 Android Keystore 를 표준 Java 방식으로 사용할 수 있도록 구현했기에 이런 방식으로 호출해야 하는 겁니다.물론 안드로이드에서는 AIDL 파일을 제공받는 방식 혹은 Context#getSystemService(String) 메소드로 서비스 인스턴스를 획득할 수도 있습니다. 하지만 첫 번째 방식은 바인드된 서비스가 언제든 Kill 될 수 있다는 문제가 있습니다. 그리고 두 방식의 공통적인 문제점은 보안을 사용하는 모든 로직에 if (currentEnvironment == "Android") then... 같은 예외 처리 로직을 넣어줘야 한다는 점입니다. 전 세계 모든 오픈소스 개발자들이 안드로이드로의 포팅을 위해 그런 귀찮은 작업을 해 줘야 하는 일인데.. 그게 가능할까요?“AndroidKeyStore” JCA Provider 등록 과정앞서 AndroidKeyStore 라는 문자열의 중요성을 알아봤습니다. 그렇다면 왜 중요한지도 알아두면 좋겠죠?안드로이드는 linux 기반의 운영체제입니다. 시스템 부팅 직후 실행되는 init.rc 스크립트에서는 /system/bin/app_process 명령을 실행하는데 이 명령은 Android Runtime 위에서 실행되는 Zygote process를 초기화 합니다.Zygote 는 간단하게 설명하자면 안드로이드 앱 실행속도를 향상시키기 위한 일종의 공용 런타임 같은 것입니다. 그리고 앱이 실행되면 Zygote 에 설정된 내용이 사전에 로드되는데, 아까 언급한 초기화 과정 중에 아래와 같은 내용이 있습니다.package com.android.internal.os; /**  * Startup class for the zygote process.  *  * Pre-initializes some classes, and then waits for commands on a UNIX domain  * socket. Based on these commands, forks off child processes that inherit  * the initial state of the VM.  *  * Please see {@link ZygoteConnection.Arguments} for documentation on the  * client protocol.  *  * @hide  */ public class ZygoteInit {    private static final String TAG = "Zygote"; /**     * Register AndroidKeyStoreProvider and warm up the providers that are already registered.      *     * By doing it here we avoid that each app does it when requesting a service from the      * provider for the first time.      */     private static void warmUpJcaProviders() {        // ...        // AndroidKeyStoreProvider.install() manipulates the list of JCA providers to insert        // preferred providers. Note this is not done via security.properties as the JCA providers        // are not on the classpath in the case of, for example, raw dalvikvm runtimes.        AndroidKeyStoreProvider.install();        Log.i(TAG, "Installed AndroidKeyStoreProvider in "                 + (SystemClock.uptimeMillis() - startTime) + "ms.");        // ...    } // ... } [리스트 8] ZygoteInit.java 의 JCA provider 설치 및 속도향상 과정package android.security.keystore; /**  * A provider focused on providing JCA interfaces for the Android KeyStore. *  * @hide  */ public class AndroidKeyStoreProvider extends Provider {    public static final String PROVIDER_NAME = "AndroidKeyStore";    public AndroidKeyStoreProvider() {        super(PROVIDER_NAME, 1.0, "Android KeyStore security provider");        // ...    } /**     * Installs a new instance of this provider.     */    public static void install() {        // ....        Security.addProvider(new AndroidKeyStoreProvider());        // ...    } } [리스트 9] AndroidKeyStoreProvider.java - “AndroidKeyStore” 라는 이름의 JCA provider 등록 과정이런 일련의 과정을 거쳐 시스템에서 등록한 AndroidKeyStore 라는 이름으로 Android KeyStore 서비스에 접근할 수 있게 됩니다. 그리고 안드로이드에서 사용 가능한 KeyStore provider 들의 종류를 뽑아보면, 아래와 같은 결과가 나타납니다.// List all security providers for (Provider p : java.security.Security.getProviders()) {    System.out.println(String.format("== %s ==", p.getName()));    for (Provider.Service s : p.getServices()) {        System.out.println(String.format("- %s", s.getAlgorithm()));    } } output: == AndroidKeyStoreBCWorkaround == == AndroidOpenSSL == ... == AndroidKeyStore ==    - AndroidKeyStore     - HmacSHA256    - AES    ... [리스트 10] 안드로이드 M(6.0.1)에서 지원하는 KeyStore provider 목록(중요) AndroidKeyStore 의 Hardware 레벨 지원 여부 확인다시 Android KeyStore 시스템의 설명으로 돌아가 봅시다.Key material of Android Keystore keys is protected from extraction using two security measures:…Key material may be bound to the secure hardware (e.g., Trusted Execution Environment (TEE), Secure Element (SE)) of the Android device. When this feature is enabled for a key, its key material is never exposed outside of secure hardware.Android KeyStore 는 키의 추출을 방지하기 위해 두 가지 보안 조치를 사용합니다:…키는 안드로이드 기기의 보안 하드웨어(e.g., Trusted Execution Environment (TEE), Secure Element (SE)) 에서만 동작할 수 있습니다. 이 기능이 활성화되면 키는 절대로 보안 하드웨어 밖으로 노출되지 않습니다.그런가보다 싶지만 유심히 읽어봐야 할 대목이 있습니다. 바로 Key material may be bound to … 부분입니다. is 가 아니라 may be 랍니다. 즉, 키가 하드웨어에 저장되지 않을 수도 있다는 사실입니다. 물론 문서에는 언급되어 있지 않지만 안드로이드 시스템 특징상 제조원가 절감을 위해 디바이스 제조사들이 KeyStore 를 소프트웨어로 구현할 수도 있다는 뜻입니다. AOSP 의 Keymaster 구현을 살펴보면 sw_enforced 라는 키워드가 있습니다. 이 keymaster API 를 하드웨어 제조사에서 Keymaster HAL 을 통해 호출하는데 만약 sw_enforced 인스턴스를 넘기는 형태로 구현할 경우 그 하드웨어는 KeyStore 를 지원하지만 (API Level 18), 그것이 반드시 별도의 보안 하드웨어 위에서 동작한다고 말할 수는 없습니다.그리고 “Inside Android Security” 의 저자 Nicolay Elenkov 에 의하면 Android M 이전의 Software-backed KeyStore 는 root 된 기기에서 유출 가능하다고 합니다. 링크의 내용이 다소 길기 때문에 요약하자면 software 기반의 KeyStore 구현은 키를 /data/misc/keystore/user_X(여기서 X 는 uid - 시스템이 앱마다 부여하는 id)에 저장하는데 이 파일의 내용은 keystore-decryptor 로 풀어볼 수 있다고 합니다. 그리고 하드웨어 보안을 지원하지 않는 기기를 확보하지 못해 실 기기에서는 확인할 수 없었습니다만, 에뮬레이터에서 실제로 확인해 본 결과 사실이었습니다.즉, (Android KeyStore)를 쓰더라도 Android M 이전의 기기에서는 우리 앱의 데이터가 100% 안전하다는 장담을 할 수는 없습니다. 아직까지 이 문제를 해결할 방법은 찾지 못했습니다만 아래와 같은 로직으로 ‘이 기기에서의 앱 실행은 안전하지 않을 수 있다’ 같은 안내를 띄우는 정도의 가이드는 개발 가능합니다.val privKey = (keyEntry as KeyStore.PrivateKeyEntry).privateKey val factory = KeyFactory.getInstance(privKey.getAlgorithm(), "AndroidKeyStore") val keyInfo: KeyInfo try {    keyInfo = factory.getKeySpec(privKey, KeyInfo::class.java)    println("HARDWARE-BACKED KEY???? " + keyInfo.isInsideSecureHardware) } catch (e: InvalidKeySpecException) {    // Not an Android KeyStore key. e.printStackTrace() } [리스트 11] KeyInfo API 로 키가 하드웨어로 안전하게 보호되고 있는지를 확인하는 방법다행히도 저희가 보유 중인 개발 시료에서 모두 확인해본 결과 모두 true 로 확인되는 것으로 보아 전 세계의 대중적인 API Level 18 이상인 Android 기기에서는 KeyStore 를 안심하고 사용할 수 있다는 결론을 얻었습니다.다만 API Level L 이전의 Android KeyStore 에는 사용자가 Lock screen 을 설정하지 않을 경우 초기화 된다거나, 직접 확인하진 못했지만 앱을 삭제하더라도 KeyStore 가 완전히 초기화되지 않는 등의 문제도 있다고 하니 유의하는 것이 좋겠습니다.맺으며이상으로 KeyStore 를 사용해 데이터를 암호화하는 방법에 대해 알아봤습니다. 저희 하이퍼커넥트에서도 현재 제작 중인 안드로이드 앱 일부에서 이 기능을 탑재해 고객 여러분들의 데이터를 안전하게 보호하려 노력하고 있습니다. 또한 iOS 도 Secure enclave라 하여 비슷한 기능을 제공하고 있으며 역시 저희 개발진은 이 기술의 적극 도입을 위한 노력을 진행 중입니다.물론 여기 적혀있는 내용들은 Android M(API Level 23) 이후에서만 100% 안전하기 때문에 저희는 그 이전의 안드로이드 버전에서도 데이터를 안전하게 저장할 방법에 대해 지금도 계속 고민 중입니다.또한 눈치 빠른 독자분들은 이 기법을 잘 응용하면 외부 저장소에 저장하는 파일도 암호화 할 수 있다는 사실을 깨달으셨을 겁니다. 이 기법은 요즘 데이터 불법 유출로 몸살을 앓고 있는 웹툰 앱들에도 유용합니다. 임시로 다운로드 한 이미지 파일을 KeyStore 가 생성해주는 키로 암호화해 버리고, WindowManager.LayoutParams#FLAG_SECURE 를 사용해 화면 캡쳐까지도 막아버린다면 대부분의 어설픈 유출 시도는 손쉽게 막으실 수 있으리라 생각합니다.꽤 길었던 1부가 끝났습니다. 2부에서는, 2017년 5월에 소개된 Room을 사용한 안드로이드 데이터베이스를 암호화하는 법에 대해 소개하겠습니다.더 보기Android KeyStore 시스템블록 암호 운용 방식초기화 벡터문자 인코딩AES 암호화RSA 암호화Padding(Cryptography)AOSP KeyStore implementation requirementsHow the Android keystore system can be secureJCA reference guideUnderstanding Android zygote and DalvikVMAndroid InternalsKeystore redesign in Android M - by Nicolay ElenkovAnalysis of Secure Key Storage Solutions on Android#하이퍼커넥트 #개발 #개발자 #안드로이드 #모바일 #앱개발 #PersistentData #개발후기 #인사이트
조회수 2585

Next.js 튜토리얼 5편: 라우트 마스킹

* 이 글은 Next.js의 공식 튜토리얼을 번역한 글입니다.** 오역 및 오탈자가 있을 수 있습니다. 발견하시면 제보해주세요!목차1편: 시작하기 2편: 페이지 이동 3편: 공유 컴포넌트4편: 동적 페이지5편: 라우트 마스킹 - 현재 글6편: 서버 사이드7편: 데이터 가져오기8편: 컴포넌트 스타일링9편: 배포하기개요이전 편에서는 쿼리 문자열을 이용하여 동적 페이지를 생성하는 법을 배웠습니다. 생성한 블로그 게시물 중 하나에 대한 링크는 다음과 같습니다:http://localhost:3000/post?title=Hello Next.js하지만 이 URL은 구립니다.다음과 같은 URL를 가지면 어떨까요? http://localhost:3000/p/hello-nextjs더 낫지 않나요?이번 편에서 이것을 구현할 예정입니다.설치이번 장에서는 간단한 Next.js 애플리케이션이 필요합니다. 다음의 샘플 애플리케이션을 다운받아주세요:아래의 명령어로 실행시킬 수 있습니다:이제 http://localhost:3000로 이동하여 애플리케이션에 접근할 수 있습니다.라우트 마스킹라우트 마스킹이라 불리는 Next.js의 특별한 기능을 사용할 예정입니다.기본적으로 애플리케이션에서 표시되는 실제 URL와 다른 URL이 브라우저에 표시됩니다.블로그 포스트 URL에 라우트 마스크를 추가해봅시다.pages/index.js에 다음과 같은 코드를 작성해주세요:다음의 코드 블럭을 살펴봅시다:<Link> 엘리먼트에서 "as"라는 또다른 prop를 사용하였습니다. 이는 브라우저에서 보여질 URL입니다. 애플리케이션에 표시되는 URL은 "href" prop에 지정되어 있습니다.첫 번째 블로그 포스트를 클릭하면 블로그 포스트로 이동할 것입니다.그 다음에 뒤로가기 버튼을 클릭하고 앞으로가기 버튼을 클릭해보세요. 무슨 일이 일어날까요?- 에러가 발생할 것이다- 인덱스 페이지로 돌아가고 포스트 페이지로 다시 이동할 것이다- 인덱스 페이지로 이동하지만 그 후에는 아무런 일도 일어나지 않을 것이다- 인덱스 페이지로 돌아가고 에러가 발생할 것이다히스토리 인식본 것처럼 라우트 마스킹은 브라우저 히스토리를 활용하여 잘 작동합니다. 해야 할 일은 링크에 "as" prop를 추가하는 것뿐입니다.새로고침하기home 페이지로 돌아가세요: http://localhost:3000/첫 번째 포스트 제목을 클릭하면 post 페이지로 이동합니다.브라우저를 새로고침하면 무슨 일이 일어날까요?- 예상대로 페이지가 첫 번째 포스트를 랜더링 할것이다- 페이지가 로드되지 않고 계속 로딩 중일 것이다- 500 에러가 발생할 것이다- 404 에러가 발생할 것이다 404서버에 불러올 페이지가 없기 때문에 404가 에러가 발생합니다. 서버는 p/hello-nextjs 페이지를 불러오려고 시도하지만 우리는 index.js와 post.js 두 개의 페이지밖에 없습니다.이 방법으로는 프로덕션으로 이 애플리케이션을 실행할 수 없습니다. 이 문제를 고쳐야 합니다.Next.js의 커스텀 서버 API는 이 문제를 해결할 수 있는 방법입니다.다음 편에서 이것을 사용하는 방법을 배울 예정입니다.#트레바리 #개발자 #안드로이드 #앱개발 #Next.js #백엔드 #인사이트 #경험공유
조회수 2014

외부 서비스 이용을 장려해서 개발력을 아끼자.

2017년 목표 중 하나인 Product Management에 관한 weekly 포스팅의 네번째 포스팅입니다. 원래는 weekly 포스팅이었는데..어느덧 biweekly 포스팅이 되고 있습니다. 이번에는 제가 Product Manager로서 “팀 내부 직접 개발 vs 외부 서비스 이용”에 대해서 어떻게 생각하는지에 대해서 정리할까 합니다. 이번에도 confidential한 내용은 생략했습니다.이거 한 달이면 만들어요.제품 개발을 하다보면 Core feature는 아니지만 더 나은 사용자 경험을 위해 필요한 기능을 추가해야 하는 경우가 있습니다. 그리고 이 feature가 개발하기에 쉽지 않다고 예상되는 경우가 있습니다. 이런 상황이 오면 PM, 제품 담당자(혹은 기획자, 대표)은 내부에서 개발할지 아니면 외주를 줄 지, 아니면 외부 서비스를 이용할 지 등을 고민합니다. 그리고 판단을 돕기 위해 기획자/개발자가 모여서 이런 대화를 나눕니다.이거 다 만드는데 얼마나 걸릴 것 같아요?이거 한 달이면 만들어요.그렇습니다. 저 대화가 바로 나중에 개발자가 “내가 이걸 왜 하고 있죠?”라고 얘기하는 그 순간의 시초입니다.하지만 기간은 두 배가 걸린다.하지만 직접 개발에 들어가면 기간(UX, UI디자인 포함해서)은 점점 늘어집니다. 십중팔구 안 됩니다. 되는게 더 이상한 법이에요.헛된 꿈을 꾸었다기간이 두 배가 되는 이유는 딱 하나입니다.  우리에겐 그 분야의 전문성이 없기 때문입니다. 물론 그런 일을 한 경험이 있는 사람들은 좀 더 낫습니다. 하지만 이 사람이 파편적인 경험(혹은 기억)만 가진 경우에는 똑같습니다. 별 차이가 안 나요.-_-;일단 제품의 개발 범위 결정이 안 됩니다. 이게 가장 크리티컬한 이유입니다. 처음에는 앞단에 보이는 것만 생각하고 시작하면서 역기획으로 풀어냅니다. 하지만 기획 단계에서 고려해야 할 요소들은 점점 추가되고 이 중에서 뭘 버리고, 뭘 해야 하는지 정확한 판단이 안 됩니다. 그럴 수 있는 데이터도 적고요.  거기에 디테일하게 개발하는 과정에서 고려해야 할 요소들이 빠지는 경우도 비일비재 합니다. 추가로 각종 정책 결정 이슈도 존재합니다. 이런저런 일들이 계속 추가되고, 해보지 않은 일을 하면서 업무 효율도 떨어집니다. 그러면서 기간은 계속 늘어납니다.결국 사람은 지치고, 일은 계속 늘고, 시간을 쓰게 됩니다. 그리고 그 과정에서 진짜로 에너지를 써야 할 일에 집중을 못 하게 됩니다.그냥 외부 서비스 쓰자!푸른밤의 PM으로서 저 스스로 가지고 있는 원칙이 있습니다.(사실 이건 예전에 프라이베리 때도 지키려고 했던 노력입니다.)기회를 놓치지 않는다.팀의 시간을 헛되이 쓰지 않는다.사람들의 에너지가 낭비되게 하지 않는다.좋은 역량을 가진 사람들은 제품의 core feature에만 집중한다.기회, 시간, 사람, 돈 중에서 가장 가치 없는 것은 돈이다.위 5가지 원칙을 준수하고자 하면, 대부분의 경우 그냥 외부 서비스를 이용하게 됩니다. 예를 들어서 서버 쪽에서 약간 낭비되는 코드가 있더라도 어떤 순간에는 그냥 돈을 더 써서 서버를 늘리는 것을 선택합니다. 메일 서버를 직접 구축해서 각종 마케팅용 메일을 직접 하는 것도 좋지만 그냥 메일침프를 씁니다. 요근래 저와 대표가 함께 부산에 미팅을 다녀왔는데..이것도 비슷한 맥락입니다. 제품 내에 꽤 중요하지만 서비스의 Major급 feature라고 하긴 좀 애매한 기능을 붙여야 하는 상황이었습니다. 개발팀에서는 1개월 정도면 될 것 같다고 했지만 그것보다는 전문적으로 이 일만 하는 곳의 제품을 이용하는 것이 좋다고 판단해서 부산에서 관련 사업을 하는 팀을 찾아갔습니다.“어설프게 우리가 하는 것보다, 인생을 건 사람들의 제품을 쓰는 것이 훨씬 좋다.”는 생각을 가지고 있습니다. 특히 제가 관리하는 제품들도 이런 생각을 가진 사람들이 돈을 쓰기 때문에 운영될 수 있는 제품이라서 다른 사람들보다 거부감이 낮을 수도 있습니다.외부 서비스 선택의 기준추가로 외부 서비스를 선택할 때는 이런 기준을 가지고 판단합니다.우리가 원하는 것이 어느 수준 정도로 충족되는가: 이게 제일 중요합니다. 원하는 것이 안 채워지는데도 돈을 쓸 필요는 없습니다.ㅠ어느 정도 커스텀이 가능하고, API가 제공 범위는 어떻게 되는가: 기존 시스템과 붙이기 얼마나 편하고, 우리 개발팀이 에너지를 어느 정도로 써야 하는지를 판단하기 위해 필요합니다. 덕분에 요즘은 API 문서 읽는 것이 일입니다.-_-;;(마케터, 운영팀 등이 쓰는 경우)개발자/디자이너가 꼭 붙지 않아도 사용할 수 있는가: 전 푸른밤의 모든 사람들이 코딩을 기초적인 수준으로는 했으면 합니다만 (진짜 잘하면 SQL까지도.) 그렇지 못 한 경우가 더 많고 그 과정에 역시 에너지/기회/시간 낭비가 좀 있다고도 생각합니다. 그래서 위 조건도 꽤 중요하게 봅니다.우리가 지금 쓰고 있는 다른 외부 서비스들과 연동이 어느 정도 되는가? 직접 연동이 안 되더라도 다른 방식으로 연동할 수 있는가: 가장 중요합니다. 세상 제일 중요합니다. 저희 같이 외부 서비스 연동을 하나씩 하나씩 하다보면 어느 순간부터 매월 SaaS 툴에만 $1000 넘게 쓰게 됩니다.(정말이에요.) 일단 가장 중요한 데이터 분석 툴과 연동되는지를 봅니다. 그리고 각 부분에서 core한 툴과 연결되는지 봅니다. 예를 들어서 마케팅 오토메이션 단계에서는 유입 관련 데이터 분석 툴과 연결되는 것이 핵심입니다. 제품 관련해서 외부 서비스 쓸 때도 메인 분석툴인 GA와 어떻게 붙는지가 핵심입니다.유기적인 연결이런 복잡한 기준을 잡으면서 외부 서비스 선택을 합니다.우리가 새로 만들자.하지만 이런 힘든 과정 거쳐서 외부 서비스 선택해서 잘 사용하다가 다시 직접 개발하게 될 때도 있습니다. 커스텀의 한계가 오거나, 외부 서비스 회사가 망하거나(ㅠㅠ), 서비스의 오픈 API 범위나 정책이 바뀌거나, 의외로 이 feature의 중요도가 크거나 하면 이런 의사결정을 할 수 있지 않을까 싶습니다. 하지만 아직 제가 이런 경험을 한 적은 없어서..향후에 이런 일이 발생하면 꼭 공유하겠습니다.정리하며스타트업에서 가장 부족한 것이 뭐냐는 질문을 하면 대체로 돈과 사람이라고 답할 것 같은데요. 여기에 기회, 시간이라는 것도 변수로 추가하길 권합니다. 그러면 어떤 경우에도 내 사업의 core가 되는 일들, 내 사업의 core랑 직결되는 제품 관련 과업들, 디자인/개발 관련 과업들만 생각하게 되고 여기에만 집중하게 됩니다.물론 돈이 부족한 것도 알고 있습니다만..정말 인생을 걸고 하는 사업에서 가장 아쉬운 것은 기회와 시간이라고 생각해서 외부 서비스 주구장창 이용하는 PM 안창영이었습니다.푸른밤 안창영#푸른밤 #알밤 #개발 #운영 #개발자 #PM #업무프로세스 #인사이트 #일지 #경험공유
조회수 1160

단일 TABLE을 SELECT하자!

OverviewDB를 다뤄봤다면 SELECT문도 아실 겁니다. 가장 먼저 접하는 명령어 중에 하나이기도 하죠. 보통은 아래처럼 사용합니다. SELECT문SELECT     * FROM 테이블명  ; 명령을 주면 지정한 테이블에 저장된 모든 내용을 검색합니다. 이번 글에서는 테이블을 만들고 SELECT하는 과정을 다뤄보겠습니다. DB는 MySQL 5.6을 기준으로 하고, Tool은 MySQLWorkbench를 사용하겠습니다.Query, 너란 녀석테이블은 위와 같이 생성할 수 있습니다. 위의 내용은 MySQLWorkbench를 이용해 Model을 표시하면 아래와 같습니다. 구성원의 정보를 저장하도록 했고, 컬럼마다 의미를 갖게 됩니다. MBR_ID (구성원 아이디) : DB에서 구성원을 식별하는 아이디MBR_INDFY_NO (구성원 식별 번호) : 구성원을 실제 구별하는 번호로 과거에는 주민등록번호가 많이 사용되었고, 요즘은 e-mail 이 많이 사용됩니다.MBR_NM (구성원 명) : 구성원의 이름 테스트 데이터를 입력해 실행하면 어떤 결과가 나오는지 보겠습니다.가장 기본적인 SELECT문 실행계획을 보면 아래와 같이 나옵니다.실행 계획은 DB가 어떻게 Query를 수행할 건지 보여줍니다. Query가 복잡해지면 실행 계획을 보면서 Query가 올바르게 작성됐는지 확인하고 필요하다면 Query를 수정해야 합니다. DB를 시작할 때부터 실행 계획을 보는 습관을 기르는 게 중요한 이유입니다. 각 항목에 대한 설명id : SELECT 문에 있는 순차 식별자로 Query 를 구분하는 아이디select_type : SELECT의 유형SIMPLE : Subquery나 union 이 없는 단순한 SELECTtable : 참조되는 테이블의 명칭TB_MBR_BAS : 참조되는 테이블명type : 검색하는 방식ALL : TABLE의 모든 ROW를 스캔 위의 이미지는 임의로 만든 자료를 이용해 Query를 실행한 결과입니다. 실행 계획은 TABLE : TB_MBR_BAS 를 TYPE : ALL 전체 검색한다고 나옵니다. 실행한 내용도 같습니다. 여기서 MBR_NM 이 “나서영”인 자료를 검색해볼까요. WHERE 조건이 들어가자 실행 계획도 내용이 변경되었습니다. rows와 Extra에도 값이 있는데요. 두 항목을 잠시 짚고 넘어가겠습니다. rows : Query를 수행하기 위해 접근해야 하는 열의 수Extra : MySQL 이 Query 를 수행할때의 추가 정보Using where : Query 수행시 TABLE에서 값을 가져와 조건을 필터링 함 위의 결과처럼 전체를 검색해 필요한 자료만 추출하는 것을 FULL TABLE SCAN or FULL SCAN 이라고 합니다. 그러나 FULL SCAN은 성능이 좋지 않기 때문에 우선 꼭 필요한 Query인지 검토해야 합니다. 보통 MBR_NM에 INDEX를 추가해서 해결하는데요. INDEX를 추가해서 같은 Query를 수행하면 실행 계획은 어떻게 달라질까요. 분명 같은 Query였는데 INDEX에 따라 실행 계획이 변경된 걸 알 수 있습니다. INDEX를 추가해도 수행한 결과는 같지만 검색 속도에 많은 차이가 있습니다. 각 항목에 대한 설명type - ref : 인덱스로 자료를 검색하는 것으로 현재는 매칭(=) 자료 검색을 나타냄possible_keys : 현재 조건에 사용가능한 INDEX를 나타냄(인덱스가 N개일 수 있음) IX_MBR_BAS_02 : 현재 조건에 사용 가능한 INDEXkey : Query 수행시 사용될 INDEX (possible_keys 가 N 개일 경우 USE INDEX, FORCE INDEX, IGNORE INDEX 로 원하는 INDEX 로 바꾸어 수행할수 있음)key_len : 수행되는 INDEX 컬럼의 최대 BYTE 수를 나타냄152 : 수행되는 INDEX 컬럼의 BYTE 수가 152ref : INDEX 컬럼과 비교되는 상수 여부 or JOIN 시 선행 컬럼 constant : 상수 조건으로 INDEX 수행rows : 678 : 678 rows 접근하여 값을 찾음Extra : using index condition : INDEX 조건에 대하여 스토리지 엔진이 처리(MySQL의 구성에서 스토리지 엔진과 MySQL 엔진이 통신을 주고 받는데 스토리지 엔진에서 처리 하여 속도가 향상됨) ConclusionINDEX가 없으면 결과가 나오기까지 5초 정도 걸리지만, 반대로 INDEX가 있으면 1초 안에 결과가 나옵니다. 별거 아닌 것 같아 보이지만 실무에서는 엄청난 차이입니다. Query를 작성할 때 실행 계획을 확인하고 조금이라도 빨리 결과가 나올 수 있도록 하는 것이 중요하기 때문이죠. 다음 글에서는 단일 TABLE 을 SELECT하는 것을 주제로 이야기를 나눠보겠습니다. 무사히 SELECT하길 바라며.글한석종 부장 | R&D 데이터팀[email protected]브랜디, 오직 예쁜 옷만#브랜디 #개발문화 #개발팀 #업무환경 #인사이트 #경험공유
조회수 3876

코드리뷰, 이렇게 하고 있습니다.

토스랩 안드로이드팀이 코드리뷰 하는 방법실리콘밸리 이야기 - 코드리뷰는 어떻게 하나요? 를 보고 토스랩이 코드리뷰 하는 프로세스와 방법에 대해서 공유해드리고자 합니다.왜 코드리뷰를 하게 되었나요?토스랩에 안드로이드가 팀 단위로 꾸려진 것은 5월 전후였습니다. 그 전에는 1인 개발 체제를 가지고 있었습니다. 갑작스럽게 인원이 많아지면서 코드스타일, 구조의 일관성 등이 계속적으로 깨지게 되고 이에 따라 제품의 안정성도 급격히 떨어지는 사태가 발생하였습니다.이에 내부적으로 제품의 품질을 강화하기 위한 대책들이 강구되었는데 그 중에 하나가 코드리뷰였습니다.코드리뷰를 위한 프로세스는 토스랩 웹 개발팀의 프로세스를 참고하여 안드로이드 개발 팀원의 내부 의견을 반영하여 진행되었습니다.1. 언제 코드리뷰를 요청하나요?안드로이드팀은 코드리뷰 요청에 대해 별도의 제약을 두지 않았습니다. 언제든지 코드리뷰 시스템이 코드리뷰를 요청할 수 있습니다. 다만 코드 리뷰가 시작되는 시점이 조금 다릅니다.모든 개발자가 코드리뷰를 각자의 업무(Task)가 완료되면 코드리뷰 시스템에 코드리뷰를 요청하고 이를 각 개발자가 언제든지 확인할 수 있도록 하고 있습니다.코드리뷰의 시작은 3. 그럼 코드리뷰는 언제 하나요? 에서 확인해보록 하겠습니다.2. 어떻게 요청하나요코드리뷰는 내가 아닌 다른 사람이 코드를 읽어야 하므로 어떤 목적에서 작업 된 코드인지를 미리 할 수 있어야 빠르게 코드리뷰를 할 수 있습니다. 최대한 자유롭게 하되 아래와 같은 형식을 지키도록 하고 있습니다.TitleFeature/Bug-fix 건인지 알 수 있도록 합니다.어떤 목적인지 간략하게 적도록 합니다.어떤 이슈와 연결된 건인지 알 수 있도록 합니다.Description어떤 로직을 추가/수정했는지를 작성합니다.어떻게 추가/수정했는지를 작성합니다ex)Title - [fix] 소켓 API 버전 처리 (JND-3986) Description@Version 커스텀 어노테이션 추가Version 없는 Event 에 Version 필드 추가, @Version어노테이션 부여SocketObject -> EventObject 로 파싱하는 로직 공통 메소드로 분리파싱 후 바로 반환하지 않고 Version Valid 로직 추가class JandiSocketServiceModel { T getObject(Object, T) // 파싱 공통 메소드 boolean validVersion(Object) // version 확인 } Java Reflection 사용.위와 같이 작성함으로써 이 이슈는 소켓 API 버전에 대한 버그 수정건으로 JND-3986 이라는 이슈와 연관된 것임을 알 수 있습니다. 상세 내용으로는 @Version 이 JandiSocketServiceModel의 getOject 와 validVersion 메소드와 연관되어 있음을 알 수 있도록 기술하였습니다.코드리뷰를 상세하게 쓰는 것은 리뷰어들이 코드리뷰를 효율적으로 하기 위함이기 때문에 리뷰할 부분을 빨리 확인 할 수 있게 적도록 하도 있습니다.3. 그럼 코드리뷰는 언제 하나요?실리콘 밸리의 큰 회사들 (구글, 페이스북 등)은 코드리뷰가 요청이 오면 업무의 최우선순위로 조정되어 즉시 응답하도록 하는 것이 원칙입니다. (지금 당장 하든지 아니면 언제부터 할 것인지를 피드백을 반드시 줘야 한다고 들었습니다.)하지만 스타트업은 일반적으로 개발해야 할 것들이 훨씬 더 많고 코드리뷰가 아니더라도 일이 산더미인 경우가 많습니다. 저희 토스랩이라고 이를 크게 벗어나지 않기 때문에 안드로이드팀은 별도로 코드리뷰하는 프로세스를 정의하였습니다.월~수 : feature/bug-fix 개발이 업무의 최우선 순위이다.목, 금 : 코드리뷰가 업무의 최우선 순위이며 코드리뷰 대상은 목요일 출근 전까지 리뷰 요청을 한 건을 대상으로 한다.이는 개발자들끼리 코드리뷰의 중요성을 이해하지만, 이것이 개발 건보다 더 큰 업무 비중을 차지하게 되면 개발 속도나 의욕을 저해할 수 있기 때문에 최대한 분리하여 해당 건에만 집중하기 위해 룰을 정하였습니다.업무에 따라서 편차는 있지만, 대개의 코드리뷰는 금요일에 모두 완료를 하고 있으며 긍정적 피드백이 나올때까지 코드를 변경해야만 완료가 됩니다.4. 무엇을 리뷰하나요?개발자 개인의 성향과 개발건의 성격에 따라 그때마다 다른 모습을 보여줍니다.성능 개선 개발 : 시간복잡도신규 feature 개발 : 잠재적인 오류에 대한 검출리팩토링 : 테스트코드나 구조에 대한 물음신규 기술 도입 : 해당 기술의 로직과 그에 대한 물음기타 : 변수명과 같은 코드 컨벤션을 하기도 합니다. 전체적인 흐름을 이해하기 위해 실제 빌드를 해서 동작을 시켜보고 이해하기도 합니다.기본적인 사항들은 CI 품질도구 리포팅 기능을 이용하기 때문에 주로 큰 그림에서의 코드리뷰를 하는 편입니다.5. 코드리뷰 코멘트는 어떻게 작성하나요?OO 보다는 XX 가 더 나은 것 같아요.XX 는 OO 부분을 참고해서 이용하면 되요.OO 는 XX 에 의해서 문제되지 않을까요?XX 를 하려다가 OO 로 했는데 어떻게 생각하세요?위와 같이 가급적이면 상대방을 공격하지 않는 느낌을 주도록 하며 단순히 문제를 이슈업하기 보다는 대안을 제시하는 방법을 주로 하고 있습니다. 코드리뷰는 서로의 코드에서 이해할 수 없는 부분을 찾고 문제가 될 수 있는 부분을 미리 찾아내는 자리인만큼 문제의 검출과 해결에 주안을 두고 진행합니다.6. 코드리뷰가 끝나면 어떻게 하나요?서로가 이해할 수 있을 만큼 리뷰가 진행되면 코드는 그때서야 개발용 브랜치에 통합을 합니다. 최소 1명의 피드백도 진행되지 않은 코드는 통합하지 않는 것이 원칙으로 하며 통합되어야 하는 건이 코드리뷰가 진행되지 않으면 늦어도 월요일 아침에 긴급히 진행해 줄 것을 환기시킵니다.7. 긴급히 코드리뷰해야 하는 건은 어떻게 하나요?긴급히 해야하는 건은 그만큼 사안이 중요하다고 생각하기 때문에 리뷰를 요청하는 즉시 진행을 하도록 합니다. 다만 해당 건이 즉시 반영해야 할만큼 중요한지를 서로간의 의논해서 진행하도록 합니다.총평안드로이드팀이 코드리뷰를 최초 시작한 것은 6월초입니다. 브랜치를 통합하기 전 개발 완료된 건에 대한 코드리뷰가 처음이었기 때문에 자리를 잡는데는 2달여 시간이 흐른 다음이었습니다. 초기에는 실수로 코드리뷰를 생략한다던가, 어떻게 코멘트를 남겨야할지에 대해서 조심스럽다던가 하는 시행착오를 겪어서 지금은 개발 건에 따라 20건이 넘는 의견이 남겨질 정도로 활발하게 의견을 교류하고 통합을 거칩니다.코드리뷰에 생소한 사람은 대개 나의 작업물을 누군가에게 검토 받는다는 느낌에 거부감을 가지기 마련입니다. 하지만 더 큰 그림에서 본다면 코드리뷰는 코드의 안정성을 서로 다른 관점에서 검토하는 것이기 때문에 코드의 신뢰성이 더욱 커지는 과정입니다. 그러기에 이에 대한 이해 없이 진행하는 코드리뷰는 금방 유명무실해지기 때문에 모두의 이해를 가진 다음에 진행 할 것을 추천합니다.제품의 안정성을 신경써야 하는 시점에 QA 강화와 같은 외부의 요인만을 찾는 것보다 내부에서 좀 더 개선 할 수 있는 요인을 찾는 것도 하나의 방법입니다. 토스랩에서는 다양한 품질 검증 과정에서 코드리뷰를 매우 중시하고 있습니다. 모든 팀이 각자만의 스타일대로 코드리뷰를 진행하고 있습니다.모든 개발자분들이 코드리뷰에 열린 자세로 올바른 코드리뷰를 진행하기를 바랍니다.#토스랩 #잔디 #JANDI #개발 #개발팀 #개발자 #개발환경 #업무환경 #코드리뷰 #인사이트 #조언
조회수 444

컴공생의 AI 스쿨 필기 노트 ⑦합성곱 신경망

안녕하세요! 이번 주 수업에서는 합성곱 신경망에 대해서 배웠어요. 제가 읽은 한 기사에 의하면 대장 내시경 검사에도 딥러닝을 이용하면 종양 식별 능력을 더 높일 수 있다고 해요. 딥러닝을 이용한 검사는 전문가 분석을 통한 대장 내시경 검사보다 종양을 9개 더 많이 발견했고 진단 정확도는 96%인 것으로 나타났어요. (원문 링크) 이 대장 내시경에 우리가 배운 CNN(Convolutional Neural Network), 이미지 기반 딥러닝 모델을 사용했다고 하는데요. 이 대장 내시경에 사용된 CNN에 대해 알아볼까요? (Cover image : Photo by Paul Carmona on Unsplash)CNN(Convolutional Neural Network, 합성곱 신경망) CNN(합성곱 신경망)은 Convolution(합성곱)연산을 사용하는 인공신경망의 한 종류에요. 합성곱 신경망은 주로 이미지 데이터를 다루는 문제에서 사용돼요. 쉽게 말해 합성곱 신경망은 이미지의 특징을 추출하고 잘 조합하여 문제를 해결하는데요.  예를 들어 왼쪽 이미지가 고양이인지 컴퓨터가 알아맞히기 위해서 합성곱 신경망은 고양이가 가져야 할 특징을 한 번에 파악하는 것이 아니라 부분부분 판단하여 종합적으로 결론을 내요. 합성곱 신경망은 사진에 고양이의 특징이 기묘하게 분포되어 있어도 정확하게 고양이의 특징을 찾아내는 높은 적응도를 갖고 있어요.이제 합성곱 신경망의 구조에 대해 알아볼까요?CNN의 네트워크 구조1. 합성곱 층 (Convolutional Layer)합성곱은 두 함수 중 하나를 반전하고 이동시켜가며 다른 하나의 함수와 곱한 결과를 적분해나간다는 아주 어려운 뜻을 가지고 있어요. 다음 예시를 보도록 할게요.여기에 2차원 배열 픽셀을 넣으면 X 인지 O 인지 알아내는 합성곱 신경망이 있다고 해봐요.이 합성곱 신경망은 똑바로 된 X와 O를 넣으면 X 인지 O 인지 정확하게 구분하는데,이렇게 크기가 바뀌고 회전되어 모양이 변형된 이미지를 보고도 X 인지 O 인지 정확히 구분할 수 있을까요?합성곱 신경망은 합성곱 신경을 이용하여 이미지의 특징을 매칭 시킨 결과가 같으면 같은 이미지라고 인식해요.‘X’ 이미지의 특징을 추출하면 위와 같은 매트릭스, 합성곱 필터(Convolution filter(=커널))가 나와요. (세 특징을 잘 조합하면 X 형태가 나오죠?)이제 제일 왼쪽의 합성곱 필터를 가지고서 이미지가 X 인지 알아볼게요. 합성곱 필터와 원본 이미지를 비교할 때는 곱셈을 이용해요. 합성곱 필터의 크기만큼 원본 이미지와 차례차례 곱해서 값을 채워나가요.위의 합성곱 정의에서 두 함수를 하나는 이미지, 또 하나는 필터라고 생각하면, 필터를 이동시켜가며 이미지와 곱한 결과를 적분 즉, 덧셈해 나간다는 뜻이 돼요.합성곱 필터의 크기만큼 값을 다 계산한 후, 계산한 원소를 다 더해서 합성곱 필터의 크기만큼 나눈 평균값을 또 다른 새 매트릭스에 채우게 되는데 이를 특징 맵(Feature map)이라고 불러요. 즉, 특징 맵은 기존의 이미지에 필터를 곱한 결과로 각 픽셀에 쓰여있는 값이 클수록 그 특징을 가지고 있다는 뜻이에요.이렇게 원본 이미지와 합성곱 필터의 곱한 결과로 특징 맵이 나왔어요.나머지 두 개의 합성곱 필터와 곱한 결과로 두 개의 특징 맵을 가질 수 있어요.한 개의 합성곱 층(Convolutional layer)에는 여러 개의 합성곱 필터가 있어요. 합성곱 층에서 기존의 이미지와 필터들을 합성곱한 결과, 처음 이미지는 필터 된 이미지(특징 맵)로 쌓이게 돼요.2. 풀링 층(Pooling Layer)풀링은 가로/세로 방향의 공간을 줄이는 연산으로 합성곱 층의 특징을 압축한 특징 맵을 형성해요. 풀링에는 최대 풀링(Max pooling)과 평균 풀링(Average pooling)이 있는데 이미지 인식 분야에서 주로 사용하는 것은 최대 풀링이에요. 그래서 보통 풀링이라고 하면 최대 풀링이라는 의미로 사용한다고 보시면 돼요.위의 예시는 2x2 최대 풀링을 적용한 예시에요. 아까 구한 특징 맵에서 2x2 픽셀에서 가장 큰 원소 값을 새로운 맵을 채워나가는데 이를 활성화 맵(Activation map)이라고 불러요. 최대 풀링을 사용하면 노이즈가 감소하고 속도도 빨라지며 영상의 분별력이 좋아진다고 해요. 마지막 출력 층은 최대 풀링의 모든 뉴런과 연결되어 출력값이 어떤 클래스에 해당하는지 파악되는데 사용돼요.이렇게 CNN을 이용하면 변형된 이미지라고 하더라도 원래 이미지의 특징을 가지고 있다면 정확히 구분할 수 있어요.코드로 연습해보기아래는 간단한 인공신경망 코드예요.Layer 1 - input:1x28x28 , output : 64x28x28 + Activation function - reluLayer 2 - input: 64x28x28 output:1x28x28Layer 3 - input: 1x28x28=784 output:10class MNIST_Net(nn.Module):    def __init__(self):        super(MNIST_Net, self).__init__() # nn.Module 생성자 호출         # an affine operation: y = Wx + b        layers = []        layers.append(nn.Conv2d(1,64,3,1,1))         layers.append(nn.ReLU())         layers.append(nn.Conv2d(64,1,3,1,1))         layers.append(nn.ReLU())         self.main = nn.Sequential(*layers)        self.fc = nn.Linear(28*28, 10)    def forward(self, x):        # x.view함수는 주어진 인자의 크기로 해당 데이터의 크기를 반환합니다. 즉, (Batch_size,1,28,28) --> (Batch_size,28*28)로 변환합니다.        x = self.main(x)        x = x.squeeze().view(-1, 28*28)        x = self.fc(x)  # 10 으로 10개의 Class에 대한 logit 값을 호출합니다.         return x합성곱 인공 신경망의 내용은 정말 배울 것이 많아서 수업 시간 내에 다 배우기가 조금 벅찼지만 다른 인공 신경망에 비해 재밌어서 집중할 수 있었어요. 이제 앞으로 1번의 이론수업만을 남겨두고 있어서 아쉽기도 하고 또 뿌듯하기도 해요. 앞으로 조원들과 함께 프로젝트를 진행하면서 지금까지 배운 이론을 적용해보게 되는데요. 프로젝트 주제를 정하는 것부터 벌써 쉽지가 않아요. 하지만 열심히 프로젝트를 해서 리쿠르팅 데이 때 실력을 뽐낼 수 있다면 좋겠네요!* 이 글은 AI스쿨 - 인공지능 R&D 실무자 양성과정 7회차 수업에 대해 수강생 최유진님이 작성하신 수업 후기입니다.
조회수 960

VCNC 개발팀 워크숍을 소개합니다. - VCNC Engineering Blog

VCNC 에서는 최근에 모빌리티 서비스 이동의 기본 타다를 출시했습니다. 신규 서비스를 준비하면서 팀도 새롭게 구성되고 새로운 멤버들이 팀에 합류했습니다. 이러한 변화 속에서도 좋은 개발 문화를 유지하기 위해서 VCNC 개발팀은 큰 노력을 하고 있습니다. 그중에서도 모두가 자랑하고 싶어 하는 VCNC 개발팀 워크숍을 소개합니다.VCNC 개발팀 워크숍최근 VCNC 개발팀 워크숍은 2018년 12월 19일 수요일에 진행되었습니다. 2016년 12월 처음 시작해서 최근까지 총 6번의 워크숍이 열렸습니다. VCNC 가 SOCAR에 인수되어 타다 서비스를 바쁘게 준비했던 2018년 8월을 제외하고 1년에 3번씩(4, 8, 12월) 꾸준히 개최되고 있습니다.VCNC 개발팀 워크숍은 개발팀 멤버들이 업무 외적으로 가지고 있던 각자의 관심사들을 공유하고 개발자들이 할 수 있는 고민을 같이 나눠보기 위한 욕구에 의해 처음 제안되었습니다. 포맷을 어떻게 할지 논의한 끝에 아래와 같은 포맷으로 워크숍을 진행하기로 했고 최근까지 이 포맷으로 워크숍을 진행하고 있습니다.오전 시간에는 모든 멤버가 각자의 관심사에 대해 5~10분 정도로 가벼운 라이트닝 톡을 하자.오후 시간에는 토의 주제를 정해서 몇 가지 깊은 토의를 나눠보자.회사의 업무에서 완전히 벗어나서 집중하기 위해 프로젝터 사용이 가능한 외부 카페를 대관하자.고기 회식을 하자!2018년 12월 제 6회 VCNC 개발팀 워크숍 단체 사진라이트닝 톡라이트닝 톡은 위에 언급했던 대로 모든 멤버가 5~10분 정도의 시간 동안 각자의 관심사에 대해서 다른 멤버들에게 소개하는 시간입니다. 발표 주제는 처음에는 개발로 한정 지었다가 더 폭넓게 관심사를 공유하기 위해 자유 주제로 변경했습니다. 다들 워크숍 전날까지는 어떤 발표를 해야 할지 걱정하며 투덜대지만, 막상 워크숍 당일이 되면 굉장히 흥미로운 주제들을 가지고 참여를 합니다. 라이트닝 톡이라는 의미에 맞게 1회 워크숍에서는 타이머를 켜고 시간 체크를 하면서 간단하게 발표를 했습니다. 그런데 기대했던 것보다 훨씬 좋은 발표들이 나오면서 발표 시간을 유동적으로 해서 발표의 퀄리티를 더 높이기로 했는데, 바로 다음 워크숍에 1시간 10분짜리 장대한 강의가 등장하는 바람에 절제의 중요성을 다시금 느끼면서 다시 타이머를 켜기로 했습니다…2017년 12월 워크숍에서는 PB팀이 상품 협찬을 해줘서 (PB팀 감사합니다!) 최고의 발표를 선정해 밀크 미니 인형을 지급했습니다. 영예의 수상자는 욕망의 흐름 이라는 발표를 정말 욕망의 흐름대로 발표한 Max로 선정되었습니다.<iframe src="https://docs.google.com/presentation/d/e/2PACX-1vQChBaARqlj8XfZx75MtkcejwupwBPt9tgD47sL99L1mHceYnPR2yDJnVAKFq8nFHXG9Pc9QbWBA5Eb/embed?start=false&loop=false&delayms=10000" frameborder="0" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"> 지금까지 워크숍을 6회나 진행했기 때문에 상당한 양의 라이트닝 톡 발표자료들이 모였습니다. 그중에서 몇 가지 발표의 슬라이드를 공유합니다.Glitches of Mario by PrinceOrigami - 종이접기와 수학 by PrinceLattice-based Cryptography by BradTADA-Android 회고 by David기반 작업들을 무엇을 했는가? + RIB 간단 설명Contract by DoogieAd Fraud by HughBB84 - 양자 역학을 이용한 절대적으로 안전한 키 분배 프로토콜 by James불완전성 정리 by James삼단논법 by JamesGAN by MaxReinforcement Learning based on AlphaGo by NelsonSteganography by Nelson재귀의 폭풍 by TedUBER: COSTS & REVENUES by TerryProbabilistic Filter by Youngboom다음 워크숍부터는 발표를 녹화해서 슬라이드와 함께 공유해보도록 하겠습니다.최고의 발표로 선정된 Max종이접기로 각의 3등분선 구하기 실습필자의 발표를 경청하는 멤버들디스크의 위험성을 온몸으로 표현 중 심층 토의VCNC 개발팀 워크숍에서는 회사의 주요 결정사항 혹은 공통으로 관심이 있는 이슈들을 선정해서 모두의 의견을 듣고 공감대를 형성하거나 액션 플랜을 세우는 토의를 진행합니다. 토의의 주제는 발전적이고 열린 커뮤니케이션을 지향하는 멤버들의 특성상 회사 생활 과정에서 자연스럽게 형성됩니다. VCNC 에서는 평소에도 서로의 의견을 공유하는 자리를 자주 가집니다. 그 예로는 매 달 진행하는 매니저와의 1:1 개인 리뷰 제도, 각 팀별 주간 회고 회의, 제품 피쳐 개발 단위로 진행하는 회고 회의 등이 있습니다. 이러한 의견 공유 과정에서 멤버 각자가 생각하는 불만, 문제점, 희망 사항들이 자연스럽게 워크숍의 토의 주제로 발전됩니다. 토의는 특별한 절차 없이 모든 구성원이 자연스럽게 끼어들면서 자신의 의견을 펼치며 진행됩니다. 모두의 의견을 듣는 것이 중요하기 때문에 특별한 주제가 아니라면 적은 인원으로 조를 구성해서 토의한 뒤 의견을 취합합니다. 정리한 내용은 제품팀 및 HR 담당자에게 전달되며 그 후 우리가 해볼 수 있는 시도들을 하거나 새로운 회사의 정책들이 생겨나기도 합니다.둘러앉아서 토의에 집중하는 멤버들 (편안한 자세 가능)아래의 항목들은 실제로 진행했던 토의의 주제들입니다.순수 개발 관련점차 높아지는 개발 복잡성을 어떻게 해결할까?서버-클라 간 프로토콜 문서화 문제제품 개발 프로세스 관련제품 개발 프로세스를 스프린트에서 칸반으로 변경하고 지금까지 겪었던 느낀 점, 문제점 및 해결 방안은?이슈 관리가 잘 안 되는데 원인 및 해결책은?QA가 필요한가? 제품 품질을 높이기 위해선 무엇을 해야 하는가?회사의 문화, 복지 등 전반회사에서 팀 간 커뮤니케이션을 원활하게 하기 위해 Manager 제도가 도입되는데 Manager 는 어떠한 역할을 맡아야 하는가?Manager 제도의 후기 공유 및 개선 방향.어떠한 모습의 회사를 원하는가?필요한 사내 문화 및 복지는 무엇이 있을까?개인의 발전 관련언제 동기부여가 되는가? 저하되게 만드는 요인은?어떠한 사람과 같이 일을 하고 싶은가?어떠한 모니터링 및 피드백을 받고 싶은가?VCNC 개발팀 워크숍의 토의 결과로 회사의 많은 부분이 발전하고 있습니다. QA 팀이 생겼고 해외 및 국내 콘퍼런스 지원 관련 복지 정책이 새로 생겼습니다. 제품 개발 프로세스는 새로운 시도를 거치면서 지속해서 발전해 나가고 있습니다.그 외우걱우걱워크숍에는 풍족한 먹을거리가 함께합니다. 카페를 대관하는 경우에는 무제한으로 음료가 제공되며 점심시간에는 배달을 시켜서 먹으면서 함께 이야기를 나눕니다. 마무리로 저녁에는 고기를 먹고 싶은 만큼 맘껏 먹으면서 역시 이야기꽃을 피웁니다.미니게임워크숍의 포맷이 라이트닝 톡 + 심층 토의 조합으로만 진행되어 느껴지는 지루함을 탈피하기 위해 2018년 4월 워크숍에서는 2인 1조로 팀을 구성해서 미니게임을 진행했습니다. 개발자 감성에 걸맞게 스크래치 게임인 Lightbot 2로 1시간 정도 플레이를 했습니다. 승패가 있는 대결은 아니었지만 다들 피로감을 호소할 정도로 엄청나게 집중하면서 시간을 보냈습니다.워크숍의 핵심은 고기를 굽는 것점심에는 피자를 시켜 먹으며 자유로운 대화를 나눕니다.집중해서 Lightbot 을 플레이하는 플레이어휴식 중에도 즐거운 대화는 계속됩니다. 마치며VCNC 개발팀 워크숍은 앞으로도 계속됩니다. 앞으로도 좋은 회사의 문화를 소개하는 기회를 자주 만들도록 노력하겠습니다. 저희와 함께 VCNC 를 발전시킬 좋은 분들을 기다리고 있으니 많은 지원 바랍니다!
조회수 3020

국내 스타트업 개발자들도 저녁이 있는 삶을 산다.

[대화 1]친구 A: 남편은 무슨일 해?아내: 어, IT회사 다녀.친구 A: 거기서 무슨일 하는데?아내: 개발자에요.친구 A: 아 그래? 그럼 퇴근 제때 못할텐데, 애들 키우기 힘들겠네.…[대화2]아내: 아니 그렇게(반바지) 입고 회사 가려고?필자: 음... 요즘 판교 쪽에서는 패피들은 반바지에 샌들 정도 신어줘야 인정받아..아내: 우리(금융회사)는 반바지 입는 사람은 생수 배달하는 사람 뿐인데. 갈아입고 가.금융기관에서 일하는 필자 아내와의 일상 대화 중 일부입니다. 대화는 짧지만 많은 의미가 함축되어있습니다. 우리 사회에서 금융권 직원이라 하면 말끔한 수트를 차려입고 아침부터 아메리카노 한잔 하면서 뭔가 중요한 딜을 성사시킬 것 같은 느낌이라면, IT개발자라 하면 그 금융권에서 사용하는 시스템 개발을 하위 위해 파견온 협력회사 직원과 그 회사에서 고용한, 소위 을, 병, 정 프리랜서들로 반바지에 좀 헝크러진 머리를 하고 밤늦게까지 그리고 주말에도 코딩하느라 제대로 씻지도 못하고 다니는 사람을 먼저 떠올립니다. 최근에 국내 유수의 게임 회사 한 곳에서만 세 명이 과로사하거나 업무 부담으로 회사에서 자살했다고 하니 그런 인식이 전혀 틀리지만은 않은 듯 합니다.미국에서는 개발자들이 대접은 잘 받지만 업무 난이도와 강도는 정말 높다고 합니다. 미국에서는 소프트웨어 개발자라고 하면 엄지손가락을 치켜 세우며 ‘6 digits’이냐고 물어보고들 합니다. 연봉이 $100,000 즉  1억 1,200만원 이상이냐고 묻는 것입니다. 연봉 10만 달러는 미국에서도 높은 편이지만, 소프트웨어 개발자들은 일반적으로 이를 상회합니다. 실리콘밸리에서는 개발자 대졸 초임이 10만 달러 정도 된다고 합니다.시가총액 상위 기업 대부분이 ICT 기업들이고 미국에서도 소프트웨어 개발 인력은 공급이 상당히 부족하니 그럴 수 밖에 없습니다. 공대중에서 최고라 하는 스탠포드와 MIT에서 최고 인기 전공은 단연 컴퓨터 사이언스라고 하는데, 대한민국에서는 인재들이 소프트웨어 분야를 기피하고, 이 분야가 더 열악해지는 악순환이 계속되고 있습니다. 자율주행 시스템, 암진단을 인간 의사보다 잘한다는 IBM 왓슨, 자산관리 로봇까지 가지 않더라도 뱅킹, 콜센터, 주차 정산, 음식 주문, 모바일 게임 등 우리 일상 생활을 소프트웨어 개발자들이 책임지고 있는데, 만성적인 개발 인력 부족으로 우리 ICT 산업의 경쟁력이 갈수록 떨어지지 않을까 걱정입니다.어제 오늘의 이야기도 아니고, 해결책이 과연 있는가?고무적인 것은 과거보다는 소프트웨어 개발자의 근무 환경에 더 관심을 가지고 야근 문화를 없애나가려고 노력하는 기업들이 많아지고 있다는 점입니다.핀테크 기업 핀다도 접근 방법은 다소 다르지만 이런 긍정적인 문화를 확산시키는 데 노력하고 있습니다. 그로 인해 우수한 인력이 한명이라도 더 핀다를 선택하고, 대한민국 젊은이 몇명이라도 더 공시생이 되기보다는 소프트웨어 개발자로 진로를 선택하기를 기대합니다.업무 환경이 중요하다.핀다의 개발자는 공유오피스 위워크(Wework) 을지로점 내의 사무실 및 라운지 등에서 자유롭게 근무합니다. 근무중에 사무실 내의 탁구장에서 함께 탁구를 치기도 하고 다트 게임을 하기도 합니다. 위워크 다른 층 라운지 쇼파에서 탁트인 전망을 보며 일하기도 합니다.물론 업무가 몰리고 데드라인에 쫓기면 야근을 하기도 하고 주말에 집에서 일하기도 하지만 이를 권장하기 보다는 지양하고 더 줄여나가려고 합니다. 저녁이 있는 삶을 보장하기 위해 지속적으로 노력할 것입니다.Wework 16층 회의실 겸 탁구장에서 열심히 탁구치는 우리 개발자. Le Viet Hoang‘월화수목금금금’ 일해도 일정 맞추기 어려운데 무슨 배부른 소리인가?소프트웨어 개발은 집중력을 요하는데, 사람이 하루 8시간도 집중해서 일하기는 쉽지 않습니다. 집중하지 못한 상황에서 작성한 낮은 품질의 코드로 더 많은 오류를 일으키고 이를 해결하기 위해 더 많은 시간을 일해야 하는 악순환이 발생합니다. 해당 직원의 행복지수도, 건강도, 로열티도 떨어지고 퇴사할 가능성이 높아집니다. 결국 회사는 잃는 것이 더 많아지게 됩니다. 하지만, 단지 초과 근무로 인해 생산성이 떨어지므로 이를 지양해야 한다고 하기에는 현실은 일반적으로 너무 열악하고 다급합니다. 초과 근무를 대신할 다른 혁신적인 방안이 있어야 기업의 관리자를 설득할 수 있을 것입니다.핀다 개발팀은 다릅니다. 개발 환경을 소개합니다.1. 이슈관리 시스템 Jira를 이용하여 태스크, 오류 등 모든 이슈를 관리합니다.      위키 시스템 Confluence를 통해 회사 및 프로젝트의 날리지를 관리합니다.  위키에 프로젝트별로 이와 같이 스페이스를 만들고 트리 구조로 페이지를 생성합니다.그림 상의 페이지에는 Jira에서 생성한 이슈들을 나열한 것을 볼 수 있습니다. 이런 방식으로 회사의 모든 지식은 체계적으로 정리되고 공유됩니다.2.  Git을 이용하여 소스코드 뿐 아니라 디자인 프로젝트까지 관리합니다.동시에 여러 버전의 소스를 유지하고, 여러 사람이 협업하기 위해 위와 같은 Git flow를 준수합니다.소스 변경(커밋) 시에는 그림과 같이 관련 이슈 번호를 넣어서 커밋과 이슈를 연동합니다.상용 배포 버전에는 그림과 같이 버전을 태그로 달아두고 버전별로 릴리즈 노트를 작성합니다.3. Jenkins를 이용하여 시스템 빌드 및 배포를 자동화하고 있습니다. 각 빌드에도 버전을 태그로 붙이고 있습니다.4. 객체지향 프로그래밍 방식을 철저히 준수합니다.시스템을 모듈로 나누고 각 모듈 간의 의존도는 최소화합니다. 논리적으로 관련된 코드는 한 패키지, 클래스 등에 모아서 응집도를 최대화합니다. 데이터와 데이터 처리 코드는 한 클래스에 모읍니다. 중복된 코드는 피할 수 있다면 한 줄이라도 허용하지 않고, 상속, 함수화, 오버로딩 등을 최대한 활용하여 코드 사이즈를 줄입니다.5. 이해하기 쉬운, 설명이 필요 없는 코드와 문서를 작성합니다.소프트웨어는 본질적으로 복잡합니다. 복잡한 문제를 최대한 쉽게 풀어내는 것이 소프트웨어 개발자의 능력의 핵심 중 하나입니다. 문제를 더 복잡하게 만들어서 다른 사람이 이해하기 어려워 하는 것을 본인의 능력이 뛰어나서라고 자만하거나, 주석을 달거나 문서화를 하지 않고서 다른 사람이 코드를 보고 이해하면 된다는 식의 생각은 아마추어리즘일 뿐입니다.핀다의 소프트웨어 프로젝트는 경험이 부족한 신입 개발자라도 30분 내에 구조와 흐름을 파악할 수 있도록 하고 있습니다.6.  웹, 안드로이드, 아이폰 앱은 철저히 통일된 MVC 구조로 구현합니다.모델(M) 부분은 서버로부터 데이터를 받아오는 모듈, 데이터의 세부사항을  처리하는 모듈, 데이터의 보존과 공급을 담당하는 모듈로 철저히 분리하여 구현합니다.화면의 부분을 담당하는 뷰(V)는 주어진 데이터로 화면을 그리는 것만 담당합니다.화면을 구성하기 위해서는 뷰를 배치하고 모델로부터 데이터를 받아서, 뷰에 전달해야 합니다. 이는 컨트롤러(C)가 담당하는데 컨트롤러는 철저히 컨트롤만 하고 세부적인 사항을 처리하지 않습니다.핀다의 웹, 안드로이드, 아이폰 앱은 모두 동일한 폴더, 클래스 구조를 가지도록 설계하고 있습니다. 이로 인해 다른 분야를 접해보지 못한 개발자라도 하루 내에 파악하여 코드 수정까지 할 수 있어서 누구나 쉽게 풀스택 개발자가 될 수 있습니다.종합해보면, 핀다 개발팀은 나만의 스타일로 코드를 작성할 자유가 없고, 프로그래밍 컨벤션을 따라 최적의 간결한 코드를 작성해야 합니다. 타이트한 프로세스를 따라야 합니다. 구글이나 마이크로소프트 보다 더 높은 수준의 클린 코드를 작성해야 합니다. 다소 타이트해보일 수 있지만, 유능한 핀다의 개발자들은 적극적으로 이를 준수하고 오히려 더 나은 개선 방안을 내놓고 있습니다. 결국 핀다의 개발자는 저녁이 있는 삶 뿐 아니라 신나고 발전적인 직장생활까지 누리게 될 것입니다.핀다의 미래가 밝아 보이나요? 아니면 너무 타이트해 보이나요?핀다는 핀다의 미래가 밝아 보인다고 느끼는 개발자에게 문을 활짝 열어놓고 있습니다.많은 기업이 핀다 방식 혹은 더 나은 방식을 도입하여 행복하게 일하는 개발자들이 더 많아지기를 기대해봅니다.#핀다 #개발 #개발팀 #개발자 #저녁이있는삶 #기업문화 #조직문화 #사내복지
조회수 3825

크몽 개발팀 문화와 구조 이야기

안녕하세요. 크몽 개발자들과 함께하고 있는 크레이그(a.k.a. 크알)입니다.크몽 개발자 그룹은 1년 내 그 규모가 3배로 커지고, Data Science, Growth Hacking 조직이 만들어지는 등 질적, 양적으로 급성장하고 있는 팀입니다.크몽 개발 부서에 계신 분들은 크몽에 대해 이렇게 이야기 합니다.(참고 : 크몽 개발팀원 더팀스 인터뷰 - '신뢰할 수 있는 동료와 함께 초고속 성장을 만들어가는 크몽 팀' )"제가 크몽에서 전반적으로 느낀 인상은 능동적인 분들이 많다는 거예요. 수동적인 업무를 책임감 있게 하는 것도 중요하지만 문제를 스스로 찾고, 동료들에게 제기하고, 문제를 해결했을 때 진심으로 기뻐하면서 행복감을 느끼시는 분들이 많아요. 그게 큰 조직에 있다가 온 저에게는 정말 많은 자극이 되었어요. "- 데이터분석 KM님"크몽이 저의 개발자 커리어에서 마지막 회사였으면 좋겠다고 생각해요. 실은 진심이고요. 그동안 회사의 성장을 지켜봤고 개발적으로도 많은 변화를 경험했어요"- BackEnd Sean님이렇게 개발자들이 행복하게 개발할 수 있는 환경을 우선시하고 있습니다. 그리고 크몽의 오픈 커뮤니케이션 문화를 지향함과 동시에 ‘Work Happy’와 'Freedom with Responsibility’ 란 가치 아래 최대한 자율성을 보장된 실무자 중심의 개발 문화를 추구합니다.크몽 개발 조직 구조위 핵심 가치 아래 크몽 개발 조직 구조는 크게 ‘Go’와 ‘Chapter’로 구성되어 있습니다.Go  ; 고우선 ‘Go’는 프로젝트 개발 팀 단위로 크몽 서비스를 개선하기 위한 목표 중심의 조직입니다. 다른 회사에서는 ‘Silo’, ‘Team'로 명칭 하기도 합니다. 물리적으로 한 공간에서 스크럼을 이루어 일할 수 있도록 자원을 갖추고 있습니다. Go 안에는 Go Leader(GL) 가 있어 팀 업무 관리 및 우선순위를 정합니다.현재 크몽 개발 파트의 Go는 아래와 같이 구성되어 있습니다.UX-Go크몽 서비스 UX를 개선하기 위한 목표로 데이터를 기반으로한 UX Iteration & Growth Mission 을 수행하는 팀Data-Go데이터 파이프라인을 구축, 활용하여 조직 내 필요한 데이터 자료를 공급하고, 크몽 서비스안에 머신러닝/딥러닝 등의 인공지능 기술 영역을 담당하는 팀Dasi-Go서비스 안정적인 운영 및 릴리즈,  CRM 기술 지원을 담당하는 팀Mobile-Go검색 서비스, 서비스 카테고리 개선 등 크몽 서비스 향상을 위한 모듈 개발팀크몽 라운지Chapter  ; 챕터'Chapter'는 직군별 조직 단위로 주 1회 정도의 커뮤니케이션 타임을 통해 업무 및 기술 동향을 교환합니다. 더불어 챕터 안에서 필요한 스터디, 외부 교육 등의 직군별 자기 능력 향상을 도모하고, 회사에선 이를 적극 지원합니다. 그리고 챕터 내 프로젝트를 통해 서비스 개선에 기여하기도 합니다.크몽 개발 파트는 아래와 같은 챕터가 있습니다.(참고 : 웹 프로트엔드 챕터의 'gulp 개선기' -  https://brunch.co.kr/@kmongdev/5 )**챕터 프로젝트는 챕터 내에서 개발자분들이 스스로 필요하다는 판단 하에 빌딩 된 프로젝트입니다. 챕터 내에는 CL(Chapter Leader)가 존재하며, Chapter 구성원 관리 및 의견을 모아 조직에 전파하는 역할을 담당합니다.Guild  ; 길드개발 파트 안에서의 'Guild'는 토이 프로젝트 같은 성격의 공통 관심 분야를 지닌 프로젝트 팀이라고 볼 수 있습니다. 길드 기획 단계에서 회사 전사적으로 적용되면서, 동호회 성격으로 피보팅(Pivoting) 되어 있지만, 기본적으로 공통의 관심 분야를 같이 학습하고 프로젝트에 적용하는 팀입니다. 매주 수요일 오후 2~3시 사이의 시간은 챕터(Chapter), 고(Go)를 떠나 본인이 원하는 길드에 들어가서 새로운 영역을 탐색하고 연구하는 시간입니다.크몽 개발 파트는 아래와 같은 길드가 있습니다.(참고 : 코틀린 길드의 코틀린 리서치 이야기  https://brunch.co.kr/@kmongdev/9 )정리모든 개발 조직은 '성과 중심' 또는 '성장 중심'의 문화를 가지고 있습니다. 균형을 꾀하는 게 이상적이긴 하지만 스타트업에선 쉽지 않은 일입니다.하지만 크몽 개발 부서에선 인적 성장 중심 문화를 고민하고, 끊임없이 시도하고 있습니다. 이를 위해 여러 전문 교육 기관과 협약을 맺고 교육 지원을 하고 있으며, 국내 정상급 권위자 분들로 구성된 외부 컨설턴트 그룹을 구성해 개발자 분들께 배움과 성장의 기회를 부여하려고 노력하고 있습니다. 1년의 기간 동안 이직률3%의 수치를 기록하고 있는 크몽 개발 파트에선 신규 인력 채용 시 제 1의 인사 기준은 '높은 학력'도, '화려한 커리어'도 아닌우리와 '오랫동안' 함께 '성장'할 수 있는가?입니다. 이를 위해선 개발자 성장을 돕기 위한 환경 구축 및 관리가 필수이고,  그것이 궁극적으로는 회사 및 팀원에게도 장기적인 발전을 가져올 꺼란 굳은 믿음이 있습니다.크몽 개발 그룹CTO#크몽 #개발팀 #개발자 #사내복지 #기업문화 #조직문화 #사내스터디 #CTO
조회수 1647

StyleShare Engineering Blog?!

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

레진 기술 블로그 - 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

기업문화 엿볼 때, 더팀스

로그인

/