• play.node 2017 발표기

    play.node 2017 에서 발표한 후기.

    얼마 전 한국 node.js 사용자 컨퍼런스 play.node 2017가 열렸다. 감사하게도 해당 컨퍼런스에 낸 TypeScript와 Flow: 자바스크립트 개발에 정적 타이핑 도입하기 라는 주제의 프로포절이 채택되어 이번 주 목요일(2017년 11월 9일), 인생 첫 기술 컨퍼런스 발표를 하고 왔다.

    준비하는 동안에는 사실 스트레스를 많이 받았다. “경험도 별로 없는 내가 사람들에게 도움이 될 만한 발표를 해낼 수 있을까?” 라는 걱정이 하나고, 발표 자료를 만드는게 처음 예상했던 것보다 시간이 많이 들면서 다른 벌려 놓은 일을 챙기지 못 하는 데에서 오는 초조함이 다른 하나였다. 하지만 이제 와 돌아보니 역시 시도 해보길 잘 했다 느끼는 소중한 경험이라, 기억이 날아가기 전에 기록으로 남겨본다.


    발표자가 되기까지

    전 회사인 스포카에 있을 때, 운이 좋게도 초기부터 신규 프로젝트를 만드는 팀에 합류했다. 덕분에 기술적으로 재미있으면서도 배울 것이 많은 시도를 할 수 있었는데, 그 중 하나가 바로 프로젝트의 Flow 관련 코드를 TypeScript로 마이그레이션 하는 작업이었다.

    개발자 경험을 극단적으로 바꿔놓는 작업이기도 하고, 무엇보다 내가 프로그래밍에서 가장 관심 있어하는 타입 시스템이라는 주제와 맞닿아 있는 작업이라 즐겁게 진행했다. 해 보고 나니 이렇게 프로젝트 전체를 갈아엎은 경험을 해 본 팀이 많지는 않을 듯 해 기술블로그에 공유 해 보고 싶은 욕심이 생겼다. 실제로 작업을 같이 진행한 최종찬 님과 함께 해당 글에 넣을 내용을 secret gist 에 차곡차곡 모으기도 했다.

    하지만 늘 그렇듯 이런저런 이유로 기술 블로그 포스팅을 차일피일 미루던 중, 8월 말에 play.node 컨퍼런스에서 발표진을 모집하는 것을 보았다. 컨퍼런스 발표를 한 번도 못 해보았기도 하고, 스스로 배수의 진을 치고 나면 빠져 나갈래야 나갈 수 없을거라는 생각에 간단히 정리해 발표를 신청했다. 얼마 후 React Seoul 2017 컨퍼런스가 열린다는 소식을 듣고 비슷한 내용으로 해당 컨퍼런스에도 발표 신청을 넣었다.

    마침 그 때쯤 이직 고민을 하고 있었는데, 최종적으로 이직하기로 결정을 내리고 남은 한 달 동안 열심히 분기 목표를 진행하던 중 9월 21일에 play.node 운영진 분들께 발표 세션 선정 메일을 받았다. 될까? 싶었지만 밑져야 본전이라는 생각으로 낸 프로포절이었는데 정말 선정되어서 기뻤다.

    playnode 2017 발표자 선정 메일

    그리고 9월 27일, React Seoul 2017 발표자로 선정되지 않았다는 메일을 받았다. 내 프로포절은 꽤나 일반적인 주제였는데, 공지를 살펴보니 세션 수가 적은만큼 React라는 라이브러리와 밀접히 연관된 주제를 우선적으로 선정하신 것 같다는 인상을 받았다. 충분히 납득이 가는 결정이었다. 아쉬움이 없진 않았지만 발표자 선정이 되고 난 후에 새삼 컨퍼런스 발표에 대한 부담도 느끼고 있었던 터라 한편으로는 오히려 잘 되었다는 생각도 들었다.

    주어진 기회에 최선을 다하자는 생각으로 발표 준비를 시작했다.


    발표 준비

    발표 준비는 한 달 전 쯤부터 시작했는데, 사실 이건 좀 오버 했던 것 같다. 괜히 시작하고부터 놀 때에는 ”할 일이 있는데 그건 안 하고 놀고 있다”는 스트레스만 받으면서도 정작 아직 꽤 많이 남았다는 생각에 별로 긴박함은 못 느껴 열심히 하지 않았다. 다음부터는 자료조사는 틈틈이 하더라도 본격적인 준비는 한 이 주 전쯤부터 시작하는게 마음이 편할 것 같다.

    발표 슬라이드에 말이 너무 많으면 사람들이 장표를 읽고 이야기를 안 들으니까 장표는 엄청 간단하게 만들거나 이미지만 넣으라는 식의 조언이 많다. 나는 이 조언에 동의하지 않는다. 그런 식으로 만든 장표는 스크립트를 전부 포함시키지 않는 이상 장표 자체를 공개하는 것만으로는 별 가치가 없기 쉽다. 기왕 시간과 노력을 들여 만들 것이라면 그 자리에 없던 사람도 슬라이드만 보고도 발표의 내용을 어느정도 짐작할 수 있는 쪽이 좋다고 생각한다.

    물론 자리에 없는 사람을 배려하느라 자리에 있는 사람을 제대로 챙기지 못 해서는 안 될 것이다. 앞서 언급한 문제는 핵심 내용을 다 장표 안에 넣되 대부분을 리스트로 만들어서 한 번에 아이템 하나씩만 보여주면서 이야기를 듣게 만드는 식으로 해결 가능하다고 생각한다. 이번에도 그런 식으로 슬라이드를 구성했고, 그럼에도 슬라이드에 충분히 들어가 있지 않은 부분은 추후 웹에 공개하기 전에 텍스트를 좀 추가하는 식으로 보완했다.

    발표의 개요

    발표의 흐름에 있어서는 사실을 쭉 나열만 하는 대신 발표 전체를 관통하는 하나의 이야기를 만들고자 노력을 했다. 그를 위해 마케팅에서 이야기하는 퍼널을 만들고, 앞 부분의 결론이 자연스레 다음 부분의 문제의식으로 이어지도록 구성했다. 실제로 얼마나 효과가 있었는지는 잘 모르겠다.


    발표 당일

    사람이 너무 적게 오면 어떻게 하나 하는 걱정을 많이 했다. 그런데 다행히도 방이 꽉 찰 정도로 많이 찾아주셨다. (사실 이번엔 두 트랙이 진행된 공간 모두가 인원에 비해 좀 좁아서 지나치게 북적였는데, 내년에 개선된다면 더 좋을 것 같다) 앞 세션이 굉장히 일찍 끝났는데, 시간이 빠듯할 걸 알았기에 먼저 시작하고 싶은 유혹이 강했다. 하지만 약속한 시간인 1시 40분보다 너무 이르게 시작하면 안 된다고 생각해서 약 7-8 분을 기다린 후 39분에 발표를 시작했다.

    석종일 님께서 찍어주신 발표하는 모습

    이야기를 하면서 틈틈이 관중들을 살폈다. 걱정 했던 것에 비해 재밌게 듣고 집중해주는 분이 많이 계셔서 감사했다. 긴장이 되긴 해도 리허설 때보다 실제 발표가 훨씬 즐거웠다. 다만 마지막에는 확실히 긴장과 더불어 주어진 시간을 맞추어야 한다는 생각에 너무 빠르게 달린 것 같아 아쉽다. 발표의 핵심이 앞 부분에 있던 건 다행이지만 개선할 부분이다.

    일부러 질의응답을 위해 마지막 슬라이드에 요약본을 띄워놓았는데 막상 시간을 너무 딱 맞추는 바람에 질의응답을 거의 진행하지 못 했다. 발표를 사람들이 얼마나 흥미롭게 들었는지 확인할 수 있는 지표가 질의응답 시간이 생각해서 좀 아쉬웠다. 헌데 세션이 끝나고는 물론이고 쉬는 시간 카페에서나 복도에서까지 궁금한 점을 여쭤봐 주시는 분들이 계셨다. 모든 분께 만족스러운 답변을 드렸는지는 모르겠지만 개인적으로 뿌듯했다.

    전날 새벽과 당일 아침까지 발표 자료를 가다듬고 리허설을 하느라 정신이 없었는데, 발표를 마치고 나니 긴장이 확 풀리면서 졸음이 몰려왔다. 다른 세션도 듣고 운영진 분들과 이야기도 좀 나누고 난 후, 컨퍼런스에 왔던 친구와 함께 저녁을 먹고 일찍 귀가했다. ‘컨퍼런스 발표라는게 이런거구나, 재미있다’ 라고 생각했다.


    발표 이후 회고

    만족스러운 점

    먼저 이번 발표를 준비하며 나름대로 잘 했다고 생각하는 만족스러운 점을 적어본다.

    주제 선정

    일단 무엇보다 주제 선정을 잘 했다. 요즘 들어 점점 타입스크립트에 대한 관심이 늘어나고 메인스트림으로 들어오고 있는데, 뉴스레터나 블로그 포스팅, 컨퍼런스 주제를 보아도 그렇고 여러 지표를 통해 숫자로도 확인할 수가 있다. 시의적절한 주제를 고른 탓에 보다 많은 관심을 받을 수 있지 않았나 싶다.

    당연한 이야기일 수 있겠지만 컨퍼런스 발표를 먼저 결정하고 난 후에 주제를 고민한 것이 아닌, “이런 주제로 이야기를 하고 싶다” 는 생각이 먼저 생긴 주제로 발표를 한 것도 좋은 흐름이었다. 내가 가치 있다 믿고 실제로 일을 하며 맞닥뜨리는 주제이다보니 열심히 준비하고 좀 더 마음에서 우러나오는 (?) 발표를 할 수 있었다 생각한다.

    리서치

    현 시점에서 인터넷에 존재하는 한국어 또는 영어로 쓰인 TypeScript와 Flow를 비교하는 자료 대부분은 이미 봤다는 근자감이 슬슬 생기고 있다.

    라는 트윗을 했었는데, 실제로 발표를 준비하면서 자료 조사를 정말 열심히 했다. 발표를 듣는 이들에게 유용할 수 있는 정보를 내가 몰라서 빼먹고 싶지 않았고 동시에 행여라도 틀린 정보를 전달하고 싶지 않았다. 문서, 블로그 포스팅, 깃헙 이슈들과 심지어는 논문까지 읽어가며 리서치에 많은 시간을 투자 했는데 그것들이 다 알찬 슬라이드를 만드는 데에 도움이 되는 좋은 투자였다.

    철저한 연습

    발표 전에 대부분의 슬라이드의 스크립트를 써 놓고 적어도 세 네 번씩은 혼자 소리내어 연습했다. 발표 직전에는 각 슬라이드에서 말할 문장들을 거의 외울 지경이 되었는데, 덕분에 긴장한 와중에도 여유로운 척을 하면서 실제 발표를 진행할 수 있었다. 공적인 자리인만큼 절대 주어진 시간을 오버하고 싶지 않다는 부담이 컸는데, 어느 슬라이드에서 몇 분 정도가 되어야 하는지를 사전에 체크 해 둔 덕분에 주어진 35분에 딱 맞춰 발표를 끝낼 수 있었다.

    아쉬운 점

    다음으로는 반대로 아쉬웠던, 다음에 또 발표를 하게 된다면 개선하고 싶은 점이다.

    발표 준비 과정

    이번 발표는 대략 다음과 같은 흐름으로 진행했다.

    • 리서치
    • 슬라이드 작성
    • 스크립트 작성
    • 리허설
    • 시간을 맞추기 위해 내용 삭제

    이러한 방식의 문제는 리허설을 할 때 드러났다. 주어진 시간이 35분인데 집에서 써놓은 스크립트를 기반으로 쭉 발표를 진행해보니 한 시간이 넘게 걸렸다. 부랴부랴 슬라이드를 마구 지우고 표현을 가다듬는데, 열심히 시간과 노력을 투자해 만든 슬라이드와 스크립트를 허공에 버리고 있자니 너무 아깝고 바보가 된 기분이었다. 중간이 텅 비어버린 흐름을 다시 자연스럽게 고치자니 추가적인 노력도 필요했다.

    다음부터는 다음과 같은 식으로 발표를 준비하는게 낫겠다는 교훈을 얻었다. 핵심은 시간이 제한된 경우에는 각 부분에 대한 시간 할당을 먼저 하는게 좋을 것 같다는 것이다. 시간 할당을 앞쪽으로 당겨 오면 내 발표의 무게 중심을 어디에 놓을지를 더 이른 시점에 고민할 수 있게 된다는 추가적인 장점도 있겠다.

    • 리서치
    • 커다란 개요 작성
    • 각 부분에 시간을 얼마나 할당할지 결정
    • 주어진 시간에 맞춰 넣을 수 있는 분량의 슬라이드 작성
    • 스크립트 작성
    • 리허설

    독창성에 대해

    리서치를 열심히 하고 다양한 자료를 넣긴 했지만, 결과적으로 발표한 내용은 원래 발표를 신청할 때의 취지와는 약간 어긋났다. 실제로 Flow에서 TypeScript로 옮기면서 얻은 노하우나 교훈 등의 비중은 줄어들고, TypeScript를 사용하고 싶도록 만들기 위한 설득의 비중이 크게 늘었다.

    물론 발표의 초점이 이동한 것 자체는 자연스러운 결과이긴 하다. 발표를 준비하다보니 사람들이 이걸 쓰고 싶도록 설득하는게 가장 중요하다는 생각이 들었기 때문이다. 하지만 한편으로는 꼭 내가 아니더라도 충분한 시간과 노력을 들여 리서치하면 누구나 다 얻을 수 있는 정보만을 짜집기해서 팔고 있는건 아닌가 하는 의문이 생겼다. 아무래도 이건 나의 내공의 문제가 아닌가 싶은데, 다음엔 좀 더 오리지널리티가 있는 발표를 해 보고 싶다.

    슬라이드 디자인

    지금 내 SlideShare 계정에는 이번 것을 포함해 총 네 개의 슬라이드가 올라가 있는데, 모두 Keynote의 기본 템플릿이다. 말인즉슨 디자인이 전혀 되어있지 않고, 배경은 흰색이요 텍스트는 검은 색이다. 엉망으로 하느니 차라리 안 하는게 낫다고는 생각한다. 하지만 이쁘면서도 내용 전달을 도와주는 디자인을 적용할 수 있었다면 더 좋았을 것 같다. 다음 발표는 언제가 될지 모르겠지만 기본 템플릿을 벗어난 디자인을 시도해볼까 하는 생각을 했다.


    마치며

    컨퍼런스 발표는 공개적으로 글을 써서 내놓는 일과 비슷하다는 어찌보면 당연한 점을 이번 발표를 통해 느꼈다. 부담이 많이 되지만 그만큼 열심히 준비했을 때 발표를 듣는 다른 누구보다도 발표자 본인이 많은 것을 얻어갈 수 있다. 특히 개선할만한 점을 많이 찾을 수 있어서 기쁘다. 아직 경력도 짧은 내가 운 좋게도 이런 기회를 얻게 되어 감사한 마음이다. 다음에도 내가 무언가 의미 있는 이야기를 할 수 있을만한 발표 자리가 있다면 기꺼이 또 덤빌 것 같다.

    감사의 말

    마지막으로 감사의 말과 함께 글을 마무리하려 합니다. 글을 평어체로 쓰는 것을 선호하는데 이런 내용을 쓸 때면 왜인지 항상 경어체를 쓰게 되네요.

    먼저 이 행사를 기획, 준비하시고 실행하신 play.node 2017 오거나이저 분들께 큰 감사를 드립니다. 자원봉사로 이렇게 많은 사람이 참가하는 컨퍼런스를 기획하고 현실로 만들어내기까지 정말 많은 시간과 노력이 들어갔을 것으로 짐작합니다.

    또한 바쁜 평일 귀한 시간을 내 컨퍼런스에 참석하고, 또 같은 시간대 다른 세션의 훌륭한 발표를 두고 제 발표를 보러 와 주신 모든 청중 여러분께 감사드립니다. 귀 기울이고 재밌게 들어주신 덕분에 즐겁게 발표 할 수 있었습니다. 무언가 얻어 가실 수 있는 시간이었기를 바랍니다.

    그리고 끝으로 같이 일하면서, 또 발표를 준비하면서 많은 도움을 주신 전 직장 동료 최종찬님과 SlideShare에서 한국어가 깨지는 문제의 해결책인 SlideShare에서 자국어 폰트 사용하기라는 포스팅을 작성하신 item4 님께 감사의 말씀을 드립니다.

  • 타입과 타입 시스템 : 기본 개념

    강타입/약타입, 정적/동적 타입 검사, 점진적 타이핑, 형변환, 안전성/완전성 등의 개념에 대해 다룹니다.

    들어가며

    먼저 첫 글을 읽고 (혹은 읽는 도중에) 창을 꺼버리거나 기억 속에서 지워버리지 않고, 두 번째 글을 찾아주신 독자 여러분께 감사의 마음을 표한다.

    ❤️🙇❤️

    첫 번째 글에서는 타입과 타입 시스템의 중요성과 의의, 그리고 타입을 바라보는데 사용할 수 있는 기본적인 직관에 대해 다루었다. 이번 글에서는 추후 구체적인 주제들에 대해 논의를 전개해나기에 앞서 필요한 기본 개념들을 확립하고자 한다. 다룰 내용은 다음과 같다.

    • 강타입과 약타입
    • 정적 타입 검사와 동적 타입 검사
    • 점진적 타이핑
    • 타입 선언
    • 형변환
    • 안전성과 완전성

    독자분들께

    평어체로 써놓으니 왜인지 어색하여 이 부분만 경어체로 작성합니다.

    저는 이 주제로 학위를 받은 적도 없거니와 가진 경력도 그다지 길지 않습니다. 글을 써나가며 공부를 게을리하지 않겠으나 부족한 부분이 많을 것입니다. 글에서 잘못된 정보를 발견하신다면 부디 메일 등의 수단으로 알려주시길 부탁드립니다. 이어질 글들이 첫 선보임 이후에도 독자분들께서 지적/보충해 주신 내용을 바탕으로 꾸준히 나아지는 것이 저의 바람입니다. 꼭 지적이 아니더라도 연재에 관한 어떤 의견이든 환영합니다. ❤️

    또한 저는 이 연재가 타입 시스템에 이미 큰 관심을 두고 있는 일부만이 즐길 수 있는 내용에서 벗어나, 가능한 많은 프로그래머에게 흥미롭게 읽히고 도움이 되길 바랍니다. 그 때문에 이론적 엄밀성을 다소 희생하는 한이 있더라도 저와 같은 보통의 프로그래머에게 더욱 쉽게 다가갈 수 있다면 그 길을 택할 예정입니다. 너그러운 양해 바랍니다. 연재의 기조에 대한 의견도 환영합니다. ❤️


    강타입과 약타입

    언어의 타입 시스템을 설명하는 글을 읽다 보면 “강타입strong type”, 그리고 “약타입weak type”이라는 용어를 맞닥뜨리게 된다. 종종, 이 용어들은 언어가 올바르지 않은 타입 정보를 가진 프로그램을 실행하는 것을 허용하는지를 나타낸다. 즉, 강타입 언어는 타입 검사를 통과하지 못한 프로그램의 실행 자체를 막지만, 약타입 언어는 런타임에 타입 오류를 만나는 한이 있더라도 실행을 막지 않는다는 것이다.

    하지만 이러한 맥락에서의 강타입, 약타입은 정확히 나뉘는 두 개념 이라기보단 스펙트럼으로 이해해야 한다. 이 두 용어에 정확히 일대일로 대응되는 개념에 대한 합의는 광범위하게 이루어지지 않았다. 다시 말해, 누군가 어떤 언어가 강타입이다, 약타입이다 라고 말할 때의 기준은 자의적인 경우가 많다. 심지어는 본인의 선호에 따라 언어의 급을 나누기 위한 용도로 사용되는 경우도 왕왕 있다.

    본 연재에서는 이 두 용어를 모호하다 간주하고 사용하지 않는다. 또한, 만약 어떤 구분이 필요한 경우, 강타입 또는 약타입이라는 모호한 용어 대신 실제로 표현하고자 하는 특성을 직접 명시한다.


    정적 타입 검사와 동적 타입 검사

    타입 시스템이 프로그램의 타입을 검사하는 시점이 언젠지에 따라 프로그래밍 언어를 크게 두 분류로 나눌 수 있다. 정적 타입 검사static type checking를 시행하는 언어와 동적 타입 검사dynamic type checking를 시행하는 언어가 바로 그 두 경우다.

    정적 타입 검사

    정적 타입 검사를 시행하는 프로그래밍 언어는 프로그램의 타입이 올바른지에 대한 검사를 런타임 이전에 시행한다. 때문에 앞서 언급한 예제들에서 드러났듯, 프로그램을 실행해보지 않고도 특정한 종류의 오류(타입 에러type error)를 예방할 수 있다. 다만 타입 검사를 통해 모든 가능한 오류를 제거할 수 있는 것은 아니다. 대표적인 예외가 바로 0으로 나눔(divide-by-zero)이다. 정적 타입 검사를 시행하는 언어를 정적 타입 언어라고 부르기도 하며, 정적 타입 언어의 대표적인 예로 C, C++, Go, Haskell, Java, Kotlin, Rust, Scala 등이 있다.

    동적 타입 검사

    동적 타입 검사를 시행하는 프로그래밍 언어는 프로그램의 타입이 올바른지에 대한 검사를 런타임에 실행한다. 즉, 프로그램을 실행해보기 전까지는 어떠한 타입 에러도 적발할 수 없고, 만약 타입 에러가 존재한다면 이는 프로그램의 실행 중 발생한다. 동적 타입 검사만을 시행하는 언어를 동적 타입 언어라고 부르기도 하며, 동적 타입 언어의 대표적인 예로 Javascript, Lisp, Lua, Perl, PHP, Python, Ruby 등이 있다.

    주의할 점

    두 가지 짚고 넘어갈 점이 있다.

    첫째로, 정적 타입 검사와 동적 타입 검사는 굉장히 거대한 범위의 개념이다. 예를 들어 C와 Haskell은 모두 컴파일 타임에 타입 검사를 시행하지만, 두 언어의 타입 시스템은 그 외에는 닮은 부분을 찾기 힘들 정도로 다르다. 따라서 “정적 타입 검사를 시행하는 언어가 동적 타입 검사를 시행하는 언어보다 낫다”와 같은 주장은 반례를 맞닥뜨리기 쉬운 취약한 주장이다. 타입 시스템과 관련된 생산적인 주장을 위해서는 구체적인 언어의 구체적인 기능을 이야기하는 것이 좋다.

    다음으로, 정적 타입 검사와 동적 타입 검사는 상호 배제적인 개념이 아니다. 정적 타입 검사를 시행하는 많은 언어가 런타임에도 어느 정도의 타입 검사를 시행한다. 런타임에만 존재하는 정보를 통해서만 그 적법성을 판단할 수 있는 경우가 존재하기 때문이다. 타입 B가 타입 A의 서브타입일 때, A 타입의 객체를 B 타입으로 형변환하는 다운캐스팅downcasting이 그런 예이다.


    점진적 타이핑

    앞서 언급했듯, 최근 들어 기존에 동적 타입 검사만을 수행하던 언어에 정적 타입 검사를 도입하고자 하는 시도가 점점 늘어나고 있다. 하지만 프로젝트가 10k LoC 정도 되는 상황만 생각해봐도, 모든 코드 베이스에 동시에 타입 정보를 전부 적어야만 정적 타이핑을 사용할 수 있다면 현실적으로는 정적 타입 검사를 도입하기가 극히 어렵거나 심지어는 불가능할 것이라 짐작할 수 있다.

    이런 상황을 피하기 위해 이런 시도 중 다수가 점진적 타이핑gradual typing을 지원한다. 프로그램 전체가 아닌 프로그래머가 명시한 일부 부분만 정적 타입 검사를 거치게 하고 나머지 부분은 그대로 동적 타입 검사가 이루어지도록 하여, 말 그대로 점진적인 개선을 가능케 하는 것이다. 점진적 타이핑이 가능한 대표적인 동적 타입 언어로 Closure, TypeScript, Hack 등이 있다. 또한, C#의 경우 정적 타입 검사 언어로 시작했으나 후에 dynamic 키워드로 동적 타입 검사를 거칠 부분을 지정하는 것을 허용함으로써 점진적 타이핑을 도입했다.


    타입 선언

    많은 정적 타입 언어가 프로그래머가 직접 모든 값 또는 변수가 어떤 타입에 속하는지 명시하는 명시적 타입 선언explicit type declaration을 작성할 것을 요구한다. 예를 들어, C에서는 모든 변수명 앞에 해당 변수의 타입을 적어주어야 한다. (int i) 이런 언어에서는 타입 선언이 수반되지 않는 값 또는 변수를 포함하는 소스 코드를 허용하지 않는다.

    한편, Haskell을 비롯한 일부 언어는 프로그래머의 명시적 타입 선언 없이도 이미 타입이 알려진 값, 변수, 메소드 등의 정보를 이용해 코드의 타입 정보를 추론 해낼 수 있다. 이렇듯, 별도의 타입 선언 없이 타입 정보를 규명하는 일을 타입 시스템에게 맡기는 방식을 암시적 타입 선언implicit type declaration이라 부른다. 어떤 상황에서 타입 정보를 얼마나 정확하게 추론해 낼 수 있느냐는 타입 시스템에 따라 극명하게 달라진다.

    타입 추론은 굉장히 유용하고 흥미로운 주제인 만큼, 차후 별도의 포스트로 좀 더 깊이 다룬다.


    형변환

    앞선 글에서 언급했듯, 한 값은 여러 타입에 속할 수 있다. 하지만 이는 가능성의 문제일 뿐, 실제로는 특정 시점에 어떤 값(또는 변수)에는 하나의 타입만이 할당 될 수 있다. 하지만 다양한 필요로 인해 이 값의 타입을 지금 할당된 타입과 다르게 바꾸어야 할 필요가 생길 수 있다. 예를 들어, 정수 타입과 부동소수점 타입을 구분하는 언어에서 두 타입에 속하는 값을 더하는 경우가 그러하다. 이때, 정수 값의 타입을 부동소수점으로 변경하거나 혹은 그 반대의 작업이 선행되어야 한다. 이렇게 값의 타입을 변경하는 작업을 형변환type conversion이라 부른다.

    형변환, 타입 캐스팅, 타입 강제

    형변환에 관해 이야기할 때 자주 쓰이는 세 용어가 있다.

    • 형변환type conversion
    • 타입 캐스팅type casting
    • 타입 강요type coercion

    이 세 용어는 그 의미와 관계가 정확히 합의되지 않았고, 언어와 플랫폼 별로 다르게 사용된다. 본 연재에서는 형변환과 타입 캐스팅을, 그리고 아래에서 언급할 암시적 형변환과 타입 강제를 각각 같은 개념으로 정의하고 사용한다.

    형변환은 크게 암시적 형변환과 명시적 형변환으로 나눌 수 있다.

    암시적 형변환

    암시적 형변환implicit type conversion은 이름이 암시🙃하듯 프로그래머의 명시적 선언 없이 컴파일러 또는 인터프리터를 통해 자동으로 일어난다. 어떤 연산이 허용하지 않는 타입에 대해 이루어질 때, 타입 에러를 내는 대신 컴파일러 혹은 인터프리터가 하나 이상의 피연산자를 ‘말이 되는’ 타입으로 자동으로 변경한 후 작업을 진행하는 것이다.

    이 때, ‘말이 되는’ 변환이란 무엇인지에 관한 정의는 언어마다 다르다. 어떤 언어는 정수 값과 부동소수점 값 사이를 포함해 모든 암시적 형변환을 전혀 허용하지 않고, 어떤 언어는 지나치다 싶을 정도로 관용적인 태도로 모든 프로그램을 어떻게든 타입 에러 없이 실행시킨다. 그리고 대부분 언어의 정책은 그 두 극단 사이 어딘가에 위치한다. 독창적인 암시적 형변환으로 악명 높은 언어로 자바스크립트를 꼽을 수 있다.

    /* Javascript, the good parts */
    console.log(1 + 1)          // 1
    console.log(1 + 3.1)        // 4.1
    console.log(1 + 3.14)       // 4.140000000000001 (IEEE 754)
    
    /* Javascript, some parts of it... at least */
    console.log(1 + []);        // '1'
    console.log(1 - []);        // 1
    console.log(1 + {});        // '1[object Object]'
    console.log(1 - {});        // NaN
    console.log([] + []);       // ''
    console.log({} + []);       // 0
    console.log({} + {});       // NaN

    명시적 형변환

    암시적 형변환과 다르게 프로그래머가 값을 다른 타입으로 해석하겠다는 의도를 명확히 밝힘에 따라 일어나는 형변환을 명시적 형변환explicit type conversion이라 부른다. 명시적 형변환은 언어에 따라 타입 캐스팅 연산자type casting operator, 타입 변환 함수type conversion function 등을 이용해 일어난다.

    // C
    double a = 3.3;
    int b = (int) a;
    
    // Python
    a = 3.3;
    b = int(a)
    
    // Haskell
    a = 3.3
    b = round a

    안전성과 완전성

    ‘올바른’ 프로그램과 그렇지 않은 프로그램을 구분하는 타입 시스템의 효용을 평가하기 위한 척도로 논리학에게서 안전성soundness와 완전성completeness이란 두 개념을 빌려올 수 있다. 이때, 프로그래밍에서의 타입 시스템과 타입, 그리고 프로그램은 각각 논리학의 논리 체계와 명제, 그리고 증명에 대응한다.

    안전성

    어떤 논리 체계에서 증명 가능한 모든 명제는 참일 것이 보장될 때, 이 논리 체계를 안전하다고 한다. 어떤 타입 시스템이 안전하다는 것은 이 타입 시스템의 타입 검사를 통과한 프로그램은 타입 오류를 일으키지 않을 것이 보장된다(no false positive)는 의미로 이해할 수 있다. 즉, 안전한 타입 시스템하에서 타입 검사를 통과한 프로그램은 모두 (타입 시스템이 보장하는 범위 내에서) 올바르다.

    완전성

    어떤 논리 체계에서 모든 참인 명제는 증명 가능할 것이 보장될 때, 이 논리 체계를 완전하다고 한다. 어떤 타입 시스템이 완전하다는 것은 타입 오류를 일으키지 않을 모든 프로그램이 이 타입 시스템의 타입 검사를 통과할 것이 보장된다(no false negative)는 의미로 이해할 수 있다. 즉, 완전한 타입 시스템하에서 모든 (타입 시스템이 보장하는 범위 내에서) 올바른 프로그램은 타입 검사를 통과한다.

    트레이드오프

    이 두 성질은 공짜가 아니다. 안전성을 높이다 보면 올바르지만 컴파일을 통과하지 못하는 프로그램이 생기는 –프로그래머가 타입 시스템의 비위를 맞추는– 상황이 발생한다. 또한 완전성을 높이다 보면 올바르지 못한 프로그램이 타입 검사를 통과해 런타임에 오류를 만날 수 있다. 비록 안전성과 완전성, 그리고 결정 가능성decidability에 관한 이론적 한계가 존재하나, 현실의 프로그래밍 언어가 맞닥뜨리는 이와 관련된 대부분의 문제는 이론적 한계라기보다는 구현 상의 문제에 해당한다. 모든 언어는 이 트레이드오프에서 어느 쪽에 무게추를 달 것인지의 디자인 결정을 내리며, 그 결정은 해당 언어를 이용한 개발 경험에 직접적인 영향을 준다.


    마치며

    이번 글에서는 앞으로 논의를 전개하기 위해 필요한 용어들에 대해 알아보았다. 다음 글에서는 프로그래머에게 제약으로 작용하는 타입을 적극적으로 활용하면서도 유연함을 잃지 않기 위한 하나의 수단인 다형성polymorphism에 대해 다룬다. 다형성이란 무엇이고 왜 필요한지, 어떤 종류의 다형성이 존재하며 각각 어떤 특징을 갖는지, 서로 어떻게 다른지 등을 소개할 예정이다.

    참고 자료

  • 타입과 타입 시스템 : 연재를 시작하며

    타입과 타입 시스템에 대한 연재를 시작하며.

    들어가며

    타입type.

    TypeError: unsupported operand type(s) for +: 'int' and 'str'
    TypeError: "x" is not a function

    타입!

    프로그래머라면 이 단어를 하루에도 몇 번은 마주할 것이다. 타입은 프로그램을 작성할 때에도, 문서를 읽을 때도, 또 디버깅을 할 때도 중요한 요소로 작용한다. 훌륭한 타입 시스템을 가진 언어를 쓸땐 타입 체커type checker가 프로그래머의 실수를 잡아줘 마치 컴파일러와 페어 프로그래밍을 하는 듯한 신기한 경험을 하게 된다. 한편 사려 깊지 않게 구현된 타입 시스템은 별로 도움은 주지 않으며 ”대체 왜 이걸 일일이 적어줘야 하는 거야” 따위의 불평만 불러일으키는 주범 취급을 받기도 한다.

    어느 쪽이든, 타입과 타입 시스템type system은 프로그래밍에서 굉장히 큰 비중을 차지한다. 그런데도 이 주제를 다루는 자료 대부분이 특정 언어의 타입 시스템에 대해 다루는 수준에서 그친다. 그 때문에 타입 시스템 전반에 대한 지식을 폭넓게 다루는 자료가 너무 없다는 아쉬움을 갖게 되었다. 그런 아쉬움을 해소하고자 하는 조그만 시도의 하나로 타입과 타입 시스템에 대한 연재를 해보려 한다.

    부디 즐기시길!

    독자분들께

    평어체로 써놓으니 왜인지 어색하여 이 부분만 경어체로 작성합니다.

    저는 이 주제로 학위를 받은 적도 없거니와 가진 경력도 그다지 길지 않습니다. 글을 써나가며 공부를 게을리하지 않겠으나 부족한 부분이 많을 것입니다. 글에서 잘못된 정보를 발견하신다면 부디 메일 등의 수단으로 알려주시길 부탁드립니다. 이어질 글들이 첫 선보임 이후에도 독자분들께서 지적/보충해 주신 내용을 바탕으로 꾸준히 나아지는 것이 저의 바람입니다. 꼭 지적이 아니더라도 연재에 관한 어떤 의견이든 환영합니다. ❤️

    또한 저는 이 연재가 타입 시스템에 이미 큰 관심을 두고 있는 일부만이 즐길 수 있는 내용에서 벗어나, 가능한 많은 프로그래머에게 흥미롭게 읽히고 도움이 되길 바랍니다. 그 때문에 이론적 엄밀성을 다소 희생하는 한이 있더라도 저와 같은 보통의 프로그래머에게 더욱 쉽게 다가갈 수 있다면 그 길을 택할 예정입니다. 너그러운 양해 바랍니다. 연재의 기조에 대한 의견도 환영합니다. ❤️


    타입과 타입 시스템, 현 위치

    흔히 타입 시스템은 (특히 새로운 언어에 막 발을 들이는 프로그래머에게) 별문제 없어 보이는 프로그램을 실행하기 위해 불필요한 노력을 더 들이게 만드는 까다로운 잔소리꾼으로 인식되곤 한다.

    하지만 이는 부정적인 편견에 불과하다. 견고한 타입 시스템을 통해, 프로그래머는:

    • (주석과 다르게) 항상 프로그램의 최신 상태와 합치되는 방식으로 프로그램의 올바른 동작을 기술할 수 있다.
    • 런타임에 발생하는 오류를 예방할 수 있다.
    • IDE를 비롯한 다양한 도구에게 도움을 줌으로써 자동 완성 등의 기능의 사용을 수월하게 만들 수 있다.

    실제로 프로그램을 실행하기 전에 프로그램의 타입이 올바른지 검사하는 정적 타이핑의 가치는 프로그래머 커뮤니티 사이에서 날이 갈수록 점점 인정받는 추세다. Python의 mypy와 3.5 버전에서 정식 문법에 들어온 옵셔널 타입 힌팅 문법, 그리고 Javascript의 FlowTypeScript 등 기존에는 런타임에만 타입의 유효성을 검사하던 언어에 정적 타이핑을 도입하려는 시도가 활발히 이루어지고있다는 사실이 이러한 기류를 증명한다.

    ‘타입스크립트’ 주제 Google Trends ‘타입스크립트‘ 주제 Google Trends

    ‘mypy’ 검색어 Google Trends ‘mypy’ 검색어 Google Trends

    인기와 별개로 실질적인 효과에 대한 의문에는 지난 5월 ICSE 2017에 발표된, 자바스크립트에 정적 타입 시스템을 도입한 결과를 분석한 논문 To type or not to: quantifying detectable bugs in JavaScript. 이 하나의 답을 제시할 것이다. 이 논문은 정적 타입 시스템을 도입했더라면 Github에 공개적으로 올라온 코드의 버그중 최소 15%는 커밋조차 되지 못하고 잡혔을 것이라고 주장하고 있다. 이 결과의 의의에 대한 논의는 논문 내에 인용된 Microsoft 사의 엔지니어링 매니저의 발언을 재인용 하는 것으로 갈음한다.

    충격적이다. 만약 개발하는 방식에 어떤 변화를 주어서 저장소에 들어오는 버그중 10% 이상을 줄일 수 있다면, 고민할 이유가 전혀 없다. 개발에 쓰이는 드는 시간이 두 배 이상 늘어나거나 하지 않는 한, 우리는 그 변화를 택할 것이다. That’s shocking. If you could make a change to the way we do development that would reduce the number of bugs being checked in by 10% or more overnight, that’s a no-brainer. Unless it doubles development time or something, we’d do it.


    타입과 타입 시스템

    그렇다면 과연 프로그래밍에서 타입이란 무엇인지, 그리고 타입 시스템의 역할은 무엇인지에 대해 이야기해보자.

    타입의 의의

    타입은 프로그램의 ‘올바른 동작’이 무엇인지에 대한 프로그래머의 의도를 인코딩하는 역할을 한다. 두 정수를 더하는 작업과 기계의 특정 메모리 주소로부터 어떤 값만큼 떨어진 주소를 찾는 작업을 생각해보자. 프로그래머는 두 연산을 전혀 다른 의도로 작성하고 사용하겠지만, 이 두 연산 모두 어셈블리 수준에서는 “한 값에 다른 값을 더한다”라는 같은 연산으로 환원된다.

    따라서 코드를 읽는 다른 프로그래머 혹은 미래의 작성자 자신에게 이런 프로그래머의 의도를 효과적으로 전달하기 위해서는 이러한 정보를 어딘가에 추가로 기록할 필요가 있다. 때로는 별도의 주석이나 문서 또는 변수명이 그런 기록의 수단이 되곤 한다. 마찬가지로 타입 또한 그러한 의도를 인코딩하는 장소가 될 수 있다.

    주석이나 변수명과 다르게 타입은 프로그램의 실제 동작과 일정 수준 이상으로 동떨어질 수 없다는 것이 보장된다. 아래의 자바스크립트 파일을 보자.

    // 자기 자신을 리턴한다
    function sum(a, b) {
      return a + b;
    }
    
    function concat_string(a, b) {
      return a - b;
    }
    concat_string("a", "b") // NaN

    위의 concat_string, sum 함수는 각각 주석 또는 함수명을 읽고 추론할 수 있는 것과는 전혀 동떨어진 동작을 하고 있다. 하지만 이 두 함수는 분명 유효한 자바스크립트 함수이며, 자바스크립트 실행기는 조금의 거리낌도 없이 이 함수를 실행할 것이다. 이런 상황이 발생할 수 있는 이유는 주석과 변수명은 프로그램의 실제 동작과 직접 결합하지 않는, 상대적으로 추상적인 정보이기 때문이다. 이런 불일치에 불만을 가지는 건 프로그래머뿐일 것이다.

    반면 타입은 어떨까? 아래 타입스크립트 코드를 보자.

    type IdentityFunction = (a: number) => number
    const sum: IdentityFunction = (a: number, b: number) => {
    	return a + b;
    }
    // error TS2322: Type '(a: number, b: number) => number' is not assignable to type 'IdentityFunction'.
    
    function concat_string(a: string, b: string): string {
        return a - b;
    }
    // error TS2322: Type 'number' is not assignable to type 'string'.
    // error TS2362: The left-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type.
    // error TS2363: The right-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type.

    두 함수는 각각 함수 아래에 주석으로 적혀진 오류를 발생시킨다. 올바르지 않은 타입 정보를 가진 프로그램을 실행할 수단이 원천적으로 차단되는 것이다. 주석이나 변수명과는 다르게, 타입 정의와 다르게 동작하는 프로그램은 실행 자체가 불가능하다는 점은 타입을 앞서 언급된 다른 수단보다 훌륭한 명세 수단으로 동작할 수 있게 해 준다.

    프로그램의 동작과 강하게 결합한, “올바른 동작”에 대한 기술 수단이라는 타입의 의의에 대해 간단히 다루어 보았다. 다음으로 ‘값이 타입을 갖는다’는 개념을 어떻게 이해할 수 있을지 살펴보자.

    집합으로 타입 이해하기

    프로그래밍에서의 타입은 수학에서의 집합과 매우 많은 특징을 공유한다.

    어떤 프로그래밍 언어 L로 쓰인 프로그램이 가질 수 있는 모든 값의 집합 V를 생각해보자. 이때, V의 원소 중 특정한 조건을 만족하는 값들을 모은 집합L에서의 타입 T라고 정의할 수 있다. 예를 들어, MIN_INT, MIN_INT + 1, ..., 0, 1, 2, ..., MAX_INT 라는 값을 모두 모은 집합을 Integer 라는 타입으로 부를 수 있겠다.

    이렇게 타입을 집합으로서 바라볼 때, 아래와 같은 대응 관계가 성립한다.

    • 원소 x가 집합 S에 속한다 (x ∈ S) 👉 값 x는 S 타입에 속한다. (혹은 S 타입을 값 x에 할당assign할 수 있다.)
    • 한 원소가 여러 집합에 속할 수 있다 👉 한 값이 여러 타입에 속할 수 있다.
    • 집합 T가 집합 S의 부분집합이다 (T ⊂ S) 👉 타입 T가 타입 S의 서브타입subtype이다.
    • 조건 제시법을 이용해 기존에 존재하는 집합으로부터 새로운 집합을 만들어낼 수 있다 ( S’ = { (x, y) | x ∈ S, y ∈ S }) 👉 기존 타입의 정의로부터 새로운 타입을 만들어낼 수 있다.
    • 모든 집합의 부분집합인 공집합 Ø이 존재한다 👉 모든 타입의 서브타입인 바닥 타입bottom type이 존재한다 (혹은 존재할 수 있다).

    구체적인 예를 들어서 위의 비유를 다시 살펴보자.

    • L 코드에서의 값 0은 위에서 정의한 Integer 집합의 원소이다. 다시 말해, 값 0에 Integer 라는 타입을 할당할 수 있다.
    • V의 값 중 0과 1만을 원소로 갖는 집합을 생각할 수 있다. 이 집합을 Binary 라는 타입이라고 부르자. 이때, 값 0은 Integer 타입과 Binary 타입에 동시에 속한다.
    • Binary 타입에 대응하는 집합의 모든 원소는 Integer 타입에 대응하는 집합의 원소이다. 따라서 Binary 타입은 Integer 타입의 서브타입이다. 다르게 표현하면, Integer 타입 값을 필요로 하는 임의의 자리에 Binary 타입의 값을 넣어도 프로그램은 정상적으로 동작할 것이 보장된다.
    • 이미 존재하는 타입을 이용해 더욱 복잡한 자료형을 만들어낼 수 있다. 예를 들어, 문자열 타입으로부터 두 개의 문자열 필드를 갖는 풀 네임이라는 새로운 타입을 정의할 수 있다. (interface FullName = { first: string; last: string; } )
    • 바닥 타입의 존재 여부는 언어의 구현마다 다른데, 그중 한 예로 타입스크립트의 never 타입을 들 수 있다. 문서에 쓰여 있듯, never 타입을 갖는 실제 값은 존재할 수 없고, never 타입은 모든 타입의 서브타입이며, 다른 어떤 타입도 never 타입의 서브타입이 될 수 없다.

    이러한 맥락에서, 프로그래밍 언어가 제공하는 내장 자료형은 언어가 허용하는 모든 값 중 특정한 유용한 특성을 공유하는 일부 값들을 미리 묶어 둔 집합의 모음이다. 또한 프로그래머는 이러한 미리 정의된 집합의 모음을 이용해 무수히 많은 새로운 집합(조합 자료형composite data type)을 정의할 수 있다. 이때, 가능한 조합 자료형의 경우의 수는 다음 (언어에 따라 달라지는) 두 요소에 크게 영향을 받는다.

    • 내장 자료형의 종류. 예를 들어, 언어가 (가상) 기계의 메모리 주소를 직접 드러내는 자료형을 제공하는가? 수(number)를 나타내는 내장 자료형을 얼마나 많이 제공하는가? 등.
    • 가능한 조합의 수단. 예를 들어, “두 번째 값이 첫 번째 값보다 큰 짝pair”과 같은 타입을 표현할 수단을 제공하는가? “원소가 모두 같은 타입인, 크기가 정해져 있지 않은 리스트”와 같은 타입은?

    타입 시스템

    지금까지 타입의 의의, 그리고 타입을 이해할 수 있는 하나의 관점에 대해 다루었다. 타입을 통해 프로그래머의 의도를 표기할 수 있지만, 프로그램의 모퉁이 모퉁이마다 그러한 의도를 기술 해두었다 한들 그 의도가 실제로 강요되지 않는다면 무의미할 것이다. 타입 시스템은 프로그램 내에 타입을 이용해 기술된 프로그래머의 여러 의도가 말이 되는지, 서로 모순을 일으키지 않는지 검사(타입 검사type check)하는 검사원이다.

    현실에서, 같은 일을 수행하는 검사원도 어떤 이는 사소한 오류는 설렁설렁 넘어가지만 어떤 이는 조금의 미심쩍은 부분이라도 보이면 깐깐하게 파고드는 것처럼 개인차가 존재한다. 마찬가지로, 타입 시스템도 큰 목적은 같되 언어마다 그 엄밀함과 능력에 있어 큰 차이가 있다. 오류의 가능성을 절대 허용하지 않고 매우 다양한 종류의 오류를 잡아내는 타입 시스템이 있는 한편, 극히 제한적인 종류의 오류만 검출수 있으며 그런 오류마저도 매우 너그러이 넘어가는 타입 시스템도 존재한다.

    더 엄밀하고 더 풍부한 표현력을 가진 타입 시스템이 항상 더 훌륭한 타입 시스템인 것은 아니다. 거의 모든 것이 그렇듯, 여기에서도 트레이드오프가 일어난다. 일반적으로, 타입 시스템이 더욱 엄밀하고 강력해질수록 언어가 복잡해지는 경향이 있으며, 어지간해선 프로그램을 실행조차 못 시키게 하는 컴파일러가 프로그래머에겐 진입 장벽으로 작용하는 경우도 많다.

    하지만 이는 어디까지나 경향일 뿐 절대적인 진리는 아니다. 관련된 분야에서 깊은 조예를 가진 이들에게서 탄생한 타입 시스템은 다른 언어들보다 더 적은 노력을 요구하면서도 더 많은 것을 가능케 한다. 다른 언어보다 보다 정확하고 많은 정보를 담을 수 있는 언어가 정작 프로그래머에게는 더 적은 노력만을 요구하는 일은 얼마든지 가능하다.

    그런 강력한 타입 시스템을 가진 언어의 예로 Haskell을 살펴보자. 대부분의 언어와는 달리, 하스켈에서는 어떤 함수가 입출력를 수행하는지가 함수의 타입에 인코딩된다. 더욱 쉬운 이해를 위해 예를 들어 살펴볼 수 있겠다. 먼저 타입스크립트로 쓰인 다음의 함수를 보자.

    function sum(a: number, b: number): number {
    	launchNuclearMissile(); // 핵미사일을 발사한다
    	return (a + b);
    }

    sum 함수는 두 수를 받아 그 합을 리턴하는 함수다. 타입 상으로는 전혀 문제가 없다. 하지만 실제로는 가져다 쓰는 사람도 모르게 핵미사일의 발사 버튼을 누르는 대참사가 발생할 수 있다. 타입스크립트의 타입 시스템은 함수의 인자의 값, 그리고 함수가 리턴하는 값만을 검사하기 때문이다. 비슷한 일을 하는 함수를 하스켈로 짜면 어떻게 될까?

    sum a b = do
    	launchNuclearMissle
    	return (a + b)

    하스켈의 타입 시스템은 위 함수의 타입을 Num a => a -> a -> IO a으로 추론한다. 정수인지, 부동 소수인지, 또는 아예 다른 어떤 포맷인지는 모르겠으나 수(number)를 나타내는 a 타입의 두 개 인자를 받아 IO와 연관된 작업을 실행하고 a 타입 값을 리턴한다는 의미다. 이 정의로부터 프로그래머는 컴파일러의 도움을 받아 아래와 같은 사고의 과정을 손쉽게 밟을 수 있다.

    • 두 수의 합을 구하는 작업에는 IO가 끼어들 여지가 없다.
    • 그런데도 이 sum이라는 함수의 타입에는 IO와 연관된 작업을 수행한다고 쓰여 있다.
    • 함수의 내부에서 내가 sum 함수에 기대하지 않는, 어쩌면 위험한, 일을 하고 있음이 틀림없다.

    이렇듯, 하스켈의 타입 시스템은 타입스크립트의 그것이 쉽사리 (혹은 절대) 담아낼 수 없는 다양한 정보를 타입 내에 담는 것을 가능케 한다. 더구나, 위 코드에서 볼 수 있듯 프로그래머가 타입에 대한 정보를 전혀 주지 않았음에도 타입 시스템이 스스로 프로그램의 내부를 살펴 함수의 타입을 도출해 내고 있다(타입 추론). 이 단순한 예제를 통해 살펴보았듯이, 서로 다른 언어의 타입 시스템은 종종 같은 개념이라고는 생각하기 힘들 정도로 전혀 다른 수준의 정보를 담아내곤 한다.


    연재 계획 및 목표

    여기까지 연재를 시작하게 된 동기와 타입과 타입 시스템, 그리고 연관된 기본 개념에 대해 알아보았다. 앞으로 연재를 진행하며 추가로 다룰 주제들에 관해 이야기하며 이번 글을 마무리하고자 한다.

    아래 계획은 조금도 틀림 없는 청사진이 아니다. 그렇다기보단, 연재 시작 전 적어도 이런 주제는 다루어야 할 것 같다는 대강의 예측에 불과하다. 주제, 분량 및 순서는 연재를 진행하면서 유연하게 변경될 수 있다.

    기본 개념

    추후 구체적인 주제들에 대해 논의를 전개해나기에 앞서 필요한 기본 개념들을 논한다. 강타입과 약타입, 정적 타입 검사/동적 타입 검사, 타입 선언과 점진적 타이핑 등을 다룬다.

    다형성

    프로그래머에게 제약으로 작용하는 타입을 적극적으로 활용하면서도 유연함을 잃지 않기 위한 하나의 수단인 다형성polymorphism에 대해 다룬다. 다형성이란 무엇이고 왜 필요한지, 어떤 종류의 다형성이 존재하며 각각 어떤 특징을 갖는지, 서로 어떻게 다른지 등을 소개한다.

    타입 간의 관계

    서로 다른 타입 간의 관계에 대해 다룬다. 타입 간의 호환성과 동일성을 판단하는 두 가지 접근법인 구조적 타입 시스템structural type system과 이름에 기반을 둔 타입 시스템nominal(name-based) type system을 소개한다. 덕 타이핑duck typing과 같은 연관된 개념도 함께 다룬다.

    그 후, 두 타입 간의 관계로부터 각각의 타입으로부터 파생된 타입 간의 관계를 규정하는 반공변성contravariance, 공변성covariance, 불변성invariance 세 방법에 관해 이야기한다.

    대수 자료형

    조합 자료형을 만들 수 있는 하나의 방법인 대수 자료형algebraic data type에 대해 간단히 소개한다. 대수 자료형에는 어떤 종류가 있고, 대수 자료형을 이용한 사고 모델이 매일 마주치는 프로그래밍의 문제들을 어떻게 우아하게 풀 수 있도록 하는지 다룬다.

    타입 추론

    특정 언어를 사용하다 보면 정적 타이핑을 곧 번거로운 타입 어노테이션type annotation 작성과 동일시 할 수 있다. 하지만 사용자를 번거롭게 하는 강제적 타입 어노테이션은 구현의 문제일 뿐, 본질적인 제약 사항이 아니다. 명시적인 타입 선언 없이도 타입 시스템이 제공하는 대부분의 이점을 누릴 수 있게 하는 강력한 기능인 타입 추론에 대해 알아본다.

    타입 시스템과 명제의 증명

    프로그램의 타입 시스템이 논리적 명제의 증명과 어떤 연관을 가지는지에 대해 보다 자세히 다룬다. 커리-하워드 대응/Curry-Howard isomorphism/을 소개하고, 프로그램이 타입 검사를 통과한다는 것의 의미를 바라보는 다른 관점을 소개한다.


    참고 자료


    감사의 말

    친절히도 글을 먼저 읽어 주시고 또 많은 유용한 조언을 주신 xtendo님, dsm_ 님, markhkim 님, lifthrasiir 님께 감사의 말씀을 전합니다.

  • The XY Problem

    더 좋은 질문을 하는 방법.

    얼마 전 동아리 슬랙에서 선배를 통해 The XY Problem이란 개념에 대해 들었다. 해당 페이지에 아주 간결하면서도 명료하게 정리가 되어 있는데, 비슷한 상황을 한 번이라도 목격/경험한 사람은 무슨 이야기인지 바로 감이 올 것이다.

    • 유저는 X를 하길 원한다. (User wants to do X.)
    • 유저는 X를 어떻게 하는지는 모르지만, 만약 Y를 해낼 수 있다면 어떻게든 X의 해결책을 더듬거려가며 찾아낼 수 있을 것이라 생각한다. (User doesn't know how to do X, but thinks they can fumble their way to a solution if they can just manage to do Y.)
    • 유저는 Y도 어떻게 해야 할지 모른다. (User doesn't know how to do Y either.)
    • 유저는 Y에 관해 도움을 요청한다. (User asks for help with Y.)
    • 다른 이들이 Y에 관해 도움을 주려 하지만, Y라는 문제를 풀려는 이유를 이해하지 못하고 혼란에 빠진다. (Others try to help user with Y, but are confused because Y seems like a strange problem to want to solve.)
    • 많은 상호작용과 시간 낭비 이후에 마침내 유저는 X에 관한 도움을 필요로 하고, Y는 사실 X라는 문제를 해결하기에 적합한 해결책이 아니었다는 것이 명백해진다. (After much interaction and wasted time, it finally becomes clear that the user really wants help with X, and that Y wasn't even a suitable solution for X.)

    다른 모든 것과 마찬가지로 질문도 하다 보면 늘기 마련이다. (나는 다른 분야의 경험은 없으니 프로그래밍에 한정해보면) 경험상 무얼 어떻게 물어야 할지 모르는, 이 분야에 막 발을 들여놓은 사람들이 자주 저지르는 실수 같다. 나도 이런 실수를 참 많이 했는데, 여러 번의 경험을 통해 얻은 교훈과 비슷한 내용이 링크된 글 본문에도 적혀 있다.

    • 질문을 할 때에는 항상 내가 이 질문을 하게 된 주변 상황을 설명한다.
    • 턴 테이킹이 일어날 필요가 없도록, 맨 처음 질문에 가능한 최대한 많은 정보를 (중요한 순서대로) 나열한다.
    • 예를 들자면 다음과 같이 질문할 수 있겠다.
      • 저는 궁극적으로 Z를 하고 싶습니다. 그러기 위한 액션 플랜으로 A, B, C 의 안을 생각했지만 A는 이런 이유, B는 저런 이유로 적합하지 않은 해결책이라 판단했습니다. 따라서 C를 하려 하는데 그러기 위해선 첫 단계로 C-1을 진행해야 할 것 같습니다. 헌데 여기서 이런 문제 때문에 막혀 있습니다. 도움을 얻을 수 있을까요? 아래에 관련해서 찾아 본 이슈와 로그를 첨부합니다.

    경험이 일천한 나도 체감 했을만한 내용이니 업계에 있는 대부분의 분들이 이미 잘 알고 있겠으나, 이런 용어가 있다는 것은 모를 수 있겠다고 생각해서 간단하게 글로 남겨본다.

  • 나의 버건디 팔면체 : Three.js를 사용한 3D 그래픽스 입문기

    자바스크립트를 이용한 손쉬운 3D 그래픽스 프로그래밍 입문을 도와주는 라이브러리 three.js 사용기.

    들어가며

    프론트엔드 프로그래머로서 요즘 나의 가장 큰 관심사는 기술적으로도 훌륭한 동시에 보는 사람이 감탄할만한 아름다운 사이트를 만드는 것이다. 그런터라 지난 주에 미국의 Stripe라는 회사의 여러 랜딩 페이지를 보면서 약간의 질투를 동반하는 경이로움을 느꼈다. 특히 다른 무엇보다 Radar 라는 제품의 랜딩 페이지에서 천천히 돌아가고 있는 이십면체가 인상 깊었다.

    Stripe Radar

    솔직히 이 이십면체가 (Radar라는 제품의 핵심인) Stripe가 사기를 방지하기 위해 고려하는 다양한 면모를 한 눈에 보여주기 위한 최고의 수단인지는 잘 모르겠다. 하지만, 이 다면체에는 분명 들어온 사이트의 사람의 눈을 확 잡아끄는, 나가는 대신 스크롤을 내려보고싶게 만드는 그런 힘이 있었다. 비록 3D 그래픽스에 대해서는 전혀 모르지만, 나도 이런 걸 할 줄 안다면 분명 재미있고 유용할 것 같다는 생각이 들었다.

    그래서 지난 주말동안 한 번 도전 해 보기로 했다. 주제는 주말 동안 ahnheejong.name 대문에 3D로 구현한 다면체 띄우기 였다. 이 글을 보고 있다면 이미 보았겠지만, 아래가 나의 최종 결과물이다. 😎

    최종 결과물

    (자신감에 가득 찬 이모지를 쓴 것 치고는) 확실히 딱 봐도 별로 화려하지 않고, 위의 이십면체에 비하면 투박하기 그지없는, 지극히 단순한 다면체다. 하지만 3D 그래픽스 프로그래밍을 해 본 경험은 커녕 기반 지식조차 하나 없는 상태에서 짧은 시간 내에 내놓은 결과물이라 나 나름대로는 뿌듯하다. 너무 뿌듯한 나머지, 남들도 이런 즐거움을 느낄 수 있도록 나의 버건디 팔면체를 그리기까지의 공부 과정을 글로 남겨보기로 했다.


    Three.js?

    The aim of the project is to create an easy to use, lightweight, 3D library.

    three.js는 mr.doob 이라는 닉네임으로 활동하는 유저가 만든 3D 자바스크립트 라이브러리이다. 웹상에서 3D 그래픽을 갖고 놀기 위해서는 HTML5 Canvas, WebGL, SVG, 플래시 😅 등의 다양한 수단을 사용할 수 있는데, three.js는 이런 여러 프리미티브를 사용한 3D 그래픽을 좀 더 쉽게 구현하기 위해 한 단계를 감싸 놓은 자바스크립트 Wrapper 역할을 하는 라이브러리이다.

    회사에서 같이 일 하는 동료분 중 그래픽스에 관심과 조예가 깊으신 분이 계셔서 3D 그래픽을 해보려고 한다니 이런 저런 툴을 추천해 주셨다. 하지만 다수가 내가 써 본 적 없는 GLSL에 기반하고 있거나, 매우 낮은 추상화 단계에서 HTML5 Canvas를 직접 조작하는 식으로 작동했다. 컴퓨터 그래픽스에 있어 문외한이라 최초 학습 커브가 높은 기술로는 의미 있는 결과를 내기 전에 지칠 것이라 생각했다. 그래서 쉽게 사용할 수 있는 API 인터페이스를 갖고 있고, 익숙한 자바스크립트를 이용해 (Canvas 와는 다르게) 너무 절차적이지 않게 프로그래밍 할 수 있을 것이라 기대되는 three.js를 선택했다.

    3D 그래픽의 구성요소

    움직이지 않는, 정적인 3D 그래픽을 구현하기 위해서는 어떤 것이 필요할까? 조금만 생각을 해보면, 화면에 3D 그래픽을 그리는 일의 요소들은 현실에서 사진을 찍는 행위의 그것들과 거의 정확하게 대응된다는 것을 알 수 있다. 예를 들어보자. 카메라로 아래의 사진을 찍기 위해선 어떤 것들이 필요할까?

    🐈 (출처:File:Felis catus-cat on snow.jpg - Wikimedia Commons)

    • 삼차원에 살고 있는 우리는 간과하기 쉽지만, 무엇보다 먼저 카메라에 담을 삼차원의 공간 이 필요하다. 이 때 공간이라 함은 사진의 배경에 해당하는 부분을 이야기하는 것이 아니다. 설령 고양이와 눈을 비롯한 다른 모든 요소가 있어도 실제 크기보다 훨씬 작은 미니어처를 제작하지 않는 이상 1세제곱센티미터의 상자 안에서는 절대로 위와 같은 사진을 찍을 수 없다는 점을 생각해보라.
    • 그리고, 짐작했겠지만, 피사체가 필요하다. 여기서 피사체란 우리가 흔히 이야기하는 주가 되는 피사체(위의 사진에서는 🐈)만을 지칭하는 것이 아니다. 고양이와 배경에 쌓여 있는 눈 입자들, 먼지, 지나가는 파리 등 부피질감을 갖는 모든 물체가 피사체에 해당한다.
    • 그리고 마지막으로, 사진을 찍기 위해서는 📷🐈📸🎆 카메라가 필요하다. 그런데 카메라만 있다고 되는 건 아니고 하나의 요소가 더 있다. 아무리 좋은 카메라로 사진을 찍어봤자 이 요소가 없이는 새까만 화면 밖엔 볼 수 없다. 바로 이다.

    그리고 위에서 언급했듯이, 사진을 찍기 위해 필요한 이런 요소들은 각각 three.js의 특정 요소와 대응한다. 다음과 같이 적어 볼 수 있다.

    • 공간 - Scene
    • 피사체 : 부피, 질감 - Mesh : Geometry, Material
    • 카메라 - Camera
    • 빛 - Light

    그럼 이제 본격적으로 팔면체를 그려에 들어가기에 앞서 다음을 명확히 하겠다.

    • 나는 컴퓨터 그래픽스의 전문가가 아니다. 앞에서 언급 했듯 이제 막 첫 발자국을 뗀 학생에 불과하다. 내가 이해한 바를 틀리지 않게 옮기고자 노력하겠지만, 틀린 부분이 있을 수 있다. 만약 글에서 틀린 점을 발견한다면 [메일을 통해](mailto:heejongahn@gmail.com?subject=나의 버건디 팔면체 : Three.js를 통한 3D 그래픽스 입문기 오류 제보) 제보해 주시면 감사하겠다.
    • 아래의 내용은 2017년 6월 27일 기준 최신 버전인 three.js r86 기준으로 쓰여졌다.
    • 모든 자바스크립트 코드는 ES2016 기준으로 작성했다.

    그럼 이제 three.js로 3D 그래픽을 구현하기 위해 필요한 요소를 하나씩 살펴보자.


    공간

    가장 먼저, 우리의 오브젝트들을 놓을 공간이 필요하다. three.js 에서는 이러한 공간을 Scene 이라고 부른다.

    Scenes allow you to set up what and where is to be rendered by three.js. This is where you place objects, lights and cameras.

    const scene = new THREE.Scene();
    

    Scene은 단어의 뜻 그대로 우리가 화면에 그리고자 하는 어떤 장면에 해당한다. 보다 정확하게는, 그 장면에 대한 정보 - 카메라는 어디서 어떤 방향으로 바라보고 있고, 광원은 어디에 존재하고, 어떤 물체들이 있고 등 - 를 모두 담고 있는 무언가라고 이야기 할 수 있겠다. Scene 공식 문서

    필요한 모든 정보를 갖고 있더라도, 이러한 정보를 실제로 사람이 보는 화면에 그리기 위해서는 이 정보가 그려야 할 그림을 화면에 한 픽셀 한 픽셀 실제로 그려내는 작업이 필요하다. 이런 작업을 하는 녀석을 렌더러라고 부른다.

    그림을 그릴 때 같은 풍경을 그리더라도 유화로 그릴 수도, 수채화로 그릴 수도 있을 것이다. 또한 종이에 그릴 수도 있고, 점토에, 철판에, 아크릴 판 에 그릴 수 있다. 이처럼 같은 그림을 그리더라도 다양한 재료와 캔버스에 그릴 수 있듯, (거칠게 비유하자면) 같은 공간Scene도 다양한 기술을 이용해 그려낼 수 있다. 나는 웹상의 3D 그래픽스에서 가장 보편적으로 사용되는 표준 기술인 WebGL이라는 기술에 기반한 렌더러를 사용했다.

    The WebGL renderer displays your beautifully crafted scenes using WebGL.

    const renderer = new THREE.WebGLRenderer({
      alpha: true,
      antialias: true,
    });
    

    WebGLRenderer 컨스트럭터는 옵션의 Object를 인자로 받는데, 나는 투명한 배경을 가질 수 있고(alpha: true) 안티얼라이어싱이 적용된(antialias: true) 화면을 그리고 싶었으므로 해당하는 옵션을 주었다. WebGLRenderer 공식 문서

    여기까지는 별로 어려울 것도, 인상적일 것도 없다. 이제 내 집 마련의 꿈을 이루었으니, 가구들을 장만해보자.

    피사체

    우리는 팔면체를 그리고자 한다. 하지만 조금 더 자세히 들어가보면, 우리가 그리고 싶은 팔면체를 두 층위로 나누어서 생각해 볼 수 있다.

    1. 여덟개의 면, 여섯 개의 꼭지점, 여덟 개의 간선을 갖는 기하학적 형태
    2. 일종의 뼈대로서 기능하는, 그 기하학적 형태 위에 덧씌워져 실제로 우리 눈에 보여지는 질감을 가진 표면

    좀 더 쉬운 이해를 위해 구슬을 예로 들어보자. 부피와 반지름 등이 완벽하게 같은 쇠 구슬과 유리 구슬을 생각해보자. 우리가 만들어낸 Scene에 이 두 구슬을 하나씩 그리고자 할 때, 쇠 구슬과 유리 구슬을 각각 아무것도 없는 상태로부터 표면의 점을 하나씩 찍어가며 그려낼 수 있을 것이다.

    하지만, 구슬의 구형 기하학적 형태를 별도로 뺄 수 있다면 어떨까? 같은 기하학적 형태를 갖고 표면만 다른 - 쇠 구슬과 유리 구슬처럼 - 여러 물체를 그릴 때 매번 기하학적 형태를 정의할 필요가 없어질 것이다. 또한, 이렇게 뼈대와 표면이 분리되면 기존에 정의한 물체의 표면을 업데이트하기도 쉬워진다.

    이런 이유로, 그래픽스에서는 어떤 물체를 보통 두 부분으로 나누어 표현한다.

    • 기하학적 형태, 뼈대를 담당하는 부분을 Geometry 라 부른다. 구슬로 치자면 “반지름이 얼마짜리 구형 물체” 라는 정보가 여기에 해당한다.
    • 특정한 질감, 색, 반사율 등을 갖는 물체의 표면을 Material이라 부른다. 구슬로 치면 “은색이고 매끈하며 반사율이 높은 쇠 표면” 혹은 “투명하며 빛을 대부분 투과시기는 유리 표면” 등의 정보가 여기에 해당한다.

    그리고 이 Geometry에 Material이 입혀진 오브젝트를 three.js 에서는 Mesh라 부른다. Mesh 라는 용어의 정확한 학문적인 의미가 궁금해서 찾아도 보고 주변에 물어도 보았는데, 정확한 의미를 갖고 있다기보다는 어느정도 관용적으로 사용되는 용어인 듯 한다. 이하 글에서는 three.js 에서 사용되는 대로 Geometry 와 Material의 합을 Mesh라 부르겠다.

    물체(Mesh) = 뼈대(Geometry) + 표면(Material)

    그럼 우리의 팔면체 메쉬를 만들어보자! 먼저 Geometry가 필요하다. 3D 모델링의 기본 단위는 삼각형이다. 즉, 모든 면은 삼각형의 합으로 표현된다. 구체와 같은 매끄러운 면 역시 충분히 작은 크기의 삼각형을 충분히 많이 모아서 표현해 낼 수 있다. 수학시간에 보았겠지만, 정삼각형을 정사각형, 정육각형, 정이십각형 등으로 점점 꼭지점의 크기를 늘려가다보면 결국 원에 가까워지는 원리와 같다.

    그렇다면, 3D 모델의 뼈대를 만들어내는 가장 기본적인 방법은 다음과 같을 것이다.

    • 꼭지점(Vertex)를 정의한다.
    • 어떤 세 꼭지점이 이어져서 삼각형 면(Face)을 이루는지를 정의한다.

    이 작업을 코드로 옮기면 아래와 같다.

    const geometry = new THREE.Geometry();
    geometry.vertices.push(
      new THREE.Vector3(-10, 10, 0),
      new THREE.Vector3(-10, -10, 0),
      new THREE.Vector3(10, -10, 0)
    );
    
    geometry.faces.push(new THREE.Face3(0, 1, 2));
    

    위 코드는 x-y 평면에 세 점 (x= -10, y=10, x=-10, y=-10, x=10, y=-10)을 찍은 후 Geometry의 첫 번째, 두 번째, 세 번째 점을 잇는 면을 추가하는 코드이다.

    이해하기 어려운 개념은 아니지만, 이런 식으로 모든 모델링을 해야 한다면 조금만 복잡한 물체를 그리려 할 때 코드의 양이 급격이 늘어나고, 의도를 파악하기 힘들어질 것이다. 그런 사태를 피하기 위해, three.js 에서는 미리 정의된 다양한 형태의 Geometry를 제공하고 있다. 사면체, 육면체 팔면체, 이십면체 등 다면체와 구, 평면 등의 Geometry 를 제공하고, 전체 목록은 공식 가이드에 들어가서 확인할 수 있다. 우리가 필요로 하는 Geometry 인 Octahedron Geometry를 다음과 같이 생성할 수 있다.

    const RADIUS = 40;
    const geometry = new THREE.OctahedronGeometry(RADIUS, 0);
    

    길게 설명한 것이 무색하도록 한 줄로 끝나버렸다 😅 여담이지만 지난 주말에 작업을 하며 three.js가 제공하는 이런 간편한 API 덕분에 의미있는 결과물을 빨리 찍어낼 수 있는 점이 좋다고 느꼈다. THREE.OctahedronGeometry가 받을 수 있는 인자의 종류와 의미는 공식 문서에서 확인 할 수 있다.

    Geometry가 준비되었으니, Material을 준비할 시간이다. 도입부의 다면체를 보면 간단한 수준이나마 회전에 따라 각 표면이 빛을 더 받거나 덜 받으면서 명도가 달라지는 것을 확인할 수 있다. 이 부분은 잠시 후 글의 후반부에서 구현하기로 하고, 일단 이 파트에선 빛과 상호작용 하지 않는, 가장 기본적인 표면인 MeshBasicMaterial을 사용하도록 한다.

    const material = new THREE.MeshBasicMaterial({ color: "#ff3030" });
    

    마지막으로, 만들어 낸 Geometry와 Material 을 이용해 Mesh 를 만들어보자. Mesh의 생성자는 (예상했겠듯이) Geometry와 Material의 두 인자를 받는다.

    const mesh = new THREE.Mesh(geometry, material);
    

    이제 그리고자 하는 물체의 준비가 끝났다. 마지막으로, 앞서 준비한 공간에 이 물체를 놓아보자. 기본적으로 scene.add 함수를 통해 공간에 추가한 물체는 (0, 0, 0) 위치에 놓인다. 나중에 관찰을 위해 팔면체를 공간에 놓은 후에는 z축에서 뒤쪽 (화면을 뚫고 들어가는 방향) 으로 약간 밀어 두자.

    scene.add(mesh);
    mesh.position.z = -RADIUS * 10;
    

    공간이 있고, 물체가 있다. 이제 카메라가 남았다.

    카메라

    지금까지의 요소들이 그랬듯, three.js의 카메라 역시 현실 세계의 카메라와 같은 역할을 한다. 같은 공간에 같은 물체들이 배치되어 있어도, 어디에 서서 어떤 시선으로 바라보느냐에 따라 보이는 풍경이 다를텐데, 이 시선에 해당하는 것이 카메라다.

    여기서는 실제 사람의 눈 또는 카메라 렌즈와 비슷하게 투시 투영을 사용하는 PerspectiveCamera를 사용하도록 하겠다. (PerspectiveCamera 이외에도 다양한 형태로 Scene을 바라볼 수 있는 Camera가 있는 것으로 알고 있지만, 나는 아직까지 사용 해 보지 않았다.) PerspectiveCamera는 아래와 같이 정의할 수 있다.

    const FIELD_OF_VIEW = 20;
    const ASPECT = WIDTH / HEIGHT;
    const NEAR = 0.1;
    const FAR = 10000;
    
    const camera = new PerspectiveCamera(FIELD_OF_VIEW, ASPECT, NEAR, FAR);
    

    생성자가 받는 네 개의 인자는 각각 다음과 같은 의미를 갖는다.

    • FIELD_OF_VIEW: 카메라의 시야각을 의미한다. 커질 수록 카메라가 바라보는 시야각이 넓어짐을 의미한다. 단위는 degree.
    • ASPECT: 시야의 가로세로비를 의미한다. 컨테이너의 가로세로비와 동일한 값을 넣어주는게 좋다. 단위 없음.
    • NEAR: 렌더링 할 물체 거리의 하한값으로, 너무 가까이 있는 물체를 그리는 것을 막기 위해 사용한다. 카메라로부터의 거리가 이 값보다 작은 물체는 화면에 그리지 않는다. 0보다 크고 FAR 보다 작은 값을 가질 수 있다.
    • FAR: 렌더링 할 물체 거리의 상한값으로, 너무 멀리 있는 물체를 그리는 것을 막기 위해 사용한다. 카메라로부터의 거리가 이 값보다 큰 물체는 화면에 그리지 않는다.

    이제 정말 모든 준비가 끝났다.

    그려내기

    앞서 잠깐 언급했듯이, 이 모든 정보를 실제로 화면에 그려내는 일은 renderer의 일이다. 여기서는 #three 라는 id를 갖는 <div>를 컨테이너로 사용하기로 하고, 가로 세로 길이를 각각 200px로 그려보도록 하겠다. 먼저 렌더러의 가로 세로 값을 정해주자.

    const WIDTH = 200;
    const HEIGHT = 200;
    
    renderer.setSize(WIDTH, HEIGHT);
    

    그 후, 렌더러가 그려낸 장면을 담을 <canvas> 엘리먼트를 DOM 트리에서 컨테이너의 자식으로 추가한다. 해당 엘리먼트는 renderer.domElement 프로퍼티를 통해 접근할 수 있다.

    const container = document.querySelector("#three");
    container.appendChild(renderer.domElement);
    

    마지막으로, 우리가 지금까지 만들어놓은 장면과 카메라를 이용해 화면을 실제로 그리라는 명령을 내린다. 이 명령은 renderer.render 메소드를 이용한다.

    renderer.render(scene, camera);
    

    여기까지 모든 과정을 따라왔다면, 아래와 같은 화면을 볼 수 있을 것이다.

    너무나도 새빨간 그대

    지금 하고 있는 생각을 맞춰보겠다:

    이건 팔면체가 아니라 마름모잖아 😳

    일단 결론부터 말하자면, 이건 팔면체가 맞다. 정확히 말하면, 다음과 같은 두 가지 특수한 상황에 놓여 있는 팔면체다.

    • 팔면체의 중심은 (0, 0, -400)에 놓여 있고, 카메라는 (0, 0, 0) 에 놓여 있다. 우리가 정의한 카메라의 시점은 z축을 따라 팔면체의 정중앙을 뚫고 지나가고 있다.
    • MeshBasicMaterial은 빛과 상호작용을 하지 않는 Material 이라고 했다. 실제로 우리는 공간에 빛을 정의조차 하지 않았다.

    빛의 부재는 곧 공간에서의 심도의 부재를 의미한다. 심도가 없는, 즉 한 축이 무의미해진 3D는 2D로 나타난다. 우리가 원했던 것은 2D 같은 3D가 아니라 3D다운 3D이므로, 이제 이 팔면체를 실제로 3D 공간의 물체와 같아 보이도록 만들어 보자.


    빛과 질감

    먼저, 이 공간에 심도를 심어보자. 위에서 잠깐 힌트를 줬듯이 이 작업은 두 단계로 나눌 수 있다.

    • 공간에 빛을 추가한다.
    • 팔면체가 빛과 상호작용하도록 한다.

    먼저 빛을 추가해보자. 모든 광원의 생성자는 기본적으로 색깔(color)와 세기(intensity)의 두 인자를 받는다. three.js는 공간 전체를 밝히는 AmbientLight, 특정 방향으로 뻗어나가는 DirectionalRight 등 다양한 종류의 광원을 제공한다. 이 글에서는 가장 기본적인 광원 중 하나인 PointLight를 사용하겠다.

    A light that gets emitted from a single point in all directions. A common use case for this is to replicate the light emitted from a bare lightbulb.

    공식 문서의 설명에서 알 수 있듯, PointLight는 마치 전구처럼 한 점에서 시작해 모든 방향으로 뻗어나가는 광원을 표현하기 위해 사용된다.

    const pointLight = new PointLight(0xffffff, 0.5);
    
    pointLight.position.x = 100;
    pointLight.position.y = 100;
    pointLight.position.z = 30;
    
    scene.add(pointLight);
    

    백색 광을 정의하고, 위치를 잡아준 뒤 공간에 빛을 더한다. 이 시점에서는 화면에 아무런 변화가 없는데, 이는 아직까지도 팔면체의 표면이 빛과 상호작용을 전혀 하지 않기 때문이다.

    이제 Material을 변경 해보자. 짐작했겠지만, three.js 에서는 빛과 상호작용하는 표면 중 자주 쓰이는 표면 모델 몇 가지를 기본적으로 제공한다. 그 중 여기에서는 람베르트 반사율을 갖는 물체의 표면을 나타내는 MeshLambertMaterial를 이용해보겠다. 기존에 작성된 코드에서 material의 정의를 아래와 같이 변경해보자.

    const material = new THREE.MeshLambertMaterial({ color: 0xff3030 });
    

    이제 아래와 같은 화면을 볼 수 있을 것이다.

    거의 완성된 모습

    앞서 생성한 빛을 (100, 100, 30)에 두었는데, 이는 우측 상단, 모니터를 약간 뚫고 나온 곳에 위치하는 점이다. 실제로 더 많은 빛을 받을 우측 상단은 더 밝은 색을 갖는 반면 좌측 하단은 빛을 거의 받지 못해 까맣게 보이는 것을 확인할 수 있다.

    아직 나한테는 평면으로 보이는데?

    그럼 마지막으로 이 팔면체가 삼차원 상에서 그려졌다는 것을 보다 명확히 하기 위해, 한 번 회전시켜보자.

    움직임

    브라우저에서는 requestAnimationFrame 함수를 사용해 매끄러운 애니메이션을 그려낼 수 있다. 이 함수는 콜백 함수를 인자로 받고, 한 프레임을 할당받아서 인자로 받은 콜백 함수를 실행한다. 앞서 적었던 renderer.render(scene, camera) 라인을 다음으로 교체한다.

    function update() {
      const speed = Math.random() / 20;
      octahedron.rotation.x += speed;
      octahedron.rotation.y += speed;
      octahedron.rotation.z += speed;
      renderer.render(scene, camera);
      requestAnimationFrame(update);
    }
    
    requestAnimationFrame(update);
    

    매 프레임마다 0 ~ 0.05 사이의 값을 임의로 정한 뒤, x, y, z 축마다 해당 값만큼의 회전을 준다. 그 뒤에 scene을 다시 그리고, 자기 자신을 requestAnimationFrame 함수의 인자로 다시 넘겨 호출하는 내용이다.

    전혀 배경 지식이 없는 사람도 이 글만 읽고도 대부분의 과정을 이해할 수 있도록 적기 위해 노력하느라 무척 길어졌지만, 실제 코드는 백 줄이 채 되지 않을 정도로 간단하다. 이 과정까지 전부 마쳤다면 글 맨 처음에 나왔던, 그리고 지금 이 사이트에서 돌아가고 있는 것과 같은, 회전하고 있는 버건디 색의 팔면체를 얻어낼 수 있다.

    전체 코드와 동작하는 모습은 코드펜에서 확인해 볼 수 있다.


    맺으며

    이 글을 통해 3D 그래픽스에 대해 전혀 문외한이던 내가 홈페이지에 팔면체를 그려내기까지의 여정을 전부 담아 보았다. 나와 같은 위치에 있는 사람을 위해 내가 궁금했던 점을 따로 찾아보지 않아도 알 수 있도록 최대한 자세히 적으려 노력했는데, 그 때문에 너무 쉽고 장황한 글이 되지는 않았는지 걱정도 된다. 개인적으로는 글을 적으면서 애매하게 알고 있다고 생각했던 개념을 보다 가다듬을 수 있어 좋았다.

    앞으로 어떤 커리어를 갖고 싶은지 누가 물었을 때, 뭐가 될지는 모르겠지만 아마 웹에서 무언가를 할 것 같고, 그래픽스는 안 건드릴 것 같다는 식으로 대답했던 적이 있다. 그만큼 그래픽스는 나에게 있어 멀고 또 어렵게 느껴지던 주제였다. 그런 내가, 비록 지극히 간단한 형태이긴 해도 어떤 목표를 세운 뒤 스스로 정한 시간 내에 구현 해 냈다는게 즐거웠다. 자신감이 조금 생기는 듯도 했다.

    무엇보다도 스스로 내가 할 수 있는 일, 또 내가 재미있다고 느끼는 일에 섣불리 한계를 정할 때 결국 손해 보는 건 나뿐이라는 간단한 이치를 다시 한 번 체감할 수 있었다.

    다음 단계

    주말 동안 다면체를 올려 보겠다는 목표 자체는 달성했지만, 지금 상태는 시작에 불과하다고 생각한다. 앞으로 시도해보고 싶은 이런 저런 목표들이 있다. 예를 들자면 다음과 같다.

    • 마우스 / 터치 이벤트에 적절히 반응하는 인터랙션 추가
    • 팔면체가 사면체, 큐브 등으로 모양을 바꾸어가는 애니메이션 추가
    • 텍스쳐를 입히고 보다 정교한 Material을 이용해 나무, 쇠 같은 느낌 구현
    • 그 외에 여러 방법으로 더 이쁘게 만들기

    더 좋은 생각이 있으면 내게 제보 해 준다면 감사하겠다. 한 번에 엄청난 격변을 이루어내기보다 천천히 조그만 부분씩 실험 해 볼 생각이다.

    감사의 말

    마지막으로, three.js를 처음 접하고 큰 도움을 받았던 블로그 포스트의 저자 Paul Lewis, 그리고 홈페이지에 팔면체를 그리고 그 과정을 이렇게 글로 옮기며 많은 가르침과 도움을 준 김지현최종찬에게 감사를 전한다.


    글을 공유하고자 하실 때에는 본문을 복사/붙여넣기 하는 대신 링크를 남겨주십시오. 이 글에 대해 남기고 싶은 코멘트나 오류 지적, 글쓴이에게 하고 싶은 말 등은 [메일을 통해](mailto:heejongahn@gmail.com?subject=나의 버건디 팔면체 : Three.js를 통한 3D 그래픽스 입문기 오류 제보) 제보해 주시면 감사하겠습니다.

    긴 글을 읽어주셔서 감사합니다.