사실 이 책을 읽은 지는 약 3~4개월 정도 지났는데, 정리를 미루다가 늦게나마 책 리뷰를 써봅니다.
책 제목 <나중에 온 이 사람에게도>는 얼핏 들으면 무언가 따뜻한 느낌이 전해져오는, 감성적인 느낌을 주는 제목인데요. 책 내용은 제목과는 전혀 다르게 경제학을 다루고 있습니다. 그리고 전반적인 느낌도 따뜻하다기 보다는 거침없는 근대 경제학(그 당시에는 현대 경제학)에 대한 비판적인 내용이 많이 있어서 날카로운 면이 있어요.
책을 제가 읽고 난 뒤 시간이 몇 개월 지난 지금까지도 계속해서 떠오르는 몇 가지 대목을 정리해보고자 합니다.
이 책이 쓰여진 시대는 약 160년 전입니다. 산업혁명 이후 활발하게 자본주의가 꽃을 피우던 시대였고, “경제학”이라는 게 정의된 지 그리 오래되지 않은 시기였습니다. 경제학의 아버지라고 불리는 애덤 스미스의 <국부론>에서는 다음과 같이 말합니다.
자신의 이익만을 추구함으로써, 개인은 더 자주, 더 효율적으로 사회의 이익을 증진시킬 수 있다.
즉, 애덤 스미스는 모두가 자신의 이익을 추구하는 것, 모두가 이기적으로 행동하는 것이 사회 전체의 관점에서도 더 경제적이라고 주장했던 것입니다. 저도 중/고등학교 시절에 얼핏 이런 내용을 배웠던 것 같은 기억이 있어서 딱히 이 관점이 틀렸다고 생각한 적이 없었고, 사실이라고 믿었습니다.
하지만 존 러스킨은 다른 주장을 내놓습니다. 경제학에는 사랑이 필요하다는 것입니다. 이게 뭔 말인가 싶은데.. 예를 한 번 들어보겠습니다.
서로 다른 회사에 다니는 A와 B가 있습니다. A의 회사에서는 직원을 쥐어짭니다. 월급은 최대한 낮게 주면서 일은 많이 시킵니다. 월급이 너무 낮다고 불평하면, 다른 사람으로 갈아치웁니다. 그게 회사 입장에서는 가성비가 좋다고 생각하니까요. B의 회사는 하는 일 대비 합당한 월급을 지급합니다. 각종 복지도 잘 되어있어서 저절로 애사심, 자부심이 뿜뿜합니다. 직원 입장에서 어떤 회사에서 일이 잘 될까요? 당연히 B의 회사겠죠.
A는 수동적으로 행동할 수 밖에 없습니다. 회사를 미워하니까요. 주말에 쉬면서 일에 관해 좋은 아이디어가 저절로 나지도 않을 것이고 어쩌다가 난다고 해도, 절대 얘기하지 않을 것입니다. 회사가 잘 되길 바라지 않으니까요. 반대로 B는 시키지 않아도 쉬는 시간에도 일을 잘 해낼 방법에 대해서 고민해보고 실제로 실행해 볼 겁니다. 회사가 잘 됐으면 좋겠으니까요. “애사심”이라는 건 사실 회사에 대한 사랑인거죠. 네, 사랑이 필요합니다.
경제학의 모든 주체는 기계가 아닌 인간이고, 인간은 서로에 대한 사랑이 있을때 경제학적으로 더 큰 성과를 낼 수 있다는 것이죠. 만약 기계라면 “가성비”를 따져서 싸면서도 효율 좋은 기계를 구입하는 게 합리적인 선택이 되겠죠. 하지만 인간은 그렇지 못하다는 것입니다.
여러분은 상인, 혹은 장사꾼이라고 하면 어떤 느낌이 떠오르시나요? 어떤 숭고하거나 고결한, 충성심 높은 그런 이미지는 아닐 겁니다. 저도 그렇지만 대체로, 탐욕스러운 이미지가 많이 그려지고요. 실제로도 가격 정찰제가 잘 안되어있는 곳에서 무언가를 구매할 때 호갱 되어본 적 다들 한 번 쯤은 있을 거에요.
우리가 성당의 신부, 군인을 떠올릴 때는 이렇게 탐욕스러운 이미지는 아닐거에요. 왜냐면 그들이 행동하는 동기는 돈 말고도 많은 것들이 있다고 우리가 믿기 때문이죠. 하지만 상인은? 상인에 대해서 떠올릴 때 우리가 돈과 떼어 놓고 생각할 수는 없을 거에요. 실제로 많은 상인들이 돈과 이해득실만을 따져서 행동하는 것도 사실이지만요.
하지만 분명히 상인이라는 직업이 필요하기 때문에 이 사회에 존재하겠죠. 현대 사회에서 상인의 역할은 사실 “돈을 버는 것”이 아니라, 제조나 유통입니다. 그러니까 존 러스킨은 돈은 내려놓고, 원래 그들이 사회에서 맡고 있는 역할이 뭔지에 대해서 다시 생각해보자는 거죠. 존 러스킨은 상인들에게도 그 역할, 소명, 직분을 잊지 말고 이해득실에 따라 행동할 것이 아니라 맡은 역할에 따라 행동하기를 촉구하는 것이죠.
우리도 마찬가지로 일을 하면서 단순히 돈을 벌기 위해서 하는 것보다는 자신이 가지고 있는 직업의 소명(직분)이 뭔지 다시 고민해본 뒤, 그 소명에 따라 행동하는 게 존 러스킨의 경제학적 관점에서는 더 “경제적”인 게 되지 않을까요?
요즘 코로나19로 인해 마스크 값이 많이 비싸졌죠. 보이지 않는 손을 떠올리며, “공급은 그대로인데 수요가 폭발적으로 올랐으니 값이 비싸지는 건 당연한 것이다” 라고 생각하는게 무척 당연합니다. 하지만 존 러스킨의 관점에서는 당연하지 않을 것 같아요. 사실 제품을 생산하고 유통하는데 드는 원가는 코로나 사태가 발발하기 전부터 지금까지 변하지 않았으니까요. 원자재의 가격이 오르지 않았느냐라고 생각할 수 있지만, 원자재 가격 또한 단순히 수요 상승으로 인한 가격 상승이니 마찬가지의 논리를 적용할 수 있을 것 같아요. 상인의 소명이 “국민들에게 필요한 물건을 공급하는 것”이라면, 이 관점에서 마스크의 원자재든, 마스크 그 자체이든, 과연 가격이 올라야 할 합당할 이유가 있을까요? 모든 상인들이 이런 위기 때도 자신의 직업적 소명을 중시해 평소와 같은 가격으로 공급한다면, 국민들에게 상인에 대한 존경심을 일으킬 수 있지 않을까요?
우리에게 부자는 일반적으로 그냥 “돈이 많은 사람”으로 정의됩니다. 그리고 우리는 그런 부자를 부러워하고요. 그런데, 부자이지만 병환으로 침대에 누워있는 사람도 부러워 하시나요? 그보다는 그 많은 유산을 물려받을 상속자를 부러워 하시겠죠. 이렇게, 단순히 돈이 많은 것보다는 그 돈을 사용할 수 있는 상태가 훨씬 더 중요합니다. 존 러스킨은 그래서 부에 대해서 이렇게 정의합니다. “부란 우리가 사용할 수 있는 물건에 대한 소유 상태를 뜻한다.” 즉, 모든 부는 사용해야만 의미가 있다는 거죠.
그런데, 비단 죽기 전이 아니더라도 우리는 우리가 가진 부를 제대로 사용하지 못하는 상황을 종종 마주치는 것 같아요. 무한리필뷔페에서 내가 먹을 양보다 많이 가져온 음식, 충동적으로 구매해놓고 한 번도 메지 않았던 가방, 사놓고 귀찮아서 한 번도 펼치지 않은 책 등등.. (네, 모두 제 얘기에요 💦) 이렇게 비효율적인 소비를 계속하는 것은 각 개인의 경제 상황에도 악영향을 미칠 뿐만 아니라 거시적인 관점에서 봤을 때는 사회의 경제에도 안 좋은 영향을 미치겠죠.
이런 관점에서 저는 각 개인이 재화를 “제대로” 소비하기 위해서 노력한다면 사회 전체의 부가 증가할 거라고 생각해요. 실제로 이런 관점에서 재화를 제대로 소비하기 위해서 노력했던 캠페인이 IMF를 극복하기 위한 운동의 일환이었던 아나바다 운동이죠. 아나바다 운동을 벌인 이유가 무엇이든, 결과적으로 이 캠페인이 성공적으로 잘 정착한다면 사회 전체 부의 증가로 이어질 거라고 믿어요. 물건을 많이 팔아야 하는 기업 입장에서는 악재겠지만요.
이 책은 간디의 인생을 완전히 바꾼 책으로 유명하기도 합니다. 국내의 많은 기업가 분들이 추천하시는 책이기도 하고요. 저도 꼭 한 번 읽어봐야 할 책이라고 생각해서 오래된 책임에도 불구하고 읽어보았던 것 같습니다.
제가 생각나는 대로 두서없이 정리를 했는데, 제 개인적인 생각이 많이 들어가 있는 게 사실이라서 원저자의 의도와는 많이 다를 수 있습니다. (제가 잘못 해석한 부분도 있을 수 있고요.) 여러분들이 책을 직접 읽으시면서 떠오른 생각을 스스로 정리 해보시는 게 가장 좋을 것 같아요. 그리고 위의 내용이 당연히 전부가 아니고 더 좋은 내용들이 많고, 느껴지는 부분도 많으니까요. 다들 꼭 한 번 쯤은 읽어보시면 좋을 것 같습니다.
]]>한 해 치 반성을 몰아서 하게 되는 회고 시즌이 어김없이 올해도 찾아왔습니다. 1년 정말 후딱가네요. 개인적으로 너무 반성할 게 많아서 이번 회고는 패스할까.. 하다가 결국 남들이 쓰는거 보면서 후달려서 이렇게 막판에 회고글을 작성하게 되었네요. 올해는 정말 일이나 GDG 활동 말고는 한 게 별로 없는 것 같아요.
저는 매도 먼저 맞는 걸 좋아해서 .. 일단 반성부터 하고 시작하려고 합니다. 적어놨던 목표는..
네.. 정말 처참하네요. 이래서 제가 회고글을 쓰기가 싫었어요.
먼저 블로그는 현재 쓰고 있는 theme 유지보수도 중단했고, 당연히 Gatsby로 마이그레이션도 못했죠. 주변에 일과 블로그 활동 두 마리 토끼 잘 잡고 계시는 분들 보면서 자괴감도 종종 느끼고 있습니다.
글도 거의 안 써서 이 글이 올해 다섯 번 째 글인데요. 그나마 Google I/O 후기 3개를 제외한다면 단 하나..의 포스팅이 되겠네요. 아이고 처참해라. 작년에 분명히
이제 이것보다 적게 쓰기도 어려우니 내년에는 그래도 올해보다는 글을 많이 쓰지 않을까 싶다.
라고 했는데 그것보다 적게 썼으니 정말 대단하다고 할 수 밖에 없겠죠? 자기 변명을 조금 하자면 쓰다가 집어친 글이 다섯 편 쯤은 있는데요. 어쨌든 퍼블리시 안했으니 아이고 의미없다.
요즘 블로그가 많이 뜸해진 건 결국 마인드의 차이가 원인인 것 같아요. 2019년에 정말 꾸준히 제 스트레스 지수가 높았는데요. 이 때문인지 거의 시간이 생기면 그냥 놀고 쉬려고만 했던 것 같아요. 2020년에는 그래도 좀 멘탈 관리하면서 다시 또 의지를 다져봐야 할 것 같습니다.
운동! 네, 운동도 역시 끊어둔 헬스장은 한 삼 개월 나가고 다신 안 나간 것 같고요. 그나마 운동은 가을 즈음부터 런닝과 링피트를 시작하게 되면서 다행히 꾸준히 보충하고 있습니다. 이 와중에 열심히 먹어서 몸무게는 우상향 그래프를 그리고 있고요. 네. 이러다 건강한 돼지가 될 것 같아요.
당장 목표는 특별히 없지만 지속이라도 하는게 목표입니다. 다행히 링피트가 저한테는 잘 맞고 각종 방어 정책을 잘 만들어 놔서 당분간은 꾸준히 할 수 있을 것 같아요.
그리고 무엇보다 건강이 최우선! 올해 목관절이 많이 안 좋아서 한동안 도수치료를 받아야 했는데 육체적으로나 정신적으로나 힘들었어요. 최대한 병원 갈 일 없도록 꾸준히 운동해서 건강을 지키는게 최우선 목표입니다.
성장은 몰라도 적응은 확실히 된 것 같습니다. 사실 뭘 성장이라고 정의내려야 할 지 모르겠어요. 적응도 성장의 일종 아닐까요? 사실 좋은 동료들과 일하는 것만으로도 성장할 수 있기 때문에 그런면에서는 성장한 것 같아요. 이제 막 일한 지도 1년이 지났는데, 시간이 지나면서 제가 미치는 영향력의 범위가 조금씩 넓어지는 것도 느끼고 있고요. 일복 터졌다 가끔이지만 주변 동료분들이 좋은 평가해주셔서 뿌듯할 때도 있었고요. 정말 큰 실수를 해서 힘들었던 적도 있었는데 그 경험이 깊게 머리에 뿌리 박히면서 결과적으로는 성장할 수 있었던 것 같아요. 저 때문에 힘드셨던 분도 있었는데 여전히 감사하면서도 죄송스러운 마음입니다.
저는 여전히 기술적인 면에서든 협업적인 면에서든 부족한 부분이 많습니다. 당장 앞에 있는 문제를 푸는데 집중하는 편이라서 올해 기술적인 챌린지는 많이 못했던 것 같은데요. 앞으로는 틈나는 대로 당장은 필요하지 않더라도 장기적인 문제를 해결하는데 신경을 쓸 수 있었으면 좋겠어요.
물론 저라고 놀고만 있었던 건 아니에요!
올해는 단연 GDG 활동이 제 개인시간에서 가장 많은 비중을 차지하는 활동이었어요. 단순 참가자로서 참가한 활동을 제외하더라도, 스태프나 스피커, 오거나이저로 참여한 이벤트가 10개가 넘어요. 작년에는 오거나이저임에도 불구하고 GDG 활동이 어떤 건지 이해를 거의 못했었는데, 이제는 잘 이해가 되기도 하고요. 특히 올해 마지막 두 이벤트인, DevFest WebTech 2019와 Angular Codelab은 정말 개인시간을 최대한 할애해서 열심히 준비했었고 그만큼 보람찼던 활동이었어요. 참가자분들이 얻어간 것도 많은 것 같고 반응도 좋았어서 뿌듯하고 기뻤습니다.
올해 GDG 활동에서 Google I/O를 빼놓을 수는 없겠죠. 첫번째 외국 컨퍼런스 참석이었는데 역시 큰 물이라서 그런건지 구글이라서 그런건지 테마파크 같은 분위기가 인상적이었고요. 전세계 IT의 중심에 있다는 뽕에 취해 즐겁게 돌아다녔던 것 같아요. 개인적으로도 첫 미국 여행이라서 재밌었어요. 한 번 쯤 꼭 가볼만한 행사라고 생각해요. 다녀오고나서 페이스북의 F8이나 마이크로소프트의 빌드 같은 다른 행사들도 궁금해졌어요. 자세한 후기는 제가 남겼던 포스팅을 참고해주세요.
발표 경험은 이전에도 여러번 있었지만 강의 경험은 올해가 처음이었던 것 같아요. 비록 네 시간의 짧은 강의였지만 제가 정말 중요하다고 생각하는 내용을 다뤘다는 뿌듯함도 있었고, 참가자분들 반응도 좋았어서 준비한 만큼의 보람이 있었던 활동이었어요. 다만 앞으로 시간 여건 상 강의는 하기 어려울 것 같아서 앞으로 이런 내용의 글을 많이 쓰면 좋을 것 같다는 생각이 드네요. 하지만 또 지키지 못하고
이 강의를 하면서 정리한 내용은 제가 올해 블로그에 유일하게 퍼블리싱한 기술적 포스팅이였어요. 나름대로 열심히 정리한 내용이어서 퍼블리시 했을 당시에 반응이 좋았었는데, 지금도 뿌듯하네요. 궁금하신 분들은 여기서 읽으실 수 있어요.
올해는 상대적으로 책을 많이 읽었던 것 같아요. 제가 올해 읽었던 책은
정도네요. 물론 절대 많이 읽는 편이라고는 할 수 없지만 적게라도 꾸준히 읽으려고 노력하고 있어요. 요즘 사두고 읽지 않는 책도 많아서, 내년에는 더 많은 책을 읽을 수 있지 않을까 싶어요. 이 중 <나중에 온 이 사람에게도>는 곧 독후감을 포스팅할 생각입니다. (쓰고 있어요 !)
올해는 유난히 일 때문에 바쁘다는 생각을 많이 했던 것 같아요. 1년전에 지금의 회사로 이직을 했었는데 환경이 완전히 바뀌면서 일이 저에게 주는 영향력도 달라졌던 것 같습니다. 팀이 일하는 분위기나 방식, 그리고 협업하는 사람들의 숫자 등도 제가 일하는 방식에 많은 영향을 끼쳤지만 그 중에서도 제게 가장 많은 영향을 미친건 제가 쓰는 코드의 영향력이었어요. 이제는 제 코드가 훨씬 많은 사람들에게 영향을 미치기 때문에 예전보다 더 무겁게 받아들이고 있는 것 같아요. 물론 처음에 비해서 많이 무뎌지긴 했지만..
고작 1년 지났지만, 그 사이 손댄 제품의 숫자는 그 어느 때보다도 많았던 것 같아요. 각각의 제품이 가진 성격이 서로 많이 달랐어요. 어떤건 0에서 1을 만들어야 했고 어떤건 1에서 10을 만들어야 했죠. 또 어떤건 일회성 프로젝트 이기도 했고요. 그리고 각각의 제품을 개선하기 위한 이터레이션 과정에서 취한 도구나 방법론도 저마다 달랐어요. 그 덕분에 기술적 경험이든 비기술적 경험이든 다양한 걸 몸소 체험할 수 있었고, 소소한 깨달음도 있었어요. 어떤 도구든 방법론이든 모든 상황에서 옳지는 않다는 깨달음. 그게 우리가 지식의 넓이를 넓히고 다양한 환경을 경험해야할 이유가 되겠죠.
이렇게 다양한 사람과 협업이 많고 이곳저곳에서 시시각각 입력이 들어오다보니 스스로 할일 관리가 안되면 놓치는 부분도 생기게 되는데요. 덕분에 할일 관리에 대한 니즈를 크게 느꼈고 개인적으로 관리 기법을 정립하고 습관화하게 되면서 제 스스로에게도 큰 도움이 되었습니다.
마지막으로, 다소 챌린징한 조직에서 1년을 무사히 보냈다는 게 저한테는 가장 뿌듯한 부분입니다. 내년에도 좋은 동료들과 함께하면서 저의 성장과 회사의 성장이 함께 했으면 좋겠네요.
잘하고 있는건 더 잘하고, 못하고 있는건 시작이라도 하면 잘하는 거 맞죠? 그 중에서도
이 정도만 지킬 수 있어도 정말 만족스러운 한 해가 될 것 같아요. 지금은 달성하기 어려운 목표라고 생각하지 않는데.. 모르죠 내년 되면 또 변명하고 있을지도
그리고 이건 목표까지는 아니지만, 전반적으로 올해는 마음의 여유가 부족했던 탓에 여러가지 디테일을 놓치고 간과하고 넘어가거나 내게 중요하지 않다고 해서 신경쓰지 않았던 적도 많았어요. 2020년에는 조금이라도 마음의 여유를 더 찾고, 사소한 부분에도 신경쓸 수 있는 제가 될 수 있었으면 좋겠네요.
2019년에 저와 함께 했던 모든 분들께 감사드립니다. 덕분에 무사히 올해를 보낼 수 있었던 것 같아요. 내년에도 함께하면서 성장하는 한 해가 될 수 있었으면 좋겠습니다.
]]>이 포스트는 지난 5월 27일 “React 사용자를 위한 리액트 부트캠프“의 5일차 강의 때 사용한 발표자료와 스크립트를 글로 옮긴 것입니다. 스크립트를 거의 그대로 옮겼기 때문에 군데군데 구어체가 혼용되었으며 평소 블로그에서 쓰던 문체와도 다릅니다. 읽으실 때 양해 부탁드립니다.
실습을 위한 코드들이 다수 포함되어있기 때문에 모바일에서 읽기 불편할 수 있습니다. 저장해두신 후 PC에서 읽으시는 걸 권장드립니다.
컴포넌트는 리액트에서 가장 중요한 구성요소라고 말해도 과언이 아닙니다. 리액트로 만들어진 앱을 이루는 가장 최소한의 단위가 컴포넌트이기 때문이죠. 앱을 리액트로 만든다는 것은 곧 작고 단단한 컴포넌트들을 만들고 이 컴포넌트들을 유기적으로 연결한다는 것을 뜻합니다.
따라서, 잘 동작하는 리액트 앱을 만들기 위해서 우리는 크게 두 가지를 잘하면 됩니다.
이 강의에서는 “작고 단단한 컴포넌트를 만드는 것”에 좀 더 집중해보고자 합니다. 컴포넌트를 잘 만든다는 것은 사실 가장 당연하고 기본적인 일 같아보이지만 리액트 숙련자들도 종종 실수하는 부분이기도 합니다. 그래서 이 강의에서는 컴포넌트를 만드는데 가장 기본적인 원칙들을 정의하고 그걸 지키려고 노력하는데 주안점을 두고자 합니다. 사실 이것만 잘해도 본인을 고급 리액트 개발자라고 부를 수 있다고 생각해요.
그러려면 컴포넌트가 무엇인지 확실하게 파악하고 넘어가야겠죠. 컴포넌트라는 단위가 리액트에서 갖는 의미란 무엇일까요? 저는 근본적으로 컴포넌트란 데이터를 입력받아 DOM Node를 출력하는 함수라고 생각해요. 이 때 입력받는 데이터란 Props나 State 같은 것들이죠. 실제로도 React 공식 문서인 “Components and Props“를 보면 이런 문장이 있죠.
Conceptually, components are like JavaScript functions. They accept arbitrary inputs (called “props”) and return React elements describing what should appear on the screen.
이 강의에서 다루고 싶은 것은 이렇습니다. 시간이 허락한다면 다 하고 싶지만, 아마 불가능할 것 같네요.
리액트에 Hooks API가 추가된 이래로 HOC는 더 이상 좋은 접근법이 아닙니다. 필요하다면 사용할 수 있겠지만 대부분은 Hooks로 대체할 수 있기 때문에 저는 사용을 추천하지 않습니다. 이번 강의에서는 HOC를 다루지 않습니다.
본격적인 강의 진행에 앞서 저의 모든 실습 코드는 Stackblitz를 사용할 예정이기 때문에 가능하시면 미리 가입해두시면 좋겠습니다. 또는 GitHub 계정으로 로그인 하셔도 됩니다.
Form을 한 번 만들어볼게요. 우선 대부분의 웹사이트에서 가지고 있는 회원가입 페이지를 만든다고 가정해볼게요. 이 Form이 일반적으로 React에서 만드는 Form과 다른 점은 email, password 값을 State로 가지지 않고 클래스의 필드로 갖고 있다는 점인데요,
이 방법에는 장점과 단점이 있습니다. 장점은 render
가 전혀 실행되지 않는다는 것이죠. 왜냐면 이벤트가 발생할 때 그걸 핸들링하기만 할 뿐, State는 전혀 갖고 있지 않거든요. State를 갖고 있지 않으니 State를 업데이트 할 수 없고, 이건 렌더가 다시 일어나지 않는다는 걸 의미하죠. 모든 동작은 Native DOM의 동작에 의존하게 되는 겁니다.
단점은 State가 없으니 State를 제어할 방법도 없다는 겁니다. React 단의 상태는 없지만, DOM 상의 상태는 있죠. 하지만 여기에 접근하고 있지 않기 때문에 React에서는 이걸 제어할 방법 자체가 없게 되는 겁니다. 만약 Form을 Reset하는 버튼이 필요하다면 어떻게 해야 할까요? 상태를 Reset하기 위해서는 React에서 반드시 State를 보유해야 합니다. State를 써봅시다.
이제 초기화 버튼까지 아주 잘 동작하네요! 지금 보신 이게 바로 Controlled 컴포넌트 입니다. 아까 보았던 State 가 없는 방식이 Uncontrolled 컴포넌트이고요.
Uncontrolled 컴포넌트는 방금도 말씀드렸지만 상태를 직접 React 에서 제어하지 않는다는 의미에서 Uncontrolled 컴포넌트로 불립니다. React 생태계에서는 사실 잘 쓰이지 않아요. 이렇게 상태를 프로그래머가 제어해야할 일이 종종 생기기 때문입니다. 하지만 아까도 말씀드렸듯이 렌더를 아예 타지않는다는 장점이 있기 때문에 상태를 제어할 일이 없다면 쓰는 것도 좋다고 생각해요.
사실 좀 더 정확하게 말씀드리면 흔히 Controlled 혹은 Uncontrolled라고 부를 때의 차이는 이게 끝이 아닙니다. 이걸 이해하기 위해서 <Input/>
컴포넌트를 만들어볼게요. 우리가 사용하고 있는 type, placeholder, value, onChange를 props로 넘겨받아야 겠네요.
그리고 보통 리액트에서 사용하는 방식은 아니지만 render
가 호출되는 횟수를 측정해보기 위해 render에 다음과 같이 console.log를 집어넣을게요. 그리고 똑같이 이메일과 비밀번호를 입력해봅시다.
자 어떤가요? 지금은 이메일을 입력하고 있는데 불필요하게 비밀번호 Input의 render 함수가 호출되고 있습니다. 이걸 해결하기 위해서는 두가지 방법이 있습니다.
첫번째로는 이 Input 컴포넌트를 Uncontrolled 컴포넌트로 고치는 겁니다. 다만 JoinForm의 관점으로 보았을 때 Uncontrolled 인것이지 React 단에서 상태를 만들지 않겠다는 건 아닙니다. Input 컴포넌트의 내부에 상태를 만들겠다는 얘기죠.
자, 이렇게 JoinForm 내부에서 가지는 상태까지 제거하면 이제 완성입니다. 똑같이 이메일과 비밀번호를 입력해보면 이메일이 입력될 때 비밀번호 Input 컴포넌트의 render가 호출되지 않습니다! 성공인가요? 아닙니다. 초기화가 동작하지 않아요!
Uncontrolled Component에서 초기화를 동작시키게 하기 위해서 약간은 Tricky한 방법을 써야합니다. Form에 한가지 상태를 추가하고 그 상태를 Input 컴포넌트의 key 프로퍼티로 내려줍니다.
짠! 잘 동작하죠? 왜 이런걸까요?
key
라는 프로퍼티는 React에서 특수하게 다루어지는 프로퍼티입니다. 사실 Array.map
메소드를 통해 배열을 컴포넌트 노드로 렌더시켜 보신적 있다면 아마 알고계신 프로퍼티일 것 같아요. key
는 배열내에서 특정 아이템을 “identify”하기 위한, 특정짓기 위한 도구에요. 따라서 React의 관점에서 key
가 다른 노드는 그냥 다른 노드로 분류되는 것이죠. 예를 들어 여기서 key
만 바꿔준 email-1
과 email-2
는 우리 입장에서 봤을 때는 같은 것 같지만, React의 관점에서는 아예 다른 노드인겁니다.
그래서 우리가 email-1
을 email-2
로 바꾸면 React에서는 email-1
인스턴스가 사라졌다고 인식해 email-1
을 아예 지워버리고, 새롭게 email-2
를 그리는 겁니다. 즉, 이렇게 key
를 바꾸게 되면 단순히 상태가 초기화 되는 것이 아니라 컴포넌트 인스턴스 자체가 사라진 후, 다시 생깁니다. 이게 너무 빠르니 마치 상태가 초기화 된 것처럼 보이는 것이죠. (물론 이건 굉장히 단순화된 설명이에요.) 실제로도 각 컴포넌트의 renderCount
마저 초기화된 걸 확인하실 수 있죠? 단순히 상태만 초기화된 거라면 이런일이 일어날 수 없을 겁니다.
참조: Recommendation: Fully uncontrolled component with a key
위 링크에서 이 기법에 대한 더 자세한 정보를 확인하실 수 있어요. 이렇게 컴포넌트를 제거하고 새로 만드는 작업은 당연하게도 State를 단순히 초기화하는 것보다 성능이 나쁠 거라고 예상하게 되지만, 실제로는 성능차이가 별로 중요하지 않은 수준이고 특정 상황에서는 State를 초기화는 것보다도 빠를 수 있다고 해요. 저는 사실 Uncontrolled Component를 선호하는 편입니다. State가 외부에 공개되지 않아서(캡슐화) 가지는 장점이 크다고 생각해요.
하지만 이 방법도 근본적으로 완벽하지는 않습니다. 우선 초기화에만 쓰는 상태가 하나 추가 되었다는 단점이 있고요. 또, JoinForm 입장에서 봤을 때 Input 컴포넌트를 원하는 값으로 만들어줄 방법이 없죠. 이제 두 번째 방법을 알아봅시다.
PureComponent는 사실 React에서 기본이라면 기본적인 내용입니다. 어찌보면 렌더링 Performance를 Optimize한다는 점에서 Advanced라고 볼 수도 있지만 누구든 쉽게 사용하니까요.
PureComponent와 일반 컴포넌트의 차이점은 간단합니다. Component는 항상 render를 다시 실행하지만 PureComponent는 Props나 State를 얕은 비교해서 변경점이 없으면 render를 다시 실행하지 않아요.
아까의 Input 컴포넌트로 돌아가서 Component를 PureComponent로 바꿔볼게요. 그리고 이메일을 입력해보면? 비밀번호는 다시 렌더링되지 않는걸 보실 수 있을겁니다. 비밀번호에 들어가는 Props의 값이 모두 이전과 같기 때문이죠. 이렇게 PureComponent를 잘 사용하면 렌더링 최적화와 Controlled Component라는 두 마리 토끼를 잡을 수 있습니다.
그렇다면 모든 컴포넌트를 PureComponent로 도배하면 좋지 않을까요? 그렇지는 않습니다.
Think about it. If component’s props are shallowly unequal more often than not, it re-renders anyway, but it also had to run the checks.
— Dan Abramov (@dan_abramov) January 15, 2017
만약 항상 re-render 되는 PureComponent가 있으면 의미가 없다는거죠. 아니, 어찌보면 불필요한 비교 로직이 들어가기 때문에 더 느리다는 의미입니다. 실제로도 이런 실수는 언제든 쉽게 발생할 수 있습니다.
이 예제를 보시면 마찬가지로 PureComponent를 사용함에도 불구하고, 관련없는 두개의 Input이 서로 렌더링되고 있는걸 확인하실 수 있습니다. 왜일까요?
바로 이전의 예제와 이 예제의 차이점은 이 예제에서는 인라인 함수를 썼다는 것입니다. 인라인 함수를 render 메소드 내에서 쓰면 매 render
실행시마다 함수 인스턴스가 새로 생성되겠죠? 따라서 이 코드를 실행되면 결과는 false
로 출력됩니다.
1 | (() => null) === (() => null); // false |
이 말은 매 render
실행마다 Input에 Props로 내려오는 함수가 모두 다르다는 것이고, 함수의 실행과는 관계 없이 Shallow compare로 함수 자체가 다른지를 비교하기 때문에 항상 다르다는 결과를 반환하겠죠. 결론적으로 항상 re-render 되는 것입니다.
이런 상황은 인라인 함수 사용이 대표적이지만 이것 말고도 여러가지가 있습니다. 자주 부딪치는 문제중에서는 렌더 메소드 내에서 객체를 새로 만드는 케이스가 있죠. 이렇게요.
1 | class Parent extends React.Component { |
또 한 가지 정말 쉽게 실수할 수 있는 케이스는 ReactNode를 넘길 때, children
등을 사용할 때 인데요, 이 부분은 조금 이따가 Component Composition을 할 때 다뤄보겠습니다.
key
Props를 이용해 초기화 할 수 있다.render
를 최적화 할 수 있다.render
를 실행시키지 않는다일반적인 React 앱을 상상해보죠. View는 하나의 루트를 가지는 컴포넌트 트리입니다.
1 | <div id="root" />; |
이 경우에 가장 꼭대기에 <App />
이라는 이름의 컴포넌트가 렌더링 될 것이고 그 다음엔 이 컴포넌트가 가진 자식 컴포넌트가 렌더링 되고, 이게 가장 아래에 있는 컴포넌트까지 반복되겠죠. 결론적으로 컴포넌트의 계층 구조가 그대로 DOM의 계층 구조로 이어집니다. 그리고 “특별한 경우”가 아니라면 이 계층 구조를 역전할 방법이 전혀 없죠.
이 계층 구조는 대개의 경우 큰 문제가 없지만 React에서는 이 계층 구조가 불편한 상황이 가끔 생깁니다. 가령 컴포넌트 계층 구조로는 아래에 위치해야 하지만, 시각적으로는 상위 컴포넌트를 가려야하는 경우이죠. 그냥 position: fixed
를 쓰면 된다고 생각하실 수도 있지만 사실 그렇지 않습니다.
Dialog를 표현하려고 했는데 Dialog가 부모한테 갇혀버렸습니다. 심지어 position: fixed
를 쓴 상태임에도 불구하고 말이죠. CSS의 transform
속성이 동작하는 원리 때문인데요. 이 문제는 잘 알려진 문제중 하나고, 심지어 브라우저의 버그도 아닙니다. 아래 링크를 참조해보세요.
‘transform3d’ not working with position: fixed children
꼭 이게 아니더라도 쌓임 맥락, Stacking Context가 동작하는 원리 때문에 z-index
를 아무리 높여도 원하는대로 동작하지 않는 현상, 한 번 쯤은 다들 경험해보셨을 거에요.
여튼 position: fixed
나 z-index: 9999
로 만사 해결할 수 없다는 건 이해하셨을 겁니다. 이 때 Portal이 필요합니다. 사용법은 간단해요. html 파일에 Portal의 컨테이너가 될 <div>
를 하나 추가하고 해당 컨테이너를 대상으로 Portal을 렌더링 시키면 됩니다. 실제로 해볼까요?
차이점은 render
메소드에서 리턴을 할 때, 그냥 JSX를 쓰지않고, ReactDOM.createPortal
을 호출해서 렌더될 컨테이너를 지정한 것 밖에 없죠. ReactDOM.render
의 사용방식과 비슷하지 않나요? 이렇게 어렵지 않게 Portal을 사용할 수 있습니다. 이렇게 만들어진 컴포넌트는, 어느 컴포넌트 아래에 있건간에, Portal의 대상 컨테이너 아래에 렌더링 됩니다.
한 마디로 요약하자면 Portal 컴포넌트는 논리적으로 하위 컴포넌트여야 하는데, 시각적으로는 상위 컴포넌트를 덮어야 할 상황일 때 사용하면 됩니다.
React의 가장 좋은 점은 모듈의 단위가 컴포넌트라는 것이죠. 따라서 React에서는 컴포넌트 단위로 코드를 재사용할 수 있습니다.
일반적인 OOP에서는 상속이라는 기법을 통해 코드를 재사용하는데요. React에서는 상속이 굳이 필요할 만한 유즈케이스가 없다고 여기고, 상속을 지원하고 있지 않습니다. 대신 Composition, 합성을 지원하죠. 이 합성이라는 개념은 간단합니다. 컴포넌트에 다른 컴포넌트를 가져다 붙이는 거에요. 사실 그리고 children
을 지원하는 컴포넌트는 합성도 지원한다고 볼 수도 있죠.
참고: Composition vs Inheritance - React
실습을 위해 위에서 만들었던 Dialog
를 chlidren
을 통해 합성을 지원하는 컴포넌트로 바꿔보도록 할게요.
간단하게 Dialog
에 children
을 props
로 받게끔 변경했어요. 자, 이제 부모 컴포넌트에서는 Dialog
의 내부에 어떤 것이든 집어넣을 수 있습니다. 그리고 이게 바로 컴포넌트 합성이에요. 간단하죠?
이렇게 간단한 개념이지만, 어떻게 응용하느냐에 따라서 굉장히 강력한 기법입니다. 바로 위의 예제에서는 children
이라는 props
로 합성을 지원했는데, 실제로는 children
뿐만 아니라 다른 props
로도 합성을 사용하실 수 있어요. 예제를 좀 더 개선해볼까요?
기존의 children
을 지운 후, header
, body
, footer
를 받게 고쳤습니다. 그리고 부모 컴포넌트에서 각각의 props
에 컴포넌트를 넣어볼게요. 어떤가요? 이런식으로 얼마든지 하나의 컴포넌트가 다른 컴포넌트를 받아서 렌더링 하게끔 동작시킬 수 있습니다.
저는 이런 컴포넌트를 Template(혹은 Template Component)이라고 부르는데요, 일반적으로 React 쪽에서 사용되는 용어는 아닙니다. 하지만 골격을 미리 잡아놓고 세부적인 부분을 채워넣는 것이 Template이라고 부르는 개념과 유사하다는 생각이 들지 않나요? 실제로 Atomic Design에도 Temnplate Component가 있는데 이걸 구현할 때도 보통 합성을 사용하게 됩니다.
아까 그냥 넘어갔던 PureComponent + children
얘기를 해볼게요. 이번에는 아까 만들었던 Dialog를 PureComponent로 바꿔보겠습니다. PureComponent는 Props와 State를 Shallow Compare해서 같으면 render
를 실행시키지 않는다. 기억하시죠?
Dialog를 PureComponent로 바꾸고, Dialog의 render
가 실행되지 않는지 확인해보기 위해서 <input/>
을 하나 추가해 setState
로 계속 상태를 바꿔볼게요. 어떻게 되나요? 뭔가를 <input/>
에 타이핑할 때마다 Dialog
가 다시 render
를 실행시키는 걸 볼 수 있죠.
왜 이런걸까요? 문제는 아까와 같습니다. App
의 render
가 실행될 때마다 Dialog
에 넘길 header, body, footer를 모두 새로 만들기 때문이에요.
1 | <h2>반가워요!</h2> === <h2>반가워요!</h2>; // false |
우리가 실제로 사용하는 JSX 문법은 실제로는 객체를, 보다 정확히 말하자면 ReactNode 객체를 생성합니다. 객체는 여러분들도 잘 아시겠지만 내부적으로는 데이터가 같아도 비교 연산자를 통해 비교해보면 다르다는 결과가 나오죠. 이 때문에 render
내에서 컴포지션을 지원하는 컴포넌트에 JSX로 생성한 ReactNode를 넘기면 매 번 렌더가 다시 됩니다. 이것은 children
을 이용하더라도 마찬가지에요.
이 문제는 비교적 간단히 극복하실 수 있어요. render
내에서 ReactNode를 새로 생성하지 않으시면 됩니다.
이렇게 클래스의 프로퍼티로 넘길 ReactNode를 선언해놓고 쓰면 Dialog의 render
가 다시 일어나지 않죠! 임시적으로 이렇게 사용가능하지만 사실 근본적으로 적절한 방법은 아니에요. Dialog가 Props로 ReactNode를 받고있는 한, 다시 실수할 여지가 생길 수 있거든요. 내가 아니더라도, 내 동료는 충분히 실수할 수 있죠. 동료 입장에서 Dialog
가 PureComponent인지 아닌지 알게 뭔가요?!
따라서 ReactNode를 Props로 받는 컴포넌트.. 합성을 지원하는 컴포넌트라면 PureComponent를 쓰시는 것이 오히려 손해일 수 있습니다.
그렇지만.. Dialog의 render
를 최적화할 방법은 없을까요? 이렇게 부모 컴포넌트의 상태가 업데이트 될 때마다 불필요한 render
호출은 합성된 컴포넌트에서 반드시 받아들여야 하는 일일까요?
물론 절대 그렇지 않습니다. 간단하게 Dialog
를 래핑한 컴포넌트를 하나 더 만들기만 하면 문제는 해결됩니다.
WelcomeDialog라는 컴포넌트를 하나 더 만들었어요. PureComponent로요. Dialog는 Component로 고치고요. WelcomeDialog의 render
메소드내에서는 ReactNode를 그냥 생성하고 있죠. 왜냐하면 WelcomeDialog는 PureComponent이고, Props를 전혀 받지 않고 있기 때문에 render
가 다시 실행될 일이 없거든요. 따라서 Dialog도 다시 render
되지 않습니다.
이것을 React에서는 Specialization(특수화)이라고 부릅니다. 일반적인 목적의 컴포넌트를 좀 더 특수한 목적에 대응하는 컴포넌트로 만드는 것이죠.
지금까지의 예제는 모조리 클래스 컴포넌트로만 작성했는데요. 여러분들도 잘 아시겠지만 React에서는 클래스가 아닌 함수를 써서 컴포넌트를 작성할 수 있습니다. 개인적으로는 컴포넌트라는 것 자체가 Props를 받아 Node를 렌더하는 함수에 가까운 개념이라는 점에서 함수형 컴포넌트를 더 선호하는 편입니다. 함수형 컴포넌트가 기본적으로 문법이 간결한 편이기도 하고요.
그리고 지난번에 React 16.8 버전이 릴리즈 되면서 Hooks라는 기능이 React에 추가되었는데요. Hooks가 추가되면서 앞으로는 class 컴포넌트 없이 함수형 컴포넌트로만 앱을 구성할 수 있게 되었습니다.
이번 시간에는 아까 짜보았던 코드들을 모두 Hooks를 사용해 함수형 컴포넌트로 전환하고 어떤 hook들이 있는지 알아보도록 할게요.
먼저 아까 짰던 Form에서 Input 컴포넌트를 함수형 컴포넌트로 바꿔봅시다. Input 컴포넌트에서는 그냥 Props를 내려주기만 했기 때문에 별 어려움은 없을거에요.
다만 Input 컴포넌트는 PureComponent였죠. 때문에 PureComponent의 함수형 버전을 사용해야 합니다. 바로 React.memo
에요. 별 다를 것 없이 만든 함수형 컴포넌트를 React.memo
로 감싸기만 하면 됩니다.
그런데 이름이 왜 React.memo
일까요? 함수형 프로그래밍의 memoization을 잘 알고 계시다면 답하기는 쉬울텐데요. 동작하는 방식이 함수형 프로그래밍의 memoization이랑 비슷하기 때문이에요. (근본적으로 같지는 않아요.) 함수형 프로그래밍에서의 memoization은 순수함수의 입력(인자)이 같으면 리턴하는 값도 항상 같다는 점을 이용해 한번 계산한 반환 값을 저장해두고 다음 입력이 들어올 때 저장된 값을 반환합니다. React의 React.memo
는 “순수” 컴포넌트(PureComponent)에 들어갈 입력, 즉, Props, State가 같은 경우 같은 렌더링 결과가 나온다는 점을 렌더링에 이용하는 거죠. 다만 React.memo
의 경우엔 어디다 결과를 저장해두고 반환한다기 보다는 Props와 State가 같으면 굳이 렌더링을 하지 않는 형태로 구현하는 겁니다.
이번에는 JoinForm을 함수형 컴포넌트로 바꿔볼게요. 하지만 이번에는 난관이 하나 있습니다. 바로 JoinForm에는 State가 있다는 거죠.
이제 Hooks가 등장할 차례가 왔습니다. React 16.8 부터는 함수형 컴포넌트에서도 상태를 사용할 수 있도록 useState
라는 hook을 제공합니다. 우선은 각종 이벤트 핸들러를 모두 지우고 딱 View만 남겨볼까요? 그리고 useState
를 통해 상태를 구현해볼게요.
사용법은 간단합니다. useState
의 인자로 초기화할 값을 넘기신 후에, 반환되는 배열의 첫번째 index 그리고 두번째 index의 아이템을 사용하면 됩니다. 이렇게요.
1 | const [email, setEmail] = useState(""); |
그리고 setEmail
을 이용해서 onChange
에 넘겨줄 이벤트 핸들러를 만들면 되는거죠. password에도 똑같은 방식을 쓰면 됩니다.
아까 제가 inline함수는 PureComponent에 쓰면 항상 렌더링을 유도하기 때문에 쓰면 안된다고 했던거 기억하시죠? 이렇게 함수형 컴포넌트는 함수의 본문자체가 render
함수이기 때문에 이벤트 핸들러를 어디서 만들건간에 계속해서 새로운 함수를 만들게 됩니다. React에서는 이 문제를 해결하기 위해서 또 다른 hook, useCallback
을 제공합니다.
사용법은 간단해요. 쓰고싶은 함수를 useCallback
으로 감싸고 두번째 인자에 빈 배열을 넘기시면 됩니다. 이렇게요.
1 | const handleChangeEmail = useCallback(({ target: { value } }) => { |
이렇게하면 렌더 시마다 새로운 함수를 생성하지 않고 항상 같은 함수 인스턴스를 반환하게 됩니다. 결론적으로 render
가 실행될때마다 handleChangeEmail
에 할당되는 “값”이 항상 같은 거죠. 때문에 이제 Input
은 다시 렌더링 되지 않습니다.
이제 나머지 이벤트 핸들러들을 하나씩 만들어볼게요. handleSubmit
함수를 작성해서 이메일과 비밀번호를 로그로 찍도록 만들게요.
1 | const handleSubmit = useCallback(() => { |
근데 뭔가 이상합니다. 이메일과 비밀번호를 아무리 타이핑해도 로그에 제대로 남질 않아요. 왜일까요? 이제 useCallback
의 두 번째 인자로 넘기는 빈 배열에 대해 배워야 할 시간입니다. useCallback
에 넘기는 첫번째 인자는 클로저입니다. 이 함수가 생성되던 당시의 상태를 기억한다고 보면 되겠죠. 따라서 이 상태라면 항상 가장 초기 버전의 email
, password
를 출력하는 함수가 되는 겁니다. 결론적으로 이 상태의 callback 함수는 항상 string의 email
, password
만 갖고있겠죠.
이 문제를 해결하기 위해서 두 번째 인자가 존재하는 겁니다. 두번째 인자의 다른 이름은 “Dependency List” 인데요, 여기에 함수 내에서 참조하고 있는, 즉 의존하고 있는 값을 넣습니다. 가령 handleSubmit
에서는 email
과 password
를 외부에 의존하고 있죠. 이제 이 두 값을 배열에 넘겨볼게요. 이제 잘 동작하죠?
이게 필요한 이유는 아까도 말했듯이 useCallback
의 인자로 넘어가는 함수는 클로저의 원리를 이용해 구현된 것인데, 문제는 클로저에서는 생성된 당시에 값을 기억하지만 그 값이 업데이트되면 의미가 없어지는 겁니다. 따라서 그 값이 업데이트될 때마다 함수를 새로 만들어야 합니다. 그 기준이 두 번째 인자로 넘기는 배열, Dependency List 입니다.
useCallback
에서는 Dependency List를 이전에 넘겨 받은 값과 shallow compare로 비교해서 다르다고 판단하면 새로운 함수를 만들고, 아니라면 이전에 만들어진 함수를 그대로 유지합니다. 결과적으로, 이제는 email
혹은 password
가 바뀔 때마다 새로운 함수가 생성되고 있는 셈이죠.
마지막으로 “초기화”를 구현해볼게요. 간단하게 setEmail
그리고 setPassword
가 호출되게 구현하면 완료입니다!
Input
컴포넌트를 조금 업데이트해서 컴포지션이 가능하도록 변경해볼게요. useRef
를 이용해서 렌더링 횟수를 측정할텐데 useRef
에 대해서는 조금 이따 얘기해볼게요. 일단은 accessory
라는 Props를 추가한 뒤 여기로 버튼을 넘기겠습니다. 이 버튼은 동적으로 렌더링이 될거에요. email이 빈 값이 아닐 때만 표시되도록 만들어 보겠습니다. 이렇게요.
1 | <Input |
비밀번호도 마찬가지 작업을 해주고, 이메일 영역에 몇글자 쳐서 버튼을 나타나게 한다음에, 비밀번호를 타이핑 해볼게요. 어떤가요? 불필요하게 이메일 Input 컴포넌트가 렌더링되기 시작했죠?
이유는 여러분들도 쉽게 짐작하실 수 있겠지만 컴포지션을 사용했기 때문입니다. 아까 컴포지션을 다루면서 Props로 ReactNode를 넘기는 경우의 단점에 대해서 언급한 것 기억하시죠? 렌더구문안에서 항상 새로운 ReactNode가 생성되기 때문에 매번 렌더가 다시 실행되는 것이죠. 이 문제를 막기 위해 useCallback
을 사용했던 방식과 동일하게 useMemo
를 사용할 수 있습니다.
1 | const emailAccessory = useMemo(() => { |
React 공식문서에서는 heavy한 작업에 useMemo
를 쓰라고 권장하고 있지만 실제로 프론트엔드에서 헤비한 작업을 하게 될 일보다는 이렇게 PureComponent의 불필요한 re-render를 막기 위해서 사용하는 케이스가 더욱 흔할 것 같아요.
이제 아까 그냥 넘어갔던 useRef
에 대해서 얘기해볼게요. React의 ref 개념에 대해서는 다들 알고 계시나요? 보통은 React 상에서 DOM 객체에 접근할 때 사용하던 기능이었죠. useRef
도 마찬가지로 그렇게 사용합니다. 이메일 필드의 DOM 객체를 얻어오기 위해 useRef
를 사용해볼게요.
Input 컴포넌트 안에서 useRef
를 사용해서 DOM 객체를 담을 컨테이너를 생성합니다. 그리고 input
에 넘겨주면 되죠. console.log
를 찍어서 확인해보면 첫번째 렌더링때는 제대로 안나오지만 두 번째 렌더링 때부터는 잘 나오는 걸 보실 수가 있죠.
하지만 useRef
의 기능은 이것으로 끝이 아닙니다. 클래스로 컴포넌트를 짤 때도 State가 아닌 필드를 선언하는 경우가 종종 있었죠? 그런 경우는 대개 렌더링에 영향을 미치는 값이 아니지만, 각 노드 인스턴스별로 달라야 하는 값입니다. 그런 것들 중 가장 대표적으로 DOM 객체가 있는 것이고요. 이런 식으로 각 노드별로 렌더링된 횟수를 재고 싶을 때도 필요하죠. 각 노드별로 가져야하는 값인데, 이 값이 변경 될 때마다 렌더가 실행될 필요가 없으면 useRef
를 사용하기에 딱 적절한 상황입니다. State를 사용하게 되면 업데이트 때마다 render
가 호출될테니까요.
useEffect
는 렌더가 완료된 후에 호출되는 콜백이라고 생각하면 편합니다. componentDidMount
와 componentDidUpdate
가 실행되는 시점을 합하면 useEffect
가 실행되는 시점이라고 볼 수 있죠. 우리가 componentDidMount
에서 종종 Data Fetching을 했던 것처럼 useEffect
는 Data Fetching 같은 Side Effect를 발생시킬 때 쓰는 hook 이에요.
useRef
에 useEffect
를 결합해 컴포넌트가 마운트 되었을 때 자동 포커스 되는 기능을 구현할 수 있습니다. 사실 “포커스”라는 것 자체가 일종의 Side Effect죠. 아까 만들었던 Input에 autoFocus
라는 Props를 추가하고 이 속성이 true
일때만 마운트되었을때 포커스가 가게끔 구현해볼게요.
먼저 Input에 autoFocus
Props를 추가했고, useEffect
에 마운트되고 난 뒤 실행될 콜백을 선언합니다. autoFocus
가 true
일 때만 inputElement.current.focus()
를 실행할게요. 여기에도 마찬가지로 dependency list가 두번째 인자로 넘겨야 하는데요. 실제로 의존하고 있는 Props인 autoFocus
를 배열에 넘기도록 할게요. 만약 여기에 빈 배열을 넘기게되면 componentDidMount
와 거의 똑같이 동작합니다. 그리고 특정 값을 배열에 넘기게되면 그 값이 바뀔 때마다 넘긴 콜백이 실행되는 것이구요.
useEffect
에 넘겨준 callback에 리턴할 때 함수를 넣어주면 Unmount되면서 실행됩니다. componentWillUnmount
처럼요. 실험해볼까요? 아까 만든 Input 렌더링에 조건을 걸게요. 가령 email 값이 ‘abcd’일 때만 렌더링 하도록 말이죠. 그리고 Input 컴포넌트에 있는 useEffect
에 다른 걸 다 빼고 함수하나를 만들어 return 하도록 만들어봅시다. 로그를 출력해서 언제 실행되는지 볼 수 있게 만들고요. 어떻게 되나요?
React에서는 이걸 “Cleanup”이라고 부릅니다. 언뜻보면 기존 componentDidMount
/ componentWillUnmount
보다 비직관적으로 보이지만, 이런 접근 방식의 장점은 같은 관심사를 가진 로직을 한데 묶어놓을 수 있다는 점입니다. 이 부분은 Custom Hook 쪽에서 좀 더 자세히 설명할게요.
다시 JoinForm으로 돌아올게요. JoinForm에는 약간의 중복이 있어요. 이메일과 비밀번호에 사용할 State를 만들고 Input에서 이벤트를 받아 State를 업데이트하는 부분은 이메일과 비밀번호에 공통적으로 적용되는 부분이죠.
Custom hook을 만들면 이렇게 중복되는 로직을 묶어서 추상화하고, 이걸 재사용할 수 있어요. 이번엔 useInputState
라는 이름의 hook을 만들어서 이걸 어떻게 구현하는지 알아볼게요.
먼저 useInputState
라는 파일을 만들어놓고, 중복되는 로직을 차례차례 옮겨봅시다. 공통적으로 사용되는 useState
를 옮겨보죠. 초기 상태가 필요하기 때문에 초기 상태 값은 parameter를 통해서 받습니다. 그리고 useCallback
을 통해 이벤트 처리하던 부분을 옮깁니다. 마지막으로 이렇게 해서 만든 것들을 배열로 리턴합니다.
받는 쪽에서는 useState
를 사용할 때 처럼 배열에 있는 것들을 destructuring해서 사용하면 됩니다. 그리고 아까처럼 컴포넌트 Props로 값을 전달해주면? 잘 동작하죠!
아까 HOC를 다루지 않겠다고 했던 이유가 여기에 있습니다. HOC와 hooks가 해결하는 문제는 비슷합니다. 그건 바로 Control Logic의 재사용인데요. HOC는 로직과 컴포넌트를 강력하게 연결한다면 hooks는 로직을 느슨하게 연결함으로서 좀 더 유연하게 코드를 작성할 수 있습니다.
이번에는 조금 다른 종류의 hook을 만들어볼게요. 어떤 컴포넌트에서 window가 resize 될 때마다 변경된 window width의 값을 구독해 렌더링에 반영시킬 수 있는 hook입니다. 먼저 useState
로 상태를 담을 변수를 만들어 줍니다. 그리고 useEffect
를 통해 window의 resize 이벤트를 구독하도록 할게요. removeEventListener
를 호출하지 않는다면 addEventListener
에 전달한 이벤트 핸들러가 계속해서 메모리에 남아있을테니 퍼포먼스에 악영향을 미칠 겁니다. 따라서 addEventListener
에 전달하는 이벤트 핸들러는 useEffect
에 전달하는 콜백 스코프에서 만들도록하고, 컴포넌트가 unmount 될 때, removeEventListener
를 통해 이벤트 핸들러를 지워주세요. 이렇게하면 이벤트 핸들러가 컴포넌트의 생명 주기를 따라갈 수 있을 겁니다. (예제 개선에 도움을 주신 @KrComet님 감사합니다!)
그리고 여기서 useEffect
의 진가가 드러납니다. 기존의 방법대로라면 addEventListener
는 componentDidMount
에, removeEventListener
는 componentWillUnmount
에 위치했을 것이고 이벤트 핸들러는 클래스의 프로퍼티로 별도의 함수로서 존재했을 것입니다. “Resize 이벤트 핸들링”이라는 같은 관심사를 공유하는 코드들이 멀찍이 흩어져있었겠죠. useEffect
를 사용함으로서 같은 관심사를 공유하는 코드를 한데 모아놓을 수 있습니다. 읽기 훨씬 수월하겠죠?
이렇게 편리하고 좋은 Hook이지만 사용하기 위해서는 다음과 같이 반드시 지켜야 할 규칙이 있습니다.
저는 사실 이런 Rule이 별로 어렵지도 않고 헷갈리지도 않아 지키기 어렵다는 생각이 지금까지 들지는 않았는데요. 어쨌든 사람이 실수할 수 있는 부분인 것도 분명한지라 React에서는 이런 Rule을 쉽게 지킬 수 있도록 eslint 플러그인을 제공합니다. 이 플러그인에는 추가적으로 “exhaustive-deps”라는 룰도 포함되어 있는데요. 이 룰은 Dependency List에 전달해야하는 의존성을 빼먹으면 알려주는 Rule입니다. 이쪽은 빼먹기 쉬운 부분이라서 상당히 유용합니다.
React.memo
는 함수형 컴포넌트의 PureComponent다.useState
를 통해 함수형 컴포넌트의 상태를 구현할 수 있다.useCallback
, useMemo
를 통해 함수형 컴포넌트 렌더링 최적화를 할 수 있다.useRef
는 렌더링에 영향을 미치지 않는 값을 보관할 용도로 사용한다.useEffect
는 클래스의 Lifecycle 훅과 흡사하다.제 인생 첫 강의였습니다. 제가 선택한 주제가 결코 만만하지 않아서 준비하는데 애를 많이 먹었던 것 같아요. 제가 생각하는 이상적인 강의의 청사진 같은게 있었는데, 전혀 구현하지 못했습니다. (그래서 강의의 제목이 바뀐 것이기도 하고요.) 단순히 이상적인 그림을 상상하는 것과 실제로 그걸 구현해가면서 오는 괴리가 생각 이상으로 크더라구요. 그래서 처음에 제가 구성한 아웃라인과는 다른 강의가 되어버렸습니다.
다루고 싶은 내용은 더 많았어요. Atomic Design이라던가, Form 만드는 방법과 Formik 같은 라이브러리, Animation Component 그리고 Presentational / Container Component 같은 기법들 말이죠. 강의를 준비하는 시간 자체를 많이 투자하지 못해서 해당 내용을 모두 준비하지 못했던 것 같습니다. 아쉽기도 하고 죄송하기도 하네요.
강의를 할 때는 오디오가 빌 때마다 이상하게 마음속에 부담이 생겨서 오디오가 비지 않게끔 계속해서 말을 이어가려고 했던 것 같은데, 결과적으로 횡설수설에 버벅거리면서 아무 말이나 했던 것 같아서 어떤 분들은 듣기 좀 어려웠을 것 같아요. 결과적으로 강의가 끝나고 집에 오니까 목이 많이 아프더라고요.. 😅 그리고 제 생각에는 너무 일방적으로 말을 쏟아내다보니 듣는 입장에서 많이 지루했을 수도 있을 것 같아요. 진행도 일방적으로 했던 것 같고요.
그래도 마치고나니 뿌듯한 마음이 듭니다. 모쪼록 모든 분들께 제 강의가 조금이라도 도움이 되었으면 좋겠네요. 🙏 4시간이라는 짧지 않은 시간 동안 부족한 제 강의를 들어주신 모든 분들께 감사드립니다. 🙇
역시 웹 관련이라 들어갔는데 또 한 번 신기한 경험을 했다. 바로 Preact의 Creator인 Jason Miller가 강연을 했다는 것. 평소에 트위터에서 팔로우하고 있던 사람이기도 하고 요즘 트렌드를 쫓는 웹 개발자라면 한 번 정도는 들어봤을 이름이라서 직접 봤다는 게 신기했다. Preact의 창시자이니 Preact를 다룰 것 같았지만 Preact의 P자도 안 나왔다.
이 세션에서는 요즘 웹의 Single Page Application으로의 트렌드 변화와 더불어 여러가지 렌더링 기법(Client Side Rendering & Server Side Rendering 등)에 대한 고찰을 하는 세션이었다. 그리고 각각의 렌더링 기법이 어떤 장점과 단점을 갖는지, 그리고 선택할 만한 옵션이 무엇인지에 대해서 다루었다.
SPA에서 일반적으로 말하는 렌더링 기법이 두 가지 있다. 하나는 Client Side Rendering(CSR) 그리고 다른 하나는 Server Side Rendering(SSR)이다. 발표에서는 SPA에서 가장 쉽게 접근할 수 있는 렌더링 기법인 CSR의 약점에 대해 시작부터 짚고 넘어간다. CSR은 느린 인터넷 커넥션에서는 특히 문제가 될 수 있는데, 그건 큰 크기의 JavaScript 번들을 모두 다운로드 받고 파싱하고 실행할 때까지는 유저가 제대로된 화면을 만날 수 없다는 것이다. 처음 유저가 다운로드 받게되는 HTML은 완전히 빈 페이지일테니 말이다.
유저 경험에 영향을 미칠 수 있는 몇 가지 중요한 렌더링 퍼포먼스 지표들을 기준으로 CSR이 유저 경험에 얼마나 치명적일 수 있는지 설명했다. CSR에서는 FCP(First Contentful Paint)가 상당히 긴 시간 뒤에 일어날 수 있다. 물론 TTI(Time To Interact)는 그보다도 뒤에 일어날 것이다. 따라서 큰 자바스크립트 번들 사이즈는 항상 느린 퍼포먼스를 야기한다.
이와 반대로 Server Side Rendering(SSR)은 FCP가 CSR에 비해서 빠르다. 이 점은 분명 SSR이 CSR에 비해 갖는 장점이다. 하지만 유려한 페이지 전환과 인터랙션 역시 필요하다. 이 점은 오히려 CSR에서 가지는 장점이다. 요즘의 웹 앱은 빠른 FCP와 유려한 페이지 전환 및 인터랙션 모두 필요하다. 모두를 충족시키기 위해 SSR에도 Hydration이라는 기법을 적용할 수 있다.
Hydration 아키텍처에서는 첫 웹 페이지 렌더는 SSR로 이루어지지만 그 이후부터 전환될 모든 페이지는 CSR로 이루어진다. 특히 이후에 이루어지는 이벤트 리스너 등록작업등을 Hydrate라고 부른다. 이런 작업은 원래 개발자가 직접해야하는 것이지만 프로그래머가 신경 쓸 필요가 없도록 구현되어있는 프레임워크를 사용할 수도 있다. 바로 Next.js 와 Nuxt.js 등의 메타 프레임워크이다. Hydration을 이용하면 모든게 좋아질 것 같지만 그럼에도 TTI는 전혀 좋아지지를 않는다.
이에 대한 대안으로 Pre-rendering이 있다. Pre-rendering에서는 빌드 타임에 모든 HTML을 렌더링한다. 이미 렌더링 되어있으므로 SSR하는데 걸리는 시간이 필요하지 않아 FCP는 더욱 빨라질 것이다. 쉽게 말해 정적으로 페이지를 렌더링하는 것이고 유명한 툴로는 React 생태계의 Gatsby가 있다. 물론, Pre-rendering 접근 방식도 단점이 있는데 빌드 타임에 모든 페이지 생성을 끝내야 하기 때문에 항상 정적인 컨텐츠에만 활용이 가능하다는 것이다.
또 다른 대안으로 SSR Streaming이 소개되었다. Streaming은 브라우저가 페이지 렌더링을 서버 응답이 끝나기 전에 시작할 수 있도록 렌더링에 필요한 데이터를 청크로 쪼개 내려주는 것이다. 이 방법을 적용하면 Time To First Byte (TTFB)를 향상시킬 수 있다. React나 Vue에서는 이미 관련 API가 있기 때문에 바로 적용이 가능한 방식이다. 실제로 Spectrum이라는 웹 서비스에서 Streaming 기법을 적용한 사례가 소개되었다.
Progressive Hydration은 마찬가지로 Hydration을 수행하지만 한 번에 하지 않고 필요한 부분만 점진적으로 하는 것이다. 페이지가 로드되면 나머지 어플리케이션을 모두 로드하는 Full Hydration과는 달리 Progressive Hydration에서는 상황 혹은 유저의 인터랙션에 따라 필요한 부분을 로드한다. React에서는 아직 지원하지 않고있는 기능이지만 별도의 라이브러리를 사용하면 비슷하게 구현이 가능하고, 차후에 React Suspense를 통해 지원할 계획이다. Airbnb는 이미 Progressive Hydration을 적용해 TTI를 극적으로 향상시켰다고 한다. React Progressive 예제는 이 레포지토리에서 확인할 수 있다.
렌더링 퍼포먼스 외적인 측면도 다루었다. 흔히 많이들 하는 오해가 CSR은 SEO가 잘 되지 않는다라는 것인데, 많은 크롤러들이 JavaScript를 지원하지 않기 때문에 발생한 오해다. Google Bot(크롤러)은 JavaScript를 지원하기 때문에 CSR 사이트도 SEO가 잘 된다. 특히, 최신 버전의 Google Bot은 ES2015 이상의 최신 JavaScript도 지원한다. 또한 Full SSR 없이도 메타 태그들을 잘 활용하면 SEO를 잘 지원할 수 있다.
SSR은 단순히 HTML을 서버에서 렌더링한다는 것 이상의 의미를 갖는다. 서버는 특정 클라이언트의 요청에서 해당 클라이언트가 필요한 정보(스크립트, 데이터)가 무엇인지를 모두 판단할 수 있기 때문에 HTML이 클라이언트에서 렌더링 되기전부터 정보를 선택적으로 통합해서 내려줄 수 있다.
전반적으로 발표가 다소 딱딱하기는 했지만 연사자 분들이 열심히 준비한 느낌을 받았다. 또 내가 몰랐던 새로운 개념을 알아가게 되어 좋았던 발표다. 특히 신기했던 건 Google I/O 였음에도 불구하고 각 사례별로 React / Vue에서 어떤 방식으로 접근하는지에 대해서도 다루었으며, 특히 React를 중심으로 설명했다는 점이 인상깊었다.
보다 자세한 정보는 이 링크에서 확인해 볼 수 있다.
Puppeteer는 Chrome 혹은 Chromium 을 조작할 수 있는 API를 제공하는, Google Chrome 팀에서 만든 라이브러리다. Pre-rendering을 할 때 사용할 수도 있고(react-snap) 이 세션에서 소개된 것처럼 직접 만든 웹 앱을 테스트하는데에도 사용할 수 있다. 이 세션에서는 Puppeteer의 다양한 새로운 기능들이 소개되었다.
기존 Puppeteer를 통한 웹 테스팅의 한계는 해당 테스트가 동작한다고 해서 모든 브라우저에서 코드가 동작할 거라는 보장이 없다는 것이었다. 지금까지는 Pupeeteer가 Chrome만 지원했기 때문이었다. 이 세션에서는 Puppeteer for Firefox가 소개되었는데 브라우저를 조작할 수 있는 것은 마찬가지이지만 해당 브라우저가 Firefox인 점이 다르다. 동일한 API를 가지고 있고 아직은 Experimental 프로젝트이다. 소개에 따르면 90%의 Puppeteer API를 지원한다고 한다.
Pupeeteer를 이용한 테스트는 보통 느렸는데, 이유는 테스트 케이스 하나를 실행할 때마다 고립된 환경을 만들기 위해 브라우저를 끄고 켜기를 반복했기 때문이다. 이를 보완하기 위해 Browser Context라는 개념이 소개되었다. Browser Context를 이용하면 브라우저를 켜고 끌 필요 없이 고립된 환경을 만들 수 있다.
비동기적으로 렌더링되는 HTML Element의 경우 페이지가 열린 시점에 DOM API를 통해 접근할 수 있을거라는 보장이 없다. 이 때문에 해당 Element에 의존하는 테스트는 항상 성공할 거라고 보장할 수 없다. 이를 해결할 수 있는 waitFor..
메소드들이 소개되었다. 예를 들어 waitForSelector
의 경우 실제 그 Element를 DOM API로 선택할 수 있을 때까지 기다리는 메소드다.
요즘의 웹 환경에서 모바일을 빼놓기란 어렵다. Puppeteer는 모바일 페이지 역시 테스트 가능하게끔 하기 위해 Device Emulation을 지원한다. 물론 완벽하게 해당 디바이스 환경을 제공하는 것은 아니지만 최대한 비슷한 환경을 지원하고 있는 것 같다. 정확히는 User Agent, Device Pixel Ratio, Viewport Size, Touch Support 등을 제공한다. 이것만으로도 어느 정도는 (최소한 Viewport에 따른 레이아웃 변화 정도는) 테스트가 가능할 것 같다.
이미 PWA등을 이용해서 웹페이지에서 Offline 기능을 지원할 수 있다. Puppeteer는 이것 역시 테스트할 수 있도록 Offline 모드를 지원한다. 이 기능은 간단히 setOfflineMode(true)
로 사용할 수 있다. 또한 ServiceWorker에 접근할 수 있는 API도 열려있다.
어떤 웹페이지에서는 웹의 Geolocation API를 이용해서 유저의 위치에 따라 다른 동작을 하도록 만들 수도 있다. 마찬가지로, Puppeteer에서는 이를 테스트할 수 있도록 Geolocation을 바꿀 수 있는 API를 제공한다.
Puppetter에서는 Chrome Devtool의 Network 탭과 같은 네트워크 모니터링을 코드로도 할 수 있다. 또한 모니터링하는데에 그치지 않고, Request를 가로채서 실제 서버에 전송하지 않고 Puppeteer 단에서 적절한 Response로 바꿔치기 할 수도 있다. 이를 이용하면 서버에서 내려주는 Response가 실패했을 때를 가정하여 테스트를 할 수 있다.
특정 사용자 인터랙션에 따른 테스트를 하고자 할 때, JavaScript에서도 dispatchEvent
라는 메소드를 사용하면 Event를 Programmatically 발생시킬 수 있지만, 이는 오직 JavaScript 단에서만 Event를 발생시키는 것이지 실제로 브라우저 내에서 일어나는 다양한 동작 (focus, hover)등은 일어나지 않는다. Puppeteer를 이용하면 이런 사용자 동작을 흉내낼 수 있다.
Puppeteer는 웹페이지의 성능을 측정할 수 있는 API를 제공한다. 이를 통해 특정 수치가 너무 높거나 낮은 경우 테스트가 실패하도록 테스트를 짤 수도 있다.
Tracing은 특정 상황에서 퍼포먼스를 측정할 수 있는 도구다. 예를 들어 사용자의 인터랙션에 따른 CSS 애니메이션이 있는 경우 해당 상황에서의 퍼포먼스를 측정할 수 있다.
Code Coverage 역시 특정상황에서 코드 커버리지를 측정한다. 동적으로 측정을 시작하고 중단할 수 있다.
Puppeteer를 이용하면 접근성 관련 속성에도 쉽게 접근할 수 있다. page.accessibility.snapshot()
이라는 메소드를 이용해서 특정 시점의 접근성 트리를 얻을 수 있다.
개인적으로 전혀 Puppeteer를 사용하지 않고 있는데 소개된 기능들이 상당히 흥미롭고 사용하기 어렵지 않아보여서 한 번 쯤 시도해 볼 만 하겠다는 생각이 들었다. 보여준 데모만 놓고 봤을 때는 실무적으로도 사용할 때 번거롭지 않을 것 같았다. 코드도 읽기 쉬웠고.. 일단 테스트를 짜야
내가 Google I/O에서 들은 마지막 세션이었다. 시간이 시간인지라 사람들도 지쳤는지 발표장에 사람들이 많지 않았다. 하지만 발표 내용은 알찼다. 세션 내용은 제목 그대로, Web에서의 Media(Video, Audio 등) 경험을 어떻게 개선하는지에 관한 것이었다. 즉, 웹 관련 기술 세션이면서도 UX에 관한 내용이었다.
Chrome에서 Youtube로 음악을 들어봤던 사람이라면 음악을 끄기 위해서 수 많은 탭 속에서 한 번 쯤 헤맸던 경험이 있을 것이다. 이 문제를 Sportify가 Chrome에서 구현된 MediaSessionAPI로 해결했다는 내용이었다. MediaSessionAPI는 웹 환경에서 노트북등에 흔히 있는 재생, 일시정지, 다음, 이전 키가 눌리는 이벤트를 접근할 수 있는 API다.
웹 상에서 스포츠 경기를 본다고 할 때 대부분은 중요하지 않은 장면에서는 다른 일을 하면서 보다가 중요한 장면이 나올 때만 스포츠 경기에 집중하는 경우가 많다. Picture in Picture(PiP) API는 이런 경우를 지원할 수 있도록 만들어진 API다. v.qq.com 이라는 중국의 영상 사이트는 PiP API를 적용해 16%의 영상시청시간 증가와 16%의 시청완료 비율 증가를 얻었다고 한다. Sportify는 PiP API를 이용해 떠다니는 음악 플레이어를 만들었다.
그리고 Chrome의 Video Player 인터페이스를 어떻게 개선했는지에 대한 내용이 다뤄졌다. 기본 Video Player에서는 하나의 Bar에 모든 컨트롤 들이 들어있어 조작하기 어려웠다. 이 부분을 개선하기 위해 Shaka Player를 만들게 되었다는 내용이었다. Shaka Player에서는 각 버튼을 적절한 위치로 떼어놓고 재생위치를 표현하는 Bar를 최대폭으로 늘려 사용하기 편하도록 개선했다. 하지만, Shaka Player는 데스크탑에서 동작할 때 문제가 생겼다. 모바일에서는 적절한 사이즈였던 컨트롤들이 데스크탑에서는 너무 작고 컨트롤 간의 거리가 너무 멀다는 것이었다. 이 부분도 데스크탑에 맞게 인터페이스를 개선해 해결할 수 있었다. 이후, Shaka Player 기능적인 설명이 이어졌다. Shaka Player는 Chrome 뿐만 아니라 다양한 플랫폼을 지원하고 있다.
전반적으로 기술에 대한 내용도 다루어졌지만, 결론적으로 사용자의 경험에 포커스를 맞춘 세션이었고 인지과학적인 내용도 있어서 흥미롭게 들을 수 있었다. Shaka Player에 대한 내용은 거의 듣지 못했지만 키워드를 얻었다는 것만으로도 의미는 있었다.
기술관련 세션 내용이 아니라 신청할지 고민하다가 설명에 적혀져 있는 내용이 재밌어보여서 신청해봤는데 막상 가보니 사람이 엄청 줄이 길어서 놀랐다. 알고 보니, 발표자인 메이 제미슨(Mae Jemison)은 최초의 흑인 여성 우주인으로 유명한 사람이었다. 그래서인지 참가자들의 반응이 무척 좋았다. 세션은 완전히 이해를 못했지만 약간은 철학적인 이야기였는데 “올려다 보는 것”을 통해 우리는 지구의 한 존재인 것을 확인하고 서로 연결됨을 느낄 수 있다는 이야기였다.
이 뿐만 아니라 전혀 기대하지 않았던 스피커도 등장했는데 바로 최근에 핫 이슈였던 블랙홀 사진, 그리고 그걸 찍어냈던 팀 “Event Horizon Telescope”의 프로젝트 디렉터인 셰퍼드 도엘레만(Sheperd Doeleman)이었다. 이 분이 발표하고 난 뒤에는 기립박수까지 받을 정도로 반응이 좋았고 실제로 과학사적으로 블랙홀 사진이 큰 의미를 지닌다는 것을 느낄 수 있었다.
사실 그리고 완전히 기술과 관련이 없는 세션은 아니었다. 메이 제이슨은 일련의 과정속에서 Skyfie라는, “Looking up”할 수 있게끔 하는 앱을 실제로 출시했다. 그리고 블랙홀 사진의 경우 그것을 찍는데 성공한 것은 여러 소프트웨어가 그것을 도왔기 때문이다. 가장 흥미로웠던 건, 인류는 지금까지 블랙홀 사진을 찍어본 적이 없으므로 막상 찍더라도 그것이 블랙홀 사진이라고 확신할 수 없다는 것이었고, 이를 해결하기 위해서 시뮬레이션을 통해 관측이 예상되는 블랙홀 사진을 얻어내 실제로 비교할 수 있었다는 것이었다.
세션의 전반적인 느낌은 Google I/O가 아니라 TED에 온 것 같은 느낌을 받았다. 실제로도 두 스피커 모두 TED에서 발표한 이력이 있고, 발표자료도 Google I/O에서 지원하는 템플릿이 아니라 (본인이 자주 사용하는 것 같은) 별도의 발표자료를 가지고 발표했다. 그리고 두 사람 모두 확실히 여러번 발표한 경험때문인지 말이 너무 유창했다. (덕분에 알아듣기 어려웠지만..)
당연히 들어야 할 내용이라고 생각해서 참석했다. 대부분이 Chrome에서 새롭게 구현되었거나 신규 구현될 JavaScript 문법 혹은 신규 기능에 대한 짤막한 브리핑이었다. 그 목록은 아래와 같다.
#
을 붙여 Private Field로 선언할 수 있다.string.matchAll()
: string.match()
와 거의 비슷한 API이지만 Array가 아닌 Iterator를 반환하며, 각 아이템별로 매치된 값뿐만 아니라 index
값과 원래의 문자열 값이 포함되어있다.1_000_000
(1000000
값과 같음)같은 식으로 가독성을 높일 수 있다.n
을 붙여 BigInt 타입을 사용할 수 있다.Object.fromEntries
: Object.entries
의 정확한 역이다. Object
를 filter
, map
하기에 유용하다. 또한 Map
을 인수로 넘겨서 Object
를 생성하기 편리하다.globalThis
: JavaScript는 사용되는 플랫폼이 너무 많기 때문에 글로벌 객체를 유니버셜하게 얻기 위해서 귀찮은 처리가 필요한데, 각기 다른 이름을 가진 글로벌 객체를 유니버셜하게 globalThis
로 얻을 수 있게 되었다.array.sort
메소드가 Stable Sort가 아니었는데 Stable Sort로 바뀌게 되었다.Intl.RelativeTimeFormat
: date-fns의 distanceInWords
같은 API가 추가되었다. 그러니까.. “몇 분 전” 같은 것이 추가되었고 Intl
아래의 객체인 만큼 모든 언어를 지원한다. 다만 라이브러리 처럼 자동으로 적절하게 해주는 건 아니라서 여전히 라이브러리는 필요할 것으로 보인다.Intl.ListFormat
: List를 문자열로 표현할 때 한국어로는 “A와 B” 처럼 표기할 것이고 영어로는 “A and B” 처럼 언어 별로 다른데, 이런 일을 해주는 API가 추가되었다.Intl.DateTimeFormat#formatRange
: 이 API를 이용하면 기간을 표현할 수 있다. 2019년 5월 6일 - 9일
처럼 표현이 가능하다.Intl.Locale
: 해당 지역의 정보를 담는 로케일 객체를 만들 수 있다.Promise.allSettled
: Promise.all
과의 차이점은 모든 Promise
의 상태가 “settled”로 바뀌기만 한다는 점이다. 즉 “fulfill” 되었든 “reject”되었든 상관이 없다.Promise.any
: Promise.race
는 Promise
중 하나만 “settled” 되면 되지만, Promise.any
는 Promise
중 하나가 “fulfill” 되어야 끝난다. 즉, “reject”되면 나머지 Promise
들을 계속해서 실행한다.WeakRef
: ES2015에 추가된 WeakMap
이나 WeakSet
과 비슷하게 약한 참조를 하는 객체를 만들 수 있다. JavaScript에서 극히 제한적으로 메모리를 다룰 수 있게끔 하는 것으로 보인다. 사실 아직 제대로 이해하지 못했다.이상의 기능들은 대부분 Chrome에서만 지원하거나, 그게 아니더라도 일부 브라우저에서는 지원하지 않는데, 대부분 폴리필이 가능한 성격의 기능들이고 이미 만들어져 있어서 어렵지 않게 사용이 가능할 것 같다.
세션의 전반적인 느낌은 사실 아쉬웠다. 세션 자체가 뭔가 해석이 들어가는 내용이 아니라 그냥 새로운 기능을 단순히 전달만 하는 것이기 때문에 재미있게 만들기는 어려웠겠지만, 발표가 기계적으로 느껴졌다는 점이 다소 아쉬웠다.
원래 듣고자 했던 세션은 아니었는데, 시간이 비어서 듣게 되었다. 하지만 다 듣고나니 듣기를 잘했다는 생각이 들 정도로 알찬 발표였다. 제목 그대로, 웹 개발자의 입장에서 웹 어셈블리가 무엇인지, 어떻게 쓰면 되는 건지 왜 좋은 건지에 대해서 설명하는 세션이었다. 내 경우에는, 웹 어셈블리에 대해서 대충 어떤 건지 들어만봤고 막연하게 나랑은 거리가 먼 기술이라고만 생각해서 크게 관심이 없었다.
이 발표는 내 고정관념을 깼다. 나는 그냥 평범한 웹 개발자로서 JavaScript를 사용할 줄 알지만 C++를 잘 쓸 줄은 모른다. 그리고 내 고정관념은 WebAssembly를 사용하기 위해서 C++ 코드를 짤 줄 알아야한다는 것이었다. 이 발표에서는 JavaScript를 사용하는 웹 개발자도 WebAssembly를 유용하게 사용할 수 있다는 것을 보여줬다.
Emscripten은 최초에 asm.js를 컴파일하기 위한 도구로 활용되었지만 이제는 WebAssembly를 컴파일하는 역할을 한다. 결론적으로 Emscripten은 POSIX 시스템 위에서 돌아가도록 만들어진 C/C++ 코드를 컴파일하기 위해, POSIX 시스템 전체를 에뮬레이션한다.
웹 생태계는 이미 거대하다. npm만 보더라도 엄청나게 많은 모듈들이 등록되어있다. 하지만 어떤 경우에는 맞닥뜨린 문제가 npm 모듈로는 해결할 수 없는 반면, C / C++로 해결할 수 있는 경우가 있다. 이 경우에 WebAssembly가 도움이 될 수 있다.
대표적인 사례가 squoosh.app이다. squoosh는 이미지를 압축하는 웹 앱인데, 서버 없이도 잘 동작한다. 이미 많은 이미지 인코더가 C 혹은 C++로 쓰여져 있기 때문에 이런 인코딩 코드 베이스 자체를 가져다 써서 만들어진 것이 Squoosh다. Squoosh는 기존에 만들어져있는 C / C++ 인코딩 알고리즘, 예를 들어, mozjpeg 등을 Emscripten을 통해 WebAssembly로 만들어서 사용했다. 실제로 오픈되어있는 squoosh의 코드를 까보면 해당 부분이 어떻게 구현되어있는지 볼 수 있다.
다만 조금 의문이 드는 건 분명 C++을 배울 필요가 없다고 했지만 squoosh에서는 약간의 C++ 코드를 직접 작성해야한다는 것이다.
WebAssembly는 빠르다. 하지만 요즘에 JIT 컴파일러의 성능향상을 얻은 JavaScript도 그에 못지않을 만큼 빠르다. 하지만 JavaScript의 경우 때때로 특정 코드가 특정 브라우저에서 실행될 때 예상되지 않은 실행속도를 보여주는 경우가 생긴다. 이는 각 브라우저별로 JavaScript의 성능을 최적화하는 방법이 다른 것에서 기인한 것이다. 하지만 WebAssembly의 경우 정적 타입 시스템을 사용하기 때문에 성능을 최적화하는 것이 JavaScript에 비해 훨씬 쉽고, 일관적이다(참고).
Squoosh에서는 이미지를 회전하는(Rotating) 작업을 할 때의 성능 측정을 해봤는데 브라우저 별로 실행속도가 무척 큰 차이가 나는 것을 발견했다. 빠른 경우 400ms 정도였지만, 최악의 경우 특정 브라우저에서 8초에 육박하는 시간이 걸렸다. 이를 WebAssembly로 다시 했더니 일관적으로 500ms 미만의 시간이 걸렸다. 따라서 squoosh는 Rust로 이미지를 회전시키는 로직을 작성하게 되었다.
AssemblyScript는 TypeScirpt를 WebAssembly로 컴파일하는 컴파일러다. 즉, AssemblyScript 덕분에 이미 TypeScript를 알고있는 개발자라면 WebAssembly를 쓰기 위해서 배워야할 게 별로 없다. AssemblyScript를 쓰기 위해서 추가적으로 배워야 할 것은 JavaScript에는 없지만 WebAssembly에는 있는 타입들과 메모리를 다룰 수 있는 몇가지 함수들 뿐이다. (아직 WebAssembly에는 Garbage Collector가 없다.)
WebAssembly가 빠르고 실행속도가 일관적이긴하지만 모든 코드를 WebAssembly로 짜는 것은 별로 좋지 않은 생각이다. WebAssembly는 디버깅하기 힘들고 코드 스플리팅은 더 어렵다. 그렇기 때문에 적절한 곳에만 WebAssembly를 사용하는 것이 좋다. 병목인 부분이나 JavaScript 플랫폼이 메워주지 못하는 부분을 WebAssembly로 채울 수 있다.
마지막으로 WebAssembly에 나올 새로운 기능들이 소개되었다. 이전까지 WebAssembly를 사용한 적이 없어서 모두 이해하지는 못했다. 간단한 것만 말하자면, 멀티스레드 지원이 추가될 예정이고 앞으로 더 많은 언어를 추가하기 위해 고수준 언어를 지원하기 위한 가비지 컬렉션, 함수형 프로그래밍 언어를 지원하기 위해 Tail Call Optimization 지원이 추가된다는 등의 소개가 있었다.
GDG Korea WebTech Organizer로서 Google Korea로부터 숙소 비용 및 티켓 비용을 전액 지원받아 Google I/O 2019에 참가하게 되었다. 사실 Google I/O에는 이번에 처음으로 참가하는 것이다. Google I/O 행사 뿐만 아니라 이런 글로벌 규모의 컨퍼런스에 참가하는 것 자체가 처음이다. 후기에서는 이번에 공개된 모든 기술에 대해 내 해석을 남기는 것보다는 (사실 이건 다른 미디어에서 잘 하고 있기 때문에) 전반적인 행사의 느낌을 공유하는데 초점을 맞추려고 한다.
아침 일찍 우버를 타고 Google I/O 행사장에 도착했다. 여덟 시쯤 도착한 것 같은데 입장 줄이 길어서 입장하는데 30분 정도 걸린 것 같다.
도착해보니 행사장은 약간 놀이공원 같았다. 실제로 전체 행사장 크기도 작은 놀이공원 사이즈는 되고, 뭔가 사람들이 약간 텐션 오른 것도 놀이공원과 비슷했다. 행사장에서는 아침으로 베이글과 커피 등을 제공하고 있었는데, 간단하게 아침식사를 해결하고 키노트를 듣기 위해서 키노트 장소로 이동했다.
이번 Google I/O에는 약 5000명의 사람이 참여했다고 들었는데, 과연 키노트 장소에도 사람은 많았다. 키노트 장소에는 크게 맨 앞 쪽 스테이지와 가장 가까운 좌석들(콘서트 같은 걸로 비유하면 R석)이 가장 중심이었고 바로 그 뒤 프레스 좌석, 일반좌석, 잔디좌석이 있었다. 나는 GDG 커뮤니티 오거나이저로서 참석하는 것이어서 감사하게도 앞쪽 좌석에 갈 수 있었다.
보통 개발자 행사라고 하면 약간 정적이고 조용한 사람들이 가득한 공간에서 키보드만 두드리는 이미지를 상상하게 되는데 Google I/O는 입장할 때부터 분위기가 개발자 행사라기보다는 뭐랄까, 콘서트나 스포츠 경기장에 가까운 분위기였다. 스테이지에서는 무려 “AI” DJ가 믹싱하는 음악이 나오고 있고 행사장에는 환호소리가 끊임없이 들렸다. 이렇게 큰 행사에 참여하는 것은 처음이라서 이것이 일반적인 건지 특이한 케이스인건지는 모르겠지만 이런 축제 분위기는 좋았다. 진정한 개발자 축제가 이런 것이 아닐까 하는 생각이 들었다.
키노트의 MC는 Google의 CEO인 순다 피차이였다. 이번 키노트를 관통하는 키워드는 “Helpful”이다. 기술들을 사용자에게 진짜 도움이 되는 방법으로 쓰는 것이라고 이해하면 되려나? 이런 주제와 함께 여러가지 구글의 새로운 기술과 컴퓨터 과학적 성취들이 공개되었다.
키노트에서 공개된 새로운 기술 중에서 Google Assistant의 성능 향상, AR 관련 구글 렌즈의 새로운 기능이나 안드로이드 Q에 새로 들어갈 기능들에게도 관심이 갔지만, 내게 가장 흥미로웠던 것은 Duplex on the Web 이었다. Duplex는 작년 Google I/O에서 가장 화제가 되었던 AI가 미용실에 전화를 걸어 예약하는 바로 그 기능이다. Duplex on the Web은 이미 서비스를 운영하고 있는 웹페이지(예를 들어 항공권 결제 웹페이지)에 들어가서 이미 여러가지 앱에 들어있는 정보들을 이용해서 복잡한 회원가입과 구매절차를 자동으로 진행하는 기능이다. 더욱 놀라웠던 건 각 웹페이지들을 수동으로 Duplex에 통합하는 것이 아니라 ML을 적용해서 자동으로 통합할 수 있다는 부분이었다. 실제 사용이 가능할 때 어느정도 수준으로 통합될 지, 정말 대부분의 웹사이트에 자동으로 통합될 것인지, 사실 웹 개발자로서 약간은 회의적인 입장이라 궁금하다. 토스도 미친 유저 경험을 추구하기 위해 비슷한 기능이 들어가 있는데 공개된 시점에 참고할 부분이 많을 거라고 예상하고 있다.
요즘 소프트웨어 기술 업계에 관심이 많은 사람이라면 공감할 수 있겠지만, 많은 오프라인 비즈니스가 온라인으로 옮겨가면서, 한 사용자가 다양한 소프트웨어를 사용해야 하기 때문에 이런 상황에 대해서 피로감을 느끼는 사람이 적지 않을 거라고 생각한다. 이번에 공개된 Google의 기술들은 이런 피로감을 해소하는데 초점을 맞추고 있다는 생각이 들었다. 더불어, 이런 소프트웨어에 접근 자체가 어려웠던 사용자, 가령 문맹이라던지, 장애를 가지고 있는 사람들에게 도움이 되기 위해 노력하고 있다는 생각도 들었다. 이번에 공개된 기술 중 Google Assistant 관련 내용이 특히 많다고 느껴진 것도 같은 맥락에서 해석이 가능할 것 같다.
전반적인 느낌에 대해 표현하자면, 키노트를 듣는 내내 흥분을 감추기 어려웠다. 물론 Google이 이번에 공개한 기술 자체도 놀라운 부분이 적지 않았다. 하지만, 내가 솔직히 이걸 그냥 페이스북에서 기사로 봤을 때를 상상해 보면, “오 역시 구글”하고 짧게 한 마디하고 끝났을 것을, 그 현장의 열광적인 분위기를 온전히 느낄 수 있었기 때문에 “내가 역사의 한 순간에 서있다”라는 생각을 벅차오르는 감정과 함께 느낄 수 있었다. 동시에 Google I/O에 참가할 수 있어서 다행이라는 생각이 들었다.
여기에 소개하지 않은 기술들이나 새로 공개된 기기에 대해서는 The Verge의 기사를 참고하면 좋을 것 같다.
점심으로 제공되는 샌드위치를 가볍게 먹고나니 개발자 키노트가 시작됐다. 개발자 키노트에서는 개발자에게 중요할 만한 내용을 압축해서 전달했다.Android Studio를 포함해 Android Q의 새로운 API와 Chrome에서 제공하는 (물론 웹 표준인) 새로운 API, V8 엔진의 성능개선, Tensorflow 등 Google의 대표적인 기술들의 업데이트들을 소개하는 자리였다. 사진은 열심히 찍었는데 사실 이 키노트에서는 요약된 자료만 보여줬고, 자세한 내용들은 별도로 마련된 세션에서 소개되었기 때문에 이 부분에서 다루지 않겠다.
이 세션에서는 새로운 Chrome의 API들이 자세하게 소개되었다. 크게 세 가지 분류로 소개되었다.
“Instant” 분류에서는 늘 그렇듯이 Chrome이 사용하는 V8 엔진의 성능적인 개선이 먼저 소개되었고 다음으로 웹 개발자가 웹 사이트의 성능을 최적화하기 위한 다양한 도구들이 소개되었다. 다행히도 꽤 많은 부분이 바로 적용 가능해보였다. 가령 Image lazy load를 Chrome에서 지원하기 시작했는데, 단순히 <img loading="lazy" />
라고만 써도 적용되고, 지원하지 않는 브라우저에서는 무시될 것이기 때문에 바로 적용가능할 것으로 보인다.
“Powerful”에서는 웹 영역의 확장, 기존에서는 Native만 접근 가능했던 곳에 Web이 접근할 수 있도록 새로운 API들이 소개되었다. 예를 들어, 안드로이드 디바이스의 Native 공유 API가 웹에서 접근가능하게 되었다. 물론 이런 종류의 변화가 늘 그렇듯이 해당 기능은 Chrome에만 구현되어 있으므로 프로덕션에 바로 실제로 적용하기는 많이 어려울 것이다. 하지만 PWA와 결합한다면 상당부분 네이티브 앱의 역할을 가져올 수 있고, 이런 개선이 계속된다면 결국에는 웹(+ PWA)이 네이티브 앱들을 대체하는 수단으로 활용될 수도 있겠다는 생각이 든다. 이미 데스크탑 앱은 상당 부분 대체되기도 했고.
“Safe”에서는 Chrome의 보안 정책의 변화를 소개했다. 가장 흥미로웠던 부분은 Webauthn이었다. Webauthn은 웹에서 일반적으로 사용되는 인증방식인 ID/비밀번호 방식을 제거하기 위해, 모바일에서 주로 사용되는 인증방식인 생체인식등을 웹에서도 사용할 수 있게 만들어주는 API다. 이번에 처음 알게된 개념인데 알고보니 Safari를 제외하고 대부분의 모던 브라우저에서는 지원이 이미 되고있었던 API였다. 아직은 당장 사용이 어려워보이긴 했지만, iOS, Safari 등에서 구현된다면 도전적인 서비스에서는 사용할만 하겠다는 생각이 들었다.
사실 큰 기대를 하고 들어간 세션은 아니었는데 의외로 꿀잼이었던 세션이다. 이 세션에서는 세 가지 실제 사례를 들어 각각 겪고있던 문제를 소개하고, 해당 문제를 해결하는데 어떻게 했는지, 그리고 그걸로 얼마나 비즈니스적 성과를 나타냈는지 소개했다.
발표에 연사자로 트위터와 Hulu에서 Product Manager 롤을 가지고 있는 분이 나왔는데, 엔지니어가 아닌 매니저가 이런 기술을 잘 알고 또 관심을 가지고 있다는 사실이 개인적으로 놀라웠다.
가장 첫 번째 사례는 트위터였다. 트위터 유저라면 최근에 데스크톱 웹 디자인이 깔끔하게, 그리고 모바일과 유사하게 상당히 바뀌었다는 걸 알고 있을 것이다. 사실 트위터는 원래 “Twitter Lite”라는 가벼운 앱을 만들기 위해 2017년에 이미 웹 기술과 PWA를 이용해 모바일 앱을 만든 사례가 있었고 이것은 트위터의 모바일 웹으로도 사용되고 있었다. 하지만 데스크톱 UI는 모바일과 꽤 다른, 유저인 내가 봤을 때도 상당히 이질적인 느낌이었는데, 실제로도 다른 코드 베이스 위에서 운영되고 있었다. 결국 트위터는 새로운 기능을 추가하기 위해서는 데스크톱용 웹과 모바일 웹 두 가지의 코드 베이스에 모두 기능을 추가해야 했던 것이다.
그래서 트위터는 기존에 운영되던 모바일 웹 기반으로 반응형 웹 디자인을 적용해 새롭게 데스크톱 UI를 만들게 되었다. 다양한 환경을 대응하기 위해서 리액트를 사용해 Component Based Design을 적용했다. 간단하게 설명하면 하나의 컴포넌트가 다양한 플랫폼에 대응되게끔 설계하는 것이다. 이를 통해 트위터는 데스크톱 부터 피처폰까지 넓은 범위를 소화할 수 있었다.
타임즈 인터넷은 인도의 테크 기업이다. 인도는 인터넷 속도도 느리고, 대부분의 디바이스 사양이 좋지 않기 때문에 이 세션에서는 그런 환경적 한계를 어떻게 소프트웨어로 극복할 수 있었는지에 대해서 다뤘다. 타임즈 인터넷은 웹을 통해 뉴스를 서비스하고 있는데, 인터넷 속도가 느렸기 때문에 PWA가 중요하게 작용했다.
먼저 느린 인터넷 속도를 극복하기 위해 PWA의 오프라인 캐싱, Lighthouse의 Performance Budgets을 사용했다. Lighthouse의 Performance Budgets은 개발자 키노트에서도 소개되었는데, JavaScript, CSS 혹은 이미지 리소스등의 애셋 크기를 제한하는 도구다.
이런 도구를 통해 오프라인 혹은 인터넷 속도가 낮은 상황에서도 유저 경험을 크게 해치지 않을 수 있었고 그로 인해 가장 중요한 광고 매출을 59% 증대시켰다고 한다.
Hulu는 Netflix와 비슷한 OTT 서비스다. Hulu는 원래 유저들에게 데스크톱 앱을 제공했는데 이 데스크톱 앱을 PWA를 이용한 웹앱으로 전환한 사례를 발표했다. Hulu는 가지고 있던 “레거시” 데스크톱 앱을 Fade out 시키고 유저들을 웹으로 전환시키려고 했지만 유저들의 피드백이 좋지 않았다. 가장 큰 이유는 “설치가능성”. 그러니까, 웹에서 제공하는 서비스였기 때문에 설치할 수 없다는 점이 좋지 않은 피드백을 받았다고 한다. 이를 해결하기 위해 Hulu는 데스크톱 PWA를 적용했다. 이를 적용하는데 개발자 1명이 2주밖에 들이지 않았다는 PWA 칭찬인지 엔지니어 자랑인지 알 수 없는 말을 했다.
Hulu의 사례는 사실 PWA 데스크톱을 적용하기 이상적인 케이스라는 생각이 들었다. 사실 PWA가 아닌 Electron을 사용하는 케이스는 대부분 Native 자원에 접근하기 위해서인데 Hulu의 경우 단순한 동영상 OTT 서비스이므로 Native 자원에 접근해야할 니즈가 거의 없기 때문이고 기존에도 웹 서비스를 잘 제공했고, 단순히 Install 가능성만 추가로 충족시키면 되었기 때문이다.
올해도 어김없이 회고 시즌이 돌아왔다. 매년 느끼는 거지만 시간이 너무 빨리 흘러간다. 다른 개발자 분들의 회고를 페이스북이나 트위터 등에서 보면서 나도 늦지 않게 빨리 써야지라고 생각하다 2018년이 정말로 얼마 남지 않은 지금 이제서야 키보드를 두들기기 시작했다.
내가 삶에서 중요하게 여기는 가치 중에서 “성장”과 “변화”가 있다. 2017년까지의 나는 대부분 성장에만 포커스를 맞추고 살아왔다고 생각한다. 올해는 의도적으로 그런 건 아니지만, 성장보다는 여러가지 변화를 거듭한 한 해가 된 것 같다. 물론 2018년에도 분명 성장하긴 했지만 작년보다는 좀 부족하다고 생각한다.
약 2년 2개월 정도를 다닌 스튜디오씨드를 퇴사하고 토스에 합류한지 이제 막 1개월 정도가 되었다. 올해 나에게는 가장 큰 변화 아닐까 싶다. 이 블로그를 통해서는 공식적으로 밝힌 적이 없지만 개인적으로 나를 아시는 분이나 페이스북에 친구가 되어있는 분이라면 이미 알고 계실 것이다.
2년 2개월이 그리 길지는 않은 시간인데, 그 사이에 형성된 이미지가 있는지 지인 분들께 말씀드리면 대부분 놀라는 반응이었다. 주변 지인에게도 알리지 않고 조용하게 준비하기도 했었고. 이직 동기에 대해서 묻는 질문이 많았는데, 너무 진부하게 들리기는 하지만 “새로운 도전을 해보고 싶었다”가 나의 대답이다. 스튜디오씨드를 떠나고 싶어서 이직한 것이 아니라 토스의 문화를 경험하고 다른 방식의 성장을 이루고 싶었기에 이직했다. 또 성장을 경험하기에 환경의 변화만큼 좋은 해법이 없다고도 생각하고. 덕분에 2년 만에 이력서를 업데이트 했다.
스튜디오씨드에서도 너무 좋은 사람들을 만났고, 나름대로 허물없이 친하게 지냈던 동료들이 많아서 내색은 안 했지만 마음이 많이 아프기도 했다. 나오는 날에는 쓸쓸함과 함께 많은 감정이 교차했다.
작년에는 새로운 기술을 많이 접하면서 견문을 넓혔었다면, 올해는 넓이보다는 깊이에 조금 더 집중했던 것 같다.이미 조금씩 알고만 있었던 스킬들의 성숙도를 높여가면서 과거에 짰던 코드도 리팩토링하고 재미있게 일했다. 특히 올해 중순쯤 같은 포지션으로 안도형님이 입사하면서 한층 긴장할 수 있는 계기가 되었고 서로 좋은 영향도 많이 주고받았던 것 같다.
2017년 말부터 내가 맡아서 하고 있던 새로운 프로젝트도, 결국 1차적으로 개발 완료한 후 납품도 되었다. 그 전까지는 나 스스로에 대한 확신이 없었는데, 나도 시작부터 끝까지 맡아서 딜리버리할 수 있다는, 내가 처음부터 끝까지 디자인해서 짠 앱도 멀쩡히 돌릴 수 있다는, 소중한 자신감과 경험을 얻었던 순간이기도 했다. 나에게는 스튜디오씨드에서 했던 가장 큰 도전이기도 했다.
하지만 도전의 시간은 길지 않았다. 새로운 프로젝트를 시작할 때, 전반적인 구조를 디자인하고 재미있는 기술을 가져다 쓰면서 느꼈던 즐거움도 잠시, 곧 유지보수가 시작되면서 지루함이 느껴졌고, 일도 더디게 했다. 즐거움을 다른 곳에서 찾기 시작했다. 자기계발을 접어두고 술도 자주 마시고 사람들도 많이 만났다. 결국 이 지루함도 내가 이직한 원인이 되었던 것 같다.
지금은 더 이상 참여하고 있지 않지만, 작년 12월 즈음 페스타팀에 프론트엔드 엔지니어로 합류했다. 그리고 페스타 초기버전의 프론트엔드의 대부분을 내가 작업했었다. 현재 내 코드가 얼마나 남아있는지는 잘 모르겠지만 아직 문제없이 되고 있는 걸 보니 그래도 내가 제 몫은 한 것 같아 뿌듯한 마음이 있다. 위에서 언급한 새로운 프로젝트와 작업기간이 겹쳐서, 사실상 기술스택이 거의 똑같았다. 사이드 프로젝트였지만 일하는 방식도 애자일하게 스프린트도 만들고, 회고도 하고, 스토리 포인트도 정하면서 재밌게 일했다.
관두게 된 이유는 심플하다. 재미가 없어졌다. 처음에는 재미로 시작했는데 중간에 업무랑 같이하면서 체력적으로 힘들기도 했지만, 그래도 새로운 서비스를 만들어가는 재미, 새로운 기술 써보는 재미가 있어서 그럭저럭 할만하다고 느꼈다. 페스타의 첫 버전을 런칭하고 나서는 급격히 그 재미가 줄어들었다. 사실 사용하는 기술조차 회사에서 하던 것과 거의 같아서, 퇴근하고 회사일을 또 하는 기분이 들면서 점점 코드를 짜는 시간이 줄어들었다. 당시 이곳저곳에서 스트레스를 많이 받고 있었던지라 정리해야겠다는 마음이 들었고, 정리하게 됐다.
팀에서 나오기는 했지만 여전히 애정이 남아있고 잘 되기를 바라는 서비스 중 하나다. 실제로 이벤트를 많이 열어야하는 입장에서 사용하기가 편리하기도 하고.
올해 블로그 성적은 정말이지 처참하다. 매년 “작년보다 더!”를 외쳤지만 블로그를 시작한 이래 연도별 포스팅 갯수가 늘어난 적이 없었다. 올해는 (이 글을 제외한다면) 겨우 다섯 개의 포스팅을 했다. 글 하나하나를 나름대로 정성스럽게 써서 각 포스팅의 질이 상대적으로 높았다고는 생각하지만, 그래도 솔직히 너무 놀지 않았나.. 싶기는 하다. 자기 변호를 좀 하자면, 쓰다만 글도 꽤 있다. 이제 이것보다 적게 쓰기도 어려우니 내년에는 그래도 올해보다는 글을 많이 쓰지 않을까 싶다.
그나마 잘했던 걸 하나 꼽아보자면 “타입스크립트, 써야할까?”라는 글을 쓴 것. 이 글 자체도 꽤 많이 언급되었고 잘 됐지만, 정말 예전부터 타입스크립트를 쓰면서 꼭꼭 말하고 싶었던 주제였는데 그걸 글로 다 정리하고 설득력있게 풀어 냈다는 게 지금도 꽤 만족스럽다. 그리고 놀랍게도, 블로그 방문자 수는 꾸준히 성장 중이다.
GDG Korea WebTech 오거나이저로 합류했다. 구글 웹 기술과는 별로 친하지 않은 편이라서 공부와 노력을 많이 해야 할 것 같다. Chrome Dev Meetup #2와 DevFest WebTech 2018을 기획하고 주최했다. 사실 이전부터 TypeScript Korea 운영진을 하면서 이벤트를 준비한 경험이 없지는 않지만 내가 주체적으로 기획하고 운영했던 경험은 부족해서 쉽지 않았다. 그래도 좋은 사람들도 많이 만나고 재미있는 행사도 많아서 즐거웠다. 내년에도 재미있는 이벤트 많이 기획해보고 싶다.
2018년에는 두 번의 밋업을 열었는데 두 번째 밋업 때 여러가지 운영미스가 있어서 좀 안타까웠다. 올해 활동을 열심히 하지는 못했는데, 내년엔 노력해야겠다.
Naver FE DevTalk에서 MobX와 MobX State Tree를 주제로 발표했다. 부끄럽지만 영상도 첨부한다.
발표는 나름 열심히 준비했다. 다른 곳도 아니고 현업 네이버 엔지니어 분들이 보는 곳이라서 긴장이 많이 됐다.
졸업이 자잘한 이벤트라고 할 수는 없지만 2018년에 딱히 졸업을 위해 한 것이 없기 때문에 큰 사건은 아닌 것 같다. 졸업장이 생겼고, 뭔가 서류를 써야할 때 대졸이라고 적는 정도. 뭐, 고졸이던 때와 비교해서 일상에 차이는 없다.
2018년의 소소한 목표 중 하나가 운전면허 따기였는데, 무난하게 운전면허를 땄다. 그리고 남들처럼 장롱으로 직행할 뻔 했다가 우연히 아빠가 차를 새로 사셔서 원래 아빠가 쓰던 옛날 차를 얻게 되었다. 덕분에 장롱면허 신세는 면했다. 다만 그 사이 접촉사고를 세 번이나 내서 마음이 많이 힘들었다.
전세자금대출을 받아 자취방을 역삼동으로 옮겼다. 학자금을 제외하면 대출도 처음해보는 경험이었고 전세계약도 처음이라서 순탄치가 않은데다가 돈도 정말 아슬아슬해서 너무 정신이 없었고 불안했다. 다행히도 결국 계약을 마무리 잘해서 성공적으로 이사도 했다. 하지만 집이 문제가 많아서 다시 이사하고 싶다..
치과를 작년 말부터 다니기 시작해서, 여러가지 치료를 받고 올해 임플란트 시술을 받았다. 딱히 치아에 문제가 없어도 치과는 주기적으로 방문해야 한다는 걸 깨달았다.
전 직장에서 외국인 친구와 급격히 친해지면서 영어가 많이 늘었다. 일단 자신감 자체가 전과는 비교할 수 없을 정도로 많이 늘었다. 이직으로 인해 앞으로 영어로 대화할 일이 많이 없어져서 안타깝고 조금 슬프다.
올해 중순 쯤 다이어트를 시작해서 9kg 정도 몸무게를 감량했다. 토스가 밥을 너무 잘 먹여서 살이 1kg 정도 도로 찌기는 했지만.. 아직은 유지 중이다.
분명히 이 리스트에 있는 것들 다 안 지킬 건 알지만 그래도 백로그처럼 써봐야겠다.
그래도 나에게는 특별하고 만족스러운 한 해였다. 생각해보면 내 선택이나 행동에 전반적으로 운이 따라 줬던 것 같다. 그래서 운이 나의 게으름을 어느 정도 상쇄시킨 것 같다. 이직도 그렇고, 커뮤니티 활동도 그렇고, 기회도 자주 찾아왔고, 좋은 사람들을 많이 만날 수 있었다. 2018년에 도와주신 모든 분들께 감사드리고, 2019년도 모두 같이 성장하는 한해가 되었으면 좋겠다.
]]>이 포스트는 Evan Martin의 글, TypeScript at Google을 원 저자의 동의하에 번역한 것이다. 의역이 매우 많고 오역이 있을 수 있으니 가급적 원문도 같이 참고하길 바란다.
나는 이제 TypeScript를 2년 넘게 써왔다. 따라서 이에 대해 리뷰하는 글을 하나 둘 정도 써야겠다고 생각했다. 나는 전형적인 면책선언(Disclaimer)와 함께 글을 시작하려고 한다. 나는 그냥 Google에서 일하는 수 만 명 중에 한 명의 엔지니어 일뿐이고, 여기에 적힌 의견에 완전히 동의하지 않는 사람들도 있다.
Google은 웹 어플리케이션을 일찌감치 채택했다. Gmail이 벌써 14년이 되었다고 하면 믿을 수 있겠는가? 그 당시의 JavaScript는 완전히 미쳤었다(madness). Gmail 엔지니어들은 Internet Explorer의 구린 가비지 컬렉션 알고리즘에 대해 매우 걱정이 많았다. 따라서 엔지니어들은 가비지 컬렉션이 중지되는 상황을 피하기 위해서 for 루프 밖으로 문자열(String) 표현을 직접 호이스팅 시킬 필요가 있었다. 나는 최근 그 시절의 설계 문서를 찾았는데, 그 문서에서는 우리가 JavaScript를 “미니파이(Minifying)” 한다고 부르는 작업을 고려하고 있었다. 그러나 그 중 몇몇 후보 도구는 Windows 전용이었다. 요즘에는 상상할 수도 없는 일이다.
그 후 몇 년 간, Google은 큰 JavaScript 앱을 개발하는 데 쓰일 수 많은 기반 기술을 개발했다. 예를 들면, 소스 파일이 상호의존성을 표기하도록 만드는 모듈 시스템이 있다. 소스 파일을 합치고 미니파이해서 브라우저에 호환되는 결과물로 만드는 번들러도 있다. 또 다른 어떤 도구는 동적으로 로드할 수 있는 진입점과 서빙을 위한 공통 부분 요소를 통해 앱의 의존성 그래프를 분석한다. 서버 사이드 렌더링은 일반적이다. 모든 이러한 개념들은 요즘의 웹 개발자들에게는 친숙한 것들이지만, Google의 기술 스택의 발전은 오늘날의 발전과 병렬적이지만 더 앞서서 개발되어왔기 때문에, 결과적으로 개념상으로는 비슷하지만 구체적으로는 달라졌다. 프로세스, 도구, 심지어는 이러한 개념들의 이름까지도 완전히 다르다.
병렬적인 진화의 다른 예는, Google, Facebook, Microsoft가 JavaScript에 정적 체크를 더한 컴파일러를 각각 비슷하지만 서로 호환되지 않게 만들었다는 것이다. 그 중 Google의 컴파일러는 구어체로 Closure라고 한다. (Clojure 언어와 헷갈리지 말자. 더 헷갈리는 건 ClojureScript가 Closure 컴파일러를 쓴다는 것이다.)
Google의 JavaScript 스택은 너무나 뛰어났고, Google이 인터넷의 얼굴을 바꾸어버린 웹 앱을 만들고 유지보수할 수 있게 해주었다. (Google 지도가 릴리즈된 시점이 얼마나 놀라운지 기억하는가? 이제는 위젯 형태의 드래그 가능한 지도를 만드는 것은 너무나 당연하다.) 어떤 부분들은 오늘날의 기술을 능가한다. 예를 들면, Closure 컴파일러는 타입 정보를 사용해서 코드를 최적화하고, 핫 로딩 청크 바운더리를 가로지르는 인라인 함수를 사용하고, 사용하지 않는 코드를 제거해 개별적인 심볼로 만들 수 있는, 여전히 가장 세련된 JavaScript 최적화 도구일지도 모른다.
Google의 JavaScript 스택은 문제 또한 안고있다. Closure가 린터에서부터 출발해 점진적으로 진화해왔다는 사실은 Closure가 정적 타입 문법을 주석에서 사용해야 한다는 것을 의미한다. Closure는 예측하기 어려운 시맨틱을 가졌고 느리며, 버그가 많다. 그리고 코드를 정확하게 작성하지 않으면 맹글링(Mangling)하는 경향이 있다. 오픈 소스임에도 불구하고, 이러한 이유들 때문에 업계 전반적으로 사용되지 않았다. Closure에 익숙한 구글러들을 고용한 회사들을 빼고 말이다. 나는 Google 내에서 JavaScript가 낮은 평가를 받고 있다고 생각하는데, 우리의 까다로운 도구 Closure가 정적 언어의 수다스러움(verbosity)을 동적인 언어의 예측불가능성과 결합했기 때문이라고 생각한다.
그 사이 Google 밖에서도, JavaScript는 진화를 계속했고 점점 더 많은 인기를 얻었다. 앞서 말한 IE 가비지 컬렉션 버그를 우회하기 위해, 우리는 Chrome을 만들었고 Chrome은 v8을, v8은 Node.js를 낳았다. 이는 요즘 대부분의 웹 도구가 그 자체로 JavaScript로 만들어졌다는 걸 의미한다. 과거에 이러한 도구를 만드는데 쓰였었던 Java와는 달리 말이다. 모듈 시스템(UMD, AMD, CommonJS)은 급격히 늘어났다. (ES6가 등장해서 자체 모듈 시스템을 개발했지만, 다른 시스템들과는 어떤 이유로 호환되지 않는다. 에휴.) npm은 도구들과 라이브러리가 공유되는 방식을 통합했다. Webpack은 개발자가 개발하는 동안 실행되고 있는 앱에 동적으로 모듈을 교체할 수 있는 기능을 제공했다.
Google은 이 중 어떤 것도 쓰지 않는다. 숙련된 웹 개발자가 Google에 나타났지만, 그가 다른 시간대에 방문한 것과 같다. Google에는 SASS 같은 CSS 전처리 언어가 있지만 이는 SASS가 아니며 누구도 그것을 좋아하지 않는다. 어떤 팬시한 청크 스플리터도 있지만 이 도구는 써드 파티 JavaScript 라이브러리를 지원하지 않는다. 왜냐하면 이 도구는 JavaScript 라이브러리 생태계가 갖춰지기 전에 만들어졌기 때문이다.
이건 모두 역사에 불과하다. 누군가 Google이 그런 길을 가지 말았어야 했다고 주장할 수 있지만, 그렇게 한다고 우리가 이러한 상황에 있다는 사실을 바꾸지는 않는다. 대신, 흥미로운 질문을 해보자. “우리는 이제 어디로 가야하는가?” 몇 가지의 옵션이 있다. 내 관점은 물론 내 선호에 따라 편향되어 있다.
첫번째 매력적인 옵션은 이 망가진 행성을 버리고 JavaScript를 아예 포함하지 않는 새로운 행성에 정착하는 것이다. 만약 우리가 GWT(Java를 JavaScript로 컴파일 하는 Google의 프로젝트) 혹은 Dart(새로운 언어를 JavaScript로 컴파일 하는 Google의 프로젝트) 혹은 WASM 혹은 당신이 좋아하는 언어 무엇이든, Clojure? Haxe? Elm?.. 등에 더 투자했다면 우리는 JavaScript에 대해 아예 걱정할 필요도 없을 것이다.
PL의 팬으로서 나는 이 옵션을 꽤 좋아한다. 그 언어들의 자격에 대해 신중하게 분석한 자료를 주고 싶지만, 이 포스팅은 충분히 길고, 그에 대한 토론은 별도의 포스팅이 되어야 한다고 생각한다. 그 토론 전에, 몇 가지 고려해야 할 사항이 있다. 만약 다른 언어를 채택하게 된다면, (1) 말 그대로, 우리가 가진 수백만 줄의 코드를 재활용 할 수 없다. “새로운 언어로 처음부터 다시 작성”하는 것은 어떤 상황에서는 적절한 선택이지만, 그게 Gmail 엔지니어들의 시간을 적절히 사용하는 방식인지에 대해서는 논쟁의 여지가 있다. 그리고 (2) 앞서 언급한, 우리가 고용하고자 하는 경험 있는 프론트엔드 프로그래머에게도 거의 도움이 되지 않는다.
모든 것을 새로 쓰는 것을 바꿔 말하면 아무것도 바꾸지 않는 것이다. 오픈된 JavaScript 세계는 아마추어 코드와 left-pad1같은 재앙으로 가득 차 있다고 지적할 수 있다. 좋은 엔지니어는 Google이 프론트엔드를 구축하는 특이한 방식에 적응할 수 있기 때문에, 우리는 언제나 우리의 자체 도구를 개선하고 만들 수 있다. Google이 만들고 있는 앱의 요구사항은 다른 회사들이 만드는 웹 앱들과 성격이 다르다(Google 검색 페이지는 하루에 수십억 회 조회된다)는 점은 우리의 자체 도구가 더 우월하며 실제로 필요하다는 것을 뜻한다. 나는 이러한 관점에도 호의적이다. 여기에는 두 가지 트레이드오프 지점이 있다고 생각한다. 하나는 우리의 자체 도구를 만드는 것이 합리적인 지점이고, 다른 하나는 우리가 자체 도구를 만듦으로서 주류로부터 갈라지도록 만드는 지점이다. 논쟁은 이제 우리가 그 사이 어디에 있느냐는 것이다. 나는 Google이 후자 쪽으로 너무 멀리 왔다고 생각한다. 우리는 C++에 의존하기 때문에 LLVM과 Clang에 기여하여 도움이 될 수 있지만, 우리의 자체 LLVM을 만드는 것으로부터는 많은 부가가치를 얻을 수 없을 것이다.
이러한 사실은 나의 소규모 팀이 추구하고 있던 중간 지점으로 이끌었다. 그것은 점진적으로 적절한 외부 도구를 도입하고 우리의 기존 코드 기반과 연동하는 방법을 찾아내는 것이다. 이 작업은 별로 즐겁지는 않지만, (우리는 그냥 우리의 레거시 코드를 던져버리고 “맞는 방법으로 바로 하지”는 않았다.) 나는 겸허하게 수용하려고 하고, 내적인 요소보다는 외부적인 요소를 보고 싶다.
Google의 JavaScript 갈라파고스 섬으로부터 본토로 돌아가는 여정의 첫 번째는 잘 지원되는 정적 체커를 도입하는 것이었다. 그건 (1) 회사 내에서만 성장한 것이 아니고 (2) 우리의 기존 코드와 유사한 문법을 사용하지만 이미 유명했고 (3) JavaScript로 연결되도록 설계되었으며 (4) 맨 처음 Closure의 동기가 되었던, 대규모의 개발을 위해서 설계 되었다. 그리고 그 도구가 바로 TypeScript다. Closure 컴파일러의 장점은 최적화된 결과물인 반면, TypeScript는 훌륭한 유저 인터페이스를 가지고 있고, 최적화를 전혀 하지 않는다. 이 두 도구는 상호보완적이고, (특정 작업에서는) 같이 사용할 수 있다.
TypeScript는 이미 대부분의 프로젝트에서 잘 사용되고 있었기 때문에 (결국 이것도 채택한 이유 중 하나다.) 우리는 자리를 잡은 언어를 도입함으로서 많은 장점 그러니까, IDE 스타일의 코드 자동완성부터, StackOverflow 답변을 쉽게 찾을 수도 있는 등의 장점을 얻을 수 있다. 우리에게 남은 자작업은 기본적으로 통합이었다. 즉, 앱을 완전히 새로 다시 쓰지 않고 점진적으로 TypeScript로 옮기는 것이었다. 점진적으로 컴파일 하기 위해서 Google 전체 빌드 시스템과의 통합은 신중해야 했다. 이는 대규모 앱에 있어서 치명적일 수 있다. 한 모듈에서 내보내는 API에 영향을 주지 않는 변화는 하위 모듈의 재컴파일을 일으키지 않는다. Closure 타입/모듈 시스템으로의 통합은 ES6 TypeScript 모듈이 대부분의 타입 정보를 보존하면서 Google의 모듈 시스템을 가진 모듈을 임포트 할 수 있다는 걸, 그리고 그 반대도 가능하다는 걸 의미한다. 한 회사는 우리가 만든 도구를 사용하여 그들의 전체 코드 기반을 미니파이된 결과물을 보존하면서도 TypeScript로 자동으로 변환하는데 성공했다.
Google 내에서 TypeScript는 이제 어디에서나 가지각색으로 사용되고 있다. 누군가 Google의 제품을 사용한다면 그 사람은 약간의 TypeScript 코드와 상호작용한다고 말할 수 있다. TypeScript 그 자체로도 정적 타입 프로그래밍과 이제 관성이 붙은 JavaScript 생태계와 잘 균형을 이룬 흥미로운 절충안이다. 이는 우리 엔지니어들이 하는 일이기도 하다. 우리는 서로 다른 관심사 간의 균형을 맞추기 위해 흥미로운 타협을 한다. 나는 우리가 지난 몇 년 간 발견한 몇 가지 흥미로운 점들에 대해서 앞으로 좀 더 글을 쓰고 싶다. 처음으로 이 길로 들어섰을 때 내가 썼던 것처럼, TypeScript는 좋은 트레이드오프를 만들고 있는 것 같다.
]]>최근 페이스북의 프론트엔드 개발그룹에 설문이 있었다. 타입스크립트를 도입하지 않는 이유에 대해서 묻는 설문이었는데, 한 사람의 타입스크립트 유저로서 보고있기가 정말 괴로운 설문이었다. 그 글을 보고 예전부터 쓰다가 말고를 여러 번 반복한 주제를 꺼냈다. 주제는 바로 타입스크립트를 써야할, 혹은 쓰지 말아야 할 이유. 사실 이 글은 타입스크립트를 써야할 이유에 더 가까울 것이다. 타입스크립트를 좋아하시는 분들이 많이 계시지만 거부감을 가지고 계신 분들도 많이 보았기에 한 번 쯤은 글로서 설득해보고 싶었다.
본론을 시작하기에 앞서, 이 글은 일개 타입스크립트 유저가 작성한 매우 주관적인 의견이라는 점을 확실히 해두고 싶다. 기술에 절대적인 선이나 악은 존재하지 않으며, 마찬가지로 기술적인 선택에 있어 정답이나 왕도는 없다. 이 글의 논조 자체가 타입스크립트 우호적으로 작성되었기 때문에 이 글을 보는 독자 입장에서는 마치 타입스크립트를 사용하는게 답이라는 듯 말하는 것처럼 느껴질 수 있지만, 실제로는 절대 그렇게 생각하지 않는다고 밝혀두고 싶다. 개인의 선택을 존중한다.
본문에서는 크게 다섯 가지로 나눠 러닝커브와 도입, 생산성, 안정성, 함수형 프로그래밍, 커뮤니티 측면에서 타입스크립트를 살펴볼 것이다. 이 글에서는 타입스크립트만 콕 집어 다루고 있지만, Flow등의 다른 정적 타입 도구도 상황은 크게 다르지 않을 것이라 예상된다.
사실 나는 아직도 타입스크립트를 주저하는 이유로 러닝커브를 꼽는 사람이 있다는 것에 놀랐다. 물론 타입스크립트가 쉽냐는 질문에는 쉽지 않다고 대답할 것 같다. 그렇지만 자바스크립트에 비해서 러닝 커브가 심하게 가파르다고 할 수도 없다. 더구나 ES2015, 그 이후의 문법에 익숙한 사용자라면 큰 어려움 없이 시작할 수 있다. 그 이유는 자바스크립트의 수퍼셋이라는 타입스크립트의 특성 때문이다.
타입스크립트의 가장 기본적인 기능은 단순히 자바스크립트에 타입 표기법을 추가하는 것이기 때문에, 만약 string
이나 number
등의 자료형을 잘 모르는 사람이 아니라면, 타입스크립트를 단순히 도입하는 것은 별로 어렵지 않다. 더구나 기존 자바스크립트 기반의 프로젝트에서도 일부 파일만 타입스크립트로 바꿔서 부분적으로 사용하는 것도 가능하기 때문에 점진적 도입도 할 수 있다.
“잘 쓰고싶다”는 얘기가 조금 다르다. 자바스크립트와 마찬가지로 타입스크립트에는 수 많은 기능들이 있고, 개 중에는 배우기 어렵고 난해한 기능들도 있다. 특히 그 전 정적 타입 언어를 사용했던 유저라면 금방 이해할 수 있겠지만 그렇지 않은 사람에게는 배우기 어려운 개념들도 있다. 이런 기능들은 처음부터 당장 잘 사용해야되는 것들은 아니다. 분명히 자바스크립트를 사용했을 때보다는 알아야 할 게 늘어난다. 하지만 그 어느 누구가 그 모든 기능들을 다 사용하고 있을까? 프로그래밍 언어라는 특성상 사용자의 역량에 따라 응용도가 크게 차이나고, 그 한계가 명확히 존재하지 않는다. 시간이 지남에 따라 커뮤니티가 성숙하고 언어를 사용하는 법 역시 성숙한다. 타입스크립트는 심지어 다른 언어에 비해 릴리즈도 자주 일어나기 때문에 더욱더 마스터에 근접하기는 어려운 언어다. 그런 상황에서 이 언어의 모든 걸 알아야 한다고 생각한다면 타입스크립트는 어려운 언어가 될 수 밖에 없다. 이 글을 쓰는 나도 잘 쓰고 있다고 말하기는 어렵다. 지속적인 개선이 있을 뿐이다.
결론적으로 배우기 어려워서, 도입 비용이 커서 타입스크립트를 선택하지 않는다는 것은 섣부른 걱정이라는 생각이다. 프로그래밍 언어를 처음 배웠을 때로 되돌아가보자. printf("Hello, world!");
컴파일러, 타입은 물론이고 세미콜론이 뭘 의미하는지도 모르는 시절에도 일단 코드부터 쳤다. 처음부터 다 하려고 하면 당연히 포기가 더 가깝다. 차근차근히 일단은 내가 할 수 있는 작은 것부터 시작하면 되는 거고, 타입스크립트는 점진적으로 나아가기에 좋은 언어다.
더 읽기: Why You Shouldn’t Be Scared of TypeScript
정적 타이핑과 동적 타이핑의 생산성에 관한 논쟁은 상당히 오래된 떡밥이고 이 역시도 아직 모든 상황에서 “타이핑을 하는 것이 우위다.” 혹은 “타이핑을 안하는 것이 우위다.”라고 절대적인 결론이 나지는 않았다. 단순히 더 많은 코드를 작성해야하니 생산성이 떨어지게 된다는 주장은 너무 일차원적인 의견이다. 상황에 따라, 사용자에 따라, 보다 적절한 선택이 있을 것이다. 이 주제에 대한 일반적인 의견은, 동적 타이핑은 단기적으로 사용할 경우에 뛰어난 생산성을 보여주지만, 다수의 사람들이 장기적으로 유지보수 할 때에는 어려움을 느낄 가능성이 높다는 것이다. 물론 정적 타이핑은 이 반대의 우위를 가지고 있다. 자바스크립트와 타입스크립트의 관계도 마찬가지다. 타입스크립트는 장기적으로 생산성에 나은 선택이 될 가능성이 높다.
최근 자바스크립트가 활용되는 영역에 한계가 거의 없다시피 하지만 여전히 가장 많이 사용되는 곳은 프론트엔드 프로젝트다. 과거 웹이 Multi Page Application(MPA) 기반으로 작성되었던 것과는 달리 요즘 웹페이지는 대부분 React와 Vue, Angular등을 사용한 Single Page Application(SPA)으로 작성된다. 두 가지 타입의 웹에서는 프론트엔드에 기대하는 바도 다르다. 과거 MPA의 프론트엔드에서는 (사실 예전에는 프론트엔드라고 부르는 영역자체가 없었다) HTML 렌더링을 백엔드에서 했기 때문에, 단순한 AJAX 처리 후 DOM 조작을 이용한 극히 부분적 렌더링 정도만을 JavaScript로 처리했었다. 반면, 요즘 SPA에서는 대부분의 HTML 렌더링을 프론트엔드에서 처리한다. 그 결과, 프론트엔드가 백엔드의 역할을 상당히 많이 흡수하게 되었다. HTML 렌더링, 페이지 라우팅, 일부 비즈니스 로직, 어플리케이션 상태 저장 등등. 더구나 예전에는 프론트엔드가 굳이 알 필요 없었던 DB 모델링 스키마도 이제는 프론트엔드에서 당연히 알아야 하는 부분이 되어버렸다. 따라서 요즘 SPA 프론트엔드 프로젝트는 과거 MPA 프로젝트보다 규모가 훨씬 커졌고, 더 많은 사람들이 더 오랜기간 만들고 유지보수해야할 가능성이 높아졌다. 최근 타입스크립트의 가파른 성장도, SPA 프로젝트가 점점 많아지고 있는 현 트렌드와 무관하지 않다는 것이 내 생각이다.
생산성 부분에 있어서 내 결론적인 의견은, 프로젝트 규모에 따라 타입스크립트는 도움이 될 수도, 안 될 수도 있다는 것이다. 프론트엔드에서는 SPA의 코드 규모가 MPA보다 크기 때문에 정적 타이핑에 유리하며, 프로젝트가 더 대규모일수록, 그리고 더 장기간 유지보수할 가능성이 높아질 수록 타입스크립트의 장점이 부각된다.
더 읽기: What is the supposed productivity gain of dynamic typing? (Stack Exchange)
정적 타이핑과 안정성 얘기하면 항상 링크가 걸리는 글이 있다. The broken promise of static typing이라는 상당히 자극적인 제목을 가지고 있는 이 글에서는 정적/동적 타이핑 여부와 버그의 발생 빈도 사이에는 큰 관계가 없다는 결론을 상당히 설득력있는 근거와 함께 설명하고 있다.
타입스크립트 유저로서 이 글의 결론이 썩 마음에 들지는 않지만, 근거가 분명하기 때문에 충분히 동의할 수 있다. 타입은 런타임에서의 에러 발생 확률을 줄여주지만, 그것이 반드시 버그 감소로 귀결되지는 않는다. 마찬가지로 어플리케이션의 안정성으로 이어지지도 않는다. 물론 여전히 안정성을 이유로 들어 타입스크립트를 선택하는 사람이 있지만, 나는 생각이 다르다. 정말로 버그를 줄이고 싶다면 타이핑에 만족하지 말고, 테스트를 더 잘 짜는데 주력해야한다. 게다가 타입스크립트를 쓴다고해서 테스트를 안짜도 되는 것도 아니다.
그럼 타입스크립트를 쓸 이유가 없지 않느냐고 반문 할 수도 있는데, 정적 타이핑의 진짜 강점은 런타임 에러 감소에서 오는 것보다는 빠른 에러 발견과 해결에 있다. 같은 타입 에러라고 하더라도 런타임에 발견하는 것과 컴파일 타임에 발견하는 것은 개발자가 보고 판단하고 수정하는 데 걸리는 시간이 다르다. 컨텍스트 스위칭 비용도 없다. 리팩토링할 때 이 장점은 더욱 빛나는데, 리팩토링 이후 Find & Replace 해놓고 런타임에서 에러를 일일히 확인하지 않으면 안심할 수 없는, 결국 리팩토링의 영향이 닿는 모든 기능을 확인하기 전에는 확신하기 어려운 자바스크립트와는 달리, (타이핑을 잘했다는 가정하에) 타입스크립트는 컴파일 타임에 어느 부분이 깨지는지 빠짐없이 알려주기 때문에 리팩토링하기 편해지고, 리팩토링에 대해서 열린 마음을 갖게 된다. 결론적으로 타입 에러를 컴파일 타임에 발견해서 얻는 이득은 런타임에서의 안정성보다는 더 좋은 개발경험과 생산성, 그리고 그로 인해 얻게 될 더 좋은 코드에 가깝다.
더 읽기: The Shocking Secret About Static Types
현재까지 이 부분은 확실한 결론이 나지 않은 주제라는 생각이 든다. 확실히 함수형 프로그래밍을 할 때 주로 사용되는 lodash나 Ramda등의 라이브러리에서 제공하는 커링, 컴포지션 그리고 맵, 필터 등의 함수를 타입스크립트로 사용할 때, 들어가는 파라미터와 리턴 타입이 자동으로 타입 추론이 되지 않기 때문에 일일이 제네릭으로 타입을 넘겨줘야하는 불상사가 일어난다. 그리고 이건 확실히 번거롭다. 결론적으로 타입스크립트는 딱히 자바스크립트에 비해 함수형 프로그래밍하기가 더 좋은 언어는 아니다. 그렇지만 함수형 프로그래밍을 하기에 좋지 않은 언어인가에 대해서는 아직 토론할 여지가 남아있다. 어쨌든 타입스크립트는 자바스크립트의 확장이기 때문에 자바스크립트로 할 수 있는 것을 할 수 없는 것은 아니다. 문제가 되는 함수형 프로그래밍 부분의 번거로움을 감수한다면 함수형 프로그래밍의 장점과 타입스크립트의 장점을 동시에 가져갈 수 있으며, 그런 번거로움을 감수하기 싫다면 타입스크립트의 장점을 해당 부분에서만 버리면 된다. 그리고 타입스크립트는 그걸 허용할만큼 관대한 언어다.
만약 타입스크립트에서의 함수형 프로그래밍이 아쉽게 느껴질정도로 함수형 아이디어를 자주, 많이 사용하고 있다면, 다른 언어를 찾아보는 것도 합리적인 대안이 될 수 있다. 자바스크립트를 지원하는 함수형 프로그래밍 언어는 Elm, Reason, ClojureScript 등이 있다.
더 읽기: Functional Programming with TypeScript
타입스크립트의 사용자는 해마다 가파르게 상승하고 있다. 스택오버플로우에서 해마다 실시하는 Developer Survey 2018년 결과를 보면 타입스크립트는 Most Popular Technologies 12위에 랭크되어있다. GitHub Octoverse 2017에서는 11위에 올랐다. 그만큼 사용자가 많고 그만큼 커뮤니티가 크다. 커뮤니티가 크다는 것은 당연하지만, 내가 참고할 자료가 많다는 것, 디테일한 부분에서 언어 자체에 대한 지원이 잘되어있다는 걸 의미한다. 후술하겠지만 이런 타입스크립트의 커뮤니티가 크다는 것을 반증하는 가장 좋은 예가 바로 DefinitelyTyped)라는 레포지토리다. 여기에는 우리가 알고 있는 웬만한 메이저 라이브러리, 프레임워크의 타이핑이 거의 모두 정의되어 있다. 또한 2017년에 가장 많은 리뷰를 받은 GitHub 레포지토리이기도 하다.
d.ts
d.ts
는 단점이다. d.ts
그 자체가 단점이라기 보다는, d.ts
파일의 존재 여부에 따라 라이브러리 선택이 갈리는 것, 의존성에 @types/***
패키지를 추가해야하고 그 역시 유지관리가 필요하다는 것. 자바스크립트와 npm을 사용하는 강점을 상당히 까먹는 부분이다. 처음 타입스크립트를 시작한 사람에게는 큰 진입장벽이다. 일단 d.ts
파일은 일반 타입스크립트 파일에서 보기 힘든 문법을 가지고 있어서 몇 번 정의해본 사람이 아니면 쉽게 정의하기도 어렵다. DefinitelyTyped에 많은 라이브러리의 타이핑이 정의되어 있긴 하지만, 마이너한 라이브러리의 d.ts
파일은 종종 정의되어있지 않은 경우가 있어서 사용을 포기한 적도 있고 직접 만들어 사용한 적도 꽤 많은 만큼, 타입스크립트 유저라면 대부분이 맞닥뜨리는 상황이다.
d.ts
도 제공을 안하고 DefinitelyTyped에도 없는 패키지라면 선택은 두 가지다. 타입스크립트를 지원하는 다른 라이브러리를 찾아보거나, 직접 d.ts
파일을 만드는 것이다. 나는 반드시 사용하고 싶은 라이브러리가 있는 경우 무조건 d.ts
파일을 만들어두고 쓰는데, 처음엔 사용하는 API 몇 개 정도만 타이핑을 해두다가 차차 타이핑을 늘려나가는 방식을 선호한다. 그리고 어느정도 타이핑이 안정적이라는 생각이 들면 해당 라이브러리 레포지토리나 DefinitelyTyped에 PR을 날려볼 수도 있다. 이 문제를 정말 신경쓰기가 싫다면 noImplicitAny
옵션을 켜두면 된다.
때로는 이 때문에 리서치에 시간을 잡아먹기도 하는 만큼 가벼운 단점은 아니라고 생각한다. 하지만 타입스크립트 커뮤니티가 커지는 만큼 맞닥뜨릴만한 상황은 앞으로 점점 적어질 것이라고 예상한다.
나는 사실 지금까지 순수 자바스크립트를 사용해서 코딩한 경험이 많지 않다. 지금 다니는 회사에 들어오기 전까지는 쭉 커피스크립트만 썼었고(그걸 까는 글도 썼었고), 지금 회사에 들어온 이후로는 쭈욱 타입스크립트만 써왔다. 따라서 정당한 비교는 되지 않을지 모르겠지만, ES2015이후의 자바스크립트와 타입스크립트 중에 뭐가 더 개발자의 경험적인 측면에서 나은 언어였냐고 물어본다면 무조건 타입스크립트를 꼽을 것이다. 프로그래밍 언어가 제공하는 가치 중에서 개발자의 경험보다 중요한 것이 있을까? 그 면에서 타입스크립트는 개발자의 경험을 거의 만족에 가깝게 끌어올려주는 언어고, 좋은 언어라고 자신있게 말할 수 있다. 타입스크립트의 깃허브 레포지토리에는 Love for TypeScript라는 이슈가 있을 정도로 사용자의 만족도가 높은 언어다. 물론 당장 사용하지 않으면 큰일날 것도 아니고, 현재 자바스크립트로 충분히 만족하고 있다면 굳이 사용할 필요가 없는 것도 사실이다. 하지만 새로운 프로젝트에 한 번 쯤 시도해봐도 나쁘지 않을 정도로 가치있는 언어이기도 하다. 만약 타입스크립트에 대한 선입견이 있었다면, 이 글을 통해 선입견이 조금이라도 줄었기를 바란다.
아래 목록은 읽기를 추천하는 타입스크립트에 관한 글들이다. 이 글을 쓰면서 참고한 부분이 많기 때문에 이 글을 재밌게 보았다면 아래 글들도 반드시 보기를 추천한다.
]]>TypeScript는 React에서의 컴포넌트 타이핑을 강력하게 할 수 있도록 돕는다. 이번 글에서는 TypeScript를 이용한 React 컴포넌트 타이핑 방법을 다양하게 알아본다.
가장 기본적인 형태의 클래스 React Component 타입 정의다. React.Component
라는 클래스에 제네릭을 사용해서 React에서 사용하는 Props와 State의 타입 정의를 할 수 있다. Props와 State는 각각 생략이 가능하며, 생략할 경우 디폴트 값은 {}
타입이다.
다음은 Stateless Component(SFC)의 타입 정의다. SFC는 문자 그대로 State를 가지지 않기 때문에 State의 타입 정의를 할 필요가 없다. 따라서 React.StatelessComponent
라는 인터페이스에 Props를 표현하는 제네릭만 넣어주면 된다. 이 때 React.StatelessComponent
는 React.SFC
로 대체해도 된다. 취향에 맞게 사용하면 된다.
기본 JSX Element인 <button/>
에 몇가지 스타일을 정의해, 새로운 컴포넌트 <Button/>
을 만든다고 가정해보자. 이 경우 당연히 onClick
이나 type
등, 기본적으로 JSX Element가 내장하고 있는 프로퍼티를 사용해야 할 것이다. 정적 타입 검사를 하지 않는 JavaScript에서는 Spreading Operator(...
)를 이용해서 내려오는 props를 그대로 아래 컴포넌트에 복사해주면 되었다.
이 방법은 React 공식 문서에도 잘 나와있다. 하지만 TypeScript를 사용하면 이 방법을 그대로 쓸 수 없다.
그 이유는 우리가 정의한 ButtonProps
에 onClick
이 정의되어있지 않기 때문이다. 이 경우 그냥 ButtonProps
에 onClick
메소드를 추가로 정의해서 해결할 수도 있지만 모든 문제가 완전히 해결되지는 않는다. 만약 type
을 쓰고 싶다면? id
는? disabled
는? aria-label
은? 그 때마다 쓰고싶은 프로퍼티를 추가해주는 건 여간 귀찮은 일이 아닐 것이다.
Definitely Typed의 React 타입 정의(@types/react
)에는 여기에 필요한 모든 타입 정의가 포함되어 있다. 필요한 타입 정의가 버튼이라면, ButtonHTMLAttributes
를 사용하면 되고, 인풋이라면 InputHTMLAttributes
를 쓰면 된다. 결국 다음과 같이 수정할 수 있다.
위처럼 ButtonProps
는 내장된 타입정의를 extends
로 상속하고 추가로 필요한 프로퍼티를 정의하면 된다. 제네릭으로 넘겨준 HTMLButtonElement
는 onClick
등의 이벤트 핸들러로 넘어올 실제 DOM 객체의 타입이다.
하지만 이렇게 타입 정의를 상속하는 경우 내가 예상하지 못했던 프로퍼티가 넘어올 가능성도 생각해야 한다. 예를 들어, <Button/>
컴포넌트에는 style
프로퍼티를 넘길 수 있게 되었다. 이 때 color
와 style
프로퍼티를 동시에 넘기는 경우 color
는 무시당하게 될 것이다. 따라서, 이렇게 프로퍼티를 Spreading할 경우에는 컴포넌트 내부에서 프로퍼티를 다룰 때 세심한 주의를 기울여야 한다.
Omit
으로 이벤트 핸들러 다시 정의하기Omit
은 TypeScript 2.8 버전에 들어간 Conditional Types 기능을 조금 확장하여 사용하는 타입 헬퍼다. 특정 인터페이스에서 원치 않는 프로퍼티를 제외할 수 있으며 더 자세한 내용은 안도형님이 쓰신 글을 참조하자. 이 문단에서는 Omit
을 사용하여 기본 이벤트 핸들러의 타입을 내가 원하는대로 재정의하는 방법을 알아본다.
다음과 같은 케이스를 고려해보자.
위 코드에서 <Button/>
컴포넌트는 기본 onClick
이벤트를 재정의했다. 본래 onClick
이벤트는 이벤트 핸들러에 이벤트 객체를 전달하도록 되어있지만 위 코드에서는 중간에서 handleClick
으로 onClick
이벤트를 가로채고 프로퍼티에 저장되어 있는 값을 대신 전달했다. 이런 패턴은 React에서는 매우 흔히 쓰이는 패턴이다.
위 코드에 TypeScript를 적용하는 경우, 그리고 아까 언급한 Spreading Props를 적용하는 경우 문제가 생긴다. 기본 onClick
이벤트와 재정의한 onClick
이벤트의 타입이 서로 맞지 않기 때문이다.
앞서 말했던 대로, onClick
이벤트의 이벤트 핸들러로는 이벤트 객체가 넘어가야 하지만, 위에 새로 만든 <Button/>
에서는 value
를 넘기고 있고, 이 value
의 타입이 이벤트 객체의 타입과는 맞지 않으므로 타입 에러를 발생시킨다.
Omit
을 사용해서 이런 문제를 해결할 수 있다. 정확히는 Omit
으로 재정의할 이벤트의 타입 정의를 빼준 다음, 내가 원하는 대로 새로 타입 정의를 넣어주면 되는 것이다.
여기서는 이벤트 핸들러를 다시 정의하는 유즈케이스를 다루었지만, 이벤트 핸들러 뿐만이 아니라 사용자 본인이 원하는 대로 기본 프로퍼티를 다시 정의할 수 있다. 예를 들면 value
를 내가 원하는 데이터 형태만 받도록 변경할 수도 있다.
TypeScript에서 클래스나 함수에 namespace
를 합성하는 건 꽤 흔한 패턴이다. TypeScript 공식 문서에서도 이를 다루고 있다.
React의 컴포넌트는 크게 보아 두 가지, 클래스이거나 함수이기 때문에 마찬가지로 namespace
를 결합할 수 있다. 예를 들어 다음과 같은 패턴으로 결합할 수 있다.
이 패턴은 위처럼 해당 컴포넌트의 Props
나 Type
따위의 타입을 외부에서 사용해야할 때 유용하다. 이렇게 하면 컴포넌트에서 사용하는 각종 인터페이스나 타입들을 모아서 한 번에 하나의 엔트리로 모아서 내보낼 수 있다. 일일이 export
해주는 것과 코드량에 큰 차이는 없지만, import
를 일일이 해줄 필요가 없다는 점에서 편의성이 크게 개선된다. 특히 컴포넌트 별로 index.ts
파일을 가지는 패턴의 경우, 두 번 export
하는 불편함이 줄어든다.
enum
의 경우는 특히, 값과 타입이 동시에 정의되는 문법이므로, type
으로 타입을 내보내는 동시에, 컴포넌트 내부에 static
키워드를 이용해서 값도 내보내야 한다는 점에 유의하자.
다음은 함수형 컴포넌트의 결합방법이다.
TypeScript의 namespace
는 변수와의 합성을 지원하지 않는다. 따라서 함수 표현식을 사용할 수 없고, 함수 선언식을 사용하여 함수를 선언해야만 namespace
와의 합성을 할 수 있다. 보통은 React에서 컴포넌트를 만들 때는 함수 표현식을 많이 사용하기 마련인데, namespace
합성을 해야 할 때는 함수 선언식이 강제되므로 아쉬운 부분이라고 할 수 있다.
React에서는 기본적으로 컴포넌트 하나당 대부분 하나의 인터페이스(Props)가 따라가기 때문에 막상 namespace
를 쓰기로 마음 먹으면 대부분의 컴포넌트에 적용할 수 있게 된다.
개인적인 의견이지만, React에 TypeScript가 어울리지 않는다는 것은 이제 옛말인 것 같다. 아직은 부족한 점이 남아있지만, TypeScript 커뮤니티는 React를 강력하게 지원하고 있다는 사실 하나만큼은 분명해보인다. 얼마전에 릴리즈된 TypeScript 3.0 RC에서는 React의 defaultProps
의 지원이 추가되었다. 그 전에도 요즘 TypeScript 릴리즈 개선사항을 보면 거의 React에 대한 직접적인 지원이 하나 정도는 포함이 되어있다. 따라서 앞으로도 컴포넌트 타이핑이 더욱 개선될 수 있을거라 기대해 볼 수 있다.
현재의 TypeScript만으로도 PropTypes를 사용할 때보다는 더욱 강력한 타이핑을 해 볼 수 있다. 앞서 다룬 방법들만 적용해도 기본적인 컴포넌트 타이핑을 개선하는데 도움이 될 것이다. 또한 React에 대한 지원이 강화되고있기 때문에 앞으로는 그 차이가 더욱 벌어질 것이다. React에 TypeScript가 안 어울린다고 생각했던 개발자라면 지금부터라도 시도해보는 것은 어떨까?
]]>React에서 사용하는 onClick
, onChange
등을 통해서 핸들링할 수 있는 이벤트 객체는 브라우저 네이티브 이벤트 객체가 아니다. 브라우저 네이티브 이벤트 객체를 감싼 React 고유의 이벤트 객체인데, 이를 React에서는 SyntheticEvent라고 부른다. SyntheticEvent는 브라우저 네이티브 이벤트와 매우 비슷한 API를 갖고 있기 때문에 네이티브 이벤트로 착각하기가 쉽지만, 실제로는 여러가지 차이점을 가진다.
그중 가장 큰 차이점은 Event Pooling이다. SyntheticEvent는 이벤트 객체를 재활용하기 때문에, 해당 이벤트가 발생하고 그 이벤트 핸들러가 실행된 이후에는 SyntheticEvent의 모든 프로퍼티가 null
로 변한다.
위의 예제에서 버튼을 클릭해보면 이벤트 핸들러에서 setTimeout
을 실행하기 전의 이벤트 객체(Before)에는 프로퍼티에 데이터가 잘 들어가 있었지만, setTimeout
을 실행한 이후의 이벤트 객체(After)에서는 모든 프로퍼티가 null
로 변했다는걸 확인할 수 있다. 이걸 두고 Event Pooling이라고 부른다. 특정 이벤트의 이벤트 객체는 단 한 번, 해당 이벤트 핸들러가 실행되는 동안에만 사용할 수 있다. 성능적인 이유로 이렇게 한다고 설명되어있는데, 구체적으로 어떻게 성능이 나아지는지는 잘 모르겠다. 이벤트 객체를 하나만 사용하니 확실히 메모리는 적게 먹겠지만.
문제는 종종 이벤트 핸들러에서 비동기적인 동작을 수행해야하는 경우가 생긴다는 것이다. 비동기 동작을 수행한 이후에 이벤트 객체가 들고있는 정보가 필요한 경우 어떻게 해야할까? 가장 간단한 해결방법은 그냥 해당 이벤트 객체가 가지는 특정 데이터를 다른 변수에 복사해두고 비동기 동작의 수행이 끝난 뒤 사용하는 것이다. 코드를 몇 줄 더 써야되기는 해도 꽤 괜찮은 해결방법이다.
React에서는 이 해결책 말고도 한 가지 특별한 해결책을 제시한다. 바로 event.persist()
다.
위의 예제는 첫번째 예제에 event.persist()
를 적용한 예제다. 버튼을 눌러보면 좀 전과는 달리 이벤트 객체의 프로퍼티들이 제대로 보존되어있는 것을 확인할 수 있다. event.persist()
를 사용하면 비동기적인 동작을 수행한 이후에도 데이터가 남아있어서 이벤트 객체를 활용할 수 있다. 물론 이렇게 하면 아까도 언급했던 성능적인 이점이 줄어들 것이다.
예전에 Lodash의 debounce
와 throttle
함수를 소개하면서 말했듯이, onmousemove
, onscroll
등의 이벤트는 1초 안에도 수십번씩 발생할 수 있는 이벤트이기 때문에 Throttle을 걸어주거나 Debounce 처리해주어야 하는 경우가 종종 있다. 물론 React를 사용하더라도 해당되는 얘기다.
하지만 Lodash의 throttle
과 debounce
는 기본적으로 이벤트 핸들러의 동기적인 실행을 막는 메소드이기 때문에 React의 Synthetic 이벤트와 궁합이 잘 맞지 않는다. 다음 예제를 보자.
이 예제에서 <input/>
의 이벤트 핸들러는 Debounce 처리 되었다. 다시 말해 <input/>
에 무언가를 입력해도 바로 이벤트 핸들러가 실행되는게 아니라, 무언가를 입력한 이후에 500ms 동안 타이핑을 멈춰야 이벤트 핸들러가 실행된다. 입력창에 실제로 무언가를 입력해보고 이벤트를 실행시켜보면 에러가 발생하는 것을 볼 수 있다. 왜냐하면 이벤트 핸들러가 마지막 이벤트의 500ms 이후에 실행되기 때문이다. SyntheticEvent의 특성상 이미 모든 프로퍼티가 null
로 바뀐지 오래일 것이다. 결과적으로, Debounce된 이벤트 핸들러 내부에서 SyntheticEvent 객체에 접근하는 경우 항상 null
로 모든 프로퍼티가 도배된 객체만을 얻을 수 있다.
이 문제는 어떻게 해결할 수 있을까? 가장 쉽게 떠올릴 수 있는 방법은 위에서 잠깐 소개한 event.persist()
를 사용하는 것이다. 하지만 이 방법은 해결책이 되지 않는다. event.persist()
는 이벤트 핸들러 내부에서만 호출할 수 있다. 이벤트가 일어나기 전에 호출할 수 없다는 말이다. 결국은 이 케이스에서 사용할 수는 없는 방법이다.
해결방법은 간단하다. 이벤트 핸들러를 두 개로 쪼개면 된다. React의 SyntheticEvent를 동기적으로 핸들링할 Debounce되지 않은 이벤트 핸들러 하나, 필요한 데이터를 받아 무거운 동작을 수행할 Debounce된 이벤트 핸들러 하나.
위의 예제에서는 onChange
이벤트에 대한 핸들러로 handleChange
와 debouncedHandleChange
이렇게 두 가지를 사용하고 있다. handleChange
에서는 온전히 SyntheticEvent를 받은 뒤에 필요한 데이터를 이벤트 객체로부터 추출해서 Debounce된 debouncedHandleChange
로 넘겨주고 있다. 이제 debouncedHandleChange
에서 얻어진 데이터를 가지고 실질적인 동작을 수행하면 될 것이다.
이 때 주의할 점은, SyntheticEvent 객체를 그대로 Debounce된 이벤트 핸들러에 넘겨주면 안된다는 것이다. JavaScript에서 함수에 객체를 넘길 때는 항상 Call by reference이므로 그냥 SyntheticEvent 객체의 참조가 넘어가고 결국 다시 모든 프로퍼티가 null
인 이벤트 객체를 다시 보게 될 것이다. 귀찮더라도 필요한 프로퍼티를 적당히 추출해서 넘기는게 좋은 방법이다.
페이지 단위의 개발이 이루어지던 과거와 달리 요즘의 프론트엔드 개발은 주로 컴포넌트 단위로 이루어진다. 이 컴포넌트라는 개념은 사용하는 라이브러리나 프레임워크에 따라 구현 방식이 다르지만, 철학은 거의 동일하다. 원래 컴포넌트는 외부 상태의 영향을 받지않는 독립된 개체로서, 고립된 환경에서도 자신만의 스타일과 상태를 가질 수 있어야 한다. React의 컴포넌트 정의에 따르면, 컴포넌트가 UI를 독립적이고 재사용 가능한 단위로 분리하고 각 단위를 고립해서 생각할 수 있게 해준다고 설명한다.
이렇게 프론트엔드 개발의 패러다임이 컴포넌트로 넘어오면서 개발자들은 일종의 모순에 봉착했다. 개발은 컴포넌트 단위로 진행하지만 실제 개발환경은 항상 페이지 단위로 만들어진다는 점이다. 현재 개발하고 있는 서비스에서 사용하는 수 많은 버튼의 상태를 의존성과 환경변수가 걸려있는 페이지에서 일일이 코드를 변경해가며 테스트해야 했었고 여전히 그렇게 진행하고 있는 케이스도 있다. 이렇게 개발을 진행하게 되면 개발자는 온전히 뷰에 집중하기 어려워지고 컴포넌트의 의존성을 쉽사리 파악하기가 어려워진다. 컴포넌트를 진짜로 고립시키지 못하게 되는 것이다. 이는 당연히 컴포넌트의 재사용성을 감소시킨다.
Storybook은 이런 문제를 해결할 수 있는, 컴포넌트 단위의 개발 환경을 지원하는 도구다. 개발자가 뷰를 개발할 때 고립된 환경을 제공해서 관심사를 의존성과 환경으로부터 분리시켜 준다. 그리고 개발자는 비로소 뷰에 집중할 수 있게 되고, 외부 상태에 의존하지 않으면서 고립된 상태로 스스로를 표현하는 컴포넌트를 개발할 수 있게 된다.
이 글에서는 Storybook을 한 번도 사용해본 적 없는 사람을 대상으로 기본적인 사용법을 안내한다.
요즘 충분한 사용자를 갖추고 있는 여러가지 프론트엔드 프레임워크, 그러니까 React, Vue, Angular 등은 컴포넌트 단위의 개발을 기본으로 한다. Storybook은 React 뿐만 아니라 Vue도 지원하며, 최근에는 Angular도 지원하게 되었다. 이 글에서는 React로 예제를 진행할 것이다.
babel이나 webpack 관련 자잘한 설정을 하기에는 설명할 부분이 너무 많기도 하거니와 사실 글의 주제에도 맞지는 않으니, create-react-app을 통해 프로젝트 스캐폴딩을 먼저 진행한다.
1 | $ create-react-app storybook-playground |
스캐폴딩이 완료되었다면 본격적으로 Storybook을 설치한다. Storybook은 고유의 커맨드라인 인터페이스를 갖추고 있다. CLI를 설치하자.
1 | $ npm install -g @storybook/cli |
그리고 다음의 명령어를 사용하여 Storybook을 프로젝트 내에 설치하면 된다.
1 | $ getstorybook |
그러면 CLI가 알아서 적당히 필요한 의존성을 설치해주고, package.json
에도 Storybook 실행 및 빌드 관련 명령어가 추가된다. 이제 실행 스크립트만 사용하면 Storybook이 실행된다.
1 | $ npm run storybook |
프로젝트 내 src
폴더를 잘 살펴보면 stories
라는 폴더가 새로 생긴 걸 확인할 수 있다. 이 폴더가 바로 Storybook에 실제로 올라가는 컴포넌트들을 정의하는 파일들이 있는 폴더다. 그 안의 index.js
파일을 잘 살펴보면 아까 웹 페이지에서 봤던 페이지의 사이드바에 있었던 컴포넌트를 정의하는 코드가 보일 것이다.
보면 알겠지만, 문법은 컴포넌트를 storiesOf
함수로 감싼 뒤에, add
로 여러가지 스토리를 추가하는 형태로 사용한다. 여기에서 스토리는 어떠한 환경일 수도 있고, 컴포넌트의 상태일 수도 있다. 예제에서는 Button
컴포넌트는 주입받는 데이터, 그러니까 텍스트와 이모지를 기준으로 스토리를 나누었는데, 뷰가 다르게 보일 수 있는 상태라면 무엇이든 스토리로 써도 된다. 여기에 딱히 엄격한 규칙은 없다. 버튼이라면 일반적으로 사이즈나 컬러, 혹은 :hover
나 :disabled
같은 상태도 별도의 스토리로 써도 된다.
먼저 Storybook을 경험해보기 위해서 새롭게 아주 간단한 컴포넌트와 스토리를 작성해보자.
아주 간단한 Input
컴포넌트를 작성하고, index.js
파일에 관련된 스토리를 추가했다. 이제 저장해서 웹페이지를 확인해보면 새로운 컴포넌트가 생긴 걸 확인해볼 수 있을 것이다.
index.js
를 다시보면 Button의 onClick
이벤트에 action
이라는 함수가 바인딩 되어있는 걸 볼 수 있다. 다시 웹페이지로 돌아가서 버튼을 클릭해보면 아래에 있는 Action Logger에 버튼을 클릭할 때마다 로그가 올라오는 것을 확인할 수 있다. action
은 Storybook에서 사용하는 일종의 로깅용 함수다. 각종 이벤트와 함께 날아오는 데이터를 쉽게 볼 수 있다.
마침 우리가 만든 Input
컴포넌트는 이런 데이터를 확인하기에 아주 적합한 컴포넌트다. Input
의 onChange
이벤트에 action
을 걸어주자.
그리고 다시 웹으로 돌아가서 <input>
요소 안에 타이핑을 해보면 로그가 계속해서 찍히는 걸 확인해 볼 수 있다. action
은 이런 네이티브 이벤트 뿐만이 아니라 프로그래머가 직접 짠 이벤트에도 반응하도록 만들 수 있다.
주의할 점은, action
자체는 함수를 반환하는 함수이므로 이벤트에 바인딩해도 로그를 남기지 않는다는 것이다. 항상 문자열과 함께 함수를 실행시킨 결과를 이벤트에 바인딩하자.
Storybook에서 말하는 데코레이터는 현재 Stage 2에 있는 ECMA 데코레이터 스펙과 무관하다. Storybook을 계속해서 사용하다보면 width: 100%
인 컴포넌트들은 실제 페이지에 들어갈 때랑 다르게 보이거나, 컴포넌트가 좌상단에 너무 가까이 붙어있어서 전체적인 뷰를 보기에 좋지 않은 경우가 많다. 이 문제를 해결하기 위해서 가장 쉽게 사용할 수 있는 솔루션은 margin
이나 padding
값을 가지고 있는 래핑 컴포넌트를 만들어서 테스트할 컴포넌트를 감싸주는 것이다. 이 방법의 문제는 모든 스토리에 래핑 컴포넌트를 번거롭게 일일이 추가시켜줘야 한다는 것이다.
데코레이터는 이러한 불편함을 해결해주는 기능이다. Story를 추가하기 전에 addDecorator
라는 메소드를 통해 손쉽게 모든 스토리에 래핑 컴포넌트를 추가할 수 있다.
story
는 뒤에 나오는 스토리에서 선언한 컴포넌트를 렌더링하는 함수다. 적절한 래핑 컴포넌트 아래에서 story
함수를 실행시키는 식으로 선언해주면 된다.
addDecorator
는 스토리 사이에도 껴넣을 수 있는데, 그러면 addDecorator
의 뒤에 있는 스토리들만 그 데코레이터의 영향을 받는다.
실제 작업할 페이지의 배경색깔이 흰색인 경우 상관 없는데, 만약 회색이거나 혹은 뭔가 다른 색이라면 컴포넌트를 작성했을 때의 룩앤필이 페이지에 실제로 들어갔을 때와 좀 다르다고 느끼게 된다. 이런 문제를 개선하기 위해서 Storybook에 별도의 플러그인인 애드온(Add-on)을 설치할 수 있다. 위에서 다뤘던 action
도 기본적으로 설치되어있는 애드온이다.
배경 색깔을 지원하기 위한 유틸리티는 크게 두 가지가 있는데, 하나는 데코레이터 형태의 react-storybook-decorator-background이고, 다른 하나는 Storybook Addon Backgrounds이다. 둘 다 사용할 수 있지만 둘 중 후자를 추천한다. 전자는 HMR이 깨지는 버그를 가지고 있고 유지보수도 중지된 것으로 보인다. 후자는 Storybook 레포에 포함되어있는 공식 애드온이라 유지보수가 중단될 일은 없을 것으로 예상된다.
1 | $ npm install @storybook/addon-backgrounds --dev |
지금도 애드온을 설치하고 쉽게 까먹게 되는 것이, .storybook/addons.js
파일을 수정하는 것이다. Storybook에 설치하는 모든 애드온은 해당 파일에 등록되어야 한다.
보이는 것처럼 addon-actions
가 이미 등록되어있고 이 글에서 다루지 않은 addon-links
도 등록되어있다. 추가로 addon-backgrounds
를 등록해주면 사용 준비가 끝난다.
그리고 아까 데코레이터를 추가했던 것처럼 데코레이터를 추가하면 된다.
그러면 아래처럼 애드온 메뉴가 하나 추가가 되고, 컴포넌트에서 배경화면을 선택할 수 있다.
모든 컴포넌트에 이 배경화면 데코레이터를 적용하고 싶다면, .storybook/config.js
파일을 수정할 수도 있다.
이 글에서는 예제로 Background 애드온을 설치했지만 Storybook에는 유용한 애드온들이 많다. 그런 애드온들을 모아둔 페이지도 있으니 한 번 구경해보고 자신의 프로젝트에 맞는 애드온을 선택해 설치하면 더 좋을 것이다.
이렇게 앞에서 스토리를 쓰는 방법에 대해서 알아봤지만 이것만으로 실제 프로젝트에 도입하기는 충분치는 않다. 각 프로젝트 별로 사용하는 기술스택이나 요구사항이 서로 다를 수 있기 때문이다. 만약 모든 스토리들을 index.js
에 써야한다면, 제 아무리 Storybook이 좋은 툴이라고 하더라도 도입하기가 매우 망설여질 것이다. 여기서는 Storybook을 실제로 프로젝트에 도입하면서 필요한 작업들을 간단하게 알아본다.
편의상 지금까지는 스토리를 구분하지 않고 index.js
에만 작성했으나, 스토리는 index.js
파일에만 작성할 수 있는 것이 아니다. 아까 잠시 살펴본 .storybook/config.js
파일을 다시 보면, loadStories
라는 함수 안에 있는 require
구문을 확인할 수 있을 것이다. 즉, loadStories
에서 로드하는 모든 파일에 있는 스토리들을 추가한다. 기본적으로 stories/index.js
파일만 로드할 뿐이다.
흔히 React에서 폴더 구조를 짤 때 흔히 접근하는 방법 중에 컴포넌트 별로 폴더를 따로 만들고 그 컴포넌트에 필요한 여러가지 파일들을 만드는 방식이 있다. 아래처럼,
1 | ├── Input |
Storybook도 컴포넌트 하나하나 스토리가 별도로 필요하기 때문에 stories
폴더 하나에 관리했다가는 곧 stories
폴더가 열기도 싫어지는 불상사가 일어날 것이다. 따라서 스토리 파일도 컴포넌트 별로 폴더에 관리하는 것을 추천한다. 아래처럼,
1 | ├── Input |
이때 확장자는 stories.js
혹은 story.js
가 주로 사용되는 것 같다. 나는 보통 stories.js
를 사용한다.
이렇게 하려면 위에서 말했던 것처럼, loadStories
함수를 수정해주면 된다.
require.context는 webpack에서 지원하는 함수다. Storybook은 내부적으로 webpack을 사용하기 때문에 쓸 수 있다. 이렇게 하면 src/components
아래에 있는 폴더란 폴더는 다돌면서 *.stories.js
파일을 찾아 모두 로드시킨다.
앞서 말했듯 Storybook은 webpack을 사용하며, 설정이 Customize 가능하다. 이 말은 물론 TypeScript를 지원한다는 말이 된다. ts-loader를 쓰면 되니까. TypeScript 설정 방법은 공식 사이트에도 나와있다. 단지 .storybook/webpack.config.js
파일을 만들어서 아래와 같이 써주면 된다.
마찬가지로 Absolute Import 같은 것이 필요하다거나 하는, webpack의 설정을 건드려야 하는 상황이 있다면 다시 webpack.config.js를 수정하면 된다.
Storybook은 컴포넌트 단위로 쪼개서 개발할 수 있는 개발환경이지만, 결국은 그 자체로 컴포넌트들의 카탈로그 혹은 말 그대로 책자가 된다. 이렇게 모아놓은 컴포넌트들은 약간의 문서화를 더한다면1 그 자체로도 인터랙티브한 문서가 되고, 디자이너 혹은 기획자와 협업하는 도구로 사용할 수도 있게 된다. 물론 이렇게 협업하는 도구로 사용하고 싶다면 배포가 필요하다.
Storybook은 기본적으로 서버가 없고, 완전한 정적 애셋만으로 구동이 가능하므로 어디에나 쉽게 배포할 수 있다. 흔히 알고 있는 정적 사이트 호스팅 서비스, GitHub Pages 혹은 AWS S3에도 물론 배포할 수 있고, 가볍게 쓰기에 좋은 Heroku나 now에도 올려놓을 수 있다.
가장 간편한 건 공식 지원이 있는 GitHub Pages다. Storybook에서는 storybook-deployer라는 배포 툴을 지원한다. 하지만 오픈 소스가 아닌이상, GitHub Pages에 배포하기는 조금 꺼려지기 마련이다.
AWS S3도 쉬운 선택지 중에 하나지만, AWS 계정도 있어야되고 셋업하기 귀찮은 면도 있으므로 상대적으로 간단한 솔루션인데다 무료인 now를 사용해서 예제를 진행할 것이다. now도 다루자면 세부적인 설정이 많기 때문에 필요한 부분만 간단히 다루겠다.
먼저 now CLI를 설치한다. now는 별도의 바이너리가 있어서 Node.js 없어도 구동이 가능하지만 이 글을 보고있는 사람들은 모두 Node.js가 설치되어 있을테니, npm으로 설치하는 게 가장 간단하다.
1 | $ npm install -g now |
now가 설치되었으면 본격적으로 Storybook을 빌드한다. 빌드 명령어는 이미 package.json
에 포함되어있으니 그냥 실행만 하면 된다.
1 | $ npm run build-storybook |
빌드된 파일은 storybook-static
폴더에 모두 들어간다. 이제 빌드된 파일을 디플로이하면 된다.
1 | $ now deploy storybook-static |
회원가입이 아직 안되어있는 상태라면 회원가입이 필요할 것이다. 회원가입이 어렵지는 않으니 여기서 다루지는 않겠다. 디플로이가 완료되면 .now.sh
로 끝나는 URL이 표시되며, 실제 그 URL로 접속하면 Storybook이 잘 나오는 걸 볼 수 있다.
하지만 now로 배포하면 URL에 랜덤하게 문자열이 붙는데, 매번 배포할 때마다 주소가 바뀌기 때문에 다른 사람들과 공유하기가 상당히 번거로워진다. 이걸 고정하기 위해서 alias 설정을 할 수 있다.
위에서 이름이나 alias 필드 같은 경우는 프로젝트에 맞게 수정하면 된다. 그리고 아래처럼 명령어를 입력한다.
1 | $ now deploy storybook-static --local-config=now.json |
name이나 public 설정이 안먹는 경우가 있는데 만약 그런 경우를 만난다면 다음과 같이 강제로 지정해주면 된다. 깊게 안봐서 그런지 아직 왜 그런지 모르겠는데.. 이 부분 아는 분 댓글 좀..
1 | $ now deploy storybook-static --local-config=now.json --public --name={프로젝트명} |
마지막으로 alias 명령어도 입력해주면 고정된 주소로 배포할 수 있다.
1 | $ now alias --local-config=now.json |
성공한다면 {프로젝트명}.now.sh 라는
주소로 배포될 것이다. 이를 모아서 npm 스크립트로 만들어두면 간단하게 배포할 수 있다.
1 | $ npm run build-storybook && now deploy storybook-static --local-config=now.json && now alias --local-config=now.json |
해외에서 널리 알려진 것에 비해서 우리나라에서는 아직까지는 주위에서 사용사례가 흔치는 않은 것 같다. 사실 별로 어렵지도 않고, 이렇다 할 사용방법도 딱히 필요없는 Storybook을 굳이 이렇게 글까지 써가면서 설명한 건, 내가 Storybook을 도입 후 사용하면서 큰 만족감을 느꼈고 다른 개발자들도 쓰길 바라는 마음에서였다. 물론 나도 그렇게 오랫동안 사용하지는 않았지만. 나 같은 경우는 전혀 알지도 못하고 있다가 작년에 React Seoul에서 진겸님의 발표를 보고 뽕맞아서 시작하게 되었는데 그 발표영상과 슬라이드도 있으니 한 번 참고하시면 좋겟다.
2017년도 끝이 다가왔다. 나는 어떤 형태로든 매년 말마다 회고를 해왔다. 지금까지는 회고를 할 때마다 항상 내 스스로 “잘했다” 혹은 “많이 발전했다”라는 생각을 주로 했던 것 같다. 작년 회고를 다시 돌아봐도 그렇다. 글 전체에서 “이 정도면 꽤 잘했지”라는 뉘앙스가 느껴진다. 뭐 작년에 내가 잘했다는 게 사실이더라도, 올해는 그렇게 생각할 수가 없다. 내가 명백히 작년보다 게을러졌기 때문이다.
작년 10월에 블로그에 남겼던 것처럼 지금 다니고 있는 회사인 스튜디오 씨드로 이직을 했었고 물론 지금도 계속 일하고 있다. 처음 입사했을 때 예상했던 것과는 달리 일렉트론은 맛만 봤고, 내내 계속 새로운 프로젝트에 뛰어들면서 질릴 틈도 없이 재미있게 일했다. 연초에는 프로토파이를 유료로 팔기 위해서 결제연동 등을 진행했었는데 혼자서 처음부터 끝까지 결제연동을 구현한 건 처음이라 힙한 기술 쓴 것 하나 없지만 좋은 경험이 되었다. 중간에 AWS Lambda를 사용할 기회도 있었고 배포를 위해서 Docker와 ECS 등도 경험해보면서, 아직 잘 안다고 할 수준은 못되지만 그나마 조금은 AWS에 익숙해지기도 했다. 최근에는 ProtoPie와 연동되는 웹 기반의 새로운 서비스를 위해서 그동안 써보고 싶었던 건 다 쓰면서 프론트 개발에 집중하고 있다.
마침내 졸업이 다가오고 있다. 아직까지 마지막 학기 성적이 나온 건 아니라서 확정은 아니지만, 일단 학점을 제외한 요건은 모두 갖췄기 때문에 F가 없다면 졸업이다. 학업과 일을 병행한 올해 1년을 돌아볼 때, 사실 전혀 쉽지는 않았다는 생각이 든다. 1학기에는 취업계를 적용할 수 없어서 매주 멀고 먼 학교로 내려갔다 와야 했었다. 수업은 물론이고 과제, 졸업 작품까지 병행해야 했기 때문에 주말이고 평일이고 퇴근하면 코딩이랑 레포트 쓰기 바빴던 것 같다. 최악이었던 건, 지난 2년간 거의 켤 필요가 없었던 한글 오피스로 문서작업을 해야 했던 것이었다. 사실 졸업 작품도, 코딩하는 시간보다는 보고서 작성하는 시간이 훨씬 길었다. 2학기에는 취업계가 적용되었기 때문에 매주마다 학교로 갈 필요는 없었지만 퇴근 후 시간을 사용해야 했던 과제는 여전히 부담스러웠다. 사실 2학기에 신청한 학점이 더 많았기도 하고.. 이제 졸업 여부는 내년이 되어야 알 수가 있지만, 작년까지 자퇴를 진지하게 고려하고 있었다는 걸 상기해본다면 주변 분들의 많은 배려 덕분에 여기까지 올 수 있었던 것 같다.
블로그는 여전히 나에게 가장 중요한 매체다. 하지만 올 한 해 작성한 글은 이 글까지 포함해도 총 14개. 약 한 달에 한 번 포스팅한 꼴이다. 작년에 27개의 포스트를 작성했으니 약 반수 이하로 줄어든 셈이다. 줄어든 이유는, 위에도 써 둔 것처럼 학교라는 좋은 핑계 거리가 있긴 하지만서도 결국, 내가 게을렀다는 결론에 이를 수 밖에 없다. 지금 가지고 있는 글감도 꽤 많은 편인데 그걸 실행에 옮기지 못했다. 특히나 올해는 새로운 기술을 많이 접했고 좋은 경험도 얻어서 기술적으로는 넓게 알게 되었던 해이기 때문에 새로 배운 것들을 단단하게 내것으로 만들기 위해서 더욱 글을 많이 썼어야 했는데, 그러지 못했다. 그나마 꾸준히 했다는 것으로 위안을 삼아야 겠다. 내년 목표는 작년 수준으로 글 쓰는 횟수를 다시 회복하는 것이다.
연초에 solved-by-flexbox라는 flexbox를 활용하는 방법을 소개한 프로젝트를 번역했고, 5월 쯤에 “함수형 프로그래밍이 뭔가요?”라는 글을 번역했었으나 원저자가 글을 내려달라고 요청해서 글을 삭제해야만 했다. 원래 번역 허락을 구해놓고 기다리면서 번역을 하다가 답변이 오랫동안 안와서 올렸던 글인데, 저자에게서 삭제요청이 와서 삭제를 하게 됐다. 그리고 9월에 Project Guidelines 라는 흔히 회사에서 사용할 법한 가이드라인을 다루는 프로젝트를 번역했다. 전체적으로 작년에 비해서 번역양이 적었다. 내가 게을렀기도 했고, 번역하고 싶다는 욕구가 적어지기도 했다. 사실 번역은 꼭 해야 좋은 거라는 생각이 들지는 않는다.
여러가지 정말 작은 규모로 레포지토리도 만들고 npm 모듈도 배포하고 했으나 모두 큰 의미는 없는 것들이었다. 올해 그나마 가장 많이 커밋했던 건 역시 내가 작년에 만들었던 hexo 테마인 hexo-theme-overdose다. 버전 1.0.0을 배포하면서 나름대로 리팩토링도 하고 소스코드에도 변경사항이 꽤 많았지만, 그래도 이걸 바닥부터 시작해서 다 만들어서 배포까지 했었던 작년에 비해서는 아쉬운 수준이다.
다른 오픈소스에 공헌한 것도 별로 많지 않다. 번역을 제외한다면, 실질적으로 의미가 있었던 건 DefinitelyTyped의 multer에 PR 날린 것과, autoprefixer에 사소한 이슈를 제보했던 것, 그리고 최근에 날렸던 PR 두개 정도.
전체적으로 작년보다 잘했느냐 하면 그냥저냥 비슷한 수준인 것 같다. 뭐 꼭 오픈소스를 잘해야 뛰어난 개발자는 아니지만, 많은 도움을 받고 있는 만큼 공헌하는 것에도 노력을 기울여야 겠다.
작년까지는 한 번도 발표 경험이 없었지만 그래도 올해는, 뭔가 하기는 했다. 2월에 기술적인 내용이 아니긴 했으나 9XD에서 개발자라면 블로그라는 주제로, 4월에는 TypeScript의 Type System에 관해서, 그리고 6월에는 React Router v4에 대해서도 발표를 했었다. 발표 경험이 그다지 많지 않아서 잘못했던 기억이 많이 나고, 올해 정말 훌륭한 발표를 많이 봐서 그런지, 내가 발표했던 건 부끄럽기만 하다.
그냥 참가자로서 참가했던 것도 많았는데, 후기를 전혀 남기지 않아서 최근 걸 제외하면 기억이 잘 남지 않는다. 앞으로는 좀 필기하는 버릇을 들이려고 한다. 타이핑 말고 필기. 이 글에서 나온 것처럼 스케치 노트를 쓰는 게 내 스스로에게도, 물론 남에게 공유할 때에도 더 도움이 많이 될 거라는 생각이 들어서 이제부터 실천을 해볼 생각이다.
올해 나는 학교와 직장을 병행했기 때문에 분명, 작년에 비해 시간이 많이 부족했던 게 사실이다. 그럼에도 불구하고 내가 게을렀기 때문에 잃은 시간이 훨씬 많다. 시간이 남으면 유튜브나 넷플릭스 보기 바빴으며, 모바일 게임에 하루 3시간씩 투자하던 때도 있었고, 해야할 일이 많을 때도 애써 회피하면서 그냥 웹서핑이나 할 때가 많았다. 물론, 사람이 24시간, 365일 생산적인 일에만 몰두하는 건 불가능하고, 그럴 필요도 없다고 생각한다. 내 진짜 문제점은 사실 게으른 게 아니라, 뭘 하는데 있어서 계획이 없었고 모든 일과 결정을 충동적으로 했었다는 것이다. 내년은 좀 더 계획성있게 쓰는 시간을 컨트롤할 수 있는 해가 될 수 있도록, 개인적으로 회고를 좀 더 자주하고, 조금 더 많이 메모와 기록을 하면서, 구체적인 계획을 만들어 보고자 한다. 2018년 회고는 올해보다는 잘한 걸로 내용을 채울 수 있었으면 좋겠다.
]]>TypeScript 를 도입하기가 망설여지는 이유 중 하나는 매번 일일이 변수를 선언할 때마다 타입을 선언해야하고 필요한 타입을 정의해야하는 비용에 대한 걱정일 것이다. 필요한 타입이 있을 때 타입을 정의하는 일은 정적 타이핑 환경에서는 피할 수 없는 문제지만 타입 선언에 대한 압박은 타입 추론이 얼마나 잘 되느냐에 따라 그 정도가 매우 다르다.
전통적인 정적 타입 언어에서는 모든 변수나 파라미터, 프로퍼티 등의 타입을 모두 선언해야하기 때문에, 타입 추론 같은 기능이 필요없다. 하지만 TypeScript 에서는 모든 변수에 항상 타입을 선언할 필요가 없으므로 컴파일러가 타입 추론을 잘해주기만 해도 타입 선언 비용이 상당히 줄어든다.
타입 추론의 기본적인 동작을 간단한 예제를 통해서 알아보자.
1 | let bool = true; |
위의 예제에서 각 변수의 타입은 적절하게 추론되므로 다시 사용할 때 Type Safe 하게 사용할 수 있다. bool
의 타입은 boolean
으로 추론되므로 number
타입인 1
을 할당하려고 하면 에러가 발생한다. arr
변수는 number[]
, 즉 number
타입의 배열로 추론된다. 이 때 배열의 길이는 항상 고정이 아닌 것으로 추론한다. 즉, 투플이 아니다. 아래의 tuple
변수도 마찬가지로 배열로 추론된다. 만약 투플로 사용하고 싶다면 별 수 없이 타입 선언을 해주어야 한다. 다만 배열의 요소가 각각 boolean
과 number
이기에 최종적으로 추론되는 타입은 boolean
과 number
의 유니온 타입의 배열, 코드로 나타내면 (boolean | number)[]
이다.
여기에서 배열에서 사용된 요소들의 타입을 각각 추론하여 유니온 타입으로 만들어 내는 방식을 TypeScirpt 에서는 Best common type이라고 부른다.
Best common type 은 말 그대로 가장 일반적인 타입이다. 여러가지 자료형이 배열 내부에서 사용되고 있을 때, 그 여러가지 자료형을 포괄할 수 있는 가장 일반적인 자료형을 추론하는 것이다. 그 결과로 위의 예제에서는 true
와 1
을 포괄할 수 있는 자료형인, (boolean | number)
가 추론된 것이다.
Best common type 은 대부분의 경우에 유니온 타입으로 추론되지만 예외적인 케이스도 몇 가지 있다. 먼저 특정 클래스가 다른 클래스를 상속한 경우.
1 | class Parent { |
위의 예제에서 arr
변수의 타입은 Parent[]
로 추론된다. Parent
타입이 Child
타입을 포괄할 수 있는 타입이므로 그렇게 추론되는 것이다. 하지만 다음의 예제에서는 조금 다르다.
1 | class Parent { |
여기에서도 분명 Parent
타입은 Child1
타입과 Child2
타입을 포괄할 수 있는 타입이다. 따라서 마찬가지로 Parent[]
로 타입이 추론되는 것이 타당해 보이지만, 실제로는 다시 유니온 타입으로, (Child1 | Child2)[]
로 추론된다. 이유는 모르겠지만 아마 약간의 기술적인 어려움이 있는 것이 아닐까 예상된다.
함수가 상황에 따라 여러가지 타입의 값들을 리턴하는 경우에서도 리턴 값이 위와 동일한 룰을 따라 추론된다. 다만 상수를 리턴하면 그 값 자체가 타입으로 인식되는 리터럴 타입으로 추론된다.
TypeScript 의 타입 추론 기능은 매우 강력하지만 어쩔 수 없는 한계가 존재한다. 타입 단언은 TypeScript 컴파일러가 타입을 실제 런타임에 존재할 변수의 타입과 다르게 추론하거나 너무 보수적으로 추론하는 경우에 프로그래머가 수동으로 컴파일러한테 특정 변수에 대해 타입 힌트를 주는 것이다.
다음 예제를 살펴보자.
1 | class Character { |
이 코드는 컴파일 에러를 낸다. Character
클래스에는 fireBall
, attack
메소드가 선언조차 되어있지 않기 때문이다. 하지만 프로그래머 입장에서 바라보면 isWizard
라는 메소드를 통해 확실히 그 캐릭터가 Wizard
인스턴스라는 걸 보장할 수 있다면, if
블록 안에서는 당연히 fireBall
이라는 메소드를 사용할 수 있어야 한다.
이 때, 타입 단언으로 적절한 타입을 다시 선언해줄 수 있다.
1 | function battle(character: Character) { |
해당 변수가 실제로 Wizard 인스턴스가 아니더라도 as
키워드를 통해서 타입 단언을 해줄 수 있기 때문에, 타입 단언은 주의해서 사용해야 한다. 실제로도 as any
라는 치트키로 대부분의 컴파일 에러를 해결할 수 있다. 하지만 이런 키워드가 코드 베이스에 득시글 거릴 수록 TypeScript 를 사용해서 얻는 장점이 점차 사라져가기 때문에, 기왕 TypeScript 를 사용하기로 마음 먹었다면 as
와 any
는 가능한 적게 사용하는 것이 좋다. 물론, 아예 사용하지 않기는 어렵다.
타입 단언은 타입을 변경한다는 사실 때문에 타입 캐스팅과 비슷하게 느껴질 수 있다. 타입 단언이 타입 캐스팅이라고 불리지 않는 이유는 런타임에 영향을 미치지 않기 때문이다. 타입 캐스팅은 컴파일타임과 런타임에서 모두 타입을 변경시키지만 타입 단언은 오직 컴파일타임에서만 타입을 변경시킨다.
<Type>
vs as Type
타입 단언 문법은 <Type>
과 as Type
으로 두 종류다. 아래처럼 사용할 수 있다.
1 | (<Wizard>character).fireBall(); |
그냥 보기에는 <Type>
키워드가 좀 더 깔끔해보이지만, 대개 as Type
키워드가 추천된다. React 와 React 에서 빼놓을 수 없는 문법인 JSX 를 사용하는 경우 <Type>
키워드는 JSX 의 문법과 겹치기 때문에 불편한 면이 있다.
타입 가드는 타입 단언을 좀 더 깔끔하게 할 수 있도록 도와준다. 앞서 타입 단언에서 소개한 예제에서는 isWizard
라는 메소드로 해당 인스턴스가 해당 타입이라는 사실을 확정했다. 하지만 이건 런타임에서만 알 수 있는 사실이고 TypeScript 컴파일러는 알 수 없었다. 타입 가드는 이러한 런타임에서의 타입 체크를 컴파일러에게 알려주는 기능이다.
1 | class Character { |
이런식으로 런타임에서 실제 타입검사를 하는 메소드의 리턴타입으로 {variable} is {Type}
같은 문법을 사용해 선언해주면 된다.
1 | function battle(character: Character) { |
이제 별도의 타입 단언 문법 없이도 if
블록 안에서 character
가 Wizard
나 Warrior
로 잘 추론된다. 그리고 사실 instanceof
와 typeof
같은 오퍼레이터가 일종의 타입 가드이기도 하다.
1 | function doSomething(val: string | number) { |
별도의 선언이 없어도 추론이 잘 되는 경우에는 타입 선언을 하지 않고, 그렇지 않은 경우에만 선언하는 것이 편리하고 자연스럽다. 예를 들면,
1 | const regex: RegExp = new RegExp(/pattern/); |
위와 같은 상황에서 : RegExp
라는 코드는 불필요한 선언으로 간주하고 지운다. 하지만 다음처럼 변수 생성과 동시에 값을 할당하지 않는 경우,
1 | let regex = null; |
이런 상황에서는 regex
변수의 타입 추론이 제대로 되지 않으므로 타입을 같이 선언해준다.
사실 애자일이나 스크럼, 칸반, 이런 용어는 이 쪽 업계에 발만 담구고 있어도 귀에 딱지가 앉도록 들을 수 있다. 나도 원래부터 지나가는 트렌드에 관심이 많고, 한창 성장하고 싶은 시기기 때문에 관련 글도 나름 많이 읽었지만, 애자일이나 스크럼에 대해 설명해보라고 하면 한 마디도 제대로 꺼낼 수 없는, 뭐랄까 느낌적인 느낌의 무언가였다. 지난 2년동안 서서히 나에게 스며들고 있었던 이 개념에 대해서 좀 더 제대로 알고 싶었고, 애자일 문화에서 말하는 좋은 팀원이 되고 싶기도 했다. 그래서 애자일은 죽었다라는 소리도 나오는 마당에 다소 뒤처진 감이 없진 않지만, 이 책을 읽게 되었다.
이 책은 사실 나 같은 팀원보다는 관리자의 직책을 가진 사람을 대상독자로 쓰여진 책이라는 느낌이다. 물론 그렇다고 내가 참고할 부분이 전혀 없는 것은 아니었다. 사실 이 책에서 강조하는 키워드는 익히 우리가 가치있는 것으로 이미 받아들이고 있는 것들이다. 책임과 자율성, 수직보다는 수평적인 관계, 경쟁보다는 협력.. 같은 가치들. 이 책에서는 이런 가치들을 강조하는 것에서 끝나지 않고 실질적으로 어떻게 해야되는가를 주로 다루고 있다. 이를테면 자기조직화된 팀으로 만들기 위해서는 팀원 개개인에게 의사결정권을 부여해야 한다, 던가.
내가 이 책을 읽기 전에 기대했었던, 좋은 팀원이란 어떤 사람인가에 대해서도 다루고 있다. 책 내용 중 아마도 가장 나에게 의미가 있을만한 부분, ‘애자일 팀원이 가져야 할 팀 스피릿’을 발췌했다.
개인적으로는 위 항목을 내가 다 실천하고 있지는 않고 있다는 생각이다. 특히 나는 아직도 2,3번을 잘 못할 때가 많은 것 같다. 갈 길이 멀다는 걸 다시 느끼게 되었다. 내가 지금 일하고 있는 회사가 애자일을 잘 실천하고 있는 사례인가에 대해서는, 확실하지는 않지만, 지금까지 일하면서 동기가 무너지거나 일하는 게 고통스러웠던 적은 없으므로 베스트 프랙티스는 아닐지언정 나름대로 잘 실천하고 있는 조직이 아닐까 한다. “아 이랬던 게 애자일에서 프랙티스로 다루고 있던 부분이구나” 하는 것들도 있었고.
이 책에서 아쉬웠던 부분은, 애자일이 성공했던 사례와 애자일을 적용하기 적합한 상황에 대해서만 다루었다는 것이다. 물론 저자의 이력을 생각해보면 이해할 수 있지만, 그런 상황일 수록 더욱 한계에 주목하고 그것을 극복할 방법에 대해서 고민해보는 것도 필요할 것이다. 책에서도 종종 강조한 부분이지만, 애자일은 모든 상황에 있어서 만능이 아니며 상황에 맞게 적절히 소화해서 적용해야 한다. 하지만 실제로 책에서는 애자일의 한계가 어떤 것인지, 그리고 그걸 해결하기 위해 실제로 어떤 것들을 할 수 있는지에 대해서는 전혀 다루고 있지 않다. 물론 나에게 애자일의 한계가 무엇이냐고 물어본다면, 뭐 솔직히 나도 잘 모르겠다.
이 책을 다 읽고나니, 애자일은 단순한 소프트웨어 개발 방법론을 넘어, 조직문화까지도 만들어가는 것이라는 걸 알게 되었다. 이제 나는 애자일에 대해서 나만의 정의를 내렸다. 애자일은 “모든 조직 구성원들이 회사가 회사라기보다는 우리 팀이라고 느낄 수 있게 하는 것”이 아닐까?
]]>이전 글에서는 React를 배제한 채로 MobX만 사용했었고, Angular 혹은 jQuery와 연계해서 사용할 수도 있지만 MobX는 React랑 함께 사용되는 게 가장 일반적이다. 이번 글에서는 MobX와 React를 연계해서 사용하는 내용에 대해서 다룬다. 만약 이전 글을 읽지 않았다면, 먼저 이 글을 읽기 전에 읽기를 권한다.
MobX와 React를 같이쓰려면 물론, MobX와 React 모듈이 필요하고, 추가로 mobx-react라는 모듈을 설치하면 시작할 수 있다. 이 mobx-react라는 라이브러리는 API도 별로 없고 하는 일도 간단하다. React 컴포넌트를 Observable 데이터에 반응하도록 만들어주는 것이다.
observer
그 몇 안되는 API 중에 가장 자주 사용하게 될 것은 observer
이다. 코드를 통해 간단히 어떻게 동작하는지 알아보자.
결과를 확인해보면 이전 글에서 autorun을
사용했을 때 처럼 데이터가 변경될 때마다 뷰가 업데이트되고 있는 것을 확인할 수 있다. 실제로 MobX 문서의 설명에도, “mobx.autorun
에 컴포넌트의 렌더 메소드를 래핑해서 컴포넌트를 렌더링 할 때 사용되는 데이터가 변경되었을 때 다시 렌더링하게 만든다”고 설명하고 있다. 즉 간단히 observer
만 선언해주면 render
메소드가 autorun(render)
되는 것이다.
observer
를 사용해야 하는가?실제로 어플리케이션을 구현하다보면 observer
를 상당히 많이, 거의 모든 컴포넌트에 선언하게 되는데, 이게 렌더링 성능에 악영향을 끼치지 않을까 걱정이 되기도 한다. MobX에서는 observer
라는 함수 혹은 데코레이터를, Observable 데이터를 렌더하는 모든 컴포넌트에 선언하는 걸 추천한다. MobX에서 이를 최적화하기 때문이다. 만약 자식 컴포넌트에서 Observable 값을 표시하면서 자식 컴포넌트와 부모 컴포넌트가 동시에 observer
를 가지고 있다고 해도, 해당 데이터는 자식 컴포넌트에만 다시 렌더링된다. 그러니까 정말 필요한 컴포넌트만 반응(렌더)을 하도록 만든다고 한다.
Dumb Component에 observer
를 선언하게 되면 더 이상 Dumb하지 않지 않는가? 라는 의견도 있었는데, MobX의 창시자인 Michel Weststrate의 의견에 따르면 observer
를 선언하는 것이 Dumb와 Smart를 가르는 기준은 아니라고 한다. 이 스레드를 참조.
위의 예제는 클래스를 이용해 React 컴포넌트를 만들어서 편리한 데코레이터를 이용해서 감쌀 수 있었지만, 함수를 이용해 React 컴포넌트를 만들 경우에는, observer
를 함수로 사용해서 컴포넌트를 감싸야 한다.
observable
사용하기MobX를 React에 적용해서 좋은 점 중 하나는 더 이상 setState
를 사용하지 않고도 컴포넌트에 내부 상태를 만들고, 변이시킬 수 있다는 점이다. 스토어를 새로 만들 필요도 없다. 그냥 컴포넌트 안에 Observable 값을 하나 선언해주기만 하면 끝이다.
코드를 보면 컴포넌트 안에 선언된 count
라는 변수는 해당 예제에서 사실상 내부 상태로 활용되고 있는데, 순수 React만 사용해서 이런 걸 표현하려면 state
를 활용해야 했다. MobX를 이용하면 내부상태를 표현할 때도 그냥 일반적인 클래스 사용하듯 너무 쉽게 코드를 짤 수 있다.
Michel Weststrate도 setState
의 단점들과 그것을 MobX로 어떻게 해결했는지에 대해 글을 남겼으니 참고하시기 바란다.
<Provider>
와 inject
MobX가 Redux처럼 단일 스토어를 권장하지는 않지만, 특별히 중복을 좋아하는 게 아니라면 어떤 하나의 데이터를 관리할 스토어를 여러 개 만들 필요는 없을 것이다. 이는 결국 스토어를 특정 컨텍스트에서만 초기화하도록 만드는데, React의 컴포넌트 프로퍼티로 스토어를 일일히 내려주려면 이만저만 귀찮은 것이 아닐 것이다. mobx-react 패키지는 이를 위해서 간단한 의존성 주입을 위한 문법 설탕을 제공한다. 그것이 바로 <Provider>
와 inject
이다.
예제 코드를 보면 특별히 <Counter>
컴포넌트에 프로퍼티를 선언해 준 것도 아닌데 스토어가 this.props
에 들어와서 컴포넌트 내부에서 사용하고 있는 것을 볼 수 있다. <Counter>
상위에 <Provider>
를 선언해주고, inject
를 데코레이터로 사용해 간단히 의존성을 주입했다. <Provider>
는 반드시 최상단에 위치하거나 부모일 필요는 없지만 의존성 주입이 필요한 컴포넌트보다는 상위에 존재해야 한다(즉, 조상이어야 한다). 그리고 컴포넌트의 상단에 inject
로 필요한 의존성을 선언하기만 하면 된다.
주입될 수 있는 의존성은 반드시 MobX 스토어일 필요가 없다. JavaScript 기본 자료형이든 뭐든 넘길 수 있다. 다만 이 값은 더 이상 변경되지 않아야 하는 값이다. 값이 변경된다고 해서 새로운 값이 주입되지는 않는다.
또 하나의 주의할 점은 @inject
가 @observer
보다 상단에 위치해야만 한다는 것이다. 그러니까 @observer
가 보다 안쪽의 데코레이터가 되어야 한다. 이것은 데코레이터가 아닌 함수 문법의 경우도 동일하다.
observer
는 기본적으로 render
메소드에 autorun
을 씌운 것이다보니, 조금만 잘못짜도 render
가 비효율적으로 동작할 수 있게된다.
위의 예제에서는 1초가 지날때마다 seconds
값이 1번 변하고, tenth
값은 10번 변하므로 1초에 약 11번의 렌더링이 일어난다. 변하는 값을 뷰에 업데이트 해야하기 때문에 1초에 11번의 렌더는 사실 어쩔 수 없다. 하지만 렌더되는 영역은 좀 비효율적이다.
seconds
값이 변했을 때, 진짜 렌더가 필요한 영역은 오직 28번째 줄에 있는 코드뿐인데, <Timer>
컴포넌트에서 렌더하는 모든 영역이 다시 렌더되고 있다. 이는 tenth
값이 변했을 떄도 마찬가지다. tenth
값이 변했을 때 seconds
값이 표현되는 뷰가 다시 렌더링되고, seconds
값이 변했을 떄 tenth
값이 표현되는 뷰가 다시 렌더링되는 것이다.
MobX에서는 이런 케이스를 위해서 컴포넌트를 최대한 잘게 쪼개라고 가이드한다. 진짜 필요한 부분만 다시 렌더링하도록 만들라는 것이다. 이제 아래의 개선된 버전을 보자.
개선된 버전에서는 더 이상 <Timer>
컴포넌트의 렌더 메소드가 seconds
와 tenth
값의 영향을 받지 않는다. 따라서 <Timer>
는 딱 1회만 렌더되는 것이다.
물론, 여전히 <SecondsTimer>
와 <TenthTimer>
는 수시로 렌더링되지만, 더 이상 그 값의 변화가 다른 컴포넌트의 렌더링에 영향을 미치지 않는다. 따라서 각 컴포넌트의 렌더링 횟수도 통째로 렌더링 되었을 때보다는 적다.
여기에 있는 예제는 굉장히 축소된 예제로, 실제로 렌더 비용이 높은 컴포넌트가 있는데 이 컴포넌트가 <input>
값 등에 의해 수시로 렌더링이 다시 된다면 그게 얼마나 끔찍하게 비효율적인지는 다들 공감할 수 있을 것이다.
이런 상황은 MobX를 사용하지 않더라도 충분히 일어날 수 있는 일이지만 MobX에서는 render
가 암시적으로 일어나고, 일으키기가 매우 쉽기 때문에 MobX를 쓸 때 더 쉽게 범하는 실수가 된다.
이외에도 다른 성능 최적화 기법들이 있는데 이 링크를 참고하기 바란다.
mobx-react 자체가 그리 많은 API를 가지고 있지 않다보니 생각보다 어렵지 않게 MobX와 React를 같이 시작해볼 수 있을 것이다. 하지만 이것을 잘 사용하고, 디자인하는 것은 그냥 시작하는 것보다 훨씬 더 고차원의 어려움이 따른다. MobX는 꽤 자유로운 라이브러리다보니 위처럼 최적화에 실패한 케이스라던지, 잘못된 디자인의 스토어를 짜게 되는 경우도 생긴다. 설상가상으로 레퍼런스도 아직은 많이 부족한 편이다.
그렇기에 내가 실제로 지금 진행하는 프로젝트에서 처음 MobX를 접하고 일주일 정도는 스토어의 전체적인 디자인을 수시로 갈아엎었다. 지금은 대충 안정적으로 사용하고 있지만, 언제 또 갈아 엎게 될지 모르겠어서 불안하다. MobX에서도 이를 어느정도 인지하고 있는지, Best Practice 문서를 제공하고 있다. 실제 프로젝트에서 반드시 참고하기를 권한다.
]]>Simple, scalable state management
MobX는 프론트엔드를 위한 어플리케이션 상태 관리 라이브러리다. 주로 React와 함꼐 사용되며, 상태 관리 라이브러리라는 특성 때문에 종종 Redux와 비교된다. Redux가 함수형 아이디어를 적용했다면 MobX는 반응형 아이디어를 적용했다. MobX에서는 반응형 프로그래밍 패러다임에서 필수적으로 언급되는 옵저버(Observer) 패턴을 적극적으로 사용한다. 다만, MobX에서는 오직 상태(데이터)만 관찰 가능하며, 상태가 변경되었을 때 반응한다. 또한 상태를 변경시키는 것도 직접 해야한다. 한마디로 말해, MobX는 상태를 Observable하게 관리할 수 있도록 돕는 라이브러리다.
간단한 예제로 시작하자.
위의 예제에서는 observable
이라는 메소드를 통해 특정 객체를 Observable 객체로 만들었다. MobX에서 Observable 객체는, 그 객체의 프로퍼티 값이 변경되었을 때 그 객체를 관찰하고 있는 Observer에게 변경된 값을 통지한다. 이 경우에는 autorun
을 통해서 관찰하는 것이고, 그 내부의 익명함수가 바로 Observer에 해당한다. 따라서 이 코드를 동작시키면, 1초마다 person.age
가 1씩 증가하고, 이 값은 변경되었기 때문에 관찰하는 autorun
내부 코드에 의해서 자동으로 값이 업데이트된다. 이것을 MobX에서는 Reaction이라고 부른다.
위에서 짤막하게 언급한 몇몇 API와 더불어서 MobX의 개념을 알아보기 위해 중요한 API만 간단히 둘러본다. 이 글에서는 우선 React는 배제하고 MobX에서 기본적으로 제공하는 API만 다룬다. 또한 개념 위주로 알아보고 디테일한 건 설명하지 않겠다.
observable
은 넘겨받은 객체나 값 등을 Observable하게 만든다. 주로 객체를 넘기거나 클래스의 내부에서 데코레이터로 사용하게 된다.
위의 예제처럼 데코레이터 문법을 활용할 수 있다면 간단히 @observable
을 프로퍼티에 선언해줌으로써 Observable한 값으로 만들 수 있다. 예제를 실행시키면 나이 값은 1초마다 업데이트되지만 이름 값은 업데이트되지 않는 것을 확인해 볼 수 있다.
observable
을 사용하게 되면 프로퍼티를 읽거나 쓰는 것이 모두 값을 관찰하는 것과 연관된다. 즉, person.age
같은 코드로 값을 얻는 것은 옵저버를 등록하고, 실제로 그 값을 관찰(observe)하는 행위가 된다. 반대로 person.age = 10
같은 코드로 값을 할당하는 것은 그 값을 관찰하는 모든 옵저버들에게 통지(notify)하는 행위가 된다.
이게 가능한 이유는 observable
로 만든 객체가 실제로 프로퍼티가 아닌 JavaScript의 Getter/Setter)를 사용하기 때문이다.
computed
는 observable
값이 파생되는, 특별히 계산된 결과가 필요한 경우에 사용한다. MobX의 개발자인 Michel Weststrate는 MobX를 스프레드시트에 비유하는 걸 좋아하는데, Observable 값이 스프레드시트에 있는 데이터이고, Computed는 일종의 수식이라는 것이다. 다음 예제를 통해 살펴보자.
computed
는 이렇게 Observable 값에 대해서 적절한 계산이 필요할 때 사용하는 API다. Observable 값이 변경되면 그 값이 파생되어 Computed 값도 변경되고 이렇게 변경되는 값 역시 참조할 수 있다.
그런데 이 코드는 조금 이상해보인다. 결과에서도 볼 수 있듯이, @computed
를 쓰든 쓰지않든 별 차이없이 값은 양 쪽 모두 변경되고 있다. 그럼 computed
는 왜 존재하는 걸까?
이 이슈를 참고하면 해답을 구할 수 있다. 간단히 옮기면, 동작에는 큰 차이가 없지만 성능상 차이는 존재한다는 것이다. 위의 예제에서 getArea
를 여러 번, 극단적으로 100번 정도 호출하는 경우를 상상해보자. width
혹은 height
값이 변경되는 경우 getArea
가 100번 호출되고 그 안의 계산식 역시 100번 반복되고 이는 당연히 낭비다. 반대로 @computed
를 사용했다면 계산 결과 값은 캐싱된다. 따라서 100번의 반복은 없고 @computed
내부의 동작이 무거울 수록 성능상 이점이 더 커진다.
Computed는 기본적으로 JavaScript의 Getter에만 사용할 수 있으며, 따라서 추가 인자를 받을 수가 없다. 입력 인자가 this
로 제한되는 순수함수라고 생각하면 이해하기 편하다. (물론 순수함수는 아니다.)
autorun
은 Observable 값이 파생된다는 점은 computed
와 같지만, 용도는 전혀 다르다. 아까 위에서 Reaction에 대해서 언급한 적이 있을 것이다. 기본적으로 파생된 값을 가지고 View를 업데이트 한다거나 로그를 찍는다거나 하는, 사이드 이펙트를 내포하고 있는 동작을 MobX에서는 Reaction이라고 부른다. autorun
은 Reaction을 하는 방법 중 하나다. React와 같이 쓰는 경우에는 다른 API로 Reaction을 할 수 있으므로 autorun
을 사용할 일이 별로 없지만, React 없이 MobX만 사용하는 경우에는, autorun
이 필수적이다. 그래서 앞에 등장한 모든 예제에 autorun
이 등장했던 것이다.
autorun
에 넘긴 익명 함수는 참조하고 있는 Observable 값이 변할때마다 반복해서 실행된다. 위에서도 언급했지만 Observer에 해당하는 것이다.
action
은 Observable 값을 변경하는(Setter)에 사용하는 API다. 기본적으로 MobX에서는 Observable 값을 변경하는 메소드에는 action
을 달아줄 것을 권장하지만 쓰지 않아도 정상적으로 동작한다. 위의 예제들에서도 action
없이 값을 계속해서 업데이트해도 동작에는 문제가 없지 않는가?
그렇다면 자연스럽게 action
은 또 무슨 이유로 사용해야 되는지 의문이 들 것이다. computed
와 마찬가지로, action
을 사용하는 이유도 성능이다. 간단한 예제를 통해 알아보자.
위의 예제에서는 incWidthAndHeight
라는, 내부의 width
값과 height
값을 동시에 1씩 올리는 메소드를 만들었다. 그리고 내부 동작은 완벽히 같지만 그 함수의 action
버전도 만들었다. 1초에 한 번씩 메소드를 실행시키면서 Reaction 함수(autorun
으로 감싸진 함수)가 각각 몇 번씩 실행되는지 볼 수 있게끔 했다. Result 탭에서 결과를 보면 알겠지만, getArea()
의 출력결과는 같은 동시에, 각각의 Reaction 함수의 실행 횟수는 2배의 차이가 나는 것을 확인할 수 있다.
이는 action
없이 Observable 값을 업데이트 했을 떄는, width
와 height
각각의 값이 업데이트 되는 시점에서 Reaction 함수가 호출되지만, action
을 사용했을 때는 그 두 값이 모두 업데이트 된 뒤에야, Reaction 함수가 호출된다는 차이에서 기인한다. 이렇게 Observable 값을 업데이트하는 동작을 묶어 일괄 실행하고 모든 동작이 끝났을 때 통지하는 것을 MobX에서는 트랜잭션(Transaction)이라고 부른다. 개념은 조금 다르지만 데이터베이스의 그 용어와 같다.
이처럼 MobX에서는 트랜잭션을 사용하는 것과 사용하지 않는 것은 큰 성능차이를 불러일으킬 수 있기 때문에, 항상 Observable 값을 변경할 때마다 action
을 강제할 수도 있다. 바로 useStrict
모드를 사용하는 것이다. useStrict
모드를 사용하면 Observable 값 변화에 반드시 action
을 사용해야 한다.
UPDATE: MobX 버전 4 이상에서는
useStrict
API가 사라지고configure
API로 대체되었다. 링크 참조.
위의 다이어그램은 위에서 설명한 API의 생명 주기를 요약해서 나타내고 있다. Action으로 인한 상태(State) 변경은 곧, 계산 결과(Computed)를 업데이트한다. 계산 결과가 업데이트되면 곧 이를 구독하는 반응(Reaction)을 야기한다.
겨우 1주일 정도 써봤지만, 나는 MobX에 대해서, Redux에 비해서도 진입장벽이 낮고 쉽게 코드를 짜고 금방 사용할 수 있는데 비해서 제대로 활용하려면 애매하고 헷갈리는 부분이 많아 쉽지가 않다는 것을 느꼈다. 단순히 Best Practice를 찾은 뒤에 Copy & Paste하는 방식만으로는 이해도 잘 안되고 한계가 금방 온다. 따라서 처음부터 라이브러리에 기본 개념에 대해서 숙지하고 넘어가는 것이 좋다. 그래서 이 글을 쓰고 있는 것이기도 하고.
MobX는 얼핏 보기에 뭔가 성능상 문제가 생길 것 같아보인다. 원래부터 우려가 많았는지, MobX에서는 이를 React와 비교하면서 굉장히 최적화가 잘 되어있다고 변호한다.
React와 MobX는 모두 어플리케이션 개발에 있어서 겪는 일반적인 문제에 대해 굉장히 최적화된 특별한 해법을 제시합니다. React는 가상 DOM을 이용해서 DOM 변화를 감소시키는 최적의 렌더링 메커니즘을 제공합니다. MobX는 엄격하게 필요할 때만 업데이트 되는 반응적인 가상 의존성 상태 그래프를 이용해서 어플리케이션 상태를 React 컴포넌트와 동기화하는 최적의 메커니즘을 제공합니다.
진실은 나도 모르지만, 아무튼 상당한 자신감이 있는 모양이다. 실제로도 이미 사용하고 있는 프로젝트도 꽤 있고 검증은 확실히 된 상황이다. 물론 Redux에 비해서는 조금 갈 길이 멀어보이지만 일단은 응원하는 입장이다. 현재까지 단점이라고는 로고가 상당히 못 생겼다는 것과 레퍼런스가 별로 없다는 것 외에는 딱히 없었다.
다음 글: MobX with React
]]>이전 글 - TypeScript: 제네릭(Generic)
JavaScript를 쓰면서 느끼게 되는 아쉬운 점 하나는, Enum(Enumeration, 열거형)의 부재다. 사실 JavaScript 같은 동적 타입 언어에서 Enum이 없는 게 당연하지만, 유한하면서도 여러가지의 상태를 가질 수 있는 변수를 정의하고 싶은 경우, boolean
으로는 해결할 수가 없기 때문에 종종 Enum의 필요성을 느끼기도 한다.
TypeScript에서는 enum
이라는 키워드로 Enum을 사용하는 것이 가능하다. 문법도 간단하다.
1 | enum Color { |
선언을 한 뒤에,
1 | function setColor(color: Color) { |
이렇게 사용하면 된다. Enum은 하나의 타입이기도 하지만, JavaScript 런타임에서도 사용되는 하나의 변수로 볼 수도 있다. JavaScript 런타임에서도 사용된다는 말은, 클래스처럼 컴파일 결과물이 존재한다는 것이다. 예를 들어, 위에서 선언한 Color
타입은 아래와 같이 컴파일 된다.
1 | var Color; |
컴파일 결과물이라 그런지 가독성이 나쁘다. 결과적으로 위의 코드애서 Color
는 다음과 같은 객체가 된다고 보면 된다.
1 | var Color = { |
enum
키워드는 기본적으로 리버스 매핑(reverse mapping)을 지원한다. 리버스 매핑이란 키로 값을 얻을 수 있을 뿐만 아니라, 값으로도 키를 얻을 수 있는 방식을 말한다. 그렇기 때문에 위와 같은 형태로 객체가 생성된 것이다.
앞에서 다룬대로, enum
으로 만들어진 변수에는 내부적으로 값이 할당된다. 별도의 명시가 없다면 값은 0부터 시작해서 1씩 증가하는 형태로 할당된다. C언어에서 보았던 Enum과 비슷한 형태다. 반대로, 별도의 명시를 해준다면 원하는 값으로 초기화 할 수 있다.
1 | enum Color { |
간단한 연산자를 사용한 표현식으로도 초기화할 수 있다. 이 때 특이한 점은, 초기화할 용도로 사용된 표현식은 런타임이 아닌 컴파일 타임에 평가된다는 것이다. 위의 예제에서 BLUE
에는 30이라는 값이 할당될 텐데, 이것이 컴파일 타임에 정해진다는 것이다. 물론 enum
내부에서 알 수 없는 외부 변수를 사용하는 경우에는 그냥 온전히 표현식이 유지된다.
또한, TypeScript Handbook에도 나와있는 것처럼 Bitwise 연산자를 이용해서 일종의 플래그처럼 활용할 수도 있다. 하지만, 대개의 경우 숫자 값은 의미가 없으므로, DB에 값을 삽입할 때를 위해서 의미가 있는 문자열 값으로 초기화하고 싶은 경우도 있다. 그럴 때는 아래처럼 그냥 문자열 값을 선언해주면 된다.
1 | enum Color { |
사실, 이 기능은 TypeScript 2.4에서 새로 추가된 기능으로 원래는 다음과 같이 조금 찜찜하게 초기화해야만 했다.
1 | // TypeScript 2.3 이하 |
<any>
가 들어간 걸 빼면 큰 차이는 없어보이지만, 두 가지 방법에는 큰 차이점이 있다. 바로 리버스 매핑의 유무. 2.4 버전 이상에서 문자열로 Enum 값을 초기화 하는 경우, 리버스 먜핑을 지원하지 않는다. 즉,
1 | console.log(Color[Color.RED]); // undefined |
이런 결과가 나타난다는 것이다.
공식적인 이유는 찾지 못했는데, 아마 리버스 매핑을 지원한다면 Key와 Value가 충돌하는 문제를 회피하기 어려워서가 아닐까 한다. 2.3 버전에서는 리버스 매핑을 지원하는 것이 아니라, 어디까지나 문제를 우회해서 선언한 것이므로 충돌 문제는 개발자가 스스로 피해야 한다.
const enum
enum
키워드는 앞에 const
와 함께 사용할 수도 있다. const
와 함께 사용할 경우, Enum은 컴파일 결과물을 가지지 않는다. 예를 들면,
1 | const enum Color { |
이러한 코드가 있다고 할 때 컴파일 결과는,
1 | var red = "red" /* RED */; |
이렇게 나타난다는 것이다. 즉, 그냥 Enum에서 정의한 변수들을 활용하는 경우 기존에 정의했던 값을 사용하기 때문에 문제가 없지만, 런타임에서 Color
에 접근해 뭔가 할 수는 없게 된다. 이로 인해서 기존의 enum
과는 달리 몇 가지 중요한 차이점이 나타나게 된다.
원래 enum
으로 정의한 모든 변수들은 기본적으로 읽기 전용이지만, 간단한 트릭을 이용하면 런타임에서 쉽게 값을 변경할 수 있었다.
1 | (Color as any).RED = 'yellow'; |
물론, 실제로 이렇게 위험한 코드를 짜는 경우는 거의 없겠지만. 아무튼 const enum
을 사용한다면 이 문제를 근본적으로 회피할 수 있다. 애초에 런타임에 생기는 객체가 없기 때문에 수정할 대상 자체가 사라지는 것이다. 물론 이 경우에는 위 코드는 컴파일도 불가능하다. 이것이 첫 번째 차이점이다.
두 번째로, 리버스 매핑을 지원하지 않는다. 마찬가지로 리버스 매핑 문법 자체가 컴파일 에러가 난다. 애초에 리버스 매핑이 런타임에 존재하는 객체를 이용하는 것이므로 안되는 것이 당연한 것이지만.
세 번째로, 컴파일 타임에 평가할 수 없는 표현식으로 값을 할당할 수도 없게 된다. 이것을 간단히 예제로 설명하면,
1 | const green = 30; |
위 코드에서는 GREEN
을 선언할 때 컴파일 에러가 난다. enum
키워드의 바깥에서 정의한 변수가 표현식으로 사용되면 컴파일 에러가 나는 것이다. 물론, RED
처럼 enum
키워드 내부에서 선언한 변수들은 잘 평가된다. 이 특징도 어찌보면 당연한 건데, 컴파일 타임에 평가하지 못한 표현식은 런타임에 평가할 수 밖에 없고, 그렇게 되면 항상 같은 값임을 보장할 수가 없기 때문이다.
이전 글: AWS Lambda: 시작하기
Lambda를 처음 쓰기 시작했다면 코드를 웹 콘솔에서 직접 수정하거나, ZIP 파일을 통해 배포하고 관리해야하는 것이 불편하게 느껴질 수 있을 것이다. AWS SDK가 제공되고 있으니 이런 작업들을 자동화하는 도구를 직접 구현할 수도 있겠지만, 이미 잘 구현되어 사용할 수 있는 도구들이 있다. Apex와 Serverless Framework가 바로 그것이다.1
이번 글에서는 그 중 Apex에 대해서 다룬다. 사실 사용자 수는 Serverless Framework쪽이 더 많은 것으로 보이지만 필자는 아직 사용해 본 적이 없어서 비교 평가가 어렵다. 대신 이 질답을 참조하시길 바란다.
먼저, AWS CLI가 있는지 확인해보고, 없다면 설치하자.
Apex는 Go로 구현되어있으며 curl
을 통해 내려받고 설치할 수 있다.
1 | $ curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh |
사용하기 위해서는 당연히 AWS Credential이 필요하다. Credential은 여러가지 방법으로 전달할 수 있는데 여기서는 ~/.aws
경로를 이용할 것이다. AWS CLI를 이용해서 새로운 프로필을 만든다.
1 | $ aws configure --profile [name] |
참고로, Access Key ID와 Secret Access Key는 AWS IAM Console에서 얻을 수 있다. 만약 유저가 없다면 새로 등록하자. Apex에서 필요로 하는 IAM Policy는 여기를 참조하면 된다.
Credential 설정까지 마쳤으면 이제 Apex를 사용할 프로젝트의 디렉토리로 이동하자. 그 뒤에, AWS_PROFILE
환경 변수를 지정하고, apex init
을 사용해서 프로젝트를 시작할 수 있다.
1 | $ export AWS_PROFILE=[name] |
프로젝트 명까지 입력하면 이제 Apex를 사용해 디플로이 및 테스트를 할 환경이 갖춰진 것이다. 시험삼아 apex deploy
를 입력하면 함수가 배포되는 것을 확인할 수 있다.
Apex로 생성한 프로젝트의 대략적인 골격은 아래와 같다.
1 | ├─ functions |
apex init
으로 project.json
파일과 functions
디렉토리가 생겼다. project.json
은 프로젝트의 여러가지 설정 및 변수가 담기는 파일이며, functions
는 실제 배포될 소스코드 파일이 위치할 디렉토리다.
functions
디렉토리 아래에 위치하는 여러 디렉토리는 실제 배포되는 각각의 Lambda 함수다. 말하자면, 위의 디렉토리 구조에서 function-a
와 function-b
는 각각 하나의 함수이고, 그 아래에 위치한 index.js
가 반영될 소스파일이다.
Apex는 몇 가지 유용한 명령어를 제공하는데, 실제로 프로젝트를 운영하면서 자주 사용하게 될 명령어는 많지 않다. 다음은 자주 사용하는 명령어에 대한 대략적인 설명이다.
apex build
를 실행한 결과물을 디플로이한다.console.log
등으로 찍은 로그도 볼 수 있다.사실 apex build
빼고 나머지 세 가지의 명령어를 주로 사용하게 된다. 아까 apex deploy
를 했었다면 이미 hello
라는 이름의 함수가 배포되어 있을테니 apex invoke
명령어를 사용해보자.
1 | $ apex invoke hello |
이번엔 apex logs
를 사용해서 어떤 로그가 찍히는 지 확인해보자.
1 | $ apex logs hello |
아마도 크게 위와 다르지 않은 결과를 얻었을 것이다. 실제로 위의 명령어들만 잘 사용해도 기본적인 운영은 할 수 있다.
실제 Node.js 프로젝트에서 외부 라이브러리 없이 단일 파일만으로 원하는 비즈니스 로직을 구성할 수 있는 가능성은 실질적으로 없다. 따라서 필연적으로 npm 패키지를 설치하고 참조하게 된다. 이 때, package.json
파일과 node_modules
디렉토리는 대체 어디에 위치해야 하는가라는 고민이 생긴다. 뿐만 아니라, 여러 함수에서 공용으로 사용하는 소스코드가 있고, DRY 원칙을 깨고 싶지 않다면 이 소스코드는 어느 함수의 아래에 위치해야 하는가? 아까 잠깐 보았던 프로젝트의 골격을 다시 한 번 보자.
1 | │ ├─ function-a |
이상적인 구조는 공용 코드 및 package.json
과 node_modules
가 특정 함수 아래가 아니라 최상단 경로에 위치하는 것이다. 하지만 node_modules
는 실제 배포될 소스코드에 포함되어야 하기 때문에 node_modules
는 당연히 각각의 함수, 말하자면 function-a
아래에 위치할 수 밖에 없다고 생각했다.
그런데 문득, 공용 소스코드와 라이브러리, 그리고 거기에 의존성을 가지는 여러 개의 엔트리 파일이라는 구조. 이거 webpack에서 이미 많이 해본 것 아닌가? 하는 생각이 들었다. webpack을 사용하기로 마음을 먹고, “이 신박한 생각을 아무도 하지 못했겠지! 혹시 난 천재..?” 라는 생각을 하면서 찾아 본 결과, Apex에서 예제까지 제공하는 것을 보고 자괴감에 빠졌다(…)
각설하고, Apex로 제대로 Lambda를 관리하기로 마음먹었다면 번들링은 선택이 아닌 필수라고 생각한다. 따라서 여기에서는 webpack을 이용해 프로젝트를 구성해 볼 것이다. babel을 이용한 예제는 위에 링크했으므로 여기에서는 TypeScript과 함께 번들링하는 예제를 다루겠다. (사실 거의 비슷하다.)
번들링을 하기로 결정했으므로 이제 개발하는 원본 소스코드와 배포되는 소스코드가 달라지게 될 것이다. 따라서 프로젝트 구조에 약간의 변경을 주어야 한다.
1 | ├─ functions |
각각의 함수 아래에 src
라는 이름의 디렉토리를 추가했고. 원본 파일을 새로 만든 디렉토리 아래에 위치시킨다. 기왕하는 김에 TypeScript 파일으로 변경해준다. 이제 webpack을 통해 이 파일을 빌드하고 그 결과물은 lib
디렉토리 아래에 들어갈 것이다. 다만 여기서 lib
디렉토리를 미리 만들어 줄 필요는 없다. webpack이 자동으로 만들어 줄 것이다.
함수 디렉토리 아래에 index.js
파일이 사라지면 Apex는 이를 파일이 없는 함수로 인식해 빌드할 때 오류를 뱉어낸다. 현재 디렉토리가 변경되었으므로 오류가 발생할 것이다. 따라서 function.json
파일을 함수 바로 아래에 추가한다.
Apex는 몇 가지 Task에 대한 Hook을 제공한다. 지원하는 Task 중에는 빌드 전에 실행되는 Hook도 있는데, apex deploy
를 실행하면 알아서 빌드가 진행되므로 디플로이 시 번들링을 자동화 할 수 있다. Hook은 project.json
을 수정하면 되는데, 아래와 같이 설정한다.
"hooks"
아래에 들어간 것들이 바로 Hook 설정이다. "build"
는 ZIP 파일 생성 전에 실행되는 Hook이며, "clean"
은 디플로이가 모두 끝난 시점에 실행되는 Hook이다. 보면 Hook이 실행되는 경로가 함수 디렉토리의 바로 아래라는 것을 짐작할 수 있을 것이다. "handler"
에 대해서는 조금 아래에서 설명한다.
아직 webpack.config.js
파일을 만들지 않았다. 아래처럼 파일을 만들자. 참고로 여기서 사용될 npm 모듈들(webpack 및 loader들)은 별도로 설치하라고 언급하지 않을 것이다.
중요한 부분만 설명하겠다. 일반적으로 webpack을 사용하는 이유와는 달리 Node.js에서 돌아갈 JS파일을 빌드해야하므로, Target을 "node"
로 잡아준다. 그리고 생소한 "commonjs2"
가 눈에 띄는데, 이는 Node.js에서 사용하는 module.exports
를 포함하겠다는 것이다. 또한, webpack이 실행되는 위치가 각각의 함수 디렉토리의 아래 경로이므로 이 파일의 위치가 아닌 프로세스가 실행된 위치를 리턴하는 process.cwd()
를 사용한다.
AWS SDK를 사용하는 경우, npm의 aws-sdk 모듈을 설치하지 않아도 Lambda에서는 기본적으로 런타임에 포함되어있으므로 externals
에 적어주면 된다.
Uglify는 하면 좋고, 굳이 하지 않아도 된다. 필자는 브라우저에서 실행되는 파일이 아니므로 하지 않는다.
이제 tsconfig.json
파일을 추가하고, tslint.json
파일도 적당한 룰을 붙여 추가하자.
완성된 프로젝트의 구조는 아래와 같아야 한다.
1 | ├─ functions |
이제 아까 확장자만 바꾸었던 TypeScript 파일을 수정해 줄 차례다.
원래 생성했었던 index.js
파일과 비교해보자. 함수를 내보내는 방식이 default
를 이용하는 것으로 바뀐 것을 확인할 수 있을 것이다. 이게 바로 위에 project.json
파일에서 "handler"
를 "lib.default"
라고 쓴 것에 해당한다. lib
디렉토리의 index.js
파일에서 내보내는 default
함수를 바라보게 한 것이다.
마침내 빌드가 잘 되는지 테스트를 해볼 시간이다.
1 | $ apex build hello > hello.zip |
hello.zip
파일을 압축 풀고, lib
디렉토리의 index.js
파일을 확인해보면 빌드가 된 결과물을 확인할 수 있을 것이다. 잘 빌드가 되었다면 디플로이도 해보고 apex invoke
명령어를 이용해 테스트도 해보자.
1 | $ apex deploy hello |
여기까지 잘 끝냈다면 npm 모듈을 사용하기는 정말 쉽다. 시험삼아 저번 글에서도 사용했던 lodash를 설치하고 사용해보자. lodash를 설치한 뒤 index.ts
파일을 아래와 같이 수정한다.
그리고 바로 다시 디플로이와 실행을 해보자. rand
라는 프로퍼티에 랜덤하게 정수가 출력되면 성공이다.
이 일련의 과정은 사실 내가 회사에서 업무를 진행하면서 했던 작업들의 일부다. 진행하면서, webpack 같은 번들러없이 Apex로 Lambda를 관리하기는 좀 어렵겠다는 인상을 강하게 받았다. 하지만 한 번만 고생(?)해서 일단 구조를 잡아주고 나면, 이후 개발은 편하게 진행할 수 있다. 이 글에서는 다루지 않았지만, 개발하다보면 으레 필요해지는 멀티 스테이지도 필수라고 할 수 있는데, 이것을 포함해서 apex-typescript-boilerplate라는 이름의 저장소에 올라가 있으니 참고하시기 바란다.
최근에 AWS Lambda를 좀 써볼만한 일이 생겨서 이래저래 삽질을 했다. 막상 시작하려니 내가 잘 모르는 분야라 부담스럽기도 해서 시도를 주저하고 있었는데, 해보고 나니 생각보다 어려운 편은 아니었다. 그 삽질의 결과를 공유해보고자 글을 남긴다.
AWS Lambda를 쓰기 위해서는 당연히 AWS 계정이 필요하다. 또한, 모든 예제는 Node.js로 진행되기 때문에 Node.js에 대한 기본적인 이해가 필요하다.
아래와 같은 절차를 밟아 새로운 Lambda 함수를 만들어 볼 수 있다.
1) AWS Lambda 콘솔에 들어간다.
2) Get Started Now 버튼을 클릭해 새로운 Lambda 함수를 만든다. 이미 Lambda 함수가 있는 경우 Create a Lambda function이라는 버튼을 통해서 만들 수 있다.
3) Blueprint는 미리 준비된 Lambda 함수의 환경 같은 것인데, 여기서는 Blank Function을 선택한다. 이 글에서 다루고 있는 예제는 그리 복잡하지 않으므로 Blank Function으로 충분하다.
4) Trigger는 아래에서 다시 다룰 것이다. 여기서는 선택하지 않은 채로 Next 버튼을 눌러 넘어간다.
5) Configure Function > Name에 적절한 이름을 입력한다.
6) 아래로 내려 Lambda function handler and role > Role에서 Create new role from template(s)를 선택한 뒤, Role name에 적절한 이름을 입력하고 Next 버튼을 눌러 넘어간다.
7) 마지막으로 Create Function 버튼을 눌러 함수를 만든다.
이렇게 해서 만들어진 Lambda 함수는 Test 버튼을 클릭하여 테스트 할 수 있다.
이제 입력으로 숫자를 하나 받아 그 숫자를 두 배로 곱해주는 함수를 만들어 보자. 코드는 아래와 같다.
함수 내부를 들여다보면 우리가 일반적으로 사용하는 함수와는 조금 다른 방식으로 함수를 사용하는 것을 볼 수 있다. 먼저, 입력 데이터는 함수의 매개변수가 아니라 event
라는 매개변수의 프로퍼티로 전달된다. 즉, event
의 프로퍼티가 실질적으로 기존 함수들의 매개변수 역할을 대신하는 것이다. 마찬가지로, return
문이 아니라 매개변수로 넘어온 callback
이라는 함수를 실행시킴으로서 결과 값을 출력한다. 물론 return
문도 여전히 사용할 수 있지만 함수를 중간에 중단시키는 것 이외의 다른 역할은 하지 않는다.
context
는 Lambda 함수의 시스템에 관련된 정보를 속성으로 담고 있는 변수다. context
의 프로퍼티 리스트는 이 링크에서 확인할 수 있다. 이 글에서는 context
변수를 다루지 않는다.
이제 만들어진 함수를 테스트할 차례다. Actions > Configure test event를 누르면 event
의 프로퍼티로 전달될 값을 편집할 수 있다.
여기서는 event.num
이라는 값을 사용하므로 key로 "num"
이라는 값을 가지는 JSON을 입력한뒤 Save and test 버튼을 누른다.
그럼 Execution result: Succeeded
라는 말과 함께 결과 값을 확인할 수 있을 것이다.
이렇게 해서 만든 Lambda 함수는 현재로서는 별 가치가 없다. 내가 Test 버튼을 누를 때에만 동작하는 함수기 때문이다. Trigger를 지정해줘야만 제대로 된 Lambda 함수로서 기능할 수 있다. 설정할 수 있는 Trigger로는 다양한 종류가 있지만 가장 흔히 사용되는 것이 API Gateway 이므로 API Gateway와 연동하는 법을 소개한다.
먼저, 아래의 절차를 따라 API를 만들 수 있다.
1) AWS API Gateway 콘솔로 진입해 Get Started 버튼을 누른다.
2) Create new API > New API를 선택하고, API name에 적당한 이름을 넣고 Create API 버튼을 눌러 API를 만든다.
API는 만들어졌지만 아직 Lambda와 연동되지는 않았다. Lambda와 연동하는 부분은 각 API의 Method이므로 새로운 메소드를 만들어 Lambda와 연동해야한다.
1) Actions > Create method 버튼을 클릭하고 나온 Select Box에서 POST 메소드를 선택하고 체크를 눌러 메소드를 새로 만든다.
2) Intergration type > Lambda Function을 선택하고 Lambda Region을 아까 Lambda 함수를 만들었던 Region을 선택한다.
3) Lambda Function이라는 새로운 입력칸이 나오는데, 여기에서 아까 만들었던 Lambda 함수 이름을 입력한 뒤, Save 버튼을 누른다.
4) 그러면 API Gateway에 Lambda 함수를 호출할 수 있는 권한을 준다는 모달이 나오는데, 당연히 줘야 하므로 OK를 눌러 진행한다.
이제 만들어진 API를 테스트해보자. 왼쪽 위의 번개모양 Test 버튼을 누르면 테스트를 진행할 수 있다. Request Body에 아까 Lambda 함수를 테스트할 때 넣었던 값을 다시 입력한 뒤 Test 버튼을 누르면 동일한 값이 출력되는 것을 볼 수 있다.
다만, 아직까지는 HTTP Request를 통해 이 Lambda 함수를 실행시키지 못한다. HTTP Request를 통해 이 함수를 실행시키려면 API를 배포해야한다. 아래와 같은 절차를 따라 API를 배포한다.
1) Actions > Deploy API 버튼을 클릭한다.
2) Deployment Stage는 [New Stage]를 선택하고, Stage name을 적당히 입력한다.
3) Deploy 버튼을 눌러 Deploy 한다.
이제 상단에 Invoke URL이 표시되는 것을 볼 수 있다. 적당한 HTTP Request용 툴을 사용해서 해당 주소로 요청을 해보자.
여기서는 Postman을 사용했다. 정상적으로 응답이 도착했다면 API Gateway와의 연동이 끝난 것이다.
이 글에서는 API Gateway에 대해서 여기까지만 다루지만, 이외에도 API Gateway는 유용한 기능이 많다.
AWS 콘솔에서도 직접 코드를 수정하거나 만들어 낼 수도 있지만, 이렇게 해서는 npm 모듈을 사용할 수도 없고 파일을 분할해서 관리할 수도 없다. Lambda는 이 부분을 보완하기 위해서 ZIP 파일을 업로드하거나 S3에서 파일을 불러오는 선택지도 제시하고 있다. 이 글에서는 ZIP 파일을 업로드하는 부분에 대해서만 다룰 것이다.
방법은 간단하다. 필요한 파일을 모두 포함한 채로 ZIP 파일을 만들어 그걸 업로드하기만 하면 된다.
여기서는 Lambda 함수의 요구사항을 조금 바꿔, 숫자 하나를 입력받으면 그만큼의 길이의 자연수 배열을 만들어서 돌려주는 함수를 만들 것이다.
이 요구사항을 만족시키기 위해서 Lodash의 range함수를 사용한다. 따라서 먼저 Lodash를 인스톨한 뒤, index.js
파일에 아래와 같이 handler
함수를 작성하면 된다.
Lodash를 import 한다는 것을 제외하면 전에 작성했었던 함수와 구조적으로 크게 다르지 않다. 이제 설치된 node_modules
와 함께 index.js
파일을 ZIP 파일로 묶어서 Lambda에 업로드 하기만 하면 모든 절차가 끝난다. ZIP 파일의 이름은 상관없으나, JavaScript 엔트리 파일은 이름이 index.js
여야 한다.1
AWS Lambda로 돌아가서 Code entry type을 Upload a ZIP file로 변경하고 만들어진 ZIP 파일을 올린다. 그 뒤 Save and tes 버튼을 클릭하여 테스트도 해보자. 실수가 없다면, 테스트 결과는 의도한대로 배열을 돌려줄 것이다.
Node.js에서 AWS에 접근하기 위해서는 aws-sdk 모듈을 사용해야 한다. AWS Lambda의 Node.js 런타임에는 이 모듈이 기본적으로 설치되어 있으므로 추가로 설치할 필요없이, 바로 사용할 수 있다.
여기서는 Lambda 함수에 파일 이름과 파일 내용을 포함해서 요청하면 그 내용대로 S3에 파일을 만드는 함수를 작성한다. 함수의 구현은 아래와 같다.
s3.putObject()
라는 함수의 콜백에서 callback()
함수를 호출해서 에러를 전달하는 것을 볼 수 있을 것이다. 에러가 발생하는 경우 저렇게 callback()
함수의 첫 번째 인자로 에러 객체를 넘기면 에러가 출력된다.
여기서는 이미 버킷이 만들어져 있는 경우를 가정하고 있다. BUCKET_NAME
이라는 상수를 각자의 버킷에 맞춰 수정한 뒤 다시 ZIP 파일을 업로드해 테스트해보자. 테스트를 실행하면 Access Denied
에러와 함께 실패할 것이다.
Lambda 함수는 기본적으로 S3에 접근할 권한이 주어져있지 않다. 그렇기 때문에 S3에 쓰기를 하니 에러가 나는 것이다. 이를 해결하기 위해서는 S3 쓰기 권한을 얻어야 하고, 권한을 얻기 위해서는 Role을 설정해야 한다.
Lambda에서 S3에 쓰기 권한을 주는 Role은 미리 템플릿으로 정의되어 있지 않으므로 직접 만들어야 한다. AWS IAM 콘솔에 들어간 뒤 아래와 같은 절차를 밟아서 Role을 만들면 된다.
1) Roles > Create new role을 클릭한 뒤,
2) Select role type > AWS Lambda를 선택한다.
3) Policy Name이 AWSLambdaExecute인 정책을 찾아 선택한 후 다음 스텝으로 넘어간다.
4) 이름을 짓고 Create role 버튼을 클릭해 Role을 만든다.
Role을 만든 뒤에는 다시 Lambda 콘솔로 돌아와서 Configuration > Role에서 Choose an existing role을 선택한 뒤, Existing role에서 방금 만든 Role을 선택하고 저장하면 된다. 저장한 뒤, 알맞는 테스트 데이터를 입력해 테스트해보면 아까와 같은 권한 에러가 나지 않고 성공하는 것을 볼 수 있다. S3 버킷에서 실제로 파일이 작성되었는지도 확인해보자.
코드를 수정하고, ZIP 파일을 올리는 과정을 몇 번 반복하다 보면 조금 귀찮아지고, 이런 것들을 좀 더 편하게 할 수 있는 방법을 찾게 된다. 물론 이미 Setup이나 Deploy를 돕는 툴이 나와있다. Node.js를 지원하는 것으로는 node-lambda와 Apex라는 툴이 있는데 node-lambda는 단일 함수만 관리할 경우 큰 불편없이 사용할 수 있지만, 여러개의 함수를 관리하기는 좀 버겁다. Apex는 여러 개의 함수를 관리하는 데 특화되어 있고, 사용자의 수도 훨씬 많다. 다음 글에서는 Apex의 기본적인 사용법과 더불어, TypeScript로 된 코드베이스에서 Deploy하는 법을 다룬다.
다음 글: AWS Lambda: Apex로 관리하기