스토리 홈

인터뷰

피드

뉴스

조회수 974

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

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

박문수 이야기

출근 첫날 이효진 대표님으로부터 입사 지원 메일을 하나 전달받았다. 이력서를 살펴보니 컴퓨터를 전공하지도 않았고, 현재 개발을 하고 있지도 않았지만 개발자로 일하고 싶다고 적혀 있었다. 개발을 할 수만 있다면 인턴부터 시작해도 좋다고 말했다. 남들이 부러워하는 삼성에 다니고 있는데 어떤 이유로 개발자가 되고 싶어 할까? 궁금한 마음에 한 번 만나보기로 했다. (뽑을 생각은 없었다)첫인상은 그냥 수수한 시골 청년이었다. 나도 입사한 지 얼마 안 되어 회사 주위 식당을 몰라 그냥 눈에 띄는 식당으로 들어갔다. (생각해 보니 그 식당을 그 이후로는 한 번도 가지 않았다) 지난 회사에서 어떤 일들을 했고, 왜 개발에 대한 목마름을 느꼈는지를 들었다. 개발자가 되기 위해 어떤 것들을 포기할 수 있는가에 대한 각오도 들었다.나는 앞으로 일 년 동안 인턴 월급을 받아야 할지 모른다고 이야기했다. 정말 열심히 하지 않으면 그저 그런 개발자가 되어 인생이 꼬일지도 모른다고 경고했다. 그런데도 흔쾌히 도전해보고 싶다고 말했고, 나는 배움의 기회를 제공하겠다는 약속을 했다. 좋은 대학을 나와 어렵게 얻은 직장을 포기하고 다시 새로운 길을 선택하려는 용기를 높이 샀다. 입사일은 3주 뒤로 정했다. 파이썬 책과 웹 프로그래밍 기본 책을 던져주고 모두 읽어 오라고 했다.입사 후 정신없이 3주가 지나고 문수님이 입사를 했다. 첫날 개발 환경을 셋업 하는 것을 도와주었다. 나에게는 너무나도 자연스러운 많은 것들이 그에게는 생소한 것이고 설명을 해야 했다. 문수님이 이해할 수 있는 간단한 것만 설명하고 나머지는 더 크면 알게 된다고 설명을 미루었다.(첫날 전체를 대상으로 자기소개를 하는 문수님. 우리 회사에는 입사자가 전체를 대상으로 자기소개를 하는 문화가 있다. 이 문화의 유래에 대해서는 다시 한 번 이야기해 보겠다.)내가 모든 것을 알려 줄 수는 없으니 코세라 수업을 같이 들어 보자고 이야기했다. 내 기준으로는 너무 쉬운 강의였지만 나는 회사 내에서 공부하는 분위기를 만들어 가고 싶었고 문수님께는 회사에서 필요한 기술 스택을 맛보는 기회가 될 수 있으리라 생각했다. (현재 시점으로 3달째 코세라 강의를 이어서 듣고 있다.)첫 강의인 HTML5를 들으면서 간단한 버그 수정부터 문수님께 요청을 하기 시작했다. 오자를 고치거나 박스의 위치를 조정하는 일부터 시작했다. 입사하고 3일이 지나서 첫 번째 배포를 했다. 처음이 어려웠을 뿐 간단한 수정을 하는 것에는 일주일이면 충분했다. 그때부터는 git과 git flow를 알려주기 시작했다. 착한 신입은 마음이 열려 있어서 불만 없이 모든 것을 따라 했다. 어느 정도 이해를 했는지는 알 수가 없다. 하지만 프로그래밍을 배우는 길에는 머리보다 손이 먼저 익히는 것들도 많다.3주가 지난 시점에는 첫 번째 데모를 전체 앞에서 보였다. (우리는 스크럼을 하고 있어서 매번 스크럼이 끝나는 날에 개발자가 스스로 자신이 개발한 것을 전 직원 앞에서 데모를 보인다.) 지금은 잠깐 문을 닫은 채권 거래소에서 채권 판매자가 손쉽게 채권을 팔 수 있는 기능이었다. 그것을 만들기 위해 일주일 넘게 꽁꽁 머리를 싸매고 있었고, 결국은 결과물을 내놓았다.(첫 번째 데모를 보이는 문수님. 긴장한 모습이 느껴진다. 데모를 마치고 다들 뜨거운 박수를 보내주었다)내가 만들면 2시간이면 끝났을 기능이라 일주일간 고생하는 것을 옆에서 지켜보는 것은 상당한 인내를 필요로 했다. 하지만 최대한 혼자만의 힘으로 첫 번째 과제를 해내기를 원했기에 최소한의 도움만을 주었다.이제 문수님이 입사한 지 만 3개월이 되었다. 그동안 많은 변화가 있었다. 회사에서 조그마한(점점 커지고 있다) 수정/기능들은 대부분 맡아 주고 있기에 다른 개발자들은 좀 더 어려운 문제를 풀 수 있게 되었다. 처음에는 코드 리뷰를 온라인으로 할 수가 없었다. 옆에 앉아서 어떤 부분을 어떻게 고쳐야 하는지를 구체적으로 알려 주어야 했고, 이해하지 못하면 관련된 지식을 얻을 방법을 알려 주어야 했기 때문이다. 하지만 이제 github의 PR을 보고 코멘트를 다는 것 만으로 코드를 적절히 수정할 수 있게 되었다. 얼마 전에는 하루에 1억이 넘는 이체를 하는 내부 시스템을 80% 이상 만들기도 했다. (내가 뼈대는 잡아 주기는 했다.)개발자라 부를 수 있는 기준이 따로 있겠냐만은 나는 이제 그를 개발자라 부를 수 있을 것 같다. 아마도 오늘의 문수님에게는 “개발자 박문수 님”이 가장 듣고 싶은 호칭이 아닐까 생각한다.  마지막으로 전공하지도 않았고, 첫 직장과도 관련 없는 새로운 도전을 하는 문수님의 용기에 박수를 보낸다. 내게 말하지는 않았지만 수많은 주위의 걱정과 우려를 이겨내기 위해 최선을 다하고 있으리라 생각한다. 나는 앞으로 그에게 “문수님은 지금 어디로 가고 있나요?"를 종종 물어봄으로 내 역할을 해야겠다.8퍼센트는 멋진 저희 팀과 함께 할 분들을 찾고 있습니다. 특히 저보다 개발을 잘 하시는 시니어 개발자, 그리고 3년 뒤에는 저 보다 잘하게 되실 주니어 개발자는 제가 모시러 갑니다. hr@8percent.kr로 연락 주세요.박문수 님이 이체 시스템 개발을 할 때 Toss의 이체 대행 API를 사용했습니다. 정말 간편합니다. 관련 개발을 하시는 분들은 사용해 보세요.#8퍼센트 #에잇퍼센트 #채용 #채용후기 #개발자 #개발자채용 #인턴 #인턴채용 #스타트업CTO
조회수 1056

레진 기술 블로그 - AWS Auto Scalinging Group 을 이용한 배포

레진코믹스의 서버 시스템은 잘 알려진대로 Google AppEngine에서 서비스되고 있지만, 이런저런 이유로 인해 최근에는 일부 컴포넌트가 Amazon Web Service에서 서비스되고 있습니다. AWS 에 새로운 시스템을 셋업하면서, 기존에 사용하던 PaaS인 GAE에서는 전혀 고민할 필요 없었던, 배포시스템에 대한 고민이 필요했습니다. 좋은 배포전략과 시스템은 안정적으로 서비스를 개발하고 운영하는데 있어서 필수적이죠.초기에는 Beanstalk을 이용한 운영에서, Fabric 을 이용한 배포 등의 시행착오 과정을 거쳤으나, 현재는 (스케일링을 위해 어차피 사용할 수밖에 없는) Auto Scaling Group을 이용해서 Blue-green deployment로 운영 중입니다. ASG는 여러 특징 덕분에 배포에도 유용하게 사용할 수 있습니다.ASG를 이용한 가장 간단한 배포는, Instance termination policy 를 응용할 수 있습니다. 기본적으로 ASG가 어떤 인스턴스를 종료할지는 AWS Documentation 에 정리되어 있으며, 추가적으로 다음과 같은 방식을 선택할 수 있습니다.OldestInstanceNewestInstanceOldestLaunchConfigurationClosestToNextInstanceHour여기서 주목할 건 OldestInstance 입니다. ASG가 항상 최신 버전의 어플리케이션으로 스케일아웃되게 구성되어 있다면, 단순히 인스턴스의 수를 두배로 늘린 뒤 Termination policy 를 OldestInstance 로 바꾸고 원래대로 돌리면 구버전 인스턴스들부터 종료되면서 배포가 끝납니다. 그러나 이 경우, 배포 직후 모니터링 과정에서 문제가 발생할 경우 기존의 인스턴스들이 이미 종료된 상태이기 때문에 롤백을 위해서는 (인스턴스를 다시 생성하면서) 배포를 다시 한번 해야 하는 반큼 빠른 롤백이 어렵습니다.Auto scaling lifecycle 을 이용하면, 이를 해결하기 위한 다른 방법도 있습니다. Lifecycle 은 다음과 같은 상태 변화를 가집니다.기본적으로,ASG의 인스턴스는 InService 상태로 진입하면서 (설정이 되어 있다면) ELB에 추가됩니다.ASG의 인스턴스는 InService 상태에서 빠져나오면서 (설정이 되어 있다면) ELB에서 제거됩니다.이를 이용하면, 다음과 같은 시나리오로 배포를 할 수 있습니다.똑같은 ASG 두 개를 구성(Group B / Group G)하고, 그 중 하나의 그룹으로만 서비스를 운영합니다.Group B가 라이브 중이면 Group G의 인스턴스는 0개입니다.새로운 버전을 배포한다면, Group G의 인스턴스 숫자를 Group B와 동일하게 맞춰줍니다.Group G가 InService로 들어가고 ELB healthy 상태가 되면, Group B의 인스턴스를 전부 Standby로 전환합니다.롤백이 필요하면 Standby 상태인 Group B를 InService 로 전환하고 Group G의 인스턴스를 종료하거나 Standby로 전환합니다.문제가 없다면 Standby 상태인 Group B의 인스턴스를 종료합니다.이제 훨씬 빠르고 안전하게 배포 및 롤백이 가능합니다. 물론 실제로는 생각보다 손이 많이 가는 관계로(특히 PaaS인 GAE에 비하면), 이를 한번에 해주는 스크립트를 작성해서 사용중입니다. 대략 간략하게는 다음과 같습니다. 실제 사용중인 스크립트에는 dry run 등의 잡다한 기능이 많이 들어가 있어서 걷어낸 pseudo code 입니다. 스크립트는 사내 PyPI 저장소를 통해 공유해서 사용 중입니다.def deploy(prefix, image_name, image_version): '''Deploy specified Docker image name and version into Auto Scaling Group''' asg_names = get_asg_names_from_tag(prefix, 'docker:image:name', image_name) groups = get_auto_scaling_groups(asg_names) # Find deployment target set future_set = set(map(lambda g: g['AutoScalingGroupName'].split('-')[-1], filter(lambda g: not g['DesiredCapacity'], groups))) if len(future_set) != 1: raise ValueError('Cannot specify target auto scaling group') future_set = next(iter(future_set)) if future_set == 'green': current_set = 'blue' elif future_set == 'blue': current_set = 'green' else: raise ValueError('Set name shoud be green or blue') # Deploy to future group future_groups = filter(lambda g: g['AutoScalingGroupName'].endswith(future_set), groups) for group in future_groups: asg_client.create_or_update_tags(Tags=[ { 'ResourceId': group['AutoScalingGroupName'], 'ResourceType': 'auto-scaling-group', 'PropagateAtLaunch': True, 'Key': 'docker:image:version', 'Value': image_version, } ]) # Set capacity, scaling policy, scheduled actions same as current group set_desired_capacity_from(current_set, group) move_scheduled_actions_from(current_set, group) move_scaling_policies(current_set, group) # Await ELB healthy of instances in group await_elb_healthy(future_groups) # Entering standby for current group for group in filter(lambda g: g['AutoScalingGroupName'].endswith(current_set), groups): asg_client.enter_standby( AutoScalingGroupName=group['AutoScalingGroupName'], InstanceIds=list(map(lambda i: i['InstanceId'], group['Instances'])), ShouldDecrementDesiredCapacity=True ) def rollback(prefix, image_name, image_version): '''Rollback standby Auto Scaling Group to service''' asg_names = get_asg_names_from_tag(prefix, 'docker:image:name', image_name) groups = get_auto_scaling_groups(asg_names) def filter_group_by_instance_state(groups, state): return filter( lambda g: len(filter(lambda i: i['LifecycleState'] == state, g['Instances'])) == g['DesiredCapacity'] and g['DesiredCapacity'], groups ) standby_groups = filter_group_by_instance_state(groups, 'Standby') inservice_groups = filter_group_by_instance_state(groups, 'InService') # Entering in-service for standby group for group in standby_groups: asg_client.exit_standby( AutoScalingGroupName=group['AutoScalingGroupName'], InstanceIds=list(map(lambda i: i['InstanceId'], group['Instances'])) ) # Await ELB healthy of instances in standby group await_elb_healthy(standby_groups) # Terminate instances to rollback for group in inservice_groups: asg_client.set_desired_capacity(AutoScalingGroupName=group['AutoScalingGroupName'], DesiredCapacity=0) current_set = group['AutoScalingGroupName'].split('-')[-1] move_scheduled_actions_from(current_set, group) move_scaling_policies(current_set, group) 몇 가지 더…Standby 로 돌리는 것 이외에 Detached 상태로 바꾸는 것도 방법입니다만, 인스턴스가 ASG에서 제거될 경우, 자신이 소속된 ASG를 알려주는 값인 aws:autoscaling:groupName 태그가 제거되므로 인스턴스나 ASG가 많아질 경우 번거롭습니다.cloud-init 를 어느 정도 최적화해두고 ELB healthcheck 를 좀 더 민감하게 설정하면, ELB 에 투입될 때까지 걸리는 시간을 상당히 줄일 수 있긴 하므로, 단일 ASG로 배포를 하더라도 롤백에 걸리는 시간을 줄일 수 있습니다. 저희는 scaleout 시작부터 ELB에서 healthy 로 찍힐 때까지 70초 가량 걸리는데, 그럼에도 불구하고 아래의 이유 때문에 현재의 방식으로 운영중입니다.같은 방식으로 단일 ASG로 배포를 할 수도 있지만, 배포중에 혹은 롤백 중에 scaleout이 돌면서 구버전 혹은 롤백 버전의 인스턴스가 투입되어버리면 매우 귀찮아집니다. 이를 방지하기 위해서라도 (Blue-green 방식의) ASG 두 개를 운영하는게 안전합니다.같은 이유로, 배포 대상의 버전을 S3나 github 등에 기록하는 대신 ASG의 태그에 버전을 써 두고 cloud-init 의 user-data에서 그 버전으로 어플리케이션을 띄우게 구성해 두었습니다. 이 경우 인스턴스의 태그만 확인해도 현재 어떤 버전이 서비스되고 있는지 확인할 수 있다는 장점도 있습니다.다만 ASG의 태그에 Tag on instance 를 체크해 두더라도, cloud-init 안에서 이를 조회하는 경우는 주의해야 합니다. ASG의 태그가 인스턴스로 복사되는 시점은 명확하지 않습니다. 스크립트 실행 중에 인스턴스에는 ASG의 태그가 있을 수도, 없을 수도 있습니다.굳이 인스턴스의 Lifecycle 을 Standby / InService 로 전환하지 않고도 ELB 를 두 개 운영하고 route 53 에서의 CNAME/ALIAS swap 도 방법이지만, DNS TTL은 아무리 짧아도 60초는 걸리고, JVM처럼 골치아픈 동작 사례도 있는만큼 선택하지 않았습니다.물론 이 방법이 최선은 절대 아니며(심지어 배포할때마다 돈이 들어갑니다!), 현재는 자원의 활용 등 다른 측면에서의 고민 때문에 새로운 구성을 고민하고 있습니다. 이건 언젠가 나중에 다시 공유하겠습니다. :)
조회수 1138

Event-Driven Programming

Overview마이크로 서비스 사이의 결합도를 낮추고 비동기적인 문제들을 처리할 때는 Event-driven 아키텍쳐가 유용합니다. 이번 글에서는 AWS에서 제공하는 SNS Topic을 이용해 Event-Driven을 알아보겠습니다. Event-Driven Programming프로그램의 제어 흐름이 이벤트의 발생에 의해 결정되는 컴퓨터 프로그래밍 패러다임입니다. publish/subscribe (이하 pub/sub)메시징서버리스 및 MSA에서 안정성 및 확장성을 높이기 위하여 사용되는 비동기 서비스 통신 방법입니다. 게시된 메시지를 다른 시스템에 비동기적으로 전달하고, Topic을 구독하는 모든 구독자는 모든 메시지를 받을 수 있습니다. 특히 게시자는 누가 구독하고 있는지 알지 않아도 되고, 구독자도 메시지의 출처를 알 필요는 없습니다. pub/sub 메시징 기본 / 출처: AWS Compute BlogAmazon SNS Topicpub/sub 방식의 메시징 서비스입니다. AWS의 여러 서비스들이 SNS에 이벤트를 게시할 수 있습니다. SNS Event Publishers / 출처: AWS Compute Blog위의 그림과 같이 구독자는 게시자 서비스에서 트리거된 이벤트에 응답해 필요한 작업을 진행합니다. 예시로 Elastic Transcoder 서비스에서의 Topic을 활용해보겠습니다. 네 가지의 순서를 거칩니다.1. SNS 토픽 생성2. Elastic Transcoder 등록Optional 항목인 Notification 영역에서 상태별 이벤트를 설정할 수 있습니다. On Completion Event에 위에서 생성한 Topic을 선택해 이벤트를 전달받도록 설정합니다. 3. SNS Topic에 구독자로 등록트랜스 코딩이 완료 후 처리할 프로세스를 가진 Lambda 함수 생성하여 위에서 생성한 SNS Topic에 구독자로 등록합니다. 현재 SNS Topic에서 제공하는 프로토콜은 HTTP, HTTPS, Email, Email-JSON, Amazon SQS, Application, AWS Lambda, SMS가 있습니다.4. 서비스 간 이벤트 전달출처: AWS Compute BlogSNS Topic으로 이벤트를 제공하는 AWS 서비스 중 하나를 살펴봤습니다. 이를 이용하면 마이크로 서비스 간에 이벤트를 전달하고 서비스의 분리 및 확장에 유용하게 사용할 수 있습니다.Conclusion오늘은 SNS Topic을 이용한 Event-Driven을 알아봤습니다. 다음 글에서는 마이크로 서비스에서 사용할 수 있는 AWS 서비스들을 다뤄보겠습니다.참고Event-Driven Computing with Amazon SNS and AWS Compute, Storage, Database, and Networking Services글이상근 팀장 | R&D 개발1팀leesg@brandi.co.kr브랜디, 오직 예쁜 옷만#브랜디 #개발문화 #개발팀 #업무환경 #인사이트 #경험공유
조회수 1500

Android 의 Sqlite Tip

Android 와 SqliteAndroid 에서 Sqlite 는 오랫동안 사용되어 왔습니다. 지금은 Realm 과 그외의 데이터베이스들이 그 위치를 넘보고 있지만 여전히 사용되고 있음은 틀림없습니다.현재 Jandi 는 서버의 대다수 정보를 앱의 Sqlite-Database 에 Cache 를 하고 있습니다. 따라서 Sqlite 를 얼마나 잘 분리하고 제어하느냐가 앱 자체의 라이프사이클에 큰 영향을 미칩니다.오늘은 Android 팀이 Sqlite 를 어떻게 사용하는지 공유해드리고자 합니다.1. ORM안드로이드만 하신 분들에게는 다소 생소한 개념일 수 있지만 다른 분야에서는 널리 사용되고 있습니다.Android 에서 Sqlite 는 Database 용 Access 객체를 통해서 column/row 단위로 정보를 가져와서 객체를 완성합니다. 하지만 Access 객체에 일일이 Query 를 작성하는 것은 실수가 많을 뿐더러 column/row 단위 정보 매핑 작업은 매우 불편하고 지루하며 잠재적 버그를 내포한 작업니다.그러기 때문에 Object-Query-Databse 를 각각에 맞게 매핑해주는 라이브러리들 통해 단순하고 반복적인 작업을 간단하게 회피할 수 있습니다.아래의 블로그들이 Sqlite-Orm 라이브러리를 사용하는 좋은 정보들이 될 것입니다. 현재 Jandi-Android 의 주요 Orm 라이브러리는 OrmLite 입니다.Sqlite-Orm : 네이버 기술블로그GreenDao BenchmarkRealm Database2. Database-Access유사 관심사 Domain 끼리 묶음수많은 데이터를 테이블로 관리하다보면 많은 Database-Access-Object(DAO) 가 필요합니다. 하지만 자세히 들여다보면 관계된 것끼리의 묶음이 생기게 되며 이를 묶어서 하나의 Access 객체를 만들 수 있습니다.Jandi 의 메시지는 크게 Text, File, Sticker 로 구분되어 있으며 이에 대한 상위로 Message 라는 개념이 있습니다. Text, File, Sticker 는 하위에 각각 2~3개의 Table 로 구성되어 있습니다. 이 전체를 각각 분리해서 관리하면 그에 따른 부수적인 제어 코드들이 불가피 하기 때문에 Jandi 에서는 최상위 Message 도메인에 맞춰서 하나의 묶음으로 관리하였습니다.코드는 다음과 같은 형태를 띄고 있습니다.public class MessageRepository { public List getMessages(/*args...*/) { /*코드 생략*/}; public Message save(/*args...*/) { /*코드 생략*/}; public Message update(/*args...*/) { /*코드 생략*/}; public Text getText(/*args...*/) { /*코드 생략*/}; /*이하 생략*/ } 이러한 형태로 독립성을 가질 수 있는 최상위 Domain 을 기준으로 Repository 클래스를 가지고 있습니다.3. Repository 요청 관리하기위의 모습처럼 관심사별로 Domain 을 분리한 이유 중 가장 큰 이유는 Domain 단위로 요청을 관리하기 위함입니다.Android-Sqlite 는 내부적으로 Read-Write lock 을 가지고 있지만 신뢰도가 높다 할 수 없으며 다양한 테이블에 동시 접근하는 경우 오류가 나지 않을 것이라 보장할 수 없습니다.따라서 보장이 안될바에 1번에 1개의 요청만 처리 할 수 있도록 Domain 단위로 요청을 제한해버리자는 결론을 냈습니다.그러기 위해 2가지 코드를 사용하였습니다.Lock 객체 사용요청을 래핑할 template interface 사용하기멀티 쓰레드로 요청을 처리할 때 Lock 객체를 통해 1번의 1개씩의 동작만 할 수 있도록 하였으며 이를 좀 더 쉽게 쓸 수 있도록 하기 위해 Template Interface 를 만들었습니다.public class LockTemplate { private Lock executorLock; LockTemplate() { executorLock = new ReentranceLock(); } protected T execute(Executable e) { executorLock.lock(); try { return e.execute(); } finally { executorLock.unlock(); } } interface Executable { T execute(); } } 위와 같은 클래스를 만들고 앞서 만든 Repository 클래스에 상속받도록 하였습니다.코드는 다음과 같습니다.public class MessageRepository extends LockTemplate { /*싱글톤으로 동작하도록 합니다. 코드 생략*/ public List getMessages(long roomId) { return execute(() -> { return dao.query(roomId); }); } public int save(List messages) { return execute(() -> { return dao.save(messages); }); } } 위와 같이 함으로써 최종적으로 같은 Repository 에 멀티쓰레드에서 요청을 하여도 1개의 처리만 할 수 있도록 원천적으로 작업하였습니다.정리Android 에서 Sqlite 는 Mysql 이나 PostSQL 과 유사한 RDBMS 를 제공하는 DB 툴입니다. 하지만 기본적인 사용이 매우 번거러울 뿐만 아니라 메모리릭과 오류에 매우 쉽게 노출됩니다. 그래서 다음과 같은 방법을 통해 최소한의 안전망을 구현했습니다.ORM 을 사용하라.반복적이고 DB 접근 과정에서 오류를 최소화 시켜줍니다.Lock 을 의도적으로 사용하라.멀티쓰레드 접근에 의한 오류를 최소화 합니다.synchroized 보다는 concurrent 패키지에서 제공해주는 Lock 을 사용해주세요.#토스랩 #잔디 #JANDI #개발 #앱개발 #인사이트
조회수 2753

레코딩 플러그인 이야기

마음챙김명상앱 '마보'의 콘텐츠들은 모두 Waves 플러그인으로 프로세싱된다.(처음과 마지막을 제외하면) Waves에 대해 간단한 생각을 정리하자면 다음과 같다.머큐리 구입시 UAD와도 고민을 많이 했지만 소프트웨어로만 비교를 한다면 waves가 훨씬 편하게 사용이 가능하다. 그 중에서도 CPU로 돌릴 수 있다는 점이 제온 CPU 에서 강력하게 작용한다.(Waves 하드웨어가 필요하다면 영국콘솔회사 DiGiCo와의 합작품인 DiGiGrid라는것도 있다.)근데 문제는... 맥에서는 더이상 CPU파워가 따라주지 않는다는 것이다.이런 상황에서 밖에서 작업하기에 딱 좋은 솔루션이 있었다. Soundgrid라는 waves의 DSP솔루션이다.이 사운드그리드에 대해 요약하면 waves의 플러그인만 따로 모아서 랜케이블로 Soundgrid 연결을 하면 CPU의 부담을 주지 않고 Daw에서 똑같은 프로세싱이 가능하다.이번에 BLS에서 데모로 받은 Waves Soundgrid IMPACT SERVER를 까페에 들고 나왔다.문제는 예상했던 사이즈가 아닌 맥북보다 훨씬 커서 카페에 가지고 다니기 부담스러운 크기... 휴대성이라는 측면에서는 역시 좀 무리가 있지 않나 싶다.(사진참조)어쨌든 카페에서 작업이 가능하게 되었다는 점. 다만, 카페에 가는데 차가 필요하다는 점이 있겠다.앞서 말했듯 마보 콘텐츠는 waves외에 플러그인의 시작과 끝을 Izotope 플러그인을 쓴다.마지막에는 라우드니스를 위해 오존을 사용한다. 오존은 너무 유명한 플러그인이니 설명도 생략.녹음이 끝나면 바로 첫단에 오디오스위트로 걸어주는 RX5라는 플러그인이 있다.이 플러그인은 보이스를 녹음한다면, 혹은 볼륨이 크지 않은 클래식 악기를 녹음한다면 정말 요긴하다.첫째로 입에서 발생하는 립노이즈들을 효과적으로 빠르게 제거해 준다. 콘덴서 마이크에서 타는 쩝쩝거리는 소리들을 손으로 하나씩 잡을 필요가 없다. 그저 한번 클릭으로 모든 파형의 클릭소리들을 제거해준다.Waves Mercury에도 X-Click이있지만 Izotope의 RX5가 훨씬 퀄리티가 좋다.두번째로 De-noise의 강력한 기능이다. 녹음시에 발생되는 팬소음들은 사실 EQ를 통해 어느정도 제거가 가능한 험의 형태로 발생한다면, 전기적 접지의 부재로 인한 핑크노이즈는 쉽게 제거가 불가능하다.하지만. 이 De-noise의 노이즈 LEARN기능으로 노이즈를 분석한 후 노이즈를 획기적으로 제거할 수 있다.칭찬일색으로 보이지만 RX5는 유튜브 믹싱채널을 운영하는 Alan JS Han님도 추천을 하실 만큼 유명하다.(근데 Izotope는 품질은 정말 유명하나 CPU를 정말 힘들게 한다.)자세한 이야기는 각 사진 속에보다시피 크기가 생각보다 크고 3kg에 육박하는 mini-itx PC이다.. 공연장비가 베이스이기 때문에 랜케이블로 연결한다.프로툴 유저라면 한번쯤 겪어본 창프로툴 유저라면 한번쯤 겪어본 창보이스가 없는 부분을 선택해 기본으로 깔리는 노이즈를 분석한다.전 구간에 분석한 노이즈 커브를 적용한 모습.깔끔하게 정리되었다.(SSL프리에서 오는 하모닉스들도 제거)#마보 #콘텐츠 #프레임워크 #스택 #인사이트 #일지
조회수 1461

React 공식 튜토리얼 한글 번역

<button type="button">메뉴</button>* 오역 및 오탈자가 있을 수 있습니다. 발견하시면 댓글로 제보해주세요!** 브런치 에디터의 한계로 마크다운 적용이 되지 않아 가독성이 떨어지고 복사 기능이 지원되지 않습니다. 이왕이면 이곳에서 보시기를 권장합니다. >> 가독성 좋은 문서로 보기React 공식 튜토리얼 바로가기시작하기 전에무엇을 구현할 것인가대화형 틱택토 게임을 구현하려고 합니다.원한다면 최종 결과물을 여기에서 확인할 수 있습니다. 아직 코드가 이해되지 않거나 문법이 낯설어도 걱정하지 마세요. 튜토리얼에서 차근차근 틱택토 게임을 구현하는 방법을 배울테니까요.게임을 플레이해보세요. 이동 리스트에 있는 버튼을 클릭하여 클릭한 때로 돌아가고, 그 때로 돌아간 후 보드가 어떻게 보이는지 확인할 수 있습니다.게임에 익숙해지셨다면 탭을 닫으세요. 다음 섹션에서 간단한 템플릿을 가지고 시작할 것입니다.사전 준비HTML과 JavaScript에 익숙할 것으로 생각합니다. 하지만 HTML과 JavaScript를 사용해본 적이 없더라도 튜토리얼을 따를 수 있어야 합니다.JavaScript를 다시 봐야한다면 이 가이드를 추천합니다. 튜토리얼에서 JavaScript의 최신 버전인 ES6의 몇 가지 특징들인 화살표 함수, 클래스, let, const를 사용할 것입니다. Babel REPL을 사용하여 ES6 코드가 어떻게 컴파일되는지 확인해볼 수 있습니다.튜토리얼을 공부하는 방법튜토리얼을 공부하기 위한 두 가지 방법이 있습니다. 브라우저에서 코드를 작성하거나 컴퓨터의 로컬 개발 환경을 설치할 수 있습니다. 편한 방법을 선택하여 공부하시면 됩니다.브라우저에서 코드를 작성하기 원한다면가장 빨리 시작할 수 있습니다!새로운 탭에서 시작 코드를 여세요. 빈 틱택토 필드를 볼 수 있습니다. 튜토리얼에서는 이 코드를 수정하여 진행합니다.다음 섹션인 로컬 개발 환경 설정을 스킵할 수 있습니다. 바로 개요 섹션으로 넘어가세요.사용하던 에디터에서 코드를 작성하기 원한다면다른 방법으로 사용하는 컴퓨터에 프로젝트를 설치할 수 있습니다.이 방법은 필수가 아닌 선택 사항입니다!더 많은 준비 작업이 필요하지만 에디터의 편리함을 누리며 공부할 수 있습니다.만약 이 방법으로 공부하기를 원한다면 필요한 단계들이 있습니다.  1. 설치된 Node.js가 최신 버전인지 확인해보세요.2. 새로운 프로젝트를 생성하기 위해 설치 방법을 따르세요.$ npm install -g create-react-app$ create-react-app my-app3. 새 프로젝트의 src/ 폴더에 있는 모든 파일들을 삭제해주세요. (폴더 안의 내용만 삭제하되 폴더는 삭제하지 마세요)$ cd my-app$ rm -f src/*4. 이 CSS 코드를 src/ 폴더에 index.css 파일로 추가해주세요.5. 이 JS 코드를 src/ 폴더에 index.js 파일로 추가해주세요.6. src/ 폴더에 있는 index.js의 최상단에 아래 세 줄을 추가해주세요.import React from 'react';import ReactDOM from 'react-dom';import './index.css';이제 프로젝트 폴더에서 npm start 명령어를 실행하고 브라우저에서 http://localhost:3000 를 여세요. 빈 틱택토 필드를 볼 수 있습니다.에디터에서 문법 하이라이팅 설정을 하고 싶다면 이 문서를 따르세요.도와주세요! 막히는 부분이 있어요!막히는 부분이 생겼다면 지원하는 커뮤니티를 확인해보세요. 특히 Reactiflux chat은 빠르게 도움을 받을 수 있는 좋은 방법입니다. 어떤 커뮤니티에서도 필요한 대답을 듣지 못했다면 이슈를 제출하세요. 우리가 도와드립니다.다 끝났으면 시작해봅시다!개요React란 무엇인가요?React는 유저 인터페이스 구현을 위한 선언적이고 효율적이며 유연한 JavaScript 라이브러리입니다.React는 여러 종류의 컴포넌트들을 가지고 있지만 우리는 React.Component의 서브클래스를 사용하여 시작할 것입니다.  class ShoppingList extends React.Component {  render() {    return (            Shopping List for {this.props.name}                Instagram         WhatsApp         Oculus                );  }}// Example usage:XML과 비슷한 재밌는 태그들을 사용할 것입니다. 작성한 컴포넌트는 React에게 무엇을 랜더링하고 싶은지 알려줍니다. 그러면 React는 데이터가 변경될 때 올바른 컴포넌트들을 업데이트하고 랜더링합니다.여기에서 ShoppingList는 React 컴포넌트 클래스 혹은 React 컴포넌트 타입입니다. 하나의 컴포넌트는 props라 불리는 파라미터를 사용하고, render 메서드를 통해 표시할 뷰 계층 구조를 반환합니다.render 메서드는 랜더링하길 원하는 내용을 반환하면 React는 그 내용을 가져와 스크린에 랜더링합니다. 특히 render는 랜더링할 간단한 내용인 React 엘리먼트를 반환합니다. 대부분의 React 개발자들은 이 구조를 더 쉽게 작성할 수 있게 해주는 JSX라는 특별한 문법을 사용합니다.라 쓰면 빌드 시 React.createElement('div')로 변환됩니다. 위의 코드는 아래의 코드와 동일합니다.  return React.createElement('div', {className: 'shopping-list'},  React.createElement('h1', /* ... h1 children ... */),  React.createElement('ul', /* ... ul children ... */));전체 코드는 여기에서 볼 수 있습니다.createdElement()에 대해 더 많은 내용이 궁금하다면 API reference 에 자세한 설명이 있습니다. 튜토리얼에서는 createdElement()를 직접적으로 사용하지 않습니다. 대신 JSX를 사용할 것입니다.JSX에서는 중괄호 안에 JavaScript 문법을 사용할 수 있습니다. 각 React 엘리먼트는 변수에 저장하거나 프로그램에 여기저기에 전달할 수 있는 실제 JavaScript 객체입니다.ShoppingList 컴포넌트는 내장된 DOM 컴포넌트만 랜더링하지만  코드를 작성하여 커스텀 React 컴포넌트를 쉽게 구성할 수 있습니다. 각 컴포넌트는 캡슐화되어 독립적으로 동작할 수 있습니다. 이때문에 간단한 컴포넌트들로 복잡한 UI를 구현할 수 있습니다.시작하기시작 코드를 가지고 시작해봅시다.이 코드는 우리가 구현할 틱택토 게임의 틀을 가지고 있습니다. 필요한 스타일들을 준비해두었기 때문에 JavaScript만 신경쓰면 됩니다.세 가지 컴포넌트로 구성되어 있습니다.- Square- Board- GameSquare 컴포넌트는 하나의 <button>을 랜더링합니다. Board 컴포넌트는 9개의 사각형을 랜더링합니다. Game 컴포넌트는 나중에 우리가 채워 넣어야 할 공백이 있는 하나의 보드를 랜더링합니다. 지금 이 컴포넌트들은 아무런 동작도 하지 않습니다.props를 통해 데이터 전달하기본격적으로 시작하기 위해 Board 컴포넌트에서 Square 컴포넌트로 데이터를 전달해봅시다.Board의 renderSquare 메서드에서 Square 컴포넌트 prop에 value 값을 전달하도록 코드를 변경해주세요.  class Board extends React.Component {  renderSquare(i) {    return ;  }value 값을 보여주기 위해 Square 컴포넌트의 render 메서드 안의 코드 {/* TODO */}를 {this.props.value}로 변경해주세요.  class Square extends React.Component {  render() {    return (      <button className="square">        {this.props.value}      </button>    );  }}변경 전:변경 후: 랜더링된 결과에서는 각 사각형 안에 숫자가 위치합니다.지금까지의 코드는 이곳에서 볼 수 있습니다.대화형 컴포넌트클릭 시 "X"로 채워지는 Square 컴포넌트를 만들어봅시다. Square의 render() 함수에서 반환된 버튼 태그를 다음과 같이 변경해주세요.  class Square extends React.Component {  render() {    return (      <button className="square" onClick={() => alert('click')}>        {this.props.value}      </button>    );  }}이제 사각형을 클릭하면 브라우저에서 알럿창이 뜨는걸 확인할 수 있습니다.새로운 JavaScript 문법인 화살표 함수를 사용하였습니다. onClick prop에 함수를 전달하였습니다. onClick={alert('click')} 코드를 작성하고 버튼을 클릭하면 알럿창 대신 경고가 뜨게됩니다.React 컴포넌트는 생성자에서 this.state를 설정하여 상태를 가질 수 있습니다. 상태는 각 컴포넌트마다 가지고 있습니다. 사각형의 현재 value 값을 상태에 저장하고 클릭할 때 바뀌도록 만들어봅시다.먼저 상태를 초기화하기 위해 클래스에 생성자를 추가해주세요.  class Square extends React.Component {  constructor(props) {    super(props);    this.state = {      value: null,    };  }  render() {    return (      <button className="square" onClick={() => alert('click')}>        {this.props.value}      </button>    );  }}JavaScript 클래스에서 서브클래스의 생성자를 정의할 때 super(); 메서드를 명시적으로 호출해줘야 합니다.Square의 render 메서드에서 현재 상태의 value 값을 표시하고 클릭할 때 바뀌도록 수정해주세요.- <button> 태그 안의 this.props.value 를 this.state.value로 변경해주세요.- () => alert() 이벤트 핸들러를 () => this.setState({value: 'X'})로 변경해주세요.<button> 태그는 다음과 같습니다.  class Square extends React.Component {  constructor(props) {    super(props);    this.state = {      value: null,    };  }  render() {    return (      <button className="square" onClick={() => this.setState({value: 'X'})}>        {this.state.value}      </button>    );  }}this.setState가 호출될 때마다 컴포넌트가 업데이트되므로 업데이트된 상태가 전달되어 React가 이를 병합하고 하위 컴포넌트와 함께 다시 랜더링합니다. 컴포넌트가 랜더링될 때 this.state.value는 'X'가 되어 그리드 안에 X가 보이게 됩니다.이제 사각형을 클릭하면 그 안에 X가 표시됩니다.지금까지의 코드는 이곳에서 볼 수 있습니다.개발자 도구크롬과 파이어폭스의 React 개발자 도구 확장 프로그램은 React 컴포넌트 트리를 브라우저의 개발자 도구 안에서 검사할 수 있게 해줍니다.트리 안의 컴포넌트들의 props와 상태를 검사할 수 있습니다.설치 후 페이지에서 검사하길 원하는 컴포넌트를 오른쪽 클릭하고 "Inspect"를 클릭하여 개발자 도구를 열면 오른쪽 마지막 탭에 React 탭이 보입니다.CodePen을 사용하여 이 확장 프로그램을 동작시키고 싶다면 추가적으로 필요한 작업들이 있습니다.1. 로그인 혹은 회원가입을 하고 이메일을 인증받으세요.2. "Fork" 버튼을 클릭하세요.3. "Change View"를 클릭하고 "Debug mode"를 선택하세요.4. 새롭게 열린 탭에서 React 탭이 있는 개발자 도구를 볼 수 있습니다.상태 들어올리기이제 틱택토 게임을 위한 기본 블록들이 있습니다. 하지만 아직 각 Square 컴포넌트 안에 상태들이 캡슐화되어 있습니다. 더 원활하게 동작하는 게임을 만들기 위해 한 플레이어가 게임에서 이겼는지를 확인하고 사각형 안에 X와 O를 번갈아 표시해야 합니다. 누가 게임에서 이겼는지 확인하기 위해 Square 컴포넌트들을 쪼개지 않고 한 장소에서 9개의 사각형의 value 값을 모두 가지고 있어야 합니다.Board가 각 Square의 현재 상태가 무엇인지만 확인해야 한다고 생각할 수도 있습니다. 이 방법은 기술적으로 React에서 가능하기는 하나 코드를 이해하기 어렵고 불안정하고 리팩토링하기 힘들게 만듭니다.각 Square에 상태를 저장하는 대신에 Board 컴포넌트에 이 상태를 저장하는 것이 가장 좋은 방법입니다. 이 Board 컴포넌트는 이전에 각 사각형에 인덱스를 표시한 방법과 동일한 방법으로 무엇을 표시할지 각 Square에게 알릴 수 있습니다.여러 하위 컴포넌트로부터 데이터를 모으거나 두 개의 하위 컴포넌트들이 서로 통신하기를 원한다면 상위 컴포넌트 안으로 상태를 이동시키세요. 상위 컴포넌트는 props를 통해 하위 컴포넌트로 상태를 전달해줄 수 있습니다. 그러면 하위 컴포넌트들은 항상 하위 컴포넌트나 상위 컴포넌트와 동기할 수 있습니다.이와 같이 상태를 상위 컴포넌트로 들어올리는 것은 React 컴포넌트들을 리팩토링할 때 가장 많이 사용하는 방법입니다. 이 기회를 통해 연습해봅시다. Board에 생성자를 추가하고 9개의 사각형과 일치하는 9개의 null을 가진 배열을 포함한 상태로 초기화하세요.  class Board extends React.Component {  constructor(props) {    super(props);    this.state = {      squares: Array(9).fill(null),    };  }  renderSquare(i) {    return ;  }  render() {    const status = 'Next player: X';    return (             {status}                 {this.renderSquare(0)}         {this.renderSquare(1)}          {this.renderSquare(2)}                        {this.renderSquare(3)}          {this.renderSquare(4)}          {this.renderSquare(5)}                        {this.renderSquare(6)}          {this.renderSquare(7)}          {this.renderSquare(8)}                );  }}나중에 이것을 다음과 같이 생긴 보드로 채울 예정입니다.  [  'O', null, 'X',  'X', 'X', 'O',  'O', null, null,]현재 Board의 renderSquare 메서드는 다음과 같습니다.    renderSquare(i) {    return ;  }Square에 value prop를 전달하도록 수정하세요.    renderSquare(i) {    return ;  }지금까지의 코드는 이곳에서 볼 수 있습니다.이제 우리는 사각형이 클릭되면 발생할 변경 사항을 구현해야 합니다. Board 컴포넌트는 어떤 사각형이 채워졌는지 저장하고 있습니다. 그렇기 때문에 Square가 Board가 가지고 있는 상태로 업데이트할 방법이 필요합니다. 사각형의 컴포넌트 상태가 각자 정의되고 있기 때문에 Board가 Square의 상태를 가지고올 수 없습니다.보통의 패턴은 사각형이 클릭될 때 호출되는 함수를 Board로부터 Square에 전달하는 것입니다. Board 안의 renderSquare를 다시 변경해봅시다.    renderSquare(i) {    return (              value={this.state.squares[i]}        onClick={() => this.handleClick(i)}      />    );  }가독성을 위해 리턴 안의 요소들을 여러 줄로 나누고, 괄호를 추가하여 JavaScript가 세미콜론 없이 코드를 마무리하도록 했습니다.Board에서 Square로 value와 onClick 두 개의 props를 전달합니다. onClick Square의 rennder에 있는 this.state.value 를 this.props.value로 변경하세요.- Square의 rennder에 있는 this.state.value 를 this.props.value로 변경하세요.- Square의 rennder에 있는 this.setState() 를 this.props.onClick()로 변경하세요.- 더이상 각 Square가 상태를 가지지 않도록 Square에 정의한 constructor를 삭제하세요.모든 변경 사항을 구현한 Square 컴포넌트는 다음과 같습니다.  class Square extends React.Component {  render() {    return (      <button className="square" onClick={() => this.props.onClick()}>        {this.props.value}      </button>    );  }}이제 사각형이 클릭될 때 Board로부터 전달되는 onClick 함수를 호출합니다. 어떤 일이 일어나는지 되짚어봅시다.1. 내장된 DOM <button> 컴포넌트의 onClick prop는 React에게 클릭 이벤트 리스너를 설정하라고 알립니다.2. 버튼이 클릭될 때 React는 Square의 render() 메서드 안에 정의된 onClick 이벤트 핸들러를 호출합니다.3. 이 이벤트 핸들러는 this.props.onClick()을 호출합니다. Square의 props는 Board에서 명시한 것입니다.4. Board는 onClick={() => this.handleClick(i)}을 Square에 전달하고, 호출될 때 Board의 this.handleClick(i)가 동작합니다.5. Board에 있는 handleClick() 메서드는 아직 정의되지 않았으므로 코드는 오류가 발생합니다DOM <button> 엘리멘트의 onClick 속성이 React와는 다른 의미를 가집니다. Square의 onClick prop나 Board의 handleClick 메서드와는 다릅니다. React 애플리케이션에서는 속성에 on* 이름을 사용하고 핸들러 메서드에 handle* 을 사용하여 처리하는 것이 일반적입니다.사각형을 클릭해봅시다. handleClick을 아직 정의하지 않았으로 에러가 발생합니다. Board 클래스에handleClick 메서드를 추가해봅시다.  class Board extends React.Component {  constructor(props) {    super(props);    this.state = {      squares: Array(9).fill(null),    };  }  handleClick(i) {    const squares = this.state.squares.slice();    squares[i] = 'X';    this.setState({squares: squares});  }  renderSquare(i) {    return (              value={this.state.squares[i]}        onClick={() => this.handleClick(i)}      />    );  }  render() {    const status = 'Next player: X';    return (             {status}                 {this.renderSquare(0)}          {this.renderSquare(1)}          {this.renderSquare(2)}                        {this.renderSquare(3)}          {this.renderSquare(4)}          {this.renderSquare(5)}                        {this.renderSquare(6)}          {this.renderSquare(7)}          {this.renderSquare(8)}                );  }}지금까지의 코드는 이곳에서 볼 수 있습니다.이미 있는 배열을 수정하는 대신 squares 배열을 복사하기 위해 .slick()를 호출합니다. 왜 immutability이 중요한지 알고 싶다면 이 섹션으로 이동해주세요.이제 사각형을 클릭하여 다시 사각형을 채울 수 있어야 하지만 상태가 각 Square가 아닌 Board 컴포넌트에 저장되어 있어 게임을 계속 구현해나가야 합니다. Board의 상태가 변경될 때마다 Square 컴포넌트들은 자동으로 다시 랜더링됩니다.Square은 더 이상 각 상태를 유지하지 않습니다. 이들은 상위 Board 컴포넌트로부터 데이터를 전달받고, 클릭될 때 알립니다. 우리는 이 제어된 컴포넌트 같은 컴포넌트들을 호출합니다.왜 immutability가 중요할까전의 예제 코드에서 이미 존재하는 배열을 수정하지 않고 변경 사항을 반영하기 위해 squares 배열을 .slice()연산자를 사용하여 복사하였습니다. 이는 무엇을 의미하며 왜 이 컨셉이 중요할까요.mutation을 사용한 데이터 변경  var player = {score: 1, name: 'Jeff'};player.score = 2;// Now player is {score: 2, name: 'Jeff'}mutation을 사용하지 않은 데이터 변경  var player = {score: 1, name: 'Jeff'};var newPlayer = Object.assign({}, player, {score: 2});// Now player is unchanged, but newPlayer is {score: 2, name: 'Jeff'}// Or if you are using object spread syntax proposal, you can write:// var newPlayer = {...player, score: 2};mutation을 사용하지 않더라도(기본 데이터를 변경하여도) 결과적으로는 다를게 없습니다. 하지만 컴포넌트와 전체 애플리케이션의 성능을 향상시키는 장점이 있습니다.쉽게 Undo/Redo와 시간 여행하기immutability는 이 복잡한 기능들을 훨씬 더 쉽게 구현할 수 있게 해줍니다. 예를 들어 이 튜토리얼에서 우리는 게임의 다른 단계들 사이에 시간 여행을 구현할 것입니다. 데이터 변경을 피하면 우리가 이전 버전의 데이터를 계속 참조할 수 있게 해주고 원할 때 변경할 수 있게 해줍니다.변경 사항 트래킹하기변경되는 객체가 변경 사항이 있는지 아는 방법은 변경 사항이 객체로 만들어지기 때문에 복잡합니다. 그러면 이전 버전을 복사하기 위해 전체의 객체 트리를 현재 버전과 비교하고 각 변수와 값들을 비교해야 합니다. 이 과정은 갈수록 복잡해집니다.immutable 객체가 변경 사항이 있는지 아는 방법은 쉬워집니다. 만약 참조되고 있는 객체가 이전과 다르다면 이 객체는 변경된 것입니다. 이게 끝입니다.React에서 언제 다시 랜더링할지 결정하기React에서 immutability의 가장 큰 장점은 간단한 순수 컴포넌트들이 다시 랜더링될 때를 결정하기 쉽다는 점입니다.shouldComponentUpdate()에 대해 더 배우고 싶고 어떻게 순수 컴포넌트들을 성능 최적화 할 수 있는지 알고 싶다면 이 글을 보세요.함수 컴포넌트우리는 생성자를 지웠습니다. 사실 React는 render 메서드만으로 구성된 Square와 같은 컴포넌트 타입을 위해 함수 컴포넌트라 불리는 간단한 문법을 지원합니다. React.Component를 확장한 클래스를 정의하는 것보다 간단하게 props를 가져오고 랜더링 해야할 것을 반환하는 함수를 작성하는 것이 좋습니다.다음과 같은 함수를 사용해 Square 클래스를 변경하세요.  function Square(props) {  return (    <button className="square" onClick={props.onClick}>      {props.value}    </button>  );}여기서는 this.props를 둘 다 props로 바꿔야 합니다. 애플리케이션에 있는 여러 컴포넌트들은 함수 컴포넌트로 구현할 수 있습니다. 함수 컴포넌트는 더 쉽게 작성할 수 있고 React가 더 효율적으로 최적화할 수 있습니다.코드를 깔끔하게 만들면서 onClick={() => props.onClick()}을 onClick={props.onClick}으로 바꿨습니다. 함수를 전달하는 것은 이 코드만으로 분합니다. onClick={props.onClick()}는props.onClick을 호출하기 때문에 동작하지 않습니다.지금까지의 코드는 이곳에서 보실 수 있습니다.변화 가져오기지금 우리의 게임의 단점은 오로지 X만 플레이할 수 있다는 점입니다. 고쳐봅시다.기본적으로 첫 이동을 'X'가 되도록 설정해봅시다. Board 생성자에서 초기 상태를 수정해주세요.  class Board extends React.Component {  constructor(props) {    super(props);    this.state = {      squares: Array(9).fill(null),      xIsNext: true,    };  }이동할 때마다 xIsNext의 불린 값은 바뀌면서 상태에 저장되어야 합니다. Board의 handleClick 함수를xIsNext 값이 바뀔 수 있도록 수정해봅시다.    handleClick(i) {    const squares = this.state.squares.slice();    squares[i] = this.state.xIsNext ? 'X' : 'O';    this.setState({      squares: squares,      xIsNext: !this.state.xIsNext,    });  }이제 X와 O가 순서대로 번갈아 나타납니다. 다음에 무엇이 표시될 때 보여주기 위해 Board의 render에서 "status" 텍스트를 바꿔봅시다.    render() {    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');    return (      // the rest has not changed변경 사항을 적용한 Board 컴포넌트는 다음과 같습니다.  class Board extends React.Component {  constructor(props) {    super(props);    this.state = {      squares: Array(9).fill(null),      xIsNext: true,    };  }  handleClick(i) {    const squares = this.state.squares.slice();    squares[i] = this.state.xIsNext ? 'X' : 'O';    this.setState({      squares: squares,      xIsNext: !this.state.xIsNext,    });  }  renderSquare(i) {    return (              value={this.state.squares[i]}        onClick={() => this.handleClick(i)}      />    );  }  render() {    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');    return (             {status}                 {this.renderSquare(0)}          {this.renderSquare(1)}          {this.renderSquare(2)}                        {this.renderSquare(3)}          {this.renderSquare(4)}          {this.renderSquare(5)}                        {this.renderSquare(6)}          {this.renderSquare(7)}          {this.renderSquare(8)}                );  }}지금까지의 코드는 이곳에서 볼 수 있습니다.승자 알려주기언제 게임에서 이기는지 표시해봅시다. 파일 맨 하단에 헬퍼 함수를 추가해주세요.  function calculateWinner(squares) {  const lines = [    [0, 1, 2],    [3, 4, 5],    [6, 7, 8],    [0, 3, 6],    [1, 4, 7],    [2, 5, 8],    [0, 4, 8],    [2, 4, 6],  ];  for (let i = 0; i < lines>    const [a, b, c] = lines[i];    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {      return squares[a];    }  }  return null;}Board의 render 함수에서 누가 게임에서 이겼는지 확인할 수 있도록 호출할 수 있습니다. 또 누군가 이겼을 떄 "Winner: [X/O]" 상태 텍스트를 표시할 수 있습니다.Board의 render에서 status를 선언을 수정해주세요.    render() {    const winner = calculateWinner(this.state.squares);    let status;    if (winner) {      status = 'Winner: ' + winner;    } else {      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');    }    return (      // the rest has not changedBoard에서 handleClick을 일찍 반환하여 이미 누군가 이긴 게임에서 클릭하거나 이미 칠해진 사각형을 클릭하는 경우 무시하도록 변경할 수 있습니다.축하합니다! 틱택토 게임을 완성하셨습니다! 이제 React의 기초를 알았습니다. 여기서 진짜 승자는 여러분입니다.지금까지의 코드는 이곳에서 볼 수 있습니다.히스토리 저장하기보드의 이전 상태로 되돌려 이전 상태가 표시되도록 만들어봅시다. 이동이 있을때마다 새 squares 배열을 만들었습니다. 덕분에 이전 상태의 보드를 쉽게 저장할 수 있습니다.상태에 이와 같은 객체를 저장해봅시다.  history = [  {    squares: [      null, null, null,      null, null, null,      null, null, null,    ]  },  {    squares: [      null, null, null,      null, 'X', null,      null, null, null,    ]  },  // ...]우리는 이동 리스트를 표시하여 응답할 수 있는 더 수준 높은 Game 컴포넌트를 만들고 싶습니다. 그래서 Square 상태를 Board로 들어올린 것처럼 Board의 상태를 Game으로 들어올려 최 상위 레벨에서 필요한 모든 정보를 저장해봅시다.먼저 생성자를 추가해 Game의 초기 상태를 설정해주세요.  class Game extends React.Component {  constructor(props) {    super(props);    this.state = {      history: [{        squares: Array(9).fill(null),      }],      xIsNext: true,    };  }  render() {    return (                                              {/* status */}         {/* TODO */}                );  }}그 다음 Board를 수정하여 props를 거쳐 squares를 가져오고 이전에 Square에서 했던 것처럼 Game에서 지정한 onClick prop를 만들어줍시다. 각 사각형의 위치를 클릭 핸들러로 전달하여 어떤 사각형이 클릭되었는지 알 수 있습니다. 필요한 변경 사항은 다음과 같습니다.- Board의 constructor를 삭제하세요.- Board의 renderSquare에 있는 this.state.squares[i]를 this.props.sqaures[i]로 대체하세요.- Board의 renderSquare에 있는 this.handleClick(i)를 this.props.onClick(i)로 대체하세요.변경 사항을 반영한 Board 컴포넌트는 다음과 같습니다.  class Board extends React.Component {  handleClick(i) {    const squares = this.state.squares.slice();    if (calculateWinner(squares) || squares[i]) {      return;    }    squares[i] = this.state.xIsNext ? 'X' : 'O';    this.setState({      squares: squares,      xIsNext: !this.state.xIsNext,    });  }  renderSquare(i) {    return (              value={this.props.squares[i]}        onClick={() => this.props.onClick(i)}      />    );  }  render() {    const winner = calculateWinner(this.state.squares);    let status;    if (winner) {      status = 'Winner: ' + winner;    } else {      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');    }    return (             {status}                 {this.renderSquare(0)}          {this.renderSquare(1)}          {this.renderSquare(2)}                        {this.renderSquare(3)}          {this.renderSquare(4)}          {this.renderSquare(5)}                        {this.renderSquare(6)}          {this.renderSquare(7)}          {this.renderSquare(8)}                );  }}Game의 render는 히스토리 전체를 보고 게임 상태를 계산하여 가져올 수 있어야 합니다.  render() {    const history = this.state.history;    const current = history[history.length - 1];    const winner = calculateWinner(current.squares);    let status;    if (winner) {      status = 'Winner: ' + winner;    } else {      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');    }    return (                                  squares={current.squares}            onClick={(i) => this.handleClick(i)}          />                        {status}         {/* TODO */}                );  }Game에 상태를 랜더링하고 있기 때문에{status}를 지우고 Board의 render 함수로부터 상태를 계산하는 코드를 지울 수 있습니다.  render() {    return (                      {this.renderSquare(0)}          {this.renderSquare(1)}          {this.renderSquare(2)}                        {this.renderSquare(3)}          {this.renderSquare(4)}          {this.renderSquare(5)}                        {this.renderSquare(6)}          {this.renderSquare(7)}          {this.renderSquare(8)}                );  }그 다음 Board에서 Game으로 handleClick 메서드를 옮겨야 합니다. Board 클래스에서 잘라내기를 하고 Game 클래스로 붙여넣을 수 있습니다.Game 상태는 다르기 때문에 수정해야 할 것이 조금 있습니다. Game의 handleClick은 히스토리 항목을 연결하여 새로운 배열을 만들어 스택에 푸시해야 합니다.  handleClick(i) {    const history = this.state.history;    const current = history[history.length - 1];    const squares = current.squares.slice();    if (calculateWinner(squares) || squares[i]) {      return;    }    squares[i] = this.state.xIsNext ? 'X' : 'O';    this.setState({      history: history.concat([{        squares: squares,      }]),      xIsNext: !this.state.xIsNext,    });  }여기에서 Board는 renderSquare와 render만 필요합니다. 상태 초기화와 클릭 핸들러는 둘 다 Game에서 동작합니다.지금까지의 코드는 이곳에서 보실 수 있습니다.이동 표시하기지금까지 게임에서 진행된 이동을 표시해봅시다. 이전에 React 컴포넌트가 클래스로 JS 객체이고 그 덕에 데이터를 저장하고 전달할 수 있다고 배웠습니다. React에서 여러 아이템들을 랜더링하기 위해 React 요소의 배열을 전달했습니다. 배열을 빌드하는 가장 흔한 방법은 데이터 배열에서 map을 이용하는 것입니다. Game의 render 메서드에서 해봅시다.    render() {    const history = this.state.history;    const current = history[history.length - 1];    const winner = calculateWinner(current.squares);    const moves = history.map((step, move) => {      const desc = move ?        'Go to move #' + move :        'Go to game start';      return (                 <button onClick={() => this.jumpTo(move)}>{desc}</button>             );    });    let status;    if (winner) {      status = 'Winner: ' + winner;    } else {      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');    }    return (                                 squares={current.squares}            onClick={(i) => this.handleClick(i)}          />                        {status}         {moves}                );  }지금까지의 코드는 이곳에서 볼 수 있습니다.히스토리의 각 단계에서 <button>이 있는 리스트 아이템을 만들었습니다. 이 리스트 아이템은 우리가 곧 구현할 클릭 핸들러를 가지고 있습니다. 코드에서 다음과 같은 경고 메시지와 함께 게임에서 만들어지는 이동 목록을 볼 수 있습니다.Warning: Each child in an array or iterator should have a unique “key” prop. Check the render method of “Game”.경고: 배열이나 이터레이터에 있는 각 자식은 유니크 "key" prop을 가져야한다. "Game"의 render 메서드를 확인해보세요.이 경고의 의미가 무엇인지 얘기해봅시다.Keys아이템 리스트를 랜더링할때 React는 항상 리스트에 있는 각 아이템에 대한 정보를 저장합니다. 만약 상태를 가진 컴포넌트를 랜더링한다면 컴포넌트가 어떻게 실행되는지와 관계없이 상태는 저장 되어야 하고 React는 네이티브 뷰의 뒤에 참고할 것을 저장한다.리스트를 업데이트할 때 React는 무엇을 바꿀지 결정해야 합니다. 리스트에 아이템들을 추가하고, 지우고, 재배열하고, 수정할 수 있습니다.이 코드가 아래의 코드로 변경된다고 상상해봅시다.  Alexa: 7 tasks leftBen: 5 tasks leftBen: 9 tasks leftClaudia: 8 tasks leftAlexa: 5 tasks left사람의 눈에는 Alexa와 Ben의 자리가 바뀌고 Claudia가 추가된 것처럼 보인다. 하지만 React는 단순한 컴퓨터 프로그램이므로 여러분의 의도를 알지 못합니다. React는 리스트의 각 요소에서 key 속성을 지정해달라고 요청합니다. 문자열은 형제로부터 각 컴포넌트들을 구분합니다. 이 경우에 alexa, ben, claudia는 구분할 수 있는 키가 됩니다. 만약 아이템들이 데이터베이스의 객체와 일치시켜야 한다면 데이터베이스 ID을 사용하세요.{user.name}: {user.taskCount} tasks leftkey는 React에서 제공되는 특별한 속성입니다(ref에서 더 확장된 기능). 엘리먼트가 만들어질때 React는 key 속성을 가져오고 반환된 엘리먼트에 직접적으로 key를 저장합니다. key가 props의 한 부분으로 보일지라도 이것은 this.props.key로 참조할 수 없습니다. React는 어떤 하위 엘리먼트가 수정될지 결정하는 동안 알아서 key를 사용합니다. 컴포넌트가 자신의 키를 알 수 있는 방법은 없습니다.리스트가 랜더링될 때 React는 새로운 버전의 각 엘리먼트를 가져오고 이전 리스트에서 매칭되는 키를 가진 것을 찾습니다. key가 세트에 추가될 때 컴포넌트는 만들어집니다. 키가 삭제될 때 컴포넌트는 소멸됩니다. 키들은 React가 각 요소를 구별할 수 있도록하여 다시 랜더링하는 것을 무시하고 상태를 유지할 수 있게 합니다. 만약 컴포넌트의 키를 바꾼다면 완전히 지운 후 새롭게 생성됩니다.동적으로 리스트를 빌드할 때마다 적당한 키를 할당할 것을 강력 추천합니다. 만약 적당한 키를 가지지 못한다면 이를 위해 데이터를 재구성하여야 할지도 모릅니다.특정한 키를 구분하지 못한다면 React는 경고를 주고 배열 인덱스를 키로 사용합니다. 이는 올바른 선택이 아닙니다. 만약 리스트에 있는 엘리먼트들을 정렬하거나 리스트에 있는 버튼을 통해 지우거나 추가하면 명시적으로 key={i}를 전달하는 방법을 사용한다면 경고를 표시하지는 않지만 동일한 문제를 발생시키므로 대부분의 경우에 추천하지 않습니다.컴포넌트의 키가 전부 다를 필요는 없지만 관련있는 형제들 사이에서는 유니크해야 합니다.시간 여행 실행하기이동 리스트를 위해 우리는 각 단계에서 유니크 ID를 가졌습니다. Game의 render 메서드에서 키는로 추가하면 경고는 표시되지 않습니다.    const moves = history.map((step, move) => {      const desc = move ?        'Go to move #' + move :        'Go to game start';      return (                 <button onClick={() => this.jumpTo(move)}>{desc}</button>             );    });지금까지의 코드는 이곳에서 보실 수 있습니다.아직 junmTo가 정의되지 않았기 때문에 이동 버튼을 클릭하면 에러가 발생합니다. 지금 표시된 단계가 무엇인지 알기 위해 Game 상태에 새로운 키를 추가해봅시다.먼저Game의 constructor에  stepNumber: 0를 추가해주세요.class Game extends React.Component {  constructor(props) {    super(props);    this.state = {      history: [{        squares: Array(9).fill(null),      }],      stepNumber: 0,      xIsNext: true,    };  }그 다음 각 상태를 업데이트하기 위해 Game의 jumpTo 메서드를 정의해봅시다. 이 메서드에서는 xIsNext를 업데이트하고, 이동의 인덱스가 짝수라면 xIsNext를 true로 설정합니다.Game 클래스에jumpTo 메서드를 추가해주세요.handleClick(i) {    // this method has not changed  }  jumpTo(step) {    this.setState({     stepNumber: step,      xIsNext: (step % 2) === 0,    });  }  render() {    // this method has not changed  }Game handleClick에 상태를 업데이트 하기위해 stempNumber:history.length를 추가하여 새로운 이동이 있을 때마다  stepNumber를 업데이트 합니다. 현재 보드의 상태를 읽을 때 handleClick이 stepNumber라고 보고 클릭하는 시간대로 상태를 되돌릴 수 있습니다.  handleClick(i) {    const history = this.state.history.slice(0, this.state.stepNumber + 1);    const current = history[history.length - 1];    const squares = current.squares.slice();    if (calculateWinner(squares) || squares[i]) {      return;    }    squares[i] = this.state.xIsNext ? 'X' : 'O';    this.setState({      history: history.concat([{        squares: squares      }]),      stepNumber: history.length,      xIsNext: !this.state.xIsNext,    });  }이제 히스토리의 각 단계를 알기 위해 Game의 render를 수정할 수 있습니다.  render() {    const history = this.state.history;    const current = history[this.state.stepNumber];    const winner = calculateWinner(current.squares);    // the rest has not changed지금까지의 코드는 이곳에서 보실 수 있습니다.이제 이동 버튼을 클릭하면 보드는 즉시 그때 표시된 게임으로 변경됩니다.마무리틱택토 게임을 플레이 해보세요.- 틱택토 게임을 플레이 해보세요.- 한 명의 플레이어가 게임에서 이길 때를 이를 알려줍니다.- 게임이 진행되는 동안 이동 기록이 저장됩니다.- 게임 보드의 에전 버전을 표시하기 위해 시간을 되돌릴 수 있습니다.잘 동작하네요! React가 어떻게 동작하는지 잘 아셨기를 바랍니다.최종 결과물은 여기에서 확인하세요.시간이 더 있거나 새로운 스킬들을 연습해보고 싶다면 해볼 수 있는 몇 가지 아이디어가 있습니다. 점점 더 어려운 순으로 배치해두었습니다.1. 움직임 리스트에서 (col, row) 형태에 각 움직임 위치를 표시하세요.2. 움직임 리스트의 선택된 아이템을 볼드처리하세요.3. 하드 코딩한 것들 대신 사각형을 두 개의 루프를 사용하여 Board를 다시 작성하세요.4. 오름차순 혹은 내림차순 뭐든지 움직임을 정렬하는 버튼을 추가해보세요.5. 누군가 이겼을 때 무엇 때문에 이겼는지 세 개의 사각형을 하이라이트하세요.튜토리얼이 진행되는 동안 우리는 엘리먼트, 컴포넌트, props, 상태를 포함한 React의 수많은 컨셉들을 다뤘습니다. 각 주제에 대한 깊은 설명을 원한다면 남은 문서를 확인하세요. 컴포넌트 정의에 대해 더 많이 배우고 싶다면 이 문서를 확인하세요.#트레바리 #개발자 #안드로이드 #앱개발 #Node.js #백엔드 #인사이트 #경험공유 #React #리액트 #리액트가이드 #한글 #번역 #문서번역
조회수 2865

GUI가이드라인 정의와 목적

S/W 개발자가 디자인대로 화면을 구현할 때, 어떻게 디자인 요소 위치를 잡아야 하는지 정확한 정보가 필요합니다. 이런 정보는 GUI 디자이너가 포토샵과 같은 디자인 툴을 사용하여 개발자가 사용 가능한 형태로 사이즈 정보와 리소스를 만들어 전달하는 작업을 GUI 가이드라인 제작 작업이라 합니다.GUI 가이드 문서 상에는 화면상에 표현되는 모든 GUI 요소들의 정보가 표시가 됩니다. 화면상의 위치 X/Y 좌표값, 디자인 요소의 폭/높이 사이즈 정보, 이미지 파일 리소스명, 폰트 타입, 폰트 크기 등 다양한 그래픽 요소의 정보를 정확하게 수치화 하여 기재한 것입니다.가이드 문서의 양식은 딱 정해진 틀은 없지만, 소위 대기업의 경우 표준 템플릿을 이용합니다. 단말 하나에 탑재되는 앱 별로 수십 벌의 문서를 제작하여 관리해 왔습니다. 현재 과도기적인 단계라 스케치(.sketch) 파일과 가이드라인 문서를 함께 운영하는 곳도 있을 정도입니다.기존에 GUI 가이드 문서 제작을 위해서는 아래와 같은 일련의 순서로 작업을 하였습니다.디자인 시안 작업 > 디자인 시안 확정 > 개발 가능성 리뷰 > 최종 수정 >GUI 가이드라인 문서 제작 & 이미지 파일 리소스 작업이 중에서 가이드 문서 제작 과정을 초점에 두고 살펴보면, GUI 디자이너가 직접 이미지를 자르고 위치와 크기 정보를 확인하여, 파워포인트 문서로 정보를 입력하는 일련에 단순 노가다를 반복적으로 진행하게 됩니다.대부분의 에이전시 신입 디자이너들이 중국집 요리사 탱크트리와 유사하게 최소 2년 정도 GUI 가이드라인 작업을 하고 난 뒤에 시안 디자인 작업을 참여할 수 있는 구조였습니다. 크리에이티브를 위해 디자인 작업에 시간을 일주일 중 3일을 쓰고, 4일은 가이드를 쳐야 할 정도의 노력과 시간이 드는 노동 집약적 작업이었습니다.이렇듯 GUI 가이드라인 문서 제작은 모든 디자인 요소 정보들을 일일이 확인한 후, 파워포인트로 옮겨 적어야 하는 야근의 헬게이트를 열어주는 대표적인 업무였습니다.디자인 완료 후 개발자에게 “디자인을 이렇게 구현해 주세요.” 라고 말하면 얼마나 쉽나요? 근래에는 야근의 대부분을 차지하는 이러한 업무들로부터 스케치 툴이 많은 디자이너를 구해준 셈입니다.업무의 프로세스상 디자이너가 가이드라인 문서와 이미지 리소스 파일들을 넘겨줘야 개발자들이 개발진행을 할 수 있기에 디자이너들은 타이트한 데드라인에 쫓기듯 업무할 수 밖에 없었습니다.이러다 보니, GUI 가이드라인 문서 제작 중 휴먼에러(크기 정보 오타, 이미지 파일 누락 등)로 개발자가 작업하던 도중 디자이너에게 가이드라인 문서 업데이트 요청을 해오는 경우가 매우 빈번했습니다. 또한, 대규모 프로젝트 일수록 가이드라인 문서, 이미지 리소스 파일, PSD 디자인 파일 등 관리해야 할 대상이 많아서 개발자와 디자이너 사이의 커뮤니케이션 빈도수도 잦아지고 많은 비용이 필요했습니다.비단 3년 전만해도 GUI 디자인을 개발자가 구현하기 위해 필요한 정보를 수천 페이지나 되는 파워포인트 문서로 전달했지만, 요즘은 스케치를 활용한 제플린이나 심플리 등과 같은 가이드 정보를 제공해주는 여러 서비스를 이용하여 가이드 문서 제작은 거의 하지 않고 있습니다. 조만간 가이드 문서가 완전히 사라지는 날이 오지 않을까 싶습니다.그 끝에 크래커나인이 일조하는 날이 오기를 바라며 글을 마칩니다.#에이치나인 #디자이너 #개발자 #협업툴 #크래커나인 #솔루션기업
조회수 2568

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

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

Humans of TODAIT : 안드로이드 천재 개발자 김범준을 만나다

‘Humans of TODAIT’의 네번째 주인공, 투데잇 안드로이드 개발자 김범준씨를 만나보았습니다. 투데잇의 천재 개발자로 불리는 그의 이야기를 함께 들어볼까요?(2017.08)Q. 자기소개 부탁드려요.안녕하세요! 투데잇에서 까칠남을 맡고 있는 안드로이드 개발자 김범준입니다. 퇴사자 인터뷰를 하게 되니, 정들었던 팀원분들과 헤어질 생각에 아쉽고 싱숭생숭하네요. (웃음) 작년 초 쯤 ‘SW 마에스트로’ 프로그램에서 만난 멘토님께서 제게 투데잇 안드로이드 개발자 자리를 추천해주신 덕분에 이렇게 투데잇과 인연이 닿게 되었어요. 사실 처음에는 큰 생각이 없었는데, 대표님과 팀장님을 만나보니 저와 코드도 잘 맞고 개발 쪽으로도 많이 배워볼 수 있을 것 같아서 그 날 바로 입사 결정을 내렸고, 지금은 퇴사를 앞두고 있네요.Q. 그렇게 좋은 투데잇을 떠나는 이유는 무엇인가요?원래 병특을 가야 했어요. 제가 군대를 아직 안 갔기 때문에, 군대 문제를 해결 해야 더 많은 기회도 생기고 지금 가지고 있는 마음의 짐 같은 것도 덜 수 있거든요. 아쉽게도 투데잇이 병특 산업기능요원지정업체가 아니어서 군대 문제를 해결하기 위해서는 퇴사할 수 밖에 없는 상황이에요. 사실 원래부터 군대 문제 때문에 잠시 동안만 일하기로 했던건데, 회사생활이 너무 만족스럽고 일이 즐거워서 계속 미루다가 이제서야 결정을 내렸네요. 지금도 많이 아쉬워요. 투데잇만한 회사 없거든요.Q. 팀 내에서 평소 자기계발을 많이 하는 것으로 유명한데, 혹시 자기계발 노하우가 있나요?사실 공부는 진짜 하는 것보다 시작하는 것이 어렵잖아요. 그래서 저는 일부러 저한테 강제성을 주는 편이에요. 매주 하는 동아리 활동이라든지 발표 기회를 만든다든지 관련 세미나를 참여한다든지 그런 일정이 생기면 자연스럽게 하게 되더라고요. 하면 또 잘하고 싶은 게 사람 마음이니까 자꾸 강제적으로 그런 기회를 만들죠.그리고 저는 일상에서 배울 수 있는 기회를 얻으려고 해요. 일하다가 힘들거나 머리가 잘 안 돌아갈 때 저장해둔 아티클을 보곤 하죠. 또 술마실 때도 같은 직업군의 친구들을 만나면 그런 얘기를 많이 하잖아요. 너 이거 시도해봤냐 어땠냐 이건 어떻게 하는거냐 같은 이야기요. 제가 주위 사람들에게 자극을 많이 받거든요. 책상 앞에 앉아서 하는 공부보다는 일상적 시간을 활용하고 뭔가를 준비하기 위한 공부의 자기계발을 하는 것 같아요.Q. 지난 1년을 돌아보는 의미에서, 개발자로서의 좌우명이나 철학이 있을까요?저는 어떤 일을 하든 명확한 근거가 있어야 한다고 생각해요. 커뮤니케이션에서도 그렇고 개발에 있어도 마찬가지예요. 내가 하는 일에 대한 충분한 이유가 있어야 하고 그게 코드에 녹아 있어야 해요.예를 들면, 같은 풍경을 보고 글을 쓸 때도 여러 방법이 있잖아요. 사람마다 글 쓰는 방법이 다르고. 그 방법을 선택한 데엔 저마다 이유가 있어요. 코드도 마찬가지예요. 어떤 기능을 개발할 때 그 기능을 구현할 수 있는 여러 방법이 있는데, 개발자라면 내가 만든 코드에 대해 내가 왜 이렇게 짰는지 다른 사람에게 자신 있게 말할 수 있는 개발자가 되어야 한다고 생각해요.저는 힙한 개발자가 되고 싶어요. 그러니까 최신 트렌드에 민감하고, 새로운 것에 도전하고 두려워 하지 않는 그런 개발자요. (웃음)Q. 힙한 개발자 멋지네요. 그렇다면 10년 후에는 무엇을 하고 싶은지 궁금한데요?제 꿈은 그냥 행복하게 사는거예요. (하하) 추상적인 이야기 같겠지만, 행복하게 살기 위해선 많은 것들이 필요하잖아요? 우리가 말하는 이상적인 행복이란 것은 돈, 인간관계, 사회적 직위, 건강과 같은 모든 박자가 잘 맞아 떨어졌을 때 이루어지는 행복이거든요. 그래서 저는 행복하기 위해서는 끊임없이 노력해야 한다고 생각해요. 장차 10년 후에 제가 뭘 하고 있을지는 모르지만, 지금 현재의 상황에서 제가 할 수 있는 최선의 선택을 하면서 열심히 단계적으로 이루어나가면, 10년 후에도 충분히 행복할 것 같아요. 저는 지금 행복하거든요. (웃음)Q. 일하다 보면 해결하기 힘든 난제를 만날 때가 있을 것 같은데, 그럴 땐 어떻게 극복하나요?내가 스트레스를 많이 받고 있다는 걸 깨달으면, 그냥 최대한 스트레스 받지 않으려고 해요. 그냥 뭐 하면 되지 라는 생각이죠. 하면 되지 하면서 하다보면 결국 되는 것 같아요. 어차피 해야 될 일인데, 스트레스 받으면서 하기 보다는 그냥 아무 생각 없이 열심히 하는 게 나으니까요. 만약에 제가 몰라서 못하고 있는 일이면 여러 사람들에게 물어보려고 하면서 어떻게든 해결하려고 하고요.Q. 그렇다면 투데잇에서 가장 만족스러운 결과물은 무엇인가요? 개인적으로 뿌듯하다거나 실제 반응이 좋았다거나 그런 것들이요!‘스탑워치’ 기능이 두 개 다 포함돼요. 이전 개발자가 스파게티 코드(엉망진창의 코드)로 만들어 놓았던 것이 있는데 그 코드를 제가 깔끔하게 다 수정했고, 계속 유저분들이 요청해주셨던 시간 잠금, 극강의 잠금 모드 같은 기능들을 추가해서 코드를 예쁘게 잘 만들어놓았거든요. 일단 제가 기발한 기능과 함께 코드를 예쁘게 잘 만들어냈다는 점에서 스스로도 만족을 했었고, 유저분들도 팀원분들도 좋은 피드백을 해주셔서 굉장히 좋았습니다.Q. 지금 이 글을 보고 계시는 스탑워치 기능 애용 유저분들께 한마디 해주세요!우선 잘 사용해주셔서 감사해요! 제가 만든 기능을 이용해 공부하시는 걸 보면, 저도 정말 큰 자부심을 느끼거든요. :) 다만, 아직 스탑워치 기능에 문제가 조금 있는 거로 알고 있어요. 약간 불편하더라도 이왕이면 둥글게 좋게 별 5점으로 리뷰 주시면! 저희와 의사소통하면서 함께 좋은 서비스 만들어 나갈 수 있을 것 같아요. 안 보는 것 같지만 투데잇 개발자 전체가 매일 열심히 읽고 있거든요. 정말 리뷰 하나에 울고 리뷰 하나에 웃습니다. 저희 투데잇 지금까지 사랑해주셨지만, 앞으로도 계속 사랑해주시면 감사하겠습니다. :)Q. 반대로 투데잇 안드로이드 개발에 있어 아쉬운 부분도 있을 것 같아요. 나 이거 진짜 욕심났다! 혹시 있을까요?음.. 저는 옛날에 있던 아키텍처를 일단 전부 바꾸고 싶어요. 최근에 꽂힌 아키텍쳐가 있는데, 그 아키텍쳐에 맞게 코드를 다 변경해보고 싶다는 욕심이 있거든요. 근데 그 아키텍쳐 특성상 현재 코드에서는 완전히 대대적인 수정이 들어가야되는데, 제가 남은 시간이 얼마 없어서 많이 수정을 못했죠. 우리가 좀 더 많은 시간이 있고 여유가 있었더라면 더 바꿔볼 수 있었을텐데 그런 부분들을 못한 게 조금 아쉬워요.“투데잇의 힘은 서로에 대한 믿음인 것 같아요”Q. 범준님에게 투데잇이란? 투데잇 팀의 힘이 무엇이라고 생각하시나요?무엇보다 투데잇의 힘은 서로에 대한 믿음인 것 같아요. 커뮤니케이션이 잘 되려면 그 사람에 대한 믿음이 있어야 되잖아요. 근데 저흰 그게 되게 잘 되고 있다고 생각되거든요. 업무적으로 제 이야기를 자신있게 할 수 있었던 이유도 이 사람들은 전부 다 각자 일을 열심히 하고 책임을 지려는 사람, 멋있는 사람이라는 걸 알고 있었기 때문에 가능했거든요. 다들 맡은 바에 있어서 최선을 다하고 정말 열심히해요. 그 분위기가 서로에 대한 믿음을 만들고 우리의 원동력을 만들죠. 확실히 저희 팀은 일단은 진짜 서로에 대한 믿음이 강하다? 업무적 믿음이 강하다? 그런 게 있는 것 같아요.Q. 투데잇에서 가장 고마웠던 사람은 누구였나요?솔직히 다 고마운데, 저는 대표님께 가장 감사했어요. 이번에도 혼자 고민하다가 힘들게 퇴사 의사를 밝혔는데, 대표님께서 그건 당연한 거라고 이야기해주시더라고요. 저는 투데잇 팀이 참 좋은 게 어떤 이야기를 했을 때 명확한 근거가 있다면 그 후에 뒤끝이 하나도 없어요. 이번 일도 그렇고 일적으로 이야기 할 때도 그렇고, 이유가 확실하면 OK하고 쿨하게 가곤 하셨거든요. 다 업무적 믿음이 있기 때문이라고 생각해요 저는. 여러모로 저를 많이 믿어주신 대표님한테 제일 감사하죠. 대표님 에너지도 너무 좋고 카리스마도 본받고 싶고 제가 되게 좋아하는 분이에요.Q. 범준님의 다음 타자가 될! 투데잇에 입사하고 싶은, 입사할 분들에게 한 마디 부탁드려요!“팀원 하나하나가 굉장히 중요한 역할을 하고 있는 사람들이어서 그만큼 책임감이 있지만, 그만큼의 자율성도 있는 회사에요”굉장히 좋은 팀이에요. 일적에서는 절대 스트레스 주는 일이 없고요. 뭔가 일이 밀리거나 못하는 거에 있어서는 스트레스가 있을 수도 있어요. 팀원 하나하나가 굉장히 중요한 역할을 하고 있는 사람들이어서 그만큼 책임감이 있지만, 그만큼의 자율성도 있는 회사에요. 노력하는 그대로의 모습을 사람들에게 보여줄 수 있고 인정 받을 수 있기 때문에 흔히 말하는 꼰대 문화가 싫으신 분들은 투데잇에서 행복하게 일할 수 있을 거예요. 업무적으로나 환경적으로나 대우도 근무 환경도 굉장히 좋으니까 관심 있으신 분이면, 특히 안드로이드 개발자 분이면 지금 바로 들어오실 수 있을 것 같아요. 유저한테 피드백도 받을 수 있고 개인적으로 리스펙하는 멋진 CTO분도 계시고, 개발자로서 특히 굉장히 좋은 곳입니다. 주저 마세요!#투데잇 #팀원소개 #팀원인터뷰 #팀원자랑 #기업문화 #조직문화
조회수 1542

"코인원 중심에서 '보안'을 외치다." - 보안전략기획팀 정지원

‘보안팀'을 생각했을 때 어떤 단어들이 떠오르시나요? 조금은 무시무시하지만 우람한 팔뚝, 강력한 눈빛, 태평양같은 어깨를 소유한 영화배우 ‘마요미' 마동석님이 떠오르네요. 코인원에서도 무시무시한 매의 눈으로 코인원 크루가 자리를 비울때 화면잠금이 되었는지 확인하는 ‘정요미'가 있습니다. 바로 코인원 보안을 책임지는 보안전략기획팀의 지원님이에요. 코인원 크루의 보안뿐만 아니라 고객들의 소중한 자산을 지키는 코인원의 수문장, 지원님을 만나볼까요?Q. 안녕하세요, 코인원의 ‘프로 화면잠금러'를 만나뵙게되어 정말 영광입니다.네, 저 또한 영광입니다. 제가 이전에 자리를 잠깐 비울때 화면잠금을 하지 않았는데요, 이렇게 영혼까지 털릴줄 몰랐습니다. ‘화면잠금도 모르면서 보안을 어떻게 논하느냐’ 라고들 하셔서 사죄의 의미로 커피를 쏘게 되었습니다. 이후 다시 이런 일이 없도록 스스로에게 다짐했을 뿐만 아니라 화면잠금 안하신 크루가 있는지 없는지 열심히 찾고 있습니다. (걸리기만 해 아주…-_-)Q. ‘프로 화면잠금러’로 오해하실 수도 있는 독자분들을 위해 ‘진짜’ 지원님 소개 부탁드릴게요:)안녕하세요, 코인원 보안본부 내 보안전략기획팀에서 근무하고 있는 정지원입니다. 코인원의 보안본부는 대내외 각종 보안 위협으로부터 선제적으로 대응할 수 있도록 Action Plan을 수립하고 실행하여 코인원의 모든 서비스와 자산을 보호하는 역할을 하고 있어요. 크게 보안전략기획팀, 개인정보보호팀, 보안운영팀으로 나뉘어 집니다.이 중에서 보안전략기획팀은 주로 대/내외 보안 트렌드를 파악하며 거래소 보안전략을 수립하고, 우선순위를 설정하고 조정하여 실행하고 있습니다. 더불어 코인원의 기존 서비스와 앞으로 출시될 신규 서비스의 보안 위험을 식별할 수 있도록 분석하고 대응방안을 마련하죠. 철저한 보안으로 코인원이 고객들에게 신뢰받을 수 있는 거래소가 되기 위해 최선을 다하고 있습니다.Q. 코인원을 이용하는 고객분들이라면 정말 궁금할 것 같아요. 코인원에 보관되어 있는 제 자산, 정말 안전하게 보관되어 있나요?“코인원 고객들의 자산은 100% 안전합니다" 라는 말 대신 “코인원 보안팀은 단 1%의 취약점도 허용하지 않기 위해 정말 최선을 다하고 있습니다" 라고 말씀드리고 싶어요.개인적으로 “고객의 자산은 100% 안전합니다.” 또는 “100% 완벽한 보안” 이라는 말은 성립할 수 없다고 생각해요. 취약점이 발생할 가능성은 언제나 있다고 생각하고, 그것이 1%의 가능성이라고 할지라도 해결방안을 고민해서 현실적인 대책을 세우고 실행해나가야 한다고 생각합니다.현재 코인원에서는 *DID(Defense In-Depth)의 개념으로 계층화된 보안 시스템(Multi-Layered Security)을 구축하고 발생할 수 있는 보안 위협에 대비합니다. 성을 공략하는 게임을 예를 들어 볼게요. A라는 성은 10m의 성벽 1개가 있고 B라는 성은 1m의 성벽 10개가 있다고 가정할께요. 성벽을 우회해서 성에 도착하기까지 어디가 시간이 더 걸릴까요?코인원은 마치 여러 개의 성벽처럼 계층화된 보안 방안을 구현, 거래소에 적용하고 있어요. 적용했다고 끝난게 아닙니다. 계속해서 모니터링 하면서 좋은 점과 나쁜 점을 모아놓고 좋은 점은 더 좋게, 나쁜 점은 개선할 수 있도록 재기획하고 실행합니다. 보다 더 안전하게 고객의 자산을 보호할 수 있는 방법을 고민하고 적용하고 있어요. *여기서 잠깐 DID(Defense In-Depth, 심층방어)란? 여러 계층의 보안 제어가 정보 기술(IT) 시스템 전반에 걸쳐 배치되는 정보 보증 개념입니다. 보안 제어가 실패하거나 시스템의 수명주기 동안 인력, 절차적, 기술적 및 물리적 보안 측면을 포괄 할 수있는 취약점이 악용되는 경우를 대비하여 다수의 방어 중복성을 제공하기 위한 것입니다.Q. 현재 코인원에서 진행하고 있는 보안정책은 어떤것들이 있을까요? 간단하게 소개해주세요.코인원 보안정책 중 몇가지를 소개해 드리자면, 코인원은 콜드월렛 보관 비중을 85%로 유지하여 고객자산을 보다 안전하게 보호하려고 노력하고 있습니다. 이는 사단법인 한국블록체인협회 권고 사항인 70% 보다 높은 비중이죠.또한 IT전문 보안 기업 SK infosec의 체계적인 보안관제 서비스를 제공받고 있습니다. 사이버 침해 위협을 실시간으로 감시하고 SK infosec이 보유한 방대한 위험 정보 데이터 베이스에 기반하여 고도화된 위협에 대응하고 있습니다. 마지막으로 이번에 새로 사이버 보안 기업 티오리(THEORI)의 전문적인 보안 컨설팅을 받게 되었습니다. 티오리는 미국 오스틴에 본사를 둔 기업으로 카네기멜론대학 해커팀(PPP) 핵심 멤버들이 설립한 사이버 보안 R&D 기업인데요, 데프콘(DEFCON) 같은 유명한 국제해킹방어대회에서 항상 상위권에 랭크되고는 합니다. 이렇게 검증된 역량을 바탕으로 Pen-Test(모의해킹)을 통해 코인원의 보안 아키텍쳐를 점검하고, 발생 가능한 모든 침해 시나리오를 상정하여 이에 대비하기 위한 자문을 진행할거에요.이외 다수의 테크니컬한 부분은 영업비밀(?) 입니다. (와하하하)Q. 콜드월렛을 잘 모르실 수도 있는 독자분들을 위해서 자세한 설명 부탁드려요. 또한 85%까지 비중을 유지하는것이 왜 중요한가요?먼저 콜드월렛에 대한 설명을 드릴게요. 콜드월렛은 핫월렛과 달리 네트워크가 연결되지 않은 물리적으로 분리된 저장 공간을 말합니다. 콜드월렛에 보관한다는 의미는 고객의 암호화폐 자산을 침해 또는 해킹 위협으로부터 원천적으로 차단된 별도의 장소에 보관한다는 뜻입니다. 그런일이 있어서는 안되겠지만, 사이버 침해가 발생한다고 가정할 경우 고객의 피해를 최소화할 수 있는 안전 장치에요. 블록체인 협회에서는 70%이상을 콜드월렛에 보관하는 것을 권고하고 있는데요. 저희는 협회에서 권고하기 이전부터 자체적으로 월렛 관리 정책을 만들고 그에 따라 콜드월렛을 운영해왔습니다. 참고로, 85%로 유지하는 이유는 거래소 비즈니스적으로 병목현상이 일어날 수 있는 부분을 방지하기 위한 적정 수준이라고 답할 수 있겠네요.보안팀은 무시무시하지 않아요, 부드럽습니다! (그윽한 눈빛을 발사하는 지원님)Q. 거래소 보안 전문가로서 막중한 책임감을 갖고 계실 것 같아요. 코인원 입사 후에 가장 기억에 남았던 혹은 어려움을 겪었던 에피소드가 있을까요?코인원의 보안 수준을 어떻게 하면 제1금융권 수준까지 끌어올릴 수 있을까에 대한 고민이 매우 컸습니다. 블록체인과 암호화폐 업계가 굉장히 폭발적으로 성장해왔는데요. 폭발적으로 성장하는 속도를 따라잡을 수 있도록 보안 및 인프라팀에서 무수한 노력을 해왔어요. 짧은 시간내에 보안 인프라를 효율적으로 구축할 수 있을지 치열하게 진행했던 회의들이 생각나네요. 코인원의 많은 크루들이 노력해주시고 도와주신 덕분인지 현재까지 코인원에서는 단 한건의 해킹사고도 발생하지 않았습니다. 최근에 생각나는건 금번 NH농협은행과의 재계약에서 보안 요구사항과 점검에 대한 실사가 많았는데 다행이 보안요건을 충족하며 재계약한 것이 생각나네요.Q. 지원님은 앞으로 보안본부에서 어떤 꿈을 이뤄나가고 싶으세요?글로벌 회사를 보면 유명한 보안팀들이 있어요. 예를 들어 구글에는 ‘프로젝트 제로(Project Zero)’라는 팀이 있는데, 이 팀은 ‘제로데이(0-day)’ 공격을 대비하기 위한 팀이에요. 제로데이 공격은 알려지지 않은 취약점을 발견해서 이에 대처하기 전 무방비 상태인 점을 악용하는 사이버 공격 방법이에요. 프로젝트 제로는 제로데이 공격 위협을 사전에 해소하기 위해 자사 제품 뿐만 아니라 타사 제품까지 연구하고 취약점이 발견된다면 해당 회사에 전달해서 대처할 수 있게 합니다. 또 다른 예로 야후에 “패러노이즈(Paranoids)”를 들 수 있겠네요. 야후의 모든 제품은 패러노이즈의 승인 없이는 론칭되지 않습니다. 전문성이 뛰어나지 않다면 가능하지 않은 케이스죠.저는 보안을 위해서라면 편집증적인 집착도 용서가 된다고 생각하는데요, 암호화폐 거래소 뿐만 아니라 블록체인 전반적인 영역에 대해 전문성을 발전시켜 궁극의 편집증 환자가 되는게...(?) 아 이게 아니고, 글로벌 유수의 보안팀들과 어깨와 나란히 하고 싶습니다.Q. 마지막으로 묻겠습니다. 지원님에게 ‘화면잠금' 이란?(인터뷰에서까지 영혼이 털리네요...) 회사 메신저에 제 프로필을 보시면 “화면잠금 털린 보안어린이”라고 되어 있습니다. 슬프네요 흑. 농담이구요, 어떤 일이던지 기본부터 충실해야 한다는 초심을 찾을 수 있었던 계기도 되었고 또 의도하지 않았지만 코인원 크루들이 보안은 어려운게 아니구나 라는 인식으로 바뀌게 된 계기가 된 것 같습니다. 수많은 보안 캠페인을 기획하고 시행했지만 지금처럼 크루들에게 여운이 남아있던 적이 없던 것 같아요. 앞으로 쉽지만 누구나 할 수 있는 보안 캠페인을 고민해 볼께요. (좋은 아이디어 주시면 제가 커피를 쏩니다!)충성! 단결! 필승! 오늘도 보안은 안전합니다 :-)언제나 보안을 최우선으로 고려하고, 원칙을 지키며 건전한 암호화폐 시장을 만들기 위해 지원님은 오늘도 24시간 365일 보안에 대한 고민을 풀가동하고 있습니다. 코인원을 이용하는 고객들의 안전한 거래를 위해 끊임없이 노력하는 보안전략기획팀에 많은 응원 부탁드립니다!#코인원 #블록체인 #기술기업 #암호화폐 #스타트업인사이트 #기업문화 #조직문화 #팀원소개 #인터뷰
조회수 4518

100일 간의 챗봇 디자인 실패기-1편

디자인 학도로서 4년 넘게 학교에서 UI/UX를 공부했다. 또래에 비해 학교를 오래 다녔으며 해당 분야에 대한 관심도 남달랐거니와, 심지어는 UI 디자인 소프트웨어를 만드는 회사에 다닌 경험이 있는 만큼 실무적으로는 아직 많이 부족할 지라도 이론만큼은 이제 어느 정도 자신이 있다고 생각했다.그런데 대체 이 녀석은 또 뭐지. 챗봇이라니.   지난 1월, 새로운 사업을 결심한 팀원들과 사업구상을 하며 챗봇이라는 아이템을 마주하게 되었다. 우리가 챗봇에 대한 무한 신뢰를 했던 이유는 한 가지였다. '일상적 편리함에 있어 메신저만 한 것은 없다'는 것.한때 SNS에 화제가 되었던 '엄마의 메모장'챗봇은 이미 한 차례 미국 본토를 강타하고 조금씩 국내 시장에 진입하고 있던 상황이었고, 새로운 기술에 호기심을 가진 우리 팀은 챗봇에 희망을 품고 해당 분야에 대한 학습을 진행하기 시작했다.  자연어 처리, 형태소 분석 등 기술적인 부분들을 개발팀원들이 검토하고 있는 동안 디자이너로서 챗봇에 대한 리서치를 시작하려는 찰나, 아무리 검색을 해도 평소에 비해 아무것도 나오지 않는 매우 당황스러운 시추에이션이 발생했다.  일반적인 웹이나 어플리케이션 기획의 경우 이미 레퍼런스 삼을 만한 사례가 충분히 있었고, 설령 국내 자료 중에 없다고 한들 영어로 조금만 검색해보면 해외 자료들을 금세 찾을 수 있었다. 그러나 챗봇은 상황이 달랐다. 영어권 챗봇 또한 이제 막 성장하는 단계인 만큼 해외 챗봇 사례 중에서도 이렇다 할 벤치마킹 대상을 찾는 것이 쉽지 않았다.우선 우리가 만들고자 한 챗봇은 '일정' 관련 봇이었다. '자연스러운 대화를 이해하여 사용자의 일정 입력을 돕는 챗봇이 있다면 어떨까'라는 것이 우리의 가설이었다.괜찮지 않을까?지난 4년 간 학교에서 배운 과정대로라면 브레인스토밍, AEIOU, 컨셉맵핑, 유저 인터뷰, 포커스그룹 인터뷰 등에 걸친 여러 기법들을 통해 디자인을 시작해야 했다. 하지만 현 상황은 우리가 대체 정확히 무엇을 만드는 것인지에 대한 정의조차 내려지지 않은 상태였다.이 챗봇의 기능은 무엇이며, 타겟은 누구이고, 어떻게 구현될 수 있는 걸까. 너무나 생소한 분야였던 만큼 우선 첫 한 달 동안은 챗봇 관련 국내외 글을 꾸준히 읽기 시작했다. 4차 산업혁명, 완전자동화 등 챗봇에 대한 여러 이론적인(쓸데없는) 내용들이 있었지만 그중에서도 유독 눈에 띄는 글이 하나 있었다.https://chatbotsmagazine.com/bots-hype-or-glory-656f4d614efb#.g6s68jvkgI was an undercover-bot for 2 months. Here is what I learned.Bots: hype or glory?chatbotsmagazine.com 해당 글의 주요 내용을 번역 및 요약하자면 이러하다.- UX 매니아로서, 그 수많은 챗봇 중에 쓸만한 게 없더라.- 그래서 챗봇을 개발하기 전 직접 실험을 해보기로 했다.- 약 2달간 직접 서비스 내에 사용자를 돕는 봇인'척' 했다(틈틈이 사람이라고 힌트는 줬다).- 우리 서비스를 사용하는 사용자들은 컴퓨터나 기술을 좋아하는 사람들이 아닌, 일반인이었다.- 봇이 아닌 사람이 실시간으로 응대한다고 인지는 시켜주었지만 사실 신경 쓰는 사람은 없었다.본문은 '아직 챗봇은 기술적으로도, 시대적으로도 준비가 되지 않았다'로 최종 결론을 지으며 마무리되는데, 이미 챗봇에 콩깍지가 씌여 있던 나에게는 그저 앞부분의 내용이 중요할 뿐이었다."사람이 챗봇인 척 테스트를 한다고?"서비스 기획 및 디자인에 갈피를 못 잡고 있었던 우리 팀은 긴말할 것 없이 곧바로 실행에 들어갔다. 대학교 게시판에 피실험자 알바 구인 글을 올리고 약 30명의 캘린더 유저를 확보했다. 실험에 대한 대략적인 안내사항은 이러했다.1. 우리는 현재 일정 관련 챗봇을 만들기 위해 수동으로 실험 중이며, 주 기능은 '일정등록' 이다.2. 구글 또는 네이버 캘린더 작성 권한을 사용자로부터 공유받아 일정을 입력한다(캘린더 공유 기능 활용).3. 사용자는 최소 주 1회 이상 카톡을 통해 캘린더에 일정을 입력하여야 한다(페이 지급 조건).4. 사용자는 챗봇에게 일정 등록뿐만이 아닌 일정 관련 어떠한 요청도 할 수 있다.5. 이에 대한 예시로 문자/메일 분석, 공개 캘린더 추가, 키워드 일정 추천 등을 제시한다.6. 대화의 형태는 정해져 있지 않으며 원하는 어떠한 형태(말투, 축약어, 신조어)로든 가능하다.응대에 사용한 옐로아이디 관리자 툴지금은 플러스친구로 업데이트된 카카오톡 옐로아이디 관리자 툴을 활용하여 사용자들과 대화(채팅)를 진행했다. 데스크탑용 웹 인터페이스를 통해 대화를 입력할 수 있었기에 입력 속도는 빨랐지만 사용자가 언제 무슨 말을 걸어올지 도저히 예측이 불가능했다. 팀 내 개발자들이 자연어 처리에 대한 공부를 지속하는 동안 운영을 맡은 팀원과 함께 2명이서 상시 대기하며 사용자들의 요청에 응대했다.운영 초기 우리가 기대했던 이상적인 요청들은 이러했다.하지만 현실은 아래와 같았다.목적어 및 각각의 형태소가 매우 명료하고 명확한, 챗봇 개발 시 자동화가 가능한 텍스트들을 기대하고 있었지만 실상 대부분의 요청은 실제 사람이 개입하지 않는 이상 과연 처리가 가능할까 싶은 내용들이 태반이었다.텍스트 입력 시간도 사용자마다 다 제각각이었다. 아침 일과를 시작할 때 일정을 입력하는 사용자들이 있는 반면 하루를 정리하며 다음날 일정을 계획하는 사용자들도 있었다. 밥을 먹다가도, 샤워를 하다가도 옐로아이디 알람이 울리면 컴퓨터로 달려가 응답을 했다. 아무리 상시 대기를 한다 해도 잠은 자야 했기에 결국 자정부터 다음날 아침 8시까지는 옐로 아이디의 자동 응답기능을 활용하여 '잠시만 기다려주세요'를 출력하였다.(물론 잠시는 아니었지만)여러 시행착오를 거쳐 약 한 달 간의 기나긴 응대 끝에 실험이 종료되었고, 우리는 사용자들을 대상으로 설문 및 인터뷰를 진행하였다.우선 가장 중요하게 생각한 전체 캘린더 일정 입력률(데스크탑/모바일 캘린더를 포함한 모든 입력) 대비 카톡을 통한 일정 입력률은 약 절반 정도로 확인되었다.카톡을 통한 일정 입력률 / 전체 일정 입력률  = 51%이와 더불어 '카톡을 통해 캘린더에 일정을 등록하는 방식에 대해 불편한 점'을 질문한 결과1. 즉각적이지 않은, 늦은 응답 - 40%2. 개인 일정 정보 유출에 대한 불안 - 20%3. 익숙하지 않은 카톡 입력의 불편함 - 13.3%순으로 응답함을 확인하였다.생각보다 나쁘지 않은 결과였다.비록 입력 된 내용들을 정형화 하기가 쉽지는 않았지만, 기대했던 것에 비해 카톡을 통한 입력률이 높은 편이었고 가장 큰 문제점으로 지적된 '늦은 응답'과 '개인 정보 유출'은 챗봇 개발을 통해 개선할 수 있을 것으로 기대했다. 자동화를 통해 즉각적으로 응답할 수 있을뿐더러 사람의 개입을 없애 개인 일정 정보 유출을 방지할 수 있을 것이라는 판단 하에 챗봇 개발을 진행하였다.그렇게 한달 간 입력받은 텍스트 데이터를 활용, 약 2주 간의 개발 끝에 간단한 일정 등록 기능을 갖춘 일정 관리 챗봇, 린더봇이 탄생하게 되었다.https://www.youtube.com/watch?v=zSRYRYfzTFo2편에서 계속...#히든트랙 #챗봇 #기술기업 #개발자 #개발팀 #인사이트 #경험공유

기업문화 엿볼 때, 더팀스

로그인

/