<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Sujin Lee | Creative Technologist]]></title><description><![CDATA[Thoughts, stories and ideas.]]></description><link>https://sujinlee.me/</link><image><url>https://sujinlee.me/favicon.png</url><title>Sujin Lee | Creative Technologist</title><link>https://sujinlee.me/</link></image><generator>Ghost 3.31</generator><lastBuildDate>Fri, 01 May 2026 04:55:43 GMT</lastBuildDate><atom:link href="https://sujinlee.me/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[저의 멘티가 되어주세요. (1:1 온라인 멘토링)]]></title><description><![CDATA[<p> 문의: sujinlee.me@gmail.com // 소셜미디어 메시지는 보지 않습니다. 예약 시스템 superpeer 과 아무런 관련이 없습니다.</p><p>안녕하세요. 독일 베를린 스타트업에서 소프트웨어 엔지니어로 근무하고 있는 이수진입니다. (<a href="https://www.linkedin.com/in/leesujin/">링크드인 참고</a>) React.js &amp; TypeScript로 프론트엔드 웹 애플리케션을 개발하고 있습니다. 현재 재직 중인 회사에서는 프론트엔드 기술 인터뷰에 면접관으로 참여하고 있습니다. 또한 2021년부터 베를린</p>]]></description><link>https://sujinlee.me/online-mentoring/</link><guid isPermaLink="false">627028ccd3cf1058fd600759</guid><category><![CDATA[career]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Mon, 02 May 2022 18:56:58 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1553877522-43269d4ea984?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDN8fHJldmlld3xlbnwwfHx8fDE2NTE1MTc2NzE&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1553877522-43269d4ea984?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDN8fHJldmlld3xlbnwwfHx8fDE2NTE1MTc2NzE&ixlib=rb-1.2.1&q=80&w=2000" alt="저의 멘티가 되어주세요. (1:1 온라인 멘토링)"><p> 문의: sujinlee.me@gmail.com // 소셜미디어 메시지는 보지 않습니다. 예약 시스템 superpeer 과 아무런 관련이 없습니다.</p><p>안녕하세요. 독일 베를린 스타트업에서 소프트웨어 엔지니어로 근무하고 있는 이수진입니다. (<a href="https://www.linkedin.com/in/leesujin/">링크드인 참고</a>) React.js &amp; TypeScript로 프론트엔드 웹 애플리케션을 개발하고 있습니다. 현재 재직 중인 회사에서는 프론트엔드 기술 인터뷰에 면접관으로 참여하고 있습니다. 또한 2021년부터 베를린 소재 비영리 코딩 교육기관인 <a href="https://www.redi-school.org/">ReDI School</a>에서 프론트엔드 멘토로도 활동하고 있습니다. </p><p>저는  1년 후 시니어 엔지니어로 성장하기 위해 리더십 및 기술 코칭 역량을 기르고자 합니다. 하지만 안타깝게도 제가 속한 조직에 주니어가 없어 멘토가 되어보는 경험을 하기 어려운 상황입니다. 이에 저를 멘토로 키워주실 멘티 분들을 찾아보고자 합니다. 1:1 온라인 멘토링은 저의 역량 개발을 위한 것으로 무료로 진행됩니다.</p><p>아래와 같은 주제로 도움을 드릴 수 있습니다. 개인적으로 FE 페어 프로그래밍 및 코드 리뷰를 많이 하고 싶어요.</p><p> 💻 코드 리뷰</p><ul><li>부트캠프 프로젝트 코드 리뷰는 받지 않습니다. 해당 교육 기관에 문의하시기 바랍니다.</li><li>리액트, 자바스크립트, 타입스크립트 코드 리뷰를 진행합니다.</li></ul><p> 💻   페어프로그래밍</p><ul><li>페어 프로그래밍으로 코딩 문제를 같이 풀어봅니다. 문제를 준비해오셔도 되고 직접 요청해 주셔도 됩니다. 원하시면 영어로도 가능합니다.</li><li> 구체적인 질문을 해주시면 문제 - 답변을 준비해오겠습니다.</li></ul><p>👩🏻‍💻 영어 이력서 리뷰</p><ul><li>해외 취업을 준비하고 계시는 분들을 위해 영어 이력서를 리뷰해 드립니다. 경력과 성과, 프로젝트 기여도 중심으로 작성하는 것이 목표입니다. </li><li>행동 인터뷰 준비를 위해 <a href="https://en.wikipedia.org/wiki/Situation,_task,_action,_result">S-T-A-R 방식</a>을 활용해 탁월한 문제 해결 경험과 능력을 보여줄 수 있는 스토리를 함께 만들어 봅니다.</li><li>구글 닥스 등 실시간으로 협업이 가능하도록 준비해주세요.</li></ul><p>☕️ Career Coffee Chat (잠시 쉬는 중입니다)</p><ul><li>개발자가 되고 싶은 분들을 위해 고민 상담을 해드립니다. </li><li>편안 분위기에서 자유 주제로 이야기를 나눕니다.</li></ul><p>희망있으신 분들은 아래 <a href="https://superpeer.com/sujinlee">superpeer 플랫폼</a>에서 희망 시간과 주제를 선택해주시고 메시지를 남겨주세요. 구체적으로 작성해주시면 미팅을 준비하는데 큰 도움이 됩니다. 수락 후 초대 링크가 전달되며 해당 시간에 접속하시면  화상 미팅이 시작됩니다.  멘토링은 일회성으로 진행되며 추후 재 멘토링이 필요하시면 다시 예약시면 됩니다.</p><p>멘토링 세션 종료 이후 설문지를 보내드립니다. 피드백과 개선점을 남겨주시면 저에게 큰 기쁨이 될 것 같습니다.  이 포스팅에 직접 댓글을 달아주셔도 좋습니다.</p><!--kg-card-begin: html--><div style="width: 100%; height: 800px"><link rel="stylesheet" type="text/css" href="https://widgets.superpeer.com/widget.css"><script src="https://widgets.superpeer.com/widget.js"></script><div class="sp-widget-wrapper" style="width: 100%; height: 800px;"></div><script>window.addEventListener("load", () => {new Superpeer.Widget({embed:{type:"inline",wrapperSelector:".sp-widget-wrapper"},config:{username:"sujinlee",serviceSlug:""}})})</script><!--kg-card-end: html--></div>]]></content:encoded></item><item><title><![CDATA[[탈잉 - 코드리뷰] Zero To Hero: 비개발자에서 개발자로 성장할 수 있었던 4가지 비결 (유료)]]></title><description><![CDATA[<p></p><!--kg-card-begin: html--><iframe src="https://docs.google.com/presentation/d/e/2PACX-1vSdDtE-qEKZuqtwEXG8gu8KmxayuDQKrqn0K3KTLLptgKYhH33Sq1JY-Tj8S-zmqV0G9J2AQTQMrg-t/embed?start=false&loop=false&delayms=3000" frameborder="0" width="100%" height="500" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe><!--kg-card-end: html--><p></p><p>비전공자에서 출발해 유학 및 어학연수 경험이 없는 순수 국내파로 싱가포르와 독일에 해외 취업할 수 있었던 네 가지 비결을 공유했습니다.</p><h2 id="-">월간 코드리뷰</h2><blockquote>" 대학과 소프트웨어 전문 교육기관에서 많은 개발 지망생들을 보면서 개발자의 성장이라는게 혼자서는 이루기 힘들다는 것을 느꼈습니다. 커뮤니티 등 개발자 생태계에서 인정받고 있는 핵인싸 5인이 이야기 하는 성장코드를 전달하고 싶었습니다" -</blockquote>]]></description><link>https://sujinlee.me/zero-to-hero/</link><guid isPermaLink="false">6156e409eaba8d3b66d083f7</guid><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Fri, 01 Oct 2021 10:52:20 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1513384312027-9fa69a360337?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fGhlcm98ZW58MHx8fHwxNjQ1MjEwMDQ4&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1513384312027-9fa69a360337?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fGhlcm98ZW58MHx8fHwxNjQ1MjEwMDQ4&ixlib=rb-1.2.1&q=80&w=2000" alt="[탈잉 - 코드리뷰] Zero To Hero: 비개발자에서 개발자로 성장할 수 있었던 4가지 비결 (유료)"><p></p><!--kg-card-begin: html--><iframe src="https://docs.google.com/presentation/d/e/2PACX-1vSdDtE-qEKZuqtwEXG8gu8KmxayuDQKrqn0K3KTLLptgKYhH33Sq1JY-Tj8S-zmqV0G9J2AQTQMrg-t/embed?start=false&loop=false&delayms=3000" frameborder="0" width="100%" height="500" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe><!--kg-card-end: html--><p></p><p>비전공자에서 출발해 유학 및 어학연수 경험이 없는 순수 국내파로 싱가포르와 독일에 해외 취업할 수 있었던 네 가지 비결을 공유했습니다.</p><h2 id="-">월간 코드리뷰</h2><blockquote>" 대학과 소프트웨어 전문 교육기관에서 많은 개발 지망생들을 보면서 개발자의 성장이라는게 혼자서는 이루기 힘들다는 것을 느꼈습니다. 커뮤니티 등 개발자 생태계에서 인정받고 있는 핵인싸 5인이 이야기 하는 성장코드를 전달하고 싶었습니다" - 탈잉의 월간 코드리뷰 기획자 '필립' -</blockquote><p>개발자, 혹은 개발자를 꿈꾸는 사람이라면 누구나 궁금해할만한 문제,더 나아가 문제 해결 과정과 실제 적용 사례를 생생한 라이브로 전달하는 행사입니다.</p><p><a href="https://taling.me/Event/monthly-codereview/ver_01?fbclid=IwAR1Uo_xqLtL1dLdRo29O-yLLiPoyxmIdnE-nOK108-x3ouqzBBn8xAKk0II" rel="nofollow noopener">https://taling.me/Event/monthly-codereview/ver_01</a><br></p><p></p>]]></content:encoded></item><item><title><![CDATA["단숨에 배우는 타입스크립트(2021)" 를 출간했습니다.]]></title><description><![CDATA[<figure class="kg-card kg-embed-card"><blockquote class="instagram-media" data-instgrm-captioned data-instgrm-permalink="https://www.instagram.com/p/CREaT62l6Dv/?utm_source=ig_embed&amp;utm_campaign=loading" data-instgrm-version="13" style=" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:658px; min-width:326px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);"><div style="padding:16px;"> <a href="https://www.instagram.com/p/CREaT62l6Dv/?utm_source=ig_embed&amp;utm_campaign=loading" style=" background:#FFFFFF; line-height:0; padding:0 0; text-align:center; text-decoration:none; width:100%;" target="_blank"> <div style=" display: flex; flex-direction: row; align-items: center;"> <div style="background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 40px; margin-right: 14px; width: 40px;"></div> <div style="display: flex; flex-direction: column; flex-grow: 1; justify-content: center;"> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 100px;"></div> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 60px;"></div></div></div><div style="padding: 19% 0;"></div> <div style="display:block; height:50px; margin:0 auto 12px; width:50px;"><svg width="50px" height="50px" viewbox="0 0 60 60" version="1.1" xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g transform="translate(-511.000000, -20.000000)" fill="#000000"><g><path d="M556.869,30.41 C554.814,30.41 553.148,32.076 553.148,34.131 C553.148,36.186 554.814,37.852 556.869,37.852 C558.924,37.852 560.59,36.186 560.59,34.131 C560.59,32.076 558.924,30.41 556.869,30.41 M541,60.657 C535.114,60.657 530.342,55.887 530.342,50 C530.342,44.114 535.114,39.342 541,39.342 C546.887,39.342 551.658,44.114 551.658,50 C551.658,55.887 546.887,60.657 541,60.657 M541,33.886 C532.1,33.886 524.886,41.1 524.886,50 C524.886,58.899 532.1,66.113 541,66.113 C549.9,66.113 557.115,58.899 557.115,50 C557.115,41.1 549.9,33.886 541,33.886 M565.378,62.101 C565.244,65.022 564.756,66.606 564.346,67.663 C563.803,69.06 563.154,70.057 562.106,71.106 C561.058,72.155 560.06,72.803 558.662,73.347 C557.607,73.757 556.021,74.244 553.102,74.378 C549.944,74.521 548.997,74.552 541,74.552 C533.003,74.552 532.056,74.521 528.898,74.378 C525.979,74.244 524.393,73.757 523.338,73.347 C521.94,72.803 520.942,72.155 519.894,71.106 C518.846,70.057 518.197,69.06 517.654,67.663 C517.244,66.606 516.755,65.022 516.623,62.101 C516.479,58.943 516.448,57.996 516.448,50 C516.448,42.003 516.479,41.056 516.623,37.899 C516.755,34.978 517.244,33.391 517.654,32.338 C518.197,30.938 518.846,29.942 519.894,28.894 C520.942,27.846 521.94,27.196 523.338,26.654 C524.393,26.244 525.979,25.756 528.898,25.623 C532.057,25.479 533.004,25.448 541,25.448 C548.997,25.448 549.943,25.479 553.102,25.623 C556.021,25.756 557.607,26.244 558.662,26.654 C560.06,27.196 561.058,27.846 562.106,28.894 C563.154,29.942 563.803,30.938 564.346,32.338 C564.756,33.391 565.244,34.978 565.378,37.899 C565.522,41.056 565.552,42.003 565.552,50 C565.552,57.996 565.522,58.943 565.378,62.101 M570.82,37.631 C570.674,34.438 570.167,32.258 569.425,30.349 C568.659,28.377 567.633,26.702 565.965,25.035 C564.297,23.368 562.623,22.342 560.652,21.575 C558.743,20.834 556.562,20.326 553.369,20.18 C550.169,20.033 549.148,20 541,20 C532.853,20 531.831,20.033 528.631,20.18 C525.438,20.326 523.257,20.834 521.349,21.575 C519.376,22.342 517.703,23.368 516.035,25.035 C514.368,26.702 513.342,28.377 512.574,30.349 C511.834,32.258 511.326,34.438 511.181,37.631 C511.035,40.831 511,41.851 511,50 C511,58.147 511.035,59.17 511.181,62.369 C511.326,65.562 511.834,67.743 512.574,69.651 C513.342,71.625 514.368,73.296 516.035,74.965 C517.703,76.634 519.376,77.658 521.349,78.425 C523.257,79.167 525.438,79.673 528.631,79.82 C531.831,79.965 532.853,80.001 541,80.001 C549.148,80.001 550.169,79.965 553.369,79.82 C556.562,79.673 558.743,79.167 560.652,78.425 C562.623,77.658 564.297,76.634 565.965,74.965 C567.633,73.296 568.659,71.625 569.425,69.651 C570.167,67.743 570.674,65.562 570.82,62.369 C570.966,59.17 571,58.147 571,50 C571,41.851 570.966,40.831 570.82,37.631"/></g></g></g></svg></div><div style="padding-top: 8px;"> <div style=" color:#3897f0; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:550; line-height:18px;"> View this post on Instagram</div></div><div style="padding: 12.5% 0;"></div> <div style="display: flex; flex-direction: row; margin-bottom: 14px; align-items: center;"><div> <div style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(0px) translateY(7px);"></div> <div style="background-color: #F4F4F4; height: 12.5px; transform: rotate(-45deg) translateX(3px) translateY(1px); width: 12.5px; flex-grow: 0; margin-right: 14px; margin-left: 2px;"></div> <div style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(9px) translateY(-18px);"></div></div><div style="margin-left: 8px;"> <div style=" background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 20px; width: 20px;"></div> <div style=" width: 0; height: 0; border-top: 2px solid transparent; border-left: 6px solid #f4f4f4; border-bottom: 2px solid transparent; transform: translateX(16px) translateY(-4px) rotate(30deg)"></div></div><div style="margin-left: auto;"> <div style=" width: 0px; border-top: 8px solid #F4F4F4; border-right: 8px solid transparent; transform: translateY(16px);"></div> <div style=" background-color: #F4F4F4; flex-grow: 0; height: 12px; width: 16px; transform: translateY(-4px);"></div> <div style=" width: 0; height: 0; border-top: 8px solid #F4F4F4; border-left: 8px solid transparent; transform: translateY(-4px) translateX(8px);"></div></div></div> <div style="display: flex; flex-direction: column; flex-grow: 1; justify-content: center; margin-bottom: 24px;"> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 224px;"></div> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 144px;"></div></div></a><p style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;"><a href="https://www.instagram.com/p/CREaT62l6Dv/?utm_source=ig_embed&amp;utm_campaign=loading" style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none;" target="_blank">A post shared by Sujin Lee 이수진  李秀珍 (@sujinleeme)</a></p></div></blockquote>
<script async src="//www.instagram.com/embed.js"></script></figure><p>저의 두 번째 IT번역서인 &lt;단숨에 배우는 타입스크립트(영진닷컴) 원제: TypeScript Quickly, 저자: <a href="https://www.linkedin.com/in/ACoAAAA2AVMBxQ2ncVRhtHkCliPTENJAE5xTzFI">Yakov Fain</a> <a href="https://www.linkedin.com/in/ACoAAB2XNF8BhMtN_2l-GJi-vRNNu-kF0rt8TaA">Anton Moiseev</a> <a href="https://lnkd.in/dExBKe4">https://lnkd.in/dExBKe4</a>&gt; 공식적으로 출간되었습니다. 예약 판매 중이며 곧 서점에서 만나보실 수 있습니다. 많은 관심 부탁드립니다. 🇰🇷<br><br>📗 Yes24:</p>]]></description><link>https://sujinlee.me/typescript-quickly/</link><guid isPermaLink="false">60da175d633e9a059ebc7681</guid><category><![CDATA[career]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Mon, 28 Jun 2021 18:45:56 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1527176930608-09cb256ab504?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDd8fGJvb2t8ZW58MHx8fHwxNjUwOTkxMDkw&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<figure class="kg-card kg-embed-card"><blockquote class="instagram-media" data-instgrm-captioned data-instgrm-permalink="https://www.instagram.com/p/CREaT62l6Dv/?utm_source=ig_embed&amp;utm_campaign=loading" data-instgrm-version="13" style=" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:658px; min-width:326px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);"><div style="padding:16px;"> <a href="https://www.instagram.com/p/CREaT62l6Dv/?utm_source=ig_embed&amp;utm_campaign=loading" style=" background:#FFFFFF; line-height:0; padding:0 0; text-align:center; text-decoration:none; width:100%;" target="_blank"> <div style=" display: flex; flex-direction: row; align-items: center;"> <div style="background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 40px; margin-right: 14px; width: 40px;"></div> <div style="display: flex; flex-direction: column; flex-grow: 1; justify-content: center;"> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 100px;"></div> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 60px;"></div></div></div><div style="padding: 19% 0;"></div> <div style="display:block; height:50px; margin:0 auto 12px; width:50px;"><svg width="50px" height="50px" viewbox="0 0 60 60" version="1.1" xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g transform="translate(-511.000000, -20.000000)" fill="#000000"><g><path d="M556.869,30.41 C554.814,30.41 553.148,32.076 553.148,34.131 C553.148,36.186 554.814,37.852 556.869,37.852 C558.924,37.852 560.59,36.186 560.59,34.131 C560.59,32.076 558.924,30.41 556.869,30.41 M541,60.657 C535.114,60.657 530.342,55.887 530.342,50 C530.342,44.114 535.114,39.342 541,39.342 C546.887,39.342 551.658,44.114 551.658,50 C551.658,55.887 546.887,60.657 541,60.657 M541,33.886 C532.1,33.886 524.886,41.1 524.886,50 C524.886,58.899 532.1,66.113 541,66.113 C549.9,66.113 557.115,58.899 557.115,50 C557.115,41.1 549.9,33.886 541,33.886 M565.378,62.101 C565.244,65.022 564.756,66.606 564.346,67.663 C563.803,69.06 563.154,70.057 562.106,71.106 C561.058,72.155 560.06,72.803 558.662,73.347 C557.607,73.757 556.021,74.244 553.102,74.378 C549.944,74.521 548.997,74.552 541,74.552 C533.003,74.552 532.056,74.521 528.898,74.378 C525.979,74.244 524.393,73.757 523.338,73.347 C521.94,72.803 520.942,72.155 519.894,71.106 C518.846,70.057 518.197,69.06 517.654,67.663 C517.244,66.606 516.755,65.022 516.623,62.101 C516.479,58.943 516.448,57.996 516.448,50 C516.448,42.003 516.479,41.056 516.623,37.899 C516.755,34.978 517.244,33.391 517.654,32.338 C518.197,30.938 518.846,29.942 519.894,28.894 C520.942,27.846 521.94,27.196 523.338,26.654 C524.393,26.244 525.979,25.756 528.898,25.623 C532.057,25.479 533.004,25.448 541,25.448 C548.997,25.448 549.943,25.479 553.102,25.623 C556.021,25.756 557.607,26.244 558.662,26.654 C560.06,27.196 561.058,27.846 562.106,28.894 C563.154,29.942 563.803,30.938 564.346,32.338 C564.756,33.391 565.244,34.978 565.378,37.899 C565.522,41.056 565.552,42.003 565.552,50 C565.552,57.996 565.522,58.943 565.378,62.101 M570.82,37.631 C570.674,34.438 570.167,32.258 569.425,30.349 C568.659,28.377 567.633,26.702 565.965,25.035 C564.297,23.368 562.623,22.342 560.652,21.575 C558.743,20.834 556.562,20.326 553.369,20.18 C550.169,20.033 549.148,20 541,20 C532.853,20 531.831,20.033 528.631,20.18 C525.438,20.326 523.257,20.834 521.349,21.575 C519.376,22.342 517.703,23.368 516.035,25.035 C514.368,26.702 513.342,28.377 512.574,30.349 C511.834,32.258 511.326,34.438 511.181,37.631 C511.035,40.831 511,41.851 511,50 C511,58.147 511.035,59.17 511.181,62.369 C511.326,65.562 511.834,67.743 512.574,69.651 C513.342,71.625 514.368,73.296 516.035,74.965 C517.703,76.634 519.376,77.658 521.349,78.425 C523.257,79.167 525.438,79.673 528.631,79.82 C531.831,79.965 532.853,80.001 541,80.001 C549.148,80.001 550.169,79.965 553.369,79.82 C556.562,79.673 558.743,79.167 560.652,78.425 C562.623,77.658 564.297,76.634 565.965,74.965 C567.633,73.296 568.659,71.625 569.425,69.651 C570.167,67.743 570.674,65.562 570.82,62.369 C570.966,59.17 571,58.147 571,50 C571,41.851 570.966,40.831 570.82,37.631"/></g></g></g></svg></div><div style="padding-top: 8px;"> <div style=" color:#3897f0; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:550; line-height:18px;"> View this post on Instagram</div></div><div style="padding: 12.5% 0;"></div> <div style="display: flex; flex-direction: row; margin-bottom: 14px; align-items: center;"><div> <div style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(0px) translateY(7px);"></div> <div style="background-color: #F4F4F4; height: 12.5px; transform: rotate(-45deg) translateX(3px) translateY(1px); width: 12.5px; flex-grow: 0; margin-right: 14px; margin-left: 2px;"></div> <div style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(9px) translateY(-18px);"></div></div><div style="margin-left: 8px;"> <div style=" background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 20px; width: 20px;"></div> <div style=" width: 0; height: 0; border-top: 2px solid transparent; border-left: 6px solid #f4f4f4; border-bottom: 2px solid transparent; transform: translateX(16px) translateY(-4px) rotate(30deg)"></div></div><div style="margin-left: auto;"> <div style=" width: 0px; border-top: 8px solid #F4F4F4; border-right: 8px solid transparent; transform: translateY(16px);"></div> <div style=" background-color: #F4F4F4; flex-grow: 0; height: 12px; width: 16px; transform: translateY(-4px);"></div> <div style=" width: 0; height: 0; border-top: 8px solid #F4F4F4; border-left: 8px solid transparent; transform: translateY(-4px) translateX(8px);"></div></div></div> <div style="display: flex; flex-direction: column; flex-grow: 1; justify-content: center; margin-bottom: 24px;"> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 224px;"></div> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 144px;"></div></div></a><img src="https://images.unsplash.com/photo-1527176930608-09cb256ab504?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDd8fGJvb2t8ZW58MHx8fHwxNjUwOTkxMDkw&ixlib=rb-1.2.1&q=80&w=2000" alt=""단숨에 배우는 타입스크립트(2021)" 를 출간했습니다."><p style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;"><a href="https://www.instagram.com/p/CREaT62l6Dv/?utm_source=ig_embed&amp;utm_campaign=loading" style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none;" target="_blank">A post shared by Sujin Lee 이수진  李秀珍 (@sujinleeme)</a></p></div></blockquote>
<script async src="//www.instagram.com/embed.js"></script></figure><p>저의 두 번째 IT번역서인 &lt;단숨에 배우는 타입스크립트(영진닷컴) 원제: TypeScript Quickly, 저자: <a href="https://www.linkedin.com/in/ACoAAAA2AVMBxQ2ncVRhtHkCliPTENJAE5xTzFI">Yakov Fain</a> <a href="https://www.linkedin.com/in/ACoAAB2XNF8BhMtN_2l-GJi-vRNNu-kF0rt8TaA">Anton Moiseev</a> <a href="https://lnkd.in/dExBKe4">https://lnkd.in/dExBKe4</a>&gt; 공식적으로 출간되었습니다. 예약 판매 중이며 곧 서점에서 만나보실 수 있습니다. 많은 관심 부탁드립니다. 🇰🇷<br><br>📗 Yes24: <a href="https://lnkd.in/dcdiRnB">https://lnkd.in/dcdiRnB</a><br>📕 교보문고: <a href="https://lnkd.in/d9UtS-m">https://lnkd.in/d9UtS-m</a><br>📘 알라딘: <a href="https://lnkd.in/dtvTQaS">https://lnkd.in/dtvTQaS</a><br><br>&lt;역자 서문&gt;<br><br>타입스크립트가 자바스크립트 생태계를 장악할 것이다. 이 말에 동의하시나요? Elm과 앵귤러 커뮤니티에서 유명한 개발자이자 프로그래밍 서적 작가인 리처드 펠드만(Richard Feldman)이 ReactiveConf2019에서 ‘<a href="https://www.youtube.com/watch?v=okrB3aJtUaw">웹의 미래를 예측하다'</a>라는 제목으로 진행했던 발표의 첫 문장입니다. 그는 2020년 말까지 타입스크립트는 상용 자바스크립트 프로젝트의 가장 많이 사용되는 언어가 될 것이며, 2025년에는 자바스크립트 보다 타입스크립트를 사용하는 개발자 수가 더 많을 것이라 말했습니다. 2019년 stateofjs.com에서 진행한 조사에 따르면 전세계 자바스크립트 개발자의 60% 이상이 타입스크립트를 경험했다고 답했습니다. 이처럼 자바스크립트 생태계 내에서 타입스크립트의 인기는 정말 대단합니다. 여러분이 만약 프론트엔드 개발자라면 앞으로 타입스크립트를 피하기 어려울지도 모릅니다.<br><br>그러나 단순히 모두가 쓴다는 이유로 타입스크립트를 선택하기보다는 실제 팀과 제품에 어떤 긍정적인 영향을 주는지를 알고 사용하는 것이 더 중요합니다. 제 경우 실무에서 타입스크립트를 사용하면서 자바스크립트를 사용했던 때보다 코드 가독성과 퀄리티가 크게 향상되었음을 몸소 느끼고 있습니다. 쉽게 버그와 디버깅이 가능해 업무 효율과 개발 속도와 생산성도 눈에 띄게 증가했습니다. 지금은 타입스크립트 없는 프론트엔드 개발은 생각하기 힘들 정도입니다. 주변을 돌아봐도 타입스크립트로 넘어온 개발자가 다시 자바스크립트로 돌아갔다는 이야기를 들은 적은 거의 없습니다.<br><br>그렇지만 아직도 이런 저런 이유로 타입스크립트 도입을 망설이는 분들도 많을 것이라 짐작됩니다. 첫 번째는 타입스크립트는 러닝 커브가 높다는 점입니다. 타입스크립트는 자바스크립트의 상위 집합으로 기존 자바스크립트 문법을 그대로 사용합니다. 이미 자바스크립트를 잘 알고 있는 분이라면 타입스크립트 역시 알고 있는 것이기에 그리 겁내지 않아도 됩니다. 두 번째는 자바스크립트가 표준이라는 이유입니다. 오늘날 타입스크립트는 최신 ECMA 스크립트를 지원합니다. 트랜스파일러 덕분에 구 브라우저에서도 문제없이 실행됩니다. 때문에 자바스크립트 최신 문법을 사용하면서 크로스 브라우징 이슈를 해결하기 원한다면 타입스크립트가 가장 좋은 선택입니다. 세 번째는 기존 자바스크립트 프로젝트를 타입스크립트로 마이그레이션 하기 힘들다는 점입니다. 컴파일러 옵션을 사용해 일부 모듈만 타입스크립트로 새로 개발하거나 기존 자바스크립트를 점진적으로 변환할 수 있습니다. 실제로 타입스크립트로 제품을 마이그레이션한 모 회사의 경우, 타입스크립트로 100% 마이그레이션 하기까지 1년이 넘는 시간이 걸렸다고 합니다. 그럼에도 마이그레이션을 진행한 이유는 장기적인 관점으로 볼 때 타입스크립트 프로젝트가 개발 생산성과 프로젝트 유지 보수 측면에서 그만한 가치가 있기 때문입니다.<br><br>&lt;단숨에 배우는 타입스크립트&gt;는 자바스크립트 문법과 기초 지식을 정리하고, 타입스크립트의 주요 개념을 학습하며 실제 프로젝트에 타입스크립트를 적용할 수 있는 훌륭한 입문 서적입니다. 리액트, 뷰, 앵귤러에서 타입스크립트를 도입한 블록체인 앱 개발을 경험할 수 있습니다. 배운 지식을 바로 실무에 적용할 수 있다는 점이 가장 큰 장점이라고 생각합니다.<br><br>끝으로 책을 소개해주시고 편집해주신 영진닷컴의 이민혁 님께 감사의 말씀을 드립니다. 이 책을 통해 독자 여러분들이 자바스크립트에서 타입스크립트로 시야를 넓히는 경험과 배움의 즐거움을 느끼시길 소망합니다.</p><p></p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2021/06/8931465262_01-copy-2.jpeg" class="kg-image" alt=""단숨에 배우는 타입스크립트(2021)" 를 출간했습니다." srcset="https://sujinlee.me/content/images/size/w600/2021/06/8931465262_01-copy-2.jpeg 600w, https://sujinlee.me/content/images/2021/06/8931465262_01-copy-2.jpeg 700w"></figure>]]></content:encoded></item><item><title><![CDATA[[51Conference 2020] 👩‍💻 非개발자에서 🦅 飛개발자로]]></title><description><![CDATA[<p></p><p>👩‍💻 어학 연수나 해외 유학 경험이 없는 순수한 국내파, CS 비전공자가 개발자로 해외 취업을 할 수 있을까?</p><p>🦅 한국에서 <code>print("Hello World")</code> 부터 시작해, 어떻게 베를린 스타트업의 주니어 소프트웨어 엔지니어가 될 수 있었는지 그동안의 경험을 15분으로 압축했습니다. 영어 공부, 개인 프로젝트 , 영문 이력서, 취업 정보, 면접 및 과제, 연봉 협상까지, 실제 몸으로</p>]]></description><link>https://sujinlee.me/51conf-2020/</link><guid isPermaLink="false">5f560f7c150a1a7ae0eae8fe</guid><category><![CDATA[talk]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Mon, 07 Sep 2020 11:10:09 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1525406580688-29f75b5e4c97?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1525406580688-29f75b5e4c97?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="[51Conference 2020] 👩‍💻 非개발자에서 🦅 飛개발자로"><p></p><p>👩‍💻 어학 연수나 해외 유학 경험이 없는 순수한 국내파, CS 비전공자가 개발자로 해외 취업을 할 수 있을까?</p><p>🦅 한국에서 <code>print("Hello World")</code> 부터 시작해, 어떻게 베를린 스타트업의 주니어 소프트웨어 엔지니어가 될 수 있었는지 그동안의 경험을 15분으로 압축했습니다. 영어 공부, 개인 프로젝트 , 영문 이력서, 취업 정보, 면접 및 과제, 연봉 협상까지, 실제 몸으로 부딪히며 배웠던 내용을 정리했습니다.  비전공자 프레임에 갇히지 않고(非개발자)  더 넓은 세상을 향해 도전하고, 힘껏 날개짓하는 여러분(飛개발자)이 되셨으면 좋겠습니다.</p><blockquote>발표 후, 발표 슬라이드 내  베를린 취업 정보 내용을 추가했습니다.</blockquote><h3 id="-"> 📝  슬라이드</h3><!--kg-card-begin: html--><iframe src="https://docs.google.com/presentation/d/e/2PACX-1vQt6yhJ6n2RvqOqnXLliwXqxQexd0H2VXWP58PmL8MRbp89JfhBvUCOaK4Cqxzz7bffckpavT0gtSuM/embed?start=false&loop=true&delayms=3000" frameborder="0" width="100%" height="500" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe><!--kg-card-end: html--><p></p><h3 id="--1"> 🎤  영상</h3><!--kg-card-begin: html--><iframe width="560" height="315" src="https://www.youtube.com/embed/p_uyREwiM7s" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><!--kg-card-end: html--><h2 id="51-">51 컨퍼런스</h2><p><a href="https://www.facebook.com/51conference">51컨퍼런스</a>는  실리콘밸리에서 일하는 한국인들이 해외 취/창업을 고민하는 한국 직장인들을 돕고자 2016년 시작한 비영리 컨퍼런스입니다. 스피커들 각자의 경험을 나눔으로써 50:50의 고민되는 마음을 51:49까지는 만들어주고 싶다는 바람이 담긴 이름입니다.<br><br> 9월 4일과 5일, 온라인으로 개최된<a href="http://bit.ly/2YLpRAO"> 51 컨퍼런스 2020 (이벤트 페이지)</a>은 실리콘밸리, 베를린, 도쿄 등 해외 이직에 성공한 한국 직장인들이 자신과 같은 고민을 하고 있는 직장인 동료들과 자신의 경험을 나누기 위해 마련되었습니다.</p><p>신입, 문과, 비전공자, 순수 국내파, 대기업 직장인 등 다양한 배경을 가진 한국 직장인 18인의 성공적인 글로벌 커리어를 쌓아나간 이야기, 리모트 워킹 시대를 맞이한 해외 각국의 업계 동향을 나눠주셨습니다.</p><!--kg-card-begin: html--><p><img src="https://sujinlee.me/content/images/2020/09/51conf-closing.png" alt="[51Conference 2020] 👩‍💻 非개발자에서 🦅 飛개발자로" width="100%"></p><!--kg-card-end: html-->]]></content:encoded></item><item><title><![CDATA[[데이터야놀자 2019] 웹으로 표현하는 데이터 시각화와 스토리텔링]]></title><description><![CDATA[<!--kg-card-begin: html--><iframe src="https://docs.google.com/presentation/d/e/2PACX-1vSLp57CzXIQsXhXdVYwviEBiKxWhFVfZeWe8_oYrk_TDVRsLclJBqYAZy55f0X22YtYj4z-ed1hTFZ8/embed?start=false&loop=false&delayms=3000" frameborder="0" width="100%" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe><!--kg-card-end: html--><p></p><p>데이터 시각화와 스토리텔링은 데이터 속 숨겨진 이야기를 찾고 생명력을 불어 넣는 작업입니다. 웹은 창의적이고 아름다운 시각화와 효과적인 스토리텔링을 만들 수 있는 멋진 도구입니다. 본 발표에서 최근 많은 언론사에서 사용하고 있는 스크롤링 스토리텔링 기법을 중심으로 지도, 도표, SVG 이미지 등 시각화 요소를 인터렉티브하게 표현할 수 있는 방법을 소개합니다.</p>]]></description><link>https://sujinlee.me/datayanolja-2019/</link><guid isPermaLink="false">5f55e80e150a1a7ae0eae86a</guid><category><![CDATA[talk]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Tue, 29 Oct 2019 15:01:15 GMT</pubDate><media:content url="https://sujinlee.me/content/images/2019/10/datayah-group-photo.JPG" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: html--><iframe src="https://docs.google.com/presentation/d/e/2PACX-1vSLp57CzXIQsXhXdVYwviEBiKxWhFVfZeWe8_oYrk_TDVRsLclJBqYAZy55f0X22YtYj4z-ed1hTFZ8/embed?start=false&loop=false&delayms=3000" frameborder="0" width="100%" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe><!--kg-card-end: html--><img src="https://sujinlee.me/content/images/2019/10/datayah-group-photo.JPG" alt="[데이터야놀자 2019] 웹으로 표현하는 데이터 시각화와 스토리텔링"><p></p><p>데이터 시각화와 스토리텔링은 데이터 속 숨겨진 이야기를 찾고 생명력을 불어 넣는 작업입니다. 웹은 창의적이고 아름다운 시각화와 효과적인 스토리텔링을 만들 수 있는 멋진 도구입니다. 본 발표에서 최근 많은 언론사에서 사용하고 있는 스크롤링 스토리텔링 기법을 중심으로 지도, 도표, SVG 이미지 등 시각화 요소를 인터렉티브하게 표현할 수 있는 방법을 소개합니다.</p>]]></content:encoded></item><item><title><![CDATA[SVG Icon System in Vue.js]]></title><description><![CDATA[<p>SVG(Scalable Vector Graphics) is XML-based image formats that can look crisp at all screen resolutions, can have super small file sizes, and can be easily edited and modified. There are many approaches to creating icon system like using icon fonts or loading external SVG. However, using 'Inline SVG' is</p>]]></description><link>https://sujinlee.me/vue-svg-icon-system/</link><guid isPermaLink="false">5f55e80e150a1a7ae0eae867</guid><category><![CDATA[vue]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Wed, 18 Sep 2019 13:32:29 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1506729623306-b5a934d88b53?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1506729623306-b5a934d88b53?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="SVG Icon System in Vue.js"><p>SVG(Scalable Vector Graphics) is XML-based image formats that can look crisp at all screen resolutions, can have super small file sizes, and can be easily edited and modified. There are many approaches to creating icon system like using icon fonts or loading external SVG. However, using 'Inline SVG' is the arsenal because it doesn't make any server request which could have had an effect on website's performance. Moreover, it is possible to make editable inline icons as component in Vue.js development.</p><p>Here is a general idea of how I create Vue components for SVG icons in my projects. As an example, two ' SVG icons, 'Play' and 'Pause' used in audio player component are going to be used.</p><h2 id="preparing-optimising">Preparing &amp; Optimising</h2><p>You may easily obtain<a href="https://github.com/vkarampinis/awesome-icons"> free SVG icons </a>set as like open-licensed icons <a href="https://material.io/resources/icons/">Material Design Icons</a> or <a href="https://fontawesome.com/how-to-use/on-the-web/advanced/svg-sprites">fontawesome</a>.  SVG optimisation can reduce SVG file sizes significantly as much as 20~80% smaller. Before production phase, e<a href="https://jaydenseric.com/blog/how-to-optimize-svg">very SVG should be manually optimised vector graphic editor</a>, Illustrator or Sketch, <a href="https://github.com/svg/svgo">SVGO(SVG Optimizer)</a>, a Node.js based tool or <a href="https://jakearchibald.github.io/svgomg/">Online tool</a>.</p><p>Here are some <a href="https://varun.ca/icon-component/#prepare-svg-files">SVG techniques suggested by Varun Vachhar</a> that make it easier to control the size and color of the icon.</p><ul><li>Ensure that all the icons use <code>viewBox</code> and remove any <code>width</code> or <code>height</code> attributes. You can configure SVGO to do this for you automatically. This will make it easier to control the size of the icon.</li><li>Set the fill and stroke (or whichever of the two you are using) to <code>currentColor</code>. This sets the icon colour to be the same as the surrounding text. We can then control this colour by setting the color property on the icon element.</li></ul><p>For example, the pause SVG Icon was reduced up to 45.3%.</p><!--kg-card-begin: html--><img width="50%" src="https://sujinlee.me/content/images/2019/09/pause-optimisation.png" alt="SVG Icon System in Vue.js"><!--kg-card-end: html--><h2 id="vue-svg-loader-installation-and-configuration"> <code>vue-svg-loader</code> installation and configuration</h2><p>All SVG codes should be embedded inside HTML and it is essentially DOM which can be manipulated with CSS and Javascript. <a href="https://github.com/visualfanatic/vue-svg-loader">vue-svg-loader</a> allows you to inlines SVG as Vue components and each imported SVG you import is optimised on-the-fly using powerful SVGO. After install package, configure new loader in webpack, Vue CLI, or Nuxt.js.</p><pre><code class="language-shell">npm i -D vue-svg-loader vue-template-compiler
 
yarn add --dev vue-svg-loader vue-template-compiler
</code></pre><h6 id="vue-cli">vue CLI</h6><pre><code class="language-javascript">module.exports = {
  chainWebpack: (config) =&gt; {
    const svgRule = config.module.rule('svg');
    svgRule.uses.clear();
    svgRule
      .use('vue-svg-loader')
      .loader('vue-svg-loader');
  },
};
</code></pre><h2 id="dynamic-imports-component-">Dynamic <code>imports</code> + <code>&lt;component /&gt;</code></h2><p>Let's assumes that icons are stored in <code>src/icons</code> folder. If every icon's SVG <code>viewbox</code> attribute would be same value, we may extract SVG's <code>path</code> and make vue component itself. </p><h6 id="pause-svg">pause.svg</h6><pre><code class="language-html">&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 373 373"&gt;&lt;path d="M301.201 7.721V349.19c0 4.269-3.457 7.729-7.715 7.729h-63.705a7.727 7.727 0 0 1-7.727-7.729V7.721c0-4.263 3.459-7.721 7.727-7.721h63.705c4.258 0 7.715 3.458 7.715 7.721zM127.142 0H63.438a7.72 7.72 0 0 0-7.721 7.721V349.19c0 4.269 3.455 7.729 7.721 7.729h63.703a7.726 7.726 0 0 0 7.723-7.729V7.721A7.722 7.722 0 0 0 127.142 0z"/&gt;&lt;/svg&gt;</code></pre><h6 id="pause-vue">pause.vue</h6><pre><code class="language-html">&lt;template&gt;
  &lt;path
    d="M301.201 7.721V349.19c0 4.269-3.457 7.729-7.715 7.729h-63.705a7.727 7.727 0 0 1-7.727-7.729V7.721c0-4.263 3.459-7.721 7.727-7.721h63.705c4.258 0 7.715 3.458 7.715 7.721zM127.142 0H63.438a7.72 7.72 0 0 0-7.721 7.721V349.19c0 4.269 3.455 7.729 7.721 7.729h63.703a7.726 7.726 0 0 0 7.723-7.729V7.721A7.722 7.722 0 0 0 127.142 0z"
  /&gt;
&lt;/template&gt;
</code></pre><h6 id="svg-icon-vue">svg-icon.vue</h6><p>It's time to create base component <code>svg-icon.vue</code>.  Start base code with <code>&lt;svg .../&gt;</code> that used in icon SVG, remove <code>width</code> and <code>height</code> and make parent element <code>&lt;div&gt;</code>.  <code>&lt;svg width="100%"&gt; </code> forces to make an SVG fit to the parent container 100%. <code>preserveAspectRatio="xMidYMid meet"</code> uniforms scaling for both the <code>x</code> and <code>y</code>, aligning the midpoint of the SVG object with the midpoint of the parent container.   </p><p>Next, group <code>&lt;svg-icon-type /&gt;</code> with <code>&lt;g/&gt;</code> to fill color, wrap with <code>&lt;svg /&gt;</code> tag, add <code>width</code>, <code>height</code>, <code>iconColor</code>, <code>iconType</code> props which can be dynamically updated.</p><pre><code class="language-javascript">&lt;template&gt;
  &lt;div :style="iconStyle"&gt;
    &lt;svg
      width="100%"
      preserveAspectRatio="xMidYMid meet"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 373 373"
    &gt;
      &lt;g :fill="iconColor"&gt;
        &lt;svg-icon-type /&gt;
      &lt;/g&gt;
    &lt;/svg&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  name: "SvgIcon",
  components: {},
  props: {
    iconType: {
      type: String,
      default: () =&gt; null
    },
    iconColor: {
      type: String,
      default: "currentColor"
    },
    width: {
      type: [Number, String],
      default: 18
    },
    height: {
      type: [Number, String],
      default: 18
    }
  },
  computed: {
    iconStyle() {
      const { width, height } = this;
      return {
        width: `${width}px`,
        height: `${height}px`
      };
    }
  }
};
&lt;/script&gt;</code></pre><p>You may notice that we can insert icon name instead of <code>&lt;svg-icon-type&gt;</code> and it is possible to switch between components dynamically. Vue.js supports <a href="https://vuejs.org/v2/guide/components-dynamic-async.html#keep-alive-with-Dynamic-Components">dynamic components</a>. A built-in component named <code>&lt;component /&gt;</code> acts as a placeholder for another component accepts a special <code>:is</code> prop with the name of the component it should render. <code>:is</code> prop represents registered component or component’s options object.</p><pre><code class="language-html">&lt;template&gt;
  &lt;div class="svg-icon" :style="iconStyle"&gt;
    &lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 373 373"&gt;
      &lt;g :fill="iconColor"&gt;
        &lt;component :is="iconLoader" /&gt;
      &lt;/g&gt;
    &lt;/svg&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre><p>Then how to load component? Inside computed properties, we can dynamically load specific component as a ES module. It reduces unnecessary tasks importing and registering every component. <code>iconLoader()</code>  returns a promise that is fulfilled with the loaded icon or rejected.</p><pre><code class="language-javascript">  computed: {
    iconLoader() {
      return () =&gt; import(`@/components/icons/${this.iconType}.vue`);
    }
    //....
  },</code></pre><p>Now you can import <code>SvgIcon</code> component and pass props.</p><h4 id="button-vue">button.vue</h4><pre><code class="language-javascript">&lt;template&gt;
    &lt;button&gt;
    	&lt;svg-icon width="50" height="50" iconType="play" iconColor="red" /&gt;
    &lt;/button&gt;
&lt;/template&gt;</code></pre><p> Suppose toggle between play and pause icons in audio player.</p><pre><code class="language-javascript">&lt;template&gt;
  &lt;button @click="playing = !playing"&gt;
    &lt;svg-icon
      width="50"
      height="50"
      :iconType="playing ? 'pause' : 'play'"
      iconColor="red"
    /&gt;
  &lt;/button&gt;
&lt;/template&gt;

&lt;script&gt;
import SvgIcon from "./svg-icon.vue";

export default {
  components: {
    SvgIcon
  },
  data() {
    return {
      playing: false
    };
  }
};
&lt;/script&gt;</code></pre><p>However, the play component may not be switched to pause after clicking button because <code>iconLoader</code> is not recalculating when prop changes. To trigger the import by evaluating the function, component should be connected with a  <code>:is=</code>.  It is inevitable that pause component isn't imported so that <code>&lt;component /&gt;</code> can't accept. </p><p>All icon components that you intend to use should be registered as components.</p><pre><code class="language-javascript">export default {
  name: "SvgIcon",
  components: {
    play: () =&gt; import("@/components/icons/play.vue"),
    pause: () =&gt; import("@/components/icons/pause.vue")
  },
  //...
 }</code></pre><h2 id="module-system">Module System</h2><p>The <code>components/icons</code>  folder where all icons are stored can be modularised with main entry point<code>index.js</code> where imports and exports the component from the self-contained component directory.</p><h4 id="components-icons-index-js">components/icons/index.js</h4><pre><code class="language-javascript">import pause from "./pause.vue"; 
import play from "./play.vue";
export default { pause, play };</code></pre><p>Simply, using object spread makes easier to register all icon components. </p><pre><code class="language-javascript">import icons from "@/components/icons";
export default {
  name: "svg-icons",
  components: {
  	...icons
  },
  //...
}</code></pre><p>If there are over 100 icons in folder, typing <code>import</code> and <code>export</code> over 100 times manually might be tiresome.</p><p>Fortunately, Webpack and Vue CLI 3 supports <code>require.context</code>  to register components globally and automatically. This is an example of the code which imports base components in entry file, <code>icons/index.js</code>. </p><pre><code class="language-javascript">import Vue from "vue";
import upperFirst from "lodash/upperFirst";
import camelCase from "lodash/camelCase";

// https://webpack.js.org/guides/dependency-management/#require-context
const requireComponent = require.context(
  // Look for files in the current directory
  "./",
  // Do not look in subdirectories
  false,
  // Only include "_base-" prefixed .vue files
  /[\w-]+\.vue$/
);

// For each matching file name...
requireComponent.keys().forEach(fileName =&gt; {
  // Get the component config
  const componentConfig = requireComponent(fileName);
  // Get the PascalCase version of the component name
  const componentName = upperFirst(
    camelCase(
      fileName
        // Remove the "./_" from the beginning
        .replace(/^\.\/_/, "")
        // Remove the file extension from the end
        .replace(/\.\w+$/, "")
    )
  );
  // Globally register the component
  Vue.component(componentName, componentConfig.default || componentConfig);
});
</code></pre><h3 id="demo">Demo</h3><!--kg-card-begin: html--><iframe src="https://codesandbox.io/embed/svg-icon-system-in-vuejs-k212n?fontsize=14" title="svg icon system in vue.js" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe><!--kg-card-end: html--><p></p><h2 id="simplified-solution">Simplified Solution </h2><p>So far, every icon is Vue component.<a href="https://medium.com/@andrejsabrickis/get-rid-of-bulky-icon-fonts-with-a-tiny-vue-svg-icon-component-ad9e953664bc"> Another </a>very simplified scenario is making the object of icon which presents SVG's path and rendering SVG icon dynamically corresponding prop value.</p><h3 id="icons-js">icons.js</h3><pre><code class="language-javascript">const ICONS = {
  pause:
    "M301.201 7.721V349.19c0 4.269-3.457 7.729-7.715 7.729h-63.705a7.727 7.727 0 0 1-7.727-7.729V7.721c0-4.263 3.459-7.721 7.727-7.721h63.705c4.258 0 7.715 3.458 7.715 7.721zM127.142 0H63.438a7.72 7.72 0 0 0-7.721 7.721V349.19c0 4.269 3.455 7.729 7.721 7.729h63.703a7.726 7.726 0 0 0 7.723-7.729V7.721A7.722 7.722 0 0 0 127.142 0z",
  play:
    "M61.792 2.588A19.258 19.258 0 0 1 71.444 0c3.33 0 6.663.864 9.655 2.588l230.116 167.2a19.327 19.327 0 0 1 9.656 16.719 19.293 19.293 0 0 1-9.656 16.713L81.099 370.427a19.336 19.336 0 0 1-19.302 0 19.333 19.333 0 0 1-9.66-16.724V19.305a19.308 19.308 0 0 1 9.655-16.717z"
};

export default ICONS;</code></pre><h4 id="svg-icon-vue-1">svg-icon.vue</h4><pre><code class="language-javascript">&lt;template&gt;
  &lt;div :style="iconStyle"&gt;
    &lt;svg
      width="100%"
      preserveAspectRatio="xMidYMid meet"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 373 373"
    &gt;
      &lt;g :fill="iconColor"&gt;
        &lt;path :d="path" /&gt;
      &lt;/g&gt;
    &lt;/svg&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import ICONS from './icon.js';

export default {
  name: 'SvgIcon',
  components: {},
  props: {
    iconType: {
      type: String
    },
    iconColor: {
      type: String,
      default: 'currentColor'
    },
    width: {
      type: [Number, String],
      default: 18
    },
    height: {
      type: [Number, String],
      default: 18
    }
  },

  computed: {
    iconStyle () {
      const {width, height} = this
      return {
        width: `${width}px`,
        height: `${height}px`
      }
    },
    path () {
      return ICONS[this.iconType]
    }
  }
}
&lt;/script&gt;
</code></pre><hr><p>This post is a gentle introduction to how to prepare SVG and load inline SVG with vue-svg-loader, and make the dynamic and reusable component. Which one do you prefer to set up your own SVG icon system in Vue.js?</p><h6 id="references">References</h6><ul><li>Alex Scott, <a href="https://medium.com/@codetheorist/using-vuejs-computed-properties-for-dynamic-module-imports-2046743afcaf">Using VueJS computed properties for dynamic module imports</a></li><li>Andrejs Abrickis, <a href="https://medium.com/@andrejsabrickis/get-rid-of-bulky-icon-fonts-with-a-tiny-vue-svg-icon-component-ad9e953664bc">A tiny Vue SVG icon component — an alternative to the icon-fonts</a></li><li>Jayden Seric, <a href="https://jaydenseric.com/blog/how-to-optimize-svg">How to optimize SVG</a></li><li>Varun Vachhar, <a href="https://varun.ca/icon-component/">Component based SVG Icon System</a></li><li>Vue.js Official Doc, <a href="https://vuejs.org/v2/guide/components-dynamic-async.html">Dynamic &amp; Async Components</a></li><li>Vue.js Official Doc, <a href="https://vuejs.org/v2/guide/components-registration.html#Automatic-Global-Registration-of-Base-Components">Automatic Global Registration of Base Components</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Talk: Exploring NDP Songs 
Lyrics, Sounds, Emotion]]></title><description><![CDATA[Straits Times Graphics team published National Day Parade songs project to celebrate the 54th National Day Birthday at the end of June. Since 1984, 26 NDP (National Day Parade) songs were designed to help build Singapore’s national identity by covering various themes and music styles. In this project, three visual graphic types were used; bag-of-words table, radar chart, and scatter chart.]]></description><link>https://sujinlee.me/ndpsongs-lyrics-sounds-emotion/</link><guid isPermaLink="false">5f55e80e150a1a7ae0eae865</guid><category><![CDATA[talk]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Sat, 14 Sep 2019 08:37:07 GMT</pubDate><media:content url="https://sujinlee.me/content/images/2019/09/JuniorDevSG-Code-and-Tell---Sep-2019-Exploring-NDP-Songs--Lyrics--Sounds--Emotion.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: html-->
<iframe src="https://docs.google.com/presentation/d/e/2PACX-1vSTL99l1H6pfWstz8LgqR6tqRQW6Dro_qhJkCPONDg_LuhyCJZLtnZzP9ojrO8GwFvl8cnK8sYCu9N-/embed?start=false&loop=false&delayms=3000" frameborder="0" width="100%" height="500" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe><!--kg-card-end: html--><img src="https://sujinlee.me/content/images/2019/09/JuniorDevSG-Code-and-Tell---Sep-2019-Exploring-NDP-Songs--Lyrics--Sounds--Emotion.png" alt="Talk: Exploring NDP Songs 
Lyrics, Sounds, Emotion"><p></p><p>Event : <a href="https://www.meetup.com/ko-KR/Junior-Developers-Singapore/events/264455252/">Junior Developers Singapore Code and Tell - Sep 2019</a></p><p>Straits Times Graphics team published <a href="http://str.sg/ndpsongs19">National Day Parade(NDP) songs project</a> to celebrate the 54th National Day Birthday at the end of June. Since 1984, 26 NDP songs were designed to help build Singapore’s national identity by covering various themes and music styles. In this project, three visual graphic types were used; heatmap, radar chart, and scatter chart.<br><br>First, we found how NDP song lyrics have changed over the years using basic NLP techniques. Second, we looked inside audio feature information of each song by using Spotify API and visualized to the radar chart in order to compare old and remix versions. Lastly, inspired by Russell's circumplex model of emotion, we designed interactive and dynamic user music emotion chart.<br><br>In this talk, I delivered how the music emotion chart was built with Vue.js mainly. Also, I explained how to extract sounds and lyrics information to enhance the visual story.</p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">To celebrate 🇸🇬&#39;s 54th Birthday, <a href="https://twitter.com/hashtag/stvisuals?src=hash&amp;ref_src=twsrc%5Etfw">#stvisuals</a> published National Day Parade songs project last month. Come and play with the <a href="https://twitter.com/hashtag/interactive?src=hash&amp;ref_src=twsrc%5Etfw">#interactive</a> quadrant graph which shows your/users <a href="https://twitter.com/hashtag/NDP?src=hash&amp;ref_src=twsrc%5Etfw">#NDP</a> song preferences. 🎤<br>👉 <a href="https://t.co/j8244LER7M">https://t.co/j8244LER7M</a> <br>💚 Made awesomeness with <a href="https://twitter.com/vuejs?ref_src=twsrc%5Etfw">@vuejs</a> <a href="https://t.co/dv5XNPIKbM">pic.twitter.com/dv5XNPIKbM</a></p>&mdash; Sujin Lee 👩‍🦰 (@sujinleeme) <a href="https://twitter.com/sujinleeme/status/1160511244631072768?ref_src=twsrc%5Etfw">August 11, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">👩‍🎤Wow, <a href="https://twitter.com/sujinleeme?ref_src=twsrc%5Etfw">@sujinleeme</a> &#39;s talk about using natural language processing and <a href="https://twitter.com/Spotify?ref_src=twsrc%5Etfw">@spotify</a> to look at Singapore&#39;s National Day Parade songs was amazing!<br><br>📖If you haven&#39;t been around for <a href="https://twitter.com/JuniorDevSG?ref_src=twsrc%5Etfw">@JuniorDevSG</a> tonight, you can read all about it at <a href="https://t.co/UcuiPBjTH2">https://t.co/UcuiPBjTH2</a> <a href="https://t.co/CPm6Fhx4Nd">pic.twitter.com/CPm6Fhx4Nd</a></p>&mdash; Alex Lakatos 🥑🇸🇬 (@lakatos88) <a href="https://twitter.com/lakatos88/status/1172486461469466624?ref_src=twsrc%5Etfw">September 13, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure>]]></content:encoded></item><item><title><![CDATA[뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (2) 테마 커스터마이징]]></title><description><![CDATA[<p>뚝딱뚝딱 Ghost로 기술 블로그 만들기  두 번째 시리즈로 고스트 블로그 테마 수정 과정을 소개한다.  아직 고스트(Ghost) 플랫폼에 대해서 잘  모른다면, <a href="https://sujinlee.me/how-to-build-ghost-blog/">설치와 호스팅</a> 편을 읽고 오길 바란다. 모든 단계를 건너뛰고 바로 이 블로그에 적용된 테마를 그대로 적용하거나 수정하고 싶다면,<a href="https://github.com/sujinleeme/casper-dev-blog-theme"> 깃허브 저장소(casper-dev-blog-theme)</a> 에서 소스코드를 다운받아 설치할 수 있다.</p><h2 id="-"> 사전 설치</h2>]]></description><link>https://sujinlee.me/how-to-build-ghost-blog-2/</link><guid isPermaLink="false">5f55e80e150a1a7ae0eae862</guid><category><![CDATA[career]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Tue, 10 Sep 2019 19:54:39 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1518331483807-f6adb0e1ad23?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1518331483807-f6adb0e1ad23?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (2) 테마 커스터마이징"><p>뚝딱뚝딱 Ghost로 기술 블로그 만들기  두 번째 시리즈로 고스트 블로그 테마 수정 과정을 소개한다.  아직 고스트(Ghost) 플랫폼에 대해서 잘  모른다면, <a href="https://sujinlee.me/how-to-build-ghost-blog/">설치와 호스팅</a> 편을 읽고 오길 바란다. 모든 단계를 건너뛰고 바로 이 블로그에 적용된 테마를 그대로 적용하거나 수정하고 싶다면,<a href="https://github.com/sujinleeme/casper-dev-blog-theme"> 깃허브 저장소(casper-dev-blog-theme)</a> 에서 소스코드를 다운받아 설치할 수 있다.</p><h2 id="-"> 사전 설치</h2><p><a href="https://nodejs.org/">Node</a> , <a href="https://yarnpkg.com/">Yarn</a>,  <a href="https://gulpjs.com">Gulp</a>가 반드시 설치되어야 한다.</p><p> ghost-cli 패키지를 전역으로 설치한다. ghost-cli는 고스트를 설치 및 구성하고 항상 최신 상태로 유지할 수 있는 명령형 인터페이스이다.</p><pre><code class="language-shell">npm install ghost-cli@latest -g</code></pre><h2 id="--1">고스트 블로그 설치</h2><p>먼저 새로운 프로젝트 폴더를 만들고 아래 <code>ghost install local</code> 명령어를 실행해 고스트를 로컬에 설치한다. </p><pre><code class="language-shell">$ mkdir ghost-blog &amp;&amp; cd ghost-blog 
$ ghost install local</code></pre><p> 설치가 정상적으로 되었다면 콘솔에 아래와 같은  메시지가 보일 것이다.</p><pre><code>Ghost was installed successfully! To complete setup of your publication, visit: 

    http://localhost:2368/ghost/</code></pre><p> 설치 후 자동으로 로컬 서버가 실행된다. <code><a href="http://localhost:2368/">http://localhost:2368</a></code> 을 열어보면 고스트 블로그 첫 화면을 볼 수 있다. <code>http://localhost:2368/ghost</code> 에서 관리자를 생성하고 테스트 글을 작성 또는 수정해보자. ghost-cli는 SQLite3를 기본 탑재되어 있으며 모든 데이터는 <code>/content/data</code> 폴더에 저장된다.  </p><p>고스트는 ghost 명령어로 중지할 때까지 계속 실행된다. </p><ul><li><code>ghost stop</code>  고스트를 중지한다.</li><li><code>ghost start</code> 고스트를 시작한다.</li><li><code>ghost log</code> 로그를 출력한다.</li><li><code>ghost ls</code> 설치된 모든 고스트 블로그 목록을 확인한다.</li></ul><h2 id="-casper-"> Casper 테마 커스터마이징</h2><p>고스트 블로그 테마가 저장되는  <code>content/themes</code> 폴더를 열어보면 기본 테마인 <code>casper</code> 가 있음을 확인할 수 있다. <code>casper</code> 폴더 전체를 복사하고 폴더 이름을 바꾼다. (예: <code>casper-sujin</code>) </p><blockquote> 만약 실수로 casper 폴더가 삭제되었거나 문제가 생겼다면 <code>content/themes</code> 경로에서깃허브 저장소 <a href="https://github.com/TryGhost/Casper">https://github.com/TryGhost/Casper</a> 에서 다운 받는다.</blockquote><p> <code>content/themes/casper-sujin</code> 경로에서 의존 패키지를 설치하고 로컬 서버를 실행한다. (이 때 <code>ghost</code> 가 실행되어 있어야 한다.)</p><pre><code>yarn install
yarn dev</code></pre><p> gulp 가 실행되어  <code>content/themes/</code> 내 css, js, hbs 파일이 변경될 때마다 소스코드가 자동으로 빌드되어<code>assets/built/</code> 에 저장된다.  <code>/assets/built</code> 폴더는 건드리지  않는다.</p><p> 관리자 화면에서 디자인 패널을 클릭해(<a href="http://localhost:2369/ghost/#/settings/design"><code>http://localhost:2368/ghost/#/settings/design</code></a>) 새로 만든 테마인 <code>casper-sujin</code> 옆 <code>activate</code> 버튼을 클릭해 블로그 테마를 바꾼다.</p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/09/Screenshot-2019-09-03-at-3.31.16-PM.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (2) 테마 커스터마이징"></figure><p> 이제  기본적인 설치가 끝났으니 본격적으로 커스터 마이징을 해보도록 하자.</p><h2 id="1-disqus-">1. disqus 댓글 플러그인 설치</h2><p><a href="https://disqus.com/admin/create/">https://disqus.com/admin/create/</a> 에서 disqus 사이트를 생성한다. 이때  내 disqus 게정 주소가 (e.g <a href="http://myblog-kqs1q8jfxp.disqus.com/">http://myblog.disqus.com</a>)이라면 myblog가 shortname 이 된다.</p><p>이 shortname는 전역 변수를 사용해 정의할 것이다.</p><p>이제  <code>post.hbs</code> 파일의  87번째 줄에 비활성된 코드 <code>&lt;section <em>class=</em>"post-full-comments"&gt;</code>를 찾을 수 있을 것이다. 이 부분에 바로 disqus 스크립트를 넣을 수 있지만,댓글 컴포넌트로 만들 수 있다. <code>partials</code> 폴더 내  <code>disqus.hbs</code> 새 파일을 만들고 <a href="https://github.com/sujinleeme/casper-dev-blog-theme/blob/master/partials/disqus.hbs">아래 코드를  복사 붙여넣기 한다.</a></p><pre><code class="language-html">&lt;section class="post-full-comments"&gt;
  &lt;div id="disqus_thread"&gt;&lt;/div&gt;
  &lt;script&gt;
    var disqus_config = function () {
      this.page.url = "{{url absolute="true"}}";
      this.page.identifier = "ghost-{{comment_id}}"
    };
    (function () {
      var d = document, s = d.createElement('script');
      s.src = 'https://' + window.disqus_shortname + '.disqus.com/embed.js';
      s.setAttribute('data-timestamp', +new Date());
      (d.head || d.body).appendChild(s);
    })();
  &lt;/script&gt;
&lt;/section&gt;
</code></pre><p> 게시물 템플릿인 <code>post.hbs</code> 에서 <code>&lt;/article&gt;</code> 부분에 <code>{{&gt; disqus}}</code> 를 추가한다. 이 부분이 바로 <a href="https://github.com/sujinleeme/casper-dev-blog-theme/blob/master/post.hbs#L73">댓글 컴포넌트가 들어갈 자리이다.</a></p><p> 관리자 페이지 메뉴 <code>settings &gt; code-injection</code> 페이지로 들어가  Site Footer 부분에 스크립트 태그에 <code>disqus_shortname</code>을 정의한다.</p><pre><code class="language-html">&lt;script&gt;
    var disqus_shortname = ""
&lt;/script&gt;</code></pre><p> disqus 플러그인은 localhost 로컬 웹 서버를  지원하지 않기 때문에 아래와 같은 메시지가 보일 것이다.</p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/09/Screenshot-2019-09-03-at-4.12.26-PM.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (2) 테마 커스터마이징"></figure><h2 id="2-"> 2. 테마 교체하기</h2><p> 아직 해야할 일이 더 많이 남아있지만, 수정한 테마를 압축하고 반영하는 방법을 알아보자.</p><p>gulp 테스크 패키지 명령어인 <code>zip</code> 실행하면 <code>dist/&lt;테마&gt;.zip</code> 폴더가 만들어진다.</p><pre><code>yarn zip</code></pre><p> 실제 운영 중인 블로그 관리자 페이지(<a href="https://sujinlee.me/ghost/#/settings/design/uploadtheme">https://블로그주소/ghost/#/settings/design/uploadtheme</a>)에 들어가서 <code>zip</code> 파일을 업로드하고 activate버튼을 눌러 테마를 바꿀 수 있다. <br><br> 다시 관리자 화면 내 code injection 페이지(<a href="https://sujinlee.me/ghost/#/settings/code-injection">https://블로그-주소/ghost/#/settings/code-injectio</a>n)에서 전역 변수를 선언한다. 블로그 게시글 하단에 disqus 댓글 플러그인이 잘 실행되는지 확인하자.</p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/09/main.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (2) 테마 커스터마이징"></figure><h2 id="3-prism-js-">3. Prism.js 코드 하이라이트 기능 추가하기</h2><p> 코드 태그 구문에 문법을 강조하기 위해 구문 하이라이트(Syntax Highlighter) 라이브러리인 <a href="https://prismjs.com/">Prism.js</a>을 설치할 단계다.</p><p> <a href="https://sujinlee.me/ghost/#/settings/code-injection">https://블로그-주소/ghost/#/settings/code-injecti</a>on 에서 SiteHeader과 SiteFooter에 CDN을 추가해 사용할 수 있지만, 여기서는 기존 템플릿 코드를 수정한다.<br><br><a href="https://prismjs.com/">Prism.js</a> 에서사용하고자 하는 언어를 선택한 후 각각  js와 css 파일을 다운받을 수 있다. 또는  깃허브 저장소에서 <a href="https://github.com/sujinleeme/casper-dev-blog-theme/blob/master/assets/js/prism.js">js</a>와 <a href="https://github.com/sujinleeme/casper-dev-blog-theme/blob/master/assets/css/prism.css">css</a> 파일을 다운 받을 수 있다.  이제 <code>themes/&lt;테마-이름&gt;/assets/</code> 에서 <code>js</code>, <code>css</code> 각 폴더에 저장한다. </p><p>앞에서 댓글 컴포넌트를 만든 것과 마찬가지로 이번에도 별도 파일을 만들어 관리할 수 있다. <a href="https://github.com/sujinleeme/casper-dev-blog-theme/tree/master/partials"><code>partials</code></a> 폴더에 <code>prism.hbs</code> 파일을 만들어 아래 코드를 추가한다.</p><pre><code class="language-html">&lt;link rel="stylesheet" type="text/css" href="{{asset "built/prism.css"}}" /&gt;
&lt;script type="text/javascript" src="{{asset "built/prism.js"}}"&gt;&lt;/script&gt;
</code></pre><p><code>default.hbs</code>는  기본 템플릿으로 code-injection 에서 정의된 <code>{{ghost_head}}</code>, <code>{{ghost_foot}}</code> 을 포함한다. <code>&lt;header&gt;</code> 태그 내에 <code>{{&gt;prism}}</code> 을 <a href="https://github.com/sujinleeme/casper-dev-blog-theme/blob/master/default.hbs#L25">추가한다.</a></p><h2 id="4-katex-">4. 수학 수식 KaTeX 추가하기</h2><p><a href="https://github.com/Khan/KaTeX">KaTeX</a>는 칸 아카데미에서 만든 오픈 소스 수학 입력 수식기 라이브러리이다. 기존 LaTeX는 이미지 형식으로 변환되어 깔끔하게 렌더링되지 않은 문제가 있었다. KaTeX는 수학 수식을 SVG로 그려 모든 웹 브라우저에서 사용 가능하다. 깔끔한 타이포와 빠른 성능, 서버 사이드 렌더링이 큰 장점이다. MathJax보다 렌더링 속도가 더 빠르고 기존 LaTeX 문법과 거의 유사하다.</p><p> 이번에도 <code>partials</code> 폴더에서 <code>katex.hbs</code> 파일을 만들고 아래 코드를 추가한다.</p><pre><code class="language-html">&lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.11.0/dist/katex.min.css"
  integrity="sha384-BdGj8xC2eZkQaxoQ8nSLefg4AV4/AwB3Fj+8SUSo7pnKP6Eoy18liIKTPn9oBYNG" crossorigin="anonymous"&gt;

&lt;script defer src="https://cdn.jsdelivr.net/npm/katex@0.11.0/dist/katex.min.js"
  integrity="sha384-JiKN5O8x9Hhs/UE5cT5AAJqieYlOZbGT3CHws/y97o3ty4R7/O5poG9F3JoiOYw1" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;script defer src="https://cdn.jsdelivr.net/npm/katex@0.11.0/dist/contrib/auto-render.min.js"
  integrity="sha384-kWPLUVMOks5AQFrykwIup5lo0m3iMkkHrD0uJ4H5cjeGihAutqP0yW0J6dpFiVkI" crossorigin="anonymous"&gt;&lt;/script&gt;

&lt;script type="text/javascript"&gt;
  document.addEventListener("DOMContentLoaded", function () {
    renderMathInElement(document.body, {
      delimiters: [
        { left: "$$", right: "$$", display: true },
        { left: "$", right: "$", display: false },
      ]
    });
  });
&lt;/script&gt;
</code></pre><p> 구분자(delimiters) 를 지정해 마크업과 혼용해 수학수식을 사용할 수 있다. 인라인 수식일 경우 <code>$</code>, 블록일 경우 <code>$$</code> 로 수식을 열고 닫는다.</p><!--kg-card-begin: html--><table class="tg">
  <tr>
    <th class="tg-0pky"></th>
    <th class="tg-0pky">Markdown + kaTex</th>
    <th class="tg-0pky">Render as</th>
  </tr>
  <tr>
    <td class="tg-0pky">inline</td>
      <td class="tg-0pky"><pre>The quadratic formula is $-b \pm \sqrt{b^2 - 4ac} \over 2a$</pre><br></td>
    <td class="tg-0pky">The quadratic formula is $-b \pm \sqrt{b^2 - 4ac} \over 2a$<br></td>
  </tr>
  <tr>
    <td class="tg-0pky">block</td>
      <td class="tg-0pky"><pre>The quadratic formula is $$-b \pm \sqrt{b^2 - 4ac} \over 2a$$</pre></td>
    <td class="tg-0pky">The quadratic formula is $$-b \pm \sqrt{b^2 - 4ac} \over 2a$$</td>
  </tr>
 </table>
<!--kg-card-end: html--><p> <code>default.hbs</code> 파일 내 <code>&lt;head&gt;</code> 태그에 <code>{{&gt; katex}}</code> 를 <a href="https://github.com/sujinleeme/casper-dev-blog-theme/blob/master/default.hbs#L27">추가한다.</a>  수학 수식을 입력해보고 어떻게 렌더링되는지 확인해보자.<br></p><h2 id="5-css-">5. 폰트 수정 및 css 스타일링</h2><p>한국어와 영어를 모두 지원하는 폰트로 바꿔보자. 구글 웹 폰트 CDN를 사용할 수 있지만 파일 크기가 너무 크면 <a href="https://medium.com/clio-calliope/making-google-fonts-faster-aadf3c02a36d">웹 폰트가 적용된 글자가 화면에 표시될 때까지 시간이 지연되는 문제가 생긴다. </a> 최적화를 위해 합축된 폰트 형식인 WOFF와 WOFF2을 셀프 호스팅해 사용하는 것이 좋다. WOFF2 형식은 30~50% 더 압축된 형식이다.  따라서 폰트 파일과 CSS 속성을 완전히 제어할 수 있어야 한다. 마리오 랜플(Mario Ranftl)이 제작한 <a href="https://google-webfonts-helper.herokuapp.com/fonts">google-webfonts-helper</a> 을 사용해 브라우저 사양, charsets, 글꼴 타입에 따라 @font-face 이 정의된 css 코드를 자동으로 만들 수 있다.  </p><p> 예를 들어, 구글 웹 폰트 중 Noto Sans KR 폰트를 사용한다고 가정해보자.  regular과 700 타입을 선택한 후 Copy CSS에서 최신 브라우저 용인 Modern Browsers를 선택한 후 다운로드 한다.</p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/09/font.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (2) 테마 커스터마이징"></figure><p>  <code>assests</code> 내 <code>fonts</code> 새 폴더를 만들고 다운 받은 폰트 파일을 저장한다. <code>css</code> 내 <code>fonts.css</code> 새 파일을 만들고 생성된 <a href="https://github.com/sujinleeme/casper-dev-blog-theme/blob/master/assets/css/fonts.css">css를 추가한다.</a></p><p> <code>default.hbs</code> 내 <code>&lt;header&gt;</code> 에 <code>fonts.css</code> 파일을 링크한다.</p><pre><code class="language-html">&lt;link rel="stylesheet" type="text/css" href="{{asset "built/fonts.css"}}" /&gt;</code></pre><p>이후 글 제목, 본문에 css 폰트 속성을 <code>Noto Sans KR</code> 으로 바꾼다.  별도로 css 파일을 만들어 클래스 속성을 <a href="https://github.com/sujinleeme/casper-dev-blog-theme/blob/master/assets/css/custom.css">오버라이딩해 재정의하면 보다 편하게 스타일을 관리할 수 있다.</a></p><h2 id="6-let-s-encrypt-">6. let's encrypt 보안 인증서 확인하기</h2><p>ghost-cli는 let's encrypt 보안인증서가 기본적으로 내장되어 있다.<a href="https://ghost.org/integrations/lets-encrypt/"> 인증서가 만료되더라도  쉽게 갱신 가능하다.</a> 크롬 브라우저에서 주소 창 왼쪽 자물쇠 버튼을 클릭하면 보안 인증서와 만료일을 확인할 수 있다.</p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/09/letsencrpyt.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (2) 테마 커스터마이징"></figure><p> 만약 인증서가 만료되었거나 오류가 생겼다면, 로컬 콘솔에서 ssh로 접속해 <code>ghost setup ssl</code> 를 실행해 인증서를 다시 받으면 된다. ghost 명령어는 반드시 ghost가 설치된 폴더에서 실행 가능함을 잊지 말자.</p><pre><code class="language-shell">$ ssh root@ip주소
$ root@ghost-blog:~#sudo -i -u ghost-mgr // ghost 관리자 계정으로 전환한다.
$ ghost-mgr@ghost-blog:/var/www$ cd /var/www/ghost // ghost가 설치된 경로로 이동한다.
$ ghost setup ssl // 인증서를 갱신한다.

</code></pre><p> 만약 고스트 초기 설치 시 url이 https가 아니라 http로 설정되어 있을 경우, 크롬에서 <a href="https://developers.google.com/web/fundamentals/security/prevent-mixed-content/fixing-mixed-content?hl=ko">혼합 콘텐츠 오류 메시지(Mixed Content)</a>가 출력될 수 있다. </p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/09/Screenshot-2019-09-11-at-2.45.05-PM.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (2) 테마 커스터마이징"></figure><p> 이미지 주소가 http를 가리키므로 https 페이지에서 혼합 콘텐츠가 있음을 경고하는 메시지이다. <code>ghost config url </code><a href="https://xn-----p02ii4gbun7siyjggtq"><code>https://내-블로그-주소</code></a><code>&amp;&amp; ghost restart</code> 명령어를 실행해 다시 고스트에 url를 https로 설정하고 재실행한다.</p><pre><code class="language-shell">$ ghost-mgr@ghost-blog:/var/www/ghost$ ghost config url https://내-블로그-주소 &amp;&amp; ghost restart
Successfully set 'url' to 'https://내-블로그-주소'
+ sudo systemctl is-active ghost_sujinlee-me
+ sudo systemctl restart ghost_sujinlee-me
✔ Restarting Ghost</code></pre><hr><p> 지금까지 뚝딱뚝딱 망치질을 하며 나만의 기술 블로그를 만들어 보았다.  ghost 기본 테마인 casper에 기능을 추가하고 템플릿과 스타일을 바꾸어보면서 ghost 테마 개발 과정을 맛보았다.  </p><p>나의 경우 소셜 계정 바로가기 아이콘에 미디엄, 링크드인, 깃허브 등 다양한 소셜 링크를 추가할 수 있게 개선했다. 글 목록 카드 내  <code>{{author}}</code> 을 없애고, disqus 댓글 수를 추가했다. 구글 애널리틱스도 추가해 블로그 방문자의 데이터를 수집하고 분석도 해보고 있다. 이처럼 casper를 보일러 플레이트 코드로 활용해 내가 원하는 디자인으로 변경하거나 여러 기능을 추가해 나에게 꼭 맞는 맞춤형 테마를 만들 수 있다.</p><p> 이제 근사한 집을 지었으니 이야기로 하나 둘씩 차곡차곡 채워가야 할 때다. 하지만 무엇을, 어떻게, 써야할지 늘 걱정이다. 최근 나는 데이터 캠프(datacamp) 수석 데이터 과학자인 <a href="https://twitter.com/drob">데이비드 로빈슨(David Robinson)</a>은 그의 글, '<a href="http://varianceexplained.org/r/start-blog/">주목받는 데이터 과학자가 되는 법 : 블로그를 시작하라(Advice to aspiring data scientists: start a blog)</a> ' 에서 그 해답을 찾을 수 있었다. 그는 글을 쓰며 '기술을 연마하고 있다(practice the relevant skills)'라고 말했다. 블로그를 본격적으로 시작하기 전 블로그 키워드와 글쓰기 주제를 먼저 정할 것을 강조했다. 그의 글쓰기 키워드는 데이터 정리, 통계, 머신 러닝, 시각화, 커뮤니케이션으로 실무와 프로젝트를 통해 배우고 익힌 것들을 중심으로 글을 작성하고 있다. 대학원생 시절, 그는 Bayes estimation 라는 주제로 짧은 글을 작성했다. 당시 이와 관련된 자료가 부족했기에 많은 독자들로 부터 도움이 되었다는 칭찬을 들었다. 이후 그는 자신의 블로그 글을 발전시켜 책을 완성해 출판했다. 블로그를 통해 그는 전 세계 데이터 과학자들과 네트워크를 형성했고, 더 넓은 세상과 공유하고 소통할 수 있었다고 한다.</p><p>유투브 채널 심플 프로그래머로 유명한 존 소메즈(John Somez)는 그의 최신 저서, '완벽한 개발자 인생 로드맵 (The Complete Software Developer's Career Guide)'에서 '개발자에게 블로그란 제다이의 광선검'과 같은 것이라 말했다.  그는 '블로그는 자신의 경력과 발전을 기록하는데 도움이 될 뿐만 아니라 자신이 과거에 쓴 문제해결 방법을 확인해 볼 수 있는 참고자료'이라며 블로그를 운영할 것을 적극 권장했다.</p><figure class="kg-card kg-image-card"><img src="http://giphygifs.s3.amazonaws.com/media/8IZCR0wzEIQms/giphy.gif" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (2) 테마 커스터마이징"></figure><p>그는 이제 막 블로그를 시작하는 개발자들에게 '블로그를 직접 만들려 하지 말고 워드프레스와 같은 기성 솔루션을 써라'라고 거듭 강조했다. 맞는 말이다. 감히 주제넘게 꼬투리를 잡자면, 나는 워드프레스보다 고스트가 더 가장 유연하고 사용하기 쉬운 블로그 플랫폼이라 평한다. 자, 이제 동기부여 메시지는 그만하고 글 좀 써볼까?</p>]]></content:encoded></item><item><title><![CDATA[Talk: Singapore Street Visualisation  with Open Data]]></title><description><![CDATA[<p></p><h3 id="description">Description</h3><p>Recently, The Straits Times Interactive Graphics team released the map visualisation project, <a href="http://str.sg/sgstreets">The colourful history of Singapore's street names</a>, which reveals about Singapore's community, identity, and history</p><p>I gathered 3,536 Singapore streets names from geographic.org and some other resources, 69,605 Street Geojson using OpenStreet API, and</p>]]></description><link>https://sujinlee.me/singapore-streets/</link><guid isPermaLink="false">5f55e80e150a1a7ae0eae86b</guid><category><![CDATA[talk]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Thu, 29 Aug 2019 10:58:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1493966936727-e9e3da7326ea?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1493966936727-e9e3da7326ea?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Talk: Singapore Street Visualisation  with Open Data"><p></p><h3 id="description">Description</h3><p>Recently, The Straits Times Interactive Graphics team released the map visualisation project, <a href="http://str.sg/sgstreets">The colourful history of Singapore's street names</a>, which reveals about Singapore's community, identity, and history</p><p>I gathered 3,536 Singapore streets names from geographic.org and some other resources, 69,605 Street Geojson using OpenStreet API, and over 60,000 Google Maps API.</p><!--kg-card-begin: html--><iframe width="560" height="315" src="https://www.youtube.com/embed/UhYyMEvkIaQ" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><!--kg-card-end: html--><p>In this talk, I shared practical recipe to build overall map visualisation process; from map data collection to optimisation and compression. In addition, she will talk about how they enhanced user experience on mobile with Mapbox.</p><h3 id="slides">Slides</h3><!--kg-card-begin: html--><iframe src="https://docs.google.com/presentation/d/e/2PACX-1vSmTWwaldp4dAWSoU_IKnlgQjhDJP4Y8_J7YktODBltXc7OP5bhfPqH5AmaThp3GaKwvT44bj-fg3S1/embed?start=false&loop=false&delayms=3000" frameborder="0" width="100%" height="569" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe><!--kg-card-end: html--><!--kg-card-begin: html--><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Sujin is sharing her viewpoints and practical experience in how to visualize open data!<a href="https://twitter.com/hashtag/opendata?src=hash&amp;ref_src=twsrc%5Etfw">#opendata</a> <a href="https://twitter.com/hashtag/OpenSource?src=hash&amp;ref_src=twsrc%5Etfw">#OpenSource</a> <a href="https://twitter.com/hashtag/streetmap?src=hash&amp;ref_src=twsrc%5Etfw">#streetmap</a> <a href="https://t.co/Bn2Si0zdBA">pic.twitter.com/Bn2Si0zdBA</a></p>&mdash; Open UP Summit (@openupsummit) <a href="https://twitter.com/openupsummit/status/1167048059034861568?ref_src=twsrc%5Etfw">August 29, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script><!--kg-card-end: html-->]]></content:encoded></item><item><title><![CDATA[뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (1) 설치와 호스팅]]></title><description><![CDATA[<p>몇 개월 전쯤  페이스북 친구와 독자 분께서 아래와 같은 댓글을 남겨주셨다. </p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/08/readers-email.png" class="kg-image" alt></figure><p>나는 망설임 없이 '고스트(Ghost)' 라고 답했다 . 지금 이 블로그는 고스트로 디지털오션(Digital Ocean) 에서 매월 5달러로 호스팅 되고 있다.  그동안 국민 블로그인 네이버, 티스토리부터, 워드프레스, 미디엄(Medium)까지 수많은 블로그 플랫폼을 사용해봤지만 좀처럼 나에게 꼭 맞는 플랫폼을</p>]]></description><link>https://sujinlee.me/how-to-build-ghost-blog/</link><guid isPermaLink="false">5f55e80e150a1a7ae0eae85f</guid><category><![CDATA[career]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Fri, 23 Aug 2019 15:06:14 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1544816565-c199d6f5d2d3?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1544816565-c199d6f5d2d3?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (1) 설치와 호스팅"><p>몇 개월 전쯤  페이스북 친구와 독자 분께서 아래와 같은 댓글을 남겨주셨다. </p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/08/readers-email.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (1) 설치와 호스팅"></figure><p>나는 망설임 없이 '고스트(Ghost)' 라고 답했다 . 지금 이 블로그는 고스트로 디지털오션(Digital Ocean) 에서 매월 5달러로 호스팅 되고 있다.  그동안 국민 블로그인 네이버, 티스토리부터, 워드프레스, 미디엄(Medium)까지 수많은 블로그 플랫폼을 사용해봤지만 좀처럼 나에게 꼭 맞는 플랫폼을 찾기 어려웠다.  기술 블로그를 운영하기 위해 마크다운(markdown) 문법, 코드 하이라이트, 수학 수식 기능이 꼭 필요했지만 거의 대부분 플랫폼이 지원하지 않았다. 테마를 수정하거나 입맛에 맞게 커스터마이징 하는 일도 힘들었다. 이곳 저곳을 전전하다보니 글 전체 목록을 관리하거나 옮기는 일도 쉽지 않았다 회사나 플랫폼에도 종속되지 않는 독립적인 나만의 공간, 콘텐츠를 갖고 싶었다. 한동안은 <a href="https://github.com/DjangoGirls/tutorial">장고걸스 튜토리얼</a>을 따라하며 Django 웹 프레임워크로 블로그 개발도 해봤지만 쉽지 않았다.  엉망진창인 내 코드를 뜯어 고치며 유지 보수만 하다 정작 글쓰기에 흥미를 잃어버리고 말았다. 만약 글쓰기에 좀더 집중했더라면, 지금쯤 책 한권은 나왔지 않았을까. 많은 비용을 들이지 않고 글쓰기 활동에만 집중할 수 있는 편리한 환경과 도구가 필요했다. 이집저집을 떠돌다 2년 전, 오픈 소스 블로그 플랫폼인 고스트를 알게 되었다. 군더더기 없는 깔끔한 인터페이스와 글쓰기에 최적화된 사용자 경험에 만족해 지금까지 고스트로 개인 블로그를 운영하고 있다. </p><p>본 글에서는 그동안 기술 블로그를 운영하면서 느낀 고스트의 장점을 소개하고  도메인 구입부터 고스트 설치, 디지털오션 배포까지 전 과정을 친절하게 안내하고자 한다.</p><h2 id="-">유령, 고스트?  👻</h2><p>고스트는 세계에서 가장 인기있는 최신 <strong>오픈 소스 블로그 플랫폼</strong>이다. 2013년 4월 고스트 재단(고스트 재단은 비영리로 직원 모두가 원격 근무를 한다.  마치 '유령'처럼 이 곳 저 곳을 떠돌면서 말이다.)은 고스트 플랫폼을 <a href="https://www.kickstarter.com/projects/johnonolan/ghost-just-a-blogging-platform/description">킥스타터</a>에 출시하면서 유명세를 얻게 됐다. 현재 전 세계적으로 백만 육천개가 넘는 고스트 블로그가 운영되고 있고 애플, 스카이 뉴스, 틴더 등 유명 테크 업계에서도 공식 회사 블로그로 채택해 사용하고 있다. 내가 존경하는 엔지니이자 블로거인 <a href="https://blog.codinghorror.com/10-years-of-coding-horror/">코딩 호러(coding horror)</a> 역시 <a href="https://blog.codinghorror.com/10-years-of-coding-horror/">2014년부터 고스트 블로그를 사용해 지식과 배움을 나누고 있다</a>.</p><p>고스트는 블로그 설치부터 글 작성과 관리까지 모든 과정이 쉽고, 빠르고, 가볍고, 매우 간편하다. 헤드리스 Node.js 콘텐츠 관리 시스템(CMS)으로 설치형 블로그로  JSON 문서 저장 형식을 기반으로 한다. 때문에 전문적인 지식이 없어도 누구나 쉽게 데이터를 이관할 수 있다.  이외에도 구글 AMP, 통합 SEO, 소셜미디어 도구, unsplash 이미지, 태그 , 다중 언어 지원, 그룹 블로그 등 글쓰기 도구에 필요한 거의 모든 최신 자원을 지원하고 있다.</p><p>고스트 글쓰기 기능은 미디엄과 매우 흡사하다. 만약 미디엄과 비슷한 사용자 경험을 원하면서 동시에 별도 도메인으로 나만의 브랜드와 출판물을 가지고 싶다면 고스트가 바로 탁월한 선택이 될 것이다.  도메인, 테마, 스타일 등  모두 내가 선택할 수 있고 구글 에드센스를 붙여 광고로 수익을 창출할 수 있다. 아직도 미디엄과 고스트 둘 중 고민이라면 코딩 호러가 작성한 <a href=" https://ghost.org/vs/medium/">미디엄과 고스트 비교 분석(Medium vs Ghost)</a> 글을 읽어보는 것을 추천한다.</p><h3 id="--1">고스트 프리미엄? 설치형?</h3><p> 고스트는  워드프레스와 같이 프론트엔드 및 백엔드를 설정할 수 있는 풀스택 퍼블리싱 플랫폼이다. 고스트 프리미엄(가입형)과  경우 월 29달러부터 시작하며 기본적으로 서버를 지원하기 때문에 간단하고 빠르게 블로그를 설치할 수 있다는 장점이 있다. 하지만 개인 블로그를 운영하기에 부담이 된다. </p><p> 가성비 추구한다면  디지털오션에서 월 5달러인 가장 저렴한 가상 서버를 이용하는 것이 현명한 선택일 것이다.  30분만 투자하면 전문성이 느껴지는 멋진 블로그를 만들어 볼 수 있다. 이 과정은 절대로 어렵지 않다. 그럼 시작해보자.</p><h2 id="-droplet-">프로젝트와 드롭릿(Droplet) 생성하기</h2><p> 아직 디지털 오션 계정이 없다면 <a href="https://m.do.co/t/cb472610f701"><strong>추천 링크</strong></a>를 통해 가입하자. 한 달 동안 무료로 사용 가능한 50달러 크레딧을 받을 수 있다. 필자에게도 25달러가 보상된다. </p><blockquote>가입 절차 중 계정 생성 로그인을 완료하면 카드정보 입력 창이 뜬다. 결제 정보를 입력해도 당장 결제가 되지 않으니 걱정하지 않아도 된다.</blockquote><p>로그인 후 <a href="https://cloud.digitalocean.com/projects">프로젝트 생성 페이지</a>에서 새 프로젝트 <code>my-blog</code>를 생성한다. 사용 목적(Tell us what it's for)은 website or blog를 선택한다. (아무거나 선택해도 된다.)</p><p> 프로젝트가 생성되면 드롭릿 시작하기(Get started with a droplet) 파란색 버튼을 클릭한다. 디지털오션에서는 가상서버를 드롭릿, Droplet (물방울)이라 부른다.  가상 서버를 만드는 것은 드넓은 바다와 같은 클라우드에 물방울을 하나 떨어트리는 것과 같다는 아이디어를 착안해 이름을 붙였다고 한다. </p><p>드롭릿<a href="https://cloud.digitalocean.com/droplets/new"> 생성 페이지</a>에서 이미지를 선택하기(Choose an image) 아래 탭 중 Marketplace를 선택하고 <strong>Ghost on 18.04</strong> 를 선택한다. 디지털 오션은 고스트 원-클릭 설치 애플리케이션을 지원하기 때문에 고통없이 쉽게 설치를 마칠 수 있다. 본 튜토리얼도 이 방식을 따른다. 만약 환경 구성을 변경하길 원한다면  이미지를 우분투 ubuntu로 선택하고 드롭릿을 생성한다. 공식 문서 내<a href="https://ghost.org/docs/install/ubuntu/#overview"> How to install Ghost on Ubuntu </a>에 따라 설치를 진행한다.</p><p>이제 드롭릿의 크기를 선택해야 하는데 네비게이션 왼쪽 버튼을 끝까지 클릭해 <strong>5달러</strong>를 선택한다. 많은 개발자 블로거들이 많은 하루 내 수백명이 접속해도 별 문제가 없었다고 말하고 있으니 안심해도 된다.</p><p>블록 스토리지(block storage)는 드롭릿에 연결가능한 SSD 기반 클라우드 스토리지로 건너 뛰어도 된다. 데이터 센터 지역은 한국과 제일 가까운 <strong>싱가포르</strong>를 택한다.  추가 옵션(additional options) 역시 건너뛰자.</p><h2 id="ssh-">SSH 키-페어 설정하기</h2><p>인증 방법이 어렵게 느껴질 수 있다. 인증(Authentication) 방법은 임시 비밀번호 인증(One-time password) 보다 SSH 인증을 사용하는 것이 좋다. 매번 서버에 접속할 때마다 임시 비밀번호를 메일로 전달받아 입력하는 과정이 번거롭기 때문이다. 파란색 <strong>New SSH Key </strong>버튼을 누르면 SSH 키 입력 창이 뜬다.</p><p> SSH 키를 생성하기 전에, 먼저 SSH 키가 있는지 먼저 확인하자. 콘솔을 열고 <code>ls -al ~/.ssh</code> 명령어를 입력해 <code>.ssh</code> 폴더 내 SSH 키 목록을 확인한다. </p><pre><code class="language-shell">➜ ls -al ~/.ssh
drwx------  12 sujinlee     384 Aug 23 19:15 .
drwxr-xr-x+ 65 sujinlee    2080 Aug 23 23:40 ..
-rw-r--r--   1 sujinlee     193 May  9 15:12 config
-rw-------@  1 sujinlee    1831 May  9 15:17 id_rsa
-rw-r--r--   1 sujinlee     403 May  9 15:17 id_rsa.pub</code></pre><p> 만약 <code>id_rsa.pub</code> 나 <code>id_ras</code> 파일이 없다면, 새로운 키-페어를 생성해야 한다.</p><p> 콘솔에서 <code>ssh-keygen</code> 명령어를 입력하고  <code>cat ~/.ssh/id_rsa.pub</code> 를 입력한다. </p><pre><code class="language-shell">➜ cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCjq6KSYd67Gj.....</code></pre><p> 출력된 전체 내용 (e.g  <code>ssh-rsa AAAAB3NzaC....1</code>)을 복사하고 SSH key content에 복사 붙여넣기한다.  Name 항목는 이름(예: my-macbook)을 입력하고 Add SSH Key 버튼을 클릭한다.   </p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/08/key-pair.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (1) 설치와 호스팅"></figure><p>해당 드랍렛의 SSH 인증키를 선택한다. (편의상 select all을 선택하는 것이 좋다.)</p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/08/ssh.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (1) 설치와 호스팅"></figure><p><strong>📌주의 </strong><em> </em>만약 기존 SSH 키 계정이 디지털오션 이메일 계정과 다를 경우, 계정에 해당하는 SSH 키를 생성해야 한다. 기존 <code>id_ras</code> 파일과 중복되지 않도록 새 파일을 만들어 저장하는 것이 좋다.</p><pre><code class="language-shell">ssh-keygen -t rsa -C "user@email.com&lt;이메일 계정&gt;" -f "is_rsa_user&lt;파일명&gt;" </code></pre><p>호스트 이름(Choose a hostname)을 기억하기 쉬운 이름으로 바꾸자. (예: my-ghost-blog) 프로젝트 선택(Select Project)은 <code>my-blog</code> 를 선택한다.  자동 백업 추가(Add backups)는 옵션 사항으로 무시해도된다. 모든 설정이 끝나면 초록색 드롭릿(Create Droplet) 버튼을 클릭한다. </p><p>  몇 분 후에 드롭릿 생성이 완료 되었다는 메일이 올 것이다. 프로젝트 페이지로 와서  생성된 드롭릿 IP(예: <code>178.128.217.115</code>)를 확인하자. </p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/08/domain-ghost.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (1) 설치와 호스팅"></figure><p>IP 주소 창(예: <a href="http://178.128.217.115/">http://178.128.217.115/</a>)을 열어보면  고스트 설치 페이지가 보일 것이다. 드롭릿을 설정한 다음 도메인에 연결할 수 있다는 뜻이다.</p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/08/Screenshot-2019-08-23-at-3.28.10-PM.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (1) 설치와 호스팅"></figure><p>해당 IP주소에서 드롭릿이 설정되고 호스팅된다. 이 페이지가 보이지 않는다면 드롭릿이 제대로 설정되지 않는 것이다.  </p><h2 id="--2">고스트와 도메인 연결하기</h2><p>아직 고스트 블로그 설치가 모두 끝난 것이 아니다.  등록된 도메인이름이 필요하고, 서버 ip 주소와와 도메인을 연결해야 한다. 도메인은 숫자로 된 인터넷 주소 'IP'를 기억하기 쉬운 '문자 주소'로 대체한 것이다.  '네임서버(Name Server)'를 통해 문자 주소와 숫자 주소가 연결된다.</p><p>보통 도메인 구입 가격 연 15달러 내외로 업체에 따라 상이하다.  <a href="https://iwantmyname.com/">iwantmyname.com</a> 에서 도메인 가격 비교를 해볼 수 있다.  <code>com</code>, <code>net</code> 으로 끝나는 도메인은 대부분 가격대가 높다.  <a href="http://www.dot.tk/en/index.html?lang=en">dot.tk</a> 에서는 <code>tk</code> , <code>ml</code>, <code>ga</code> 등 도메인을 무료로 제공하고 있으니 참고하자. </p><p>도메인이 준비되었다면 디지털 오션에 도메인을 추가하고  DNS 레코드를 설정할 차례다. 오른쪽 더보기 버튼을 클릭해 도메인 추가( 'Add a domain') 버튼을 클릭한다.</p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/08/add-domain.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (1) 설치와 호스팅"></figure><p> 도메인을 입력(예: <code>sujinlee.me</code>) 하면 아래와 같은 DNS 레코드 리스트를 볼 수 있을 것이다.</p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/08/Screenshot-2019-08-23-at-3.38.16-PM.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (1) 설치와 호스팅"></figure><p> 이제 도메인을 구입한 곳에 가서 A 레코드를 새 드롭릿 IP 주소로 업데이트 한다. 나의 경우 <a href="https://sujinlee.me/how-to-build-ghost-blog/godaddy.com">고대디(GoDaddy)</a>에서 sujinlee.me라는 도메인을 구입했다. DNS 관리 페이지에서 A레코드 항목에 새 IP주소로 업데이트 한다.</p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/08/godaddy-1.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (1) 설치와 호스팅"></figure><p> 일반적으로 몇 분 후면 DNS 레코드 전파가 완료된다. 도메인 주소( <a href="https://sujinlee.me/">https://sujinlee.me</a>) 로 접속해보면 IP 주소(<a href="http://178.128.217.115/">http://178.128.217.115/</a>) 과 동일한 설치 화면이 보일 것이다. 이제 가상 서버에서 고스트를 설치해보자.</p><h2 id="--3">고스트 설치하기</h2><p> 드롭렛에 접속하기 위해서 디지털 오션 콘솔을 이용하거나 로컬 콘솔에서 SSH 명령어를 사용할 수 있다.  이 튜토리얼에는 로컬 콘솔에서 가상 서버에 접속하고 고스트 설치한다.</p><blockquote>만약 로컬에서 SSH 명령어 사용에 어려움이 있다면 디지털 오션 콘솔을 이용하자.</blockquote><p></p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/09/console.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (1) 설치와 호스팅"></figure><p> </p><p>로컬 콘솔을 열고 <code>ssh <a>root@</a>&lt;ip주소&gt;</code> 명령어를 입력한다. '연결을 계속 진행하시겠습니까( <code>Are you sure you want to continue connecting (yes/no)</code> ')라는 메시지가 나오면 <code>yes</code> 라고 입력한다.</p><pre><code class="language-shell">➜  ~ ssh root@178.128.56.54
The authenticity of host '178.128.56.54 (178.128.56.54)' can't be established.
ECDSA key fingerprint is SHA256:lCDioAR5XalUOC0u8V0LLSoVl61r/M/2KvX3Ru2hRIo.
Are you sure you want to continue connecting (yes/no)? yes</code></pre><p> 이후 자동으로<a href="https://ghost.org/docs/api/v2/ghost-cli/"> ghost-cli</a> 라이브러리가 설치되고 실행된다. ghost-cli는 고스트를 설치 및 구성하고 항상 최신 상태로 유지할 수 있는 명령형 인터페이스이다.  <code>ghost</code> 명령어로 블로그를 설치하고 관리하고 업데이트 할 수 있다.</p><p>블로그 URL을 입력하세요(Enter your blog URL) 라는 메시지가 나오면 블로그 도메인 주소를 입력한다.</p><pre><code class="language-shell">Ensuring Ghost-CLI is up-to-date...
+ sudo npm i -g ghost-cli@latest
[...]

✔ Checking system Node.js version
✔ Checking logged in user
✔ Checking current folder permissions
✔ Checking operating system compatibility
✔ Checking for a MySQL installation
[...]

✔ Finishing install process
? Enter your blog URL: http://sujinlee.me

root@my-ghost-blog:~#</code></pre><p> 짜잔! 이제 블로그 주소로 들어가보면 고스트가 설치된 것을 볼 수 있다! let's encrypt도 잘 적용이 되었는지 확인해보자.</p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/08/ghost-main.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (1) 설치와 호스팅"></figure><p> 지금 보이는 화면은 고스트 블로그 기본 테마 <a href="https://github.com/TryGhost/Casper">casper</a> 다. 고스트가 업데이트 될 때마다 기본 테마도 반영하기 때문에, 기본 테마 소스를 다운받아 커스터마이징하는 것을 추천한다. 고스트는 시맨틱 템플릿(semantic templates) 언어인 <a href="https://handlebarsjs.com/">Handlebars.js</a>를 사용하고 있다. <a href="https://ghost.org/docs/api/v2/handlebars-themes/">공식 테마 개발 문서</a>를 참고해 직접 테마를 개발해 볼 수 있다.</p><h2 id="--4">관리자 계정 만들기</h2><p> 추후 고스트 버전 업그레이드를 위해서 항상 유저는 <code>ghost-mgr</code> (고스트 관리자)되어야 한다. 로컬 콘솔에서 <code>sudo -i -u ghost-mgr</code>를 입력한다. (가상 서버에 접속했는지 다시한번 확인하자) 유저네임이 <code>root</code>에서 <code>ghost-mgr</code>로 변경되었음을 볼 수 있을 것이다.</p><pre><code class="language-shell">root@my-ghost-blog:~#     sudo -i -u ghost-mgr
ghost-mgr@my-ghost-blog:~$</code></pre><p> 서버 접속을 중단하려면 <code>exit</code>를 입력한다.</p><pre><code class="language-shell">root@my-ghost-blog:~# exit
logout
Connection to 178.128.56.54 closed.</code></pre><p>이제 블로그 주소 끝에 <code>/ghost</code> 를 입력해 고스트 블로그 관리자 화면으로 들어가보자. 아직 관리자 계정이 없기 때문에 아래와 같은 화면이 나올 것이다.</p><figure class="kg-card kg-image-card"><img src="https://sujinlee.me/content/images/2019/08/admin.png" class="kg-image" alt="뚝딱뚝딱 Ghost로 기술 블로그 만들기 - (1) 설치와 호스팅"></figure><p> 관리자 계정을 만들었으니 글을 작성하고 관리할 수 있다. 전체 글을 삭제하고 싶다면 좌측 메뉴 SETTINGS &gt; Labs 버튼을 클릭하고 Delete all content를 클릭하면 된다.  태그도 추가하고 메뉴도 구성해보며 고스트를 탐색해보는 시간을 갖길 바란다.</p><p>🐛그러나 <strong>고스트도 버그가 있다! </strong>고스트 2.0 버전 이후 맥, 아이패드 사용시 에디터에서 한글 작성시 첫 문단에 자모음이 분리되는 버그가 있다. 보고된 <a href="https://github.com/TryGhost/Ghost/issues/9710">깃허브 이슈</a>에 따르면, CodeMirror와 mobile-kit 라이브러리가 원인이라고 한다. 현재로서는 문단 처음에 스페이스 바로 띄워쓰기를 하고 글을 써야 작성해야 자모음 분리 현상을 피할 수 있다.</p><p>지금까지 디지털 오션 가상 서버인 드랍렛을 만들고, 도메인을 연결하고, 원-클릭으로 고스트 블로그를 설치해봤다. </p><p> 다음 장에서는 기본 템플릿인 casper 디자인을 수정해볼 것이다. 기본 테마에서 지원하지 않는 한국어 폰트, disqus 댓글, 수학 수식 라이브러리인 kaTeX 기능, 구글 애널리틱스 기능을 추가해본다.  마지막으로 이미지와 전체 게시물 데이터를 쉽게 이관하는 방법도 소개한다.</p><p>이제 나만의 공간이 만들어졌으니 다시 글을 쓸 수 있는 용기가 생겨났을 것이다. ✍️</p>]]></content:encoded></item><item><title><![CDATA[해외 취업 - 신입 웹 개발자의 싱가포르 상륙기]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>코끝이 찡하게 추웠던 어느 겨울. 두터운 잠바를 벗어 던지고 슬리퍼와 반바지 차림으로 동남아시아의 작은 도시 국가인 싱가포르에 도착했다. 4개월이 지난 지금, 어느덧 아이스 아메리카노 대신 코피 오 코송(Kopi-O-Kosong Ais)이란 단어에 제법 익숙해졌다.</p>
<p>현재 나는 싱가포르 프레스 홀딩스 내 영자 신문인 더 스트레이츠 타임즈에서 인포그래픽 팀에 신입 웹 개발자로</p>]]></description><link>https://sujinlee.me/how-i-landed-my-dream-job-in-sg/</link><guid isPermaLink="false">5f55e80e150a1a7ae0eae856</guid><category><![CDATA[career]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Sun, 24 Mar 2019 08:04:46 GMT</pubDate><media:content url="https://sujinlee.me/content/images/2019/02/fancycrave-225468-unsplash-1.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://sujinlee.me/content/images/2019/02/fancycrave-225468-unsplash-1.jpg" alt="해외 취업 - 신입 웹 개발자의 싱가포르 상륙기"><p>코끝이 찡하게 추웠던 어느 겨울. 두터운 잠바를 벗어 던지고 슬리퍼와 반바지 차림으로 동남아시아의 작은 도시 국가인 싱가포르에 도착했다. 4개월이 지난 지금, 어느덧 아이스 아메리카노 대신 코피 오 코송(Kopi-O-Kosong Ais)이란 단어에 제법 익숙해졌다.</p>
<p>현재 나는 싱가포르 프레스 홀딩스 내 영자 신문인 더 스트레이츠 타임즈에서 인포그래픽 팀에 신입 웹 개발자로 데이터 분석과 그래픽, 데이터 시각화 업무를 하고 있다. 본 글에서는 어떻게 낯선 나라 싱가포르와 인연을 맺게 되어 오게 되었는지 개인적인 경험을 공유하고자 한다. 마지막으로 싱가포르 취업에 관심있는 분들을 위해 참고 링크를 정리했다.</p>
<h3 id>'내' 일을 찾아서</h3>
<p>나는 대한민국에서 태어나 고등 교육을 받았다. 운 좋게 대학 졸업을 하기 전, 대기업의 교육재단에서 기획자로 입사해 온라인 교육 플랫폼 업무를 맡아 2년 간 근무했다. 사회 생활의 첫 시작은 그리 호락호락하지 않았다. 나는 2년 간 직장에서 일 다운 일을 제대로 해본 기억이 없다. 회사 내부 사정으로 매번 업무가 바뀌는 터라 전문성을 갈고 닦으며 일할 수 없는 척박한 환경이었다. 누군가에게 떳떳하고 자랑스럽게 내 일을 설명하고 좋아한다고 말할 수 없었다. 아침에 눈을 뜨면 절망감이 몰려왔고 하루 종일 스트레스와 불안감에 시달렸다. 어느 날에는 버스에서 어지럽고 눈 앞이 캄캄해지고 다리에 힘이 빠지고 식은 땀을 흘리며 쓰러질 뻔 했다. 병원을 가니 미주신경성 실신이라고 했다. 나는 이날 이후로 회사 그리고 일에 대한 마음을 모두 단념했다. 그리고 보란듯이 그 시간을 내 자신을 위해 쓰기로 했다. 보란듯이 업무 시간에 기술문서 번역도 하고 독학으로 코딩을 배우기 시작했다.</p>
<p>나는 지금 웹 개발자라는 뱃지를 확득하기 까지 할 수 있는 거의 모든 일을 다 해본 것 같다. CS관련 해외 온라인 강의도 여러 개 수강했고, 유명 부트캠프도 다녔고, 유다시티 나노디그리 수료증도 취득했다. 하지만 많은 사람들은 컴공과 출신이 아닌 내가 해외 무크 수료증을 100개 넘게 딴다고 한들 모두가 꿈꾸는 으리으리한 대기업에 입사할 수 없다고 했다. 어느 선배는 개발 실력도 중요하겠으나 능력과 신분을 증명하기 위해서는 학위증이 필요하다고 말했다. 이후 대학원 석사과정에 진학했지만 연구하기 위해 내가 갖춘 능력과 지식은 미천하다는 것을 깨닫기까지 오랜 시간이 걸리지 않았다.</p>
<p>대학원 첫 학기 '데이터 시각화' 수업을 수강했다. 당시 나는 자바스크립트 기초를 마치고 리액트를 배우기 시작했다. 수업에서는 데이터 시각화 이론과 더불어 d3.js를 가지고 막대 그래프, 꺾은 선 그래프 등 아주 기본적인 차트를 코딩하는 방법을 다뤘다. 개발 실력을 높일 수 있는 기회라고 생각해 과제 요구 사항을 넘어서 좀더 난이도 높은 과제물을 제출했다. node.js로 데이터를 스크래핑하고, 리액트에서 svg와 d3.js를 사용해 데이터 시각화 차트를 구현했다. 그리고 매주 과제를 묶어 <a href="https://github.com/sujinleeme/data-visualization-experiments">포트폴리오로 만들어 사이트로 배포했다</a>. 수업을 통해 '데이터 시각화'에 관심을 가지게 되면서 자연스럽게 내 연구 분야로 자리매김 하게 되었다.</p>
<p>본격적으로 개발에 눈에 뜨게 된 것은 작년 여름 <a href="https://sujinlee.me/rgsoc-prologue/">레일즈 걸스 서머 오브 코드(Rails Girls Summer of Code) 프로그램</a>을 통해서다. 레일즈 걸스 서머 오브 코드는 전 세계 여성 개발자들을 선발해 유명 오픈 소스 프로젝트에 컨트리뷰터로 기여할 수 있는 기회를 제공하는 글로벌 프로그램이다. 나는 <a href="https://babeljs.io/">Babel.js</a> 프로젝트에 합류해 3개월 간 코어 메인테이너들과 국내 선배 개발자들의 도움을 받아 바벨 사이트 내 <a href="https://babeljs.io/repl">코드 에디터</a>를 유지 보수하는 일을 했다. 3개월 내내 영어로 커뮤니케이션을 하나보니 영어에 대한 두려움도 사라졌고 코딩 실력도 눈에 띄게 향상됐다. 인터넷이라는 공간에서 해외 개발자들과 점점 가까워지니 나도 좁은 한반도를 넘어 넓은 세상에서 맘껏 꿈을 펴고 싶다는 생각이 들었다. 그러나 마음 한 구석에는 '넌 아무 것도 할 수 없어' 라는 부정적인 메시지로 늘 가득찼다. 학위증도 없고, 아무런 경력도 없이 서른 살을 바라보고 있는 나. 나는 어디로 가야 할까.</p>
<p>모든 것을 체념한 그 때, 8월의 마지막 날. 나는 무작정 싱가포르로 여행을 떠났다.</p>
<h3 id>우연일까, 필연일까</h3>
<p>싱가포르에서 나는 최대한 많은 현지 개발자들을 만나고 이야기를 나누고 싶었다. 6시간 30분 장시간 비행의 여독을 풀지도 못한 채 싱가포르에 오자마자 <a href="https://www.meetup.com/Singapore-JS/">싱가포르 자바스크립트 커뮤니티 미트업 행사</a>으로 발걸음을 옮겼다. 이 날에는 싱가포르 중고나라 서비스인 카로셀 오피스에서 약 50여명의 현지 개발자들이 모였고, Vue.js, 객체 지향 디자인, 실무 프로젝트 사례 등 주제 등 발표가 있었다. 풍부한 경험에서 우러난 깊은 지식과 통찰은 물론, 군더더기없는 매끄러운 스토리텔링에 저절로 박수가 나왔다. 단연코 일년 중 참석했던 개발 행사 중 으뜸이었다. 누가 개발자는 말 주변이 없다고 했을까. 기술자는 인문학적인 언어를 모른다는 나의 편견이 산산조각난 행사였다. 싱가포르는 C++ 코딩을 할 수 있는 총리를 둔 나라이니 당연한 걸지도 모르겠다는 생각이 들었다.</p>
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">I attended talk.js night hosted by <a href="https://twitter.com/SingaporeJS?ref_src=twsrc%5Etfw">@SingaporeJS</a> on my very first day in Singapore. Exciting stuff. Looking forward for more in coming days. <a href="https://t.co/nMFRZU8aDT">pic.twitter.com/nMFRZU8aDT</a></p>&mdash; Sujin Lee (@sujinleeme) <a href="https://twitter.com/sujinleeme/status/1034833166991679488?ref_src=twsrc%5Etfw">August 29, 2018</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>가장 인상 깊었던 발표는 어느 시니어(vue.js 마스터로 불리는 그는 지금 내 옆-옆 자리에 앉아 열심히 키보드를 두드리고 있다.)의 인터렉티브 웹 게임 개발 사례였다. 그는 싱가포르 프레스 홀딩스 내 더 스트레이츠 타임즈 인터렉티브 팀에서 웹 개발을 하고 있다. 고전 게임인 '윌리를 찾아서'에서 영감을 받아 싱가포르 내셔널 데이를 기념하고자 '<a href="https://graphics.straitstimes.com/STI/STIMEDIA/Interactives/2018/07/ndp-hunt/index.html">NDP Hunt</a>'라는 게임을 만들었다. 제한된 시간 안에 53가지 숨겨진 아이템을 찾는 게임으로 2주 간 2만 1천명이 넘는 사용자들이 접속하는 쾌거를 이뤘다고 한다. 그는 실제 프로젝트 경험을 바탕으로 다양한 문제 해결 과정을 설명했다. 예컨데 게임 오브젝트를 만드는 방법, 알고리즘 최적화, 사용자 데이터 분석 등이다. 끝으로 그는 소속된 팀에서 주니어 시니어 웹 개발자를 찾고 있다고 말했다.</p>
<p>아이디어도 훌륭하고 기발했지만 프로젝트 방식과 업무의 흐름, 원활한 협업, 창의적이고 자유로운 분위기를 감지할 수 있었다. 그리고 내 마음 속에 무엇인가가 꿈틀거렸다. 모든 것이 즉흥적이었다. 용기를 내어 발표자를 찾아가 내 자신을 소개하고 지금 여행 중이며 외국인도 지원할 수 있냐고 물어봤다. 그는 자신의 이메일 연락처를 줬고 나는 그 자리에서 바로 <a href="https://docs.google.com/document/d/1orQ8c_a1RROAI4Rrc6jkv95VWdZzGRVwtpAfierBxjk/edit#heading=h.6wymnhinx9q5">이력서</a>와 직전학기에 만든 <a href="https://github.com/sujinleeme/data-visualization-experiments">데이터 시각화 포트폴리오 링크</a>를 별도로 첨부해 발송했다. 그리고 며칠 후, 팀 리더에게 연락이 왔다. &quot;회사에 구경오지 않을래?&quot;</p>
<h3 id>미팅 전 준비</h3>
<p>취업은 연애와 무척 닮았다. 상대방이 어떤 사람인지 알 수 없기에 소위 '스펙'을 통해 매력을 짐작한다. 예컨데 키, 외모, 신체사이즈, 학벌, 직업 등의 스펙은 상대방을 아는데 필요한 사전 정보다. 하지만 내가 가지고 있는 사전 정보는 메일 주소 뿐이였다. 하지만 링크드인을 통해 내가 지원하는 부서에 근무하는 개발자, 디자이너의 프로필을 쉽게 찾을 수 있었다. 개발자들의 깃허브에 들어가서 코드를 보기도 하고 외부 발표 자료를 찾아보기도 했다. 그러나 더 이상 알아볼 수 있는 정보가 없었다. 링크드인으로 타 부서에 다니고 있는 주니어 개발자에게 회사 정보와 업무 환경 등을 물어봤다. 흡사 썸남을 뒷조사를 한 격이였다.</p>
<p>나는 링크드인이나 홈페이지를 통해 정식적인 채용절차를 밟지 않았다. 다시 회사 채용 공고를 찾아 읽으며 요구하는 기술 스택을 점검했다. 그리고 내가 가진 스킬 셋을 바탕으로 <strong>할 수 있는 일 / 할 수 없는 일 / 단 기간에 배우면 할 수 있는 일 / 단 기간에 배워도 할 수 없는 일</strong>으로 정리했다. 나는 프론트 웹 개발에 필요한 기초 지식을 가지고 있으며 리액트와 d3.js로 데이터 시각화를 만들어 본 적이 있으므로 HTML&amp;CSS&amp;JS, react.js, d3.js 등은 jQuery, 바로 실무에 투입되어 할 수 있는 일로 분류했다. 개발팀은 vue.js를 사용하고 있으나, 나는 해본 적이 없었다. 일정 학습 기간 후에 vue.js를 할 수 있다고 판단해 단 기간에 배우면 할 수 있는 일로 분류했다. 하지만 .NET, .PHP, Drupal는 해본 적이 없다. 이 스킬 셋은 단 기간에 배워도 할 수 없는 일로 분류했다. 이렇게 나름대로 점검을 해보니 객관적으로 내 자신을 바라볼 수 있었고 팀에서 찾는 사람이 내가 맞는지를 확인할 수 있었다. 궁국적으로 나는 채용 공고 중 모든 요구사항에 60% 정도 매칭됐다. (한창 자격 미달이라 조바심이 났지만 <a href="https://www.forbes.com/sites/nextavenue/2014/09/11/are-women-too-timid-when-they-job-search/">남성 개발자들은 채용 공고 중 60%만 충족해도 지원한다는 글</a>을 읽었기에 자신감을 가지게 됐다.) 지금도 심심할 때면 링크드인에 들어가 채용 공고를 읽어보며 잡 마켓 트렌드를 읽어보곤 한다. 채용 공고는 잡 마켓에서 각광 받는 기술이 무엇인지, 회사에서 어떤 기술을 사용하고 있는지 확인해볼 수 있는 척도가 된다.</p>
<p>미팅을 앞두고 <a href="https://www.straitstimes.com/multimedia/graphics">팀에서 제작한 모든 데이터 시각화 프로젝트</a>를 리뷰했다. 인터렉티브 그래픽스 팀에서는 데이터 시각화와 데이터 분석을 주로 하고 있고 그 외에 인터렉션, 게임, WebAR과 WebVR, 3D, 애니메이션 등 분야를 가리지 않고 창의적인 작품을 만들고 있었다. 멋진 팀 포트폴리오를 보며 많은 감명을 받았고 나도 함께 일하고 싶다라는 생각이 뭉게뭉게 피어올랐다.</p>
<h3 id>이력서보다 프로젝트</h3>
<p>며칠 후 팀장과 회의실에서 단독 미팅을 가졌다. 팀장은 채용 절차를 거치기 전 업무와 핏이 맞는지 서로 확인차 만나는 자리라고 설명했다. 회사 소개와 더불어 팀 업무 설명 등 여러 이야기를 나누었고, 내가 제출한 데이터 시각화 프로젝트를 직접 하나씩 보시며 조언을 해주셨다. 기술적으로는 높지만 스토리텔링과 설득력이 부족하다며 독자들의 인사이트를 얻으려면 스토레텔링이 매우 중요하다고 강조했다. 그는 개발자들은 요구사항에 맞춰 단순한 코딩만 하지 않는다고 했다. 팀 내 모든 개발자들이 아이디어 기획 회의에 참여하며, 개발 80% 리서치 20%의 업무를 한다고 했다. 심지어 디자이너도 코딩을 하고, 깃을 사용한다. 모두가 디자이너, 개발자, 기획자 인 셈이다. 그는 외부 개발 행사 및 컨퍼런스 등 발표도 적극적으로 권장한다고 말했다. 마지막으로 영어 글쓰기와 읽기를 좋아하는지도 물어봤다. 미팅이 끝나갈 때 쯤, 그는 채용 과정을 진행하고 싶다고 말했다.</p>
<p>그 당시 잘 몰랐지만 실제로 팀에 합류한 이후 여러 지원자들을 보니 개인 프로젝트와 포트폴리오는 그 어떤 학위증 보다 중요한 것이었다. 휘황찬란한 이력서보다 지원자가 참여한 프로젝트와 포트폴리오 링크 하나로 그 사람의 지식과 업무 능력이 모두 검증되는 셈이다. 얼마 전에는 팀 동료가 팀 내 개발자 모두가 내 깃허브 코드와 블로그 글들을 읽었다고 귀뜸해줬다. 팀장은 내 블로그 글을 번역기를 돌려 영어로 읽기도 했다고 한다.</p>
<p>미팅 후 이틀 뒤, 테스트 과제를 받았다. 샘플 데모 영상과 디자인 시안, 개발 명세서를 보고 차트 라이브러리 없이 인터렉티브한 반응형 시각화 차트를 만드는 것이었다. 기한은 일주일이었다. 여유로운 동남아 여행을 꿈꾸었지만 하루종일 숙소와 카페에 쳐박혀 코딩만 해야하는 신세가 되었다. 간단한 라인 그래프였지만 쉽지 않았다. 리액트와 d3.js를 써본 기억은 희미해졌고, d3.js는 새 버전 v5이 나온 터라 다시 공부해야 했다. 설상가상으로 냉방병에 걸렸다. 시간이 충분하지 않아 요구사항 중 툴팁(tooltip)은 제대로 만들지 못했다. 마지막으로 <a href="https://github.com/sujinleeme/responsive-line-chart-react-d3v5">소스 코드는 깃허브에 올리고 깃허브 페이지로 배포한 다음 링크만 전달했다</a>. 그리고 다음 날 나는 귀국길에 올랐다.</p>
<p>인천공항에서 집으로 가던 도중 반가운 이메일이 도착했다. 과제가 통과된 것이다. 회사로 와서 코딩 인터뷰를 올 것을 제안했다. 하지만 나는 싱가포르가 아닌 한국에 있었기에 화상 면접을 부탁했다.</p>
<h3 id>영어의 중요성</h3>
<p>나는 20대 전부를 한국에서만 보냈다. 부끄럽지만 남들 모두 다가는 어학연수, 외국생활, 유학 경험을 단 한 번도 해본 적이 없다. 토익 점수도 만료된지 오래다. 그래도 다행인 것은 넷플릭스나 유투브를 하도 많이 본 덕분에 영어를 읽고 듣는데는 별 어려움이 없다. 하지만 영어 말하기는 넘을 수 없는 장벽이다. 지금도 유창하고 매끄러운 영어를 구사하지 못해 매일매일 전쟁을 치른다. 싱글리시 악센트에 익숙하지 않으니 몇 번이고 되물어보는 것이 일상이다. 면접을 코 앞에 앞두고 기술적인 질문을 영어로 대답해야 하니 엄청난 부담감이 몰려왔다. 우리나라 말로도 쉽지 않은데 영어라니. 내가 할 수 있는 일은 깨끗하게 마음을 비우는 것 뿐이였다.</p>
<p>회상 면접은 팀장과 시니어 개발자가 면접관으로 참여했다. 면접 질문은 생각보다 평이했다. 자바스크립트 어려운 개념이나 수수께끼같은 질문을 하지 않았다. 내가 제출했던 코드를 기반으로 어떻게 문제를 인식하고 해결했는지, 어떤 어려움이 있었는지 등 문제 해결 과정을 주로 물었다. 단순 코딩 테스트보다는 코드 리뷰를 받는다는 느낌이 강했다. 인터뷰를 앞두고 내가 해결하지 못한 툴팁을 구현하는 방법을 찾았는데, 인터뷰 때는 완성하지 못한 이유와 해결 방안에 대해서도 설명했다. 모든 요구 사항을 100% 만족한 것이 아니니 부정적인 피드백이 있을 거라 예상했다. 오히려 시니어 개발자는 내 코드가 가독성이 높고 잘 정리 정돈된 느낌이라며 칭찬해줬다. 실제 현장에서 인터뷰를 봤더라면 직접 코드 리뷰를 해줬을 것이라고 덧붙였다. 앞으로 10년 후 어떤 개발자가 되고 싶은지, 팀에서 기대하는 바를 끝으로 인터뷰가 마무리됐다. 인터뷰 과정에서 그 누구도 내가 나이가 몇 살이며, 전공은 무엇인지, 결혼을 했는지 등 개인사는 단 한 번도 묻지 않았다. 그저 나는 여러 후보 중 하나였고 기술적인 주제 이외에 다른 질문은 일절 없었다.</p>
<p>많은 사람들은 해외 취업 시 개발자에게 높은 영어 실력을 기대하지 않는다고 말한다. 하지만 실제로는 정반대였다. 영어 커뮤니케이션 능력은 코딩만큼 (어쩌면 더 대단한) 중요한 무기였다. 채용 과정을 통해 엔지니어에게 '영어'가 얼마나 중요한지 새삼 다시 깨닫게 되는 계기가 됐다. 영어는 마지막 관문에 다가갈수록 더더욱 중요해졌다.</p>
<p>일주일 후 기술 면접이 통과되었다는 메일을 받았다. HR 담당자는 회사 공식 이력서 및 자기소개 양식, 레퍼런스 체크, 희망 연봉, 예상 출근일 등이 적힌 문서를 보내줬다. 정말 다행인 것은 전 직장 동료가 영어 마스터였기에 레퍼런스 체크에 문제가 없었다. 전 직장 동료 중에 영어를 할 수 있는 사람이 없다면 아마 쉽지 않았을 것이다.</p>
<p>HR 면접은 기술 면접보다 더 까다로웠다. HR 담당자는 많은 국가 중 굳이 싱가포르에서 일하고 싶은 이유가 무엇인지, 왜 개발자가 되려고 하는지, 회사와 팀에 무엇을 기대하는 지, 어떤 기여를 할 수 있는지 등을 질문했다. 특히 개발자로 커리어 전환하는 이유에 대해 집중적으로 물어봤다. 공대가 아닌 음대를 졸업하고 기획자로 2년간 근무하다가 다시 개발자로 커리어 전향을 하는 것이니 매우 특이한 사례다. 인터뷰 중 작곡과 개발 과정이 공통점이 많다고 답한 것이 기억에 남는다. 곡(composition)은 음을 구성하는 것(compose)이고, 소프트웨어 개발 또한 여러 블럭들을 조합하는 것이니 그 과정이 비슷한 부분이 많다고 말했다. 임기응변이었지만 꽤 괜찮은 답변인 것 같다. 그러나 긴장을 너무 많이 한 탓에 대부분의 인터뷰를 더듬거렸고 했던 말을 반복하기도 했다. 영어를 더 잘 했더라면 좀 더 좋은 인상을 남기지 않았을까 하는 아쉬움이 크다.</p>
<h3 id>돈보다는 성장</h3>
<p>우리나라는 평생 직장을 중시하는 반면 싱가포르는 잦은 이직을 통해 경력을 업그레이드 하며 몸값을 불려나가는 구조다. 싱가포르는 우리나라와 달리 연봉이 아닌 월봉이 기준이며, 신입 초봉은 우리나라에 비해 낮은 편이다. 나의 경우 연봉 협상 경험이 적어 걱정이 이만저만 아니였다. 더군다나 싱가포르는 전 세계에서 물가가 가장 높은 국가로 악명이 높기 때문에 생활이 가능할지도 의문이었다. 무슨 배짱인지 모르겠지만 <a href="https://www.payscale.com/research/SG/Job=Web_Developer/Salary/f4527ab2/Entry-Level">IT 업계 신입 개발자 급여</a>의 최댓값을 적어냈다.</p>
<p>회사가 제안한 급여는 내가 예상했던 것보다 낮았다. 돈과 성장을 놓고 한창 저울질 하던 나에게 어느 시니어 개발자는 처음 해외 이직을 할 때 욕심을 부리지 말라고 일침을 놓았다. 그는 커리어를 전환을 희망하는 중고 신입이며, 비자 스폰이 절실한 입장에서 터무니없이 높은 연봉을 요구하는 바람직하지 않는 태도라고 지적했다. 그는 돈보다는 성장을 우선시 하라고 말했다. 신기하게도 마지막 최종 오퍼를 받았을 때는 최초 제안 연봉의 최댓값에서 훨씬 높은 수준이었다. 회사 내 외국인 근로자를 위해 이사, 항공 및 주거비 지원 등 지원 제도도 있어 만족스러웠다. 싱가포르에서 오고 나서야 업계에서 주니어로 꽤 괜찮은 대우를 받고 있다는 것도 알게 됐다.</p>
<p>그리고 몇 주 후, 드디어 최종 오퍼 메일을 받고 취업 비자를 신청했다. 출근일을 정했지만 싱가포르로 이주하기까지의 과정도 만만치 않았다. 휴학을 해야 했고, 새로운 세입자를 찾아야 했고, 그동안의 세간살이를 정리해야 했다. 그렇게 모두 정리하고 보니 이민 가방 2개가 나왔다. 29살 마지막을 앞두고 엄마와 작별 인사를 하고 고국을 떠났다.</p>
<h3 id>다양성이 주는 즐거움</h3>
<p>계획은 없었지만 싱가포르라는 국가, 그리고 지금의 회사와 팀을 만난 것은 나에게 큰 행운이다. 일평생 문화적 다양성과 포용을 경험해보지 못한 나에게 싱가포르는 풍요로운 다양성을 현장에서 보고 배울 수 있는 최고의 환경이다. 영어, 말레이, 타밀어, 중국어가 공존하는 환경에서 아시아-태평양에서 온 다양한 사람들과 부대끼며 일하고 있다.  팀원들의 출신 국가, 종교, 문화가 다양하니 협업을 통해 서로 다른 생각과 관점을 배우게 된다. 뉴스룸에서는 아시아 각국 주요 이슈를 다루느라 분주하고, 디너 파티는 세계 각국 음식으로 만찬이 펼쳐진다. 끝까지 내가 한국을 고집했더라면 다양성이 주는 혜택과 즐거움을 맛볼 수 없었을 것이다.</p>
<p>누구는 작은 도시 국가인 싱가포르를 세상에서 가장 심심하고 재미없는 나라라고 평하기도 한다. 하지만 엔지니어에게는 아시아 국가 중 가장 활발한 나라일지도 모른다. 싱가포르는 평일과 주말을 가리지 않고 컨퍼런스, 미트업, 해커톤 등 다채로운 기술 행사가 열린다. 이번 주 토요일 만해도 10개가 넘는 기술 행사가 열렸다. <a href="https://www.engineers.sg/organizations">기술 커뮤니티 수만 해도 95개가 넘는다.</a> 주말마다 기술 커뮤니티에 나가 현지 개발자들과 어울리면 지루할 틈도 없다. 이렇듯 다양한 기술, 사람, 문화, 언어가 어우러진 이 곳이 매일마다 나에게 새로운 영감을 불어 넣어준다.</p>
<h3 id>인생의 나이테</h3>
<p>아홉 수는 인생의 나이테 만드는 시기라고도 한다. 좁은 나이테는 여름이 메마르고 무더웠음을 말하고, 넓은 나이테는 강수량이 평소보다 더 많았음을 말해준다. 나무가 성장한 기록은 그 나무의 나이테에 나타난다. 가물었을 때, 질병이 들었을 때, 불이 났을 때, 정상적으로 자라지 못했을 때, 나무의 심층부에 박혀있게 된다고 한다.</p>
<p>나의 20대는 버티고 또 버티는 연습이었다. 오늘이 있기까지 값비싼 수업료를 치웠다. 가슴 뛰는 일, 내가 좋아하는 일을 찾기 위해 10년이라는 세월이 걸렸다. 그제서야 희미한 나이테 하나가 더 만들어진 모양이다. 앞으로도 문득 어제처럼 매서운 겨울이 찾아올 것이다. 그 때마다 지난 겨울을 기억하며 버티고 겹겹이 나이테를 그리며 천천히 살아가고자 한다. 겨우내 튼튼한 뿌리를 만들 수 있었고 그 뿌리의 힘을 바탕으로 비로소 성장할 수 있었다고. 다시 봄이 올 때, 그동안 버텨줘서 고맙다고, 수고했다고, 내 자신에게 그렇게 말하고 싶다.</p>
<blockquote>
<p>아래는 개발자로 첫 출근을 자-축 하기 위해 로비에서 찍은 사진이다.</p>
</blockquote>
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">I made a career change into web development at last. Before that, I worked as a project manager. Today is the first day at the strait times, Singapore Press holdings Limited. I am very glad to be a part of ST graphics team and looking forward to fabulous projects that I work. ✨ <a href="https://t.co/Th93vHG2qO">pic.twitter.com/Th93vHG2qO</a></p>&mdash; Sujin Lee (@sujinleeme) <a href="https://twitter.com/sujinleeme/status/1080498118766936064?ref_src=twsrc%5Etfw">January 2, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<h3 id>추천 사이트</h3>
<ul>
<li><a href="https://www.mycareersfuture.sg/">MyCareersFuture.sg</a> 싱가포르 노동청에서 운영하는 구직 공고 사이트다.</li>
<li><a href="https://www.engineers.sg/">engineers.sg</a> 싱가포르 내 기술 커뮤니티를 총 아카이빙 했다. 미트업, 컨퍼런스 등 모든 기술 발표 세션 동영상을 볼 수 있으며 일정도 확인할 수 있다. 싱가포르에 오게 된다면 꼭 현지 미트업에 참석해보길 권장한다.</li>
<li><a href="https://www.worldjob.or.kr/intro.do">월드잡 코리아</a> 정부에서 우리나라 청년들의 해외취업 을 위해 많은 노력을 하고 있다. 월드잡플러스 에서 해외 잡 페어, 해외 취업 공고는 물론 무료 영문 이력서 첨삭, 해외 정착금 지원 사업 서비스를 제공한다. 싱가포르의 경우 지원금 800만원까지 받을 수 있다.</li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Talk: Rails Girls Summer of Code : 3 months journey of contributing to babel.js]]></title><description><![CDATA[<!--kg-card-begin: html--><div><iframe src="https://noti.st/sujinleeme/0bIixe/embed" frameborder="0" height="500" width="798"></iframe>
</div>
<!--kg-card-end: html--><p>Rails Girls Summer of Code : 3 months journey of contributing to babel.js（Rails Girls Summer of Code で Babel.js に貢献した 3 ヶ月） Since July, I have worked for Babel.js as part of Rails Girls Summer of Code, a global fellowship program for women and non-binary coders.</p><p>Our</p>]]></description><link>https://sujinlee.me/fec-fukuoka-rgsoc/</link><guid isPermaLink="false">5f55e80e150a1a7ae0eae863</guid><category><![CDATA[talk]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Sat, 08 Dec 2018 08:08:00 GMT</pubDate><media:content url="https://sujinlee.me/content/images/2019/09/fukuoka-logo.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: html--><div><iframe src="https://noti.st/sujinleeme/0bIixe/embed" frameborder="0" height="500" width="798"></iframe>
</div>
<!--kg-card-end: html--><img src="https://sujinlee.me/content/images/2019/09/fukuoka-logo.jpg" alt="Talk: Rails Girls Summer of Code : 3 months journey of contributing to babel.js"><p>Rails Girls Summer of Code : 3 months journey of contributing to babel.js（Rails Girls Summer of Code で Babel.js に貢献した 3 ヶ月） Since July, I have worked for Babel.js as part of Rails Girls Summer of Code, a global fellowship program for women and non-binary coders.</p><p>Our team has developed REPL in babel official website, a playground for ES6; where user can write ES6 code, check the relevant code in ES5; and also the output.</p><p>In this talk, as a member of Open Source ecosystem, I am going to share my experiences and learning from project and program.<br></p><figure class="kg-card kg-embed-card"><blockquote class="instagram-media" data-instgrm-captioned data-instgrm-permalink="https://www.instagram.com/p/BrMU2RBFdxg/?utm_source=ig_embed&amp;utm_campaign=loading" data-instgrm-version="12" style=" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:658px; min-width:326px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);"><div style="padding:16px;"> <a href="https://www.instagram.com/p/BrMU2RBFdxg/?utm_source=ig_embed&amp;utm_campaign=loading" style=" background:#FFFFFF; line-height:0; padding:0 0; text-align:center; text-decoration:none; width:100%;" target="_blank"> <div style=" display: flex; flex-direction: row; align-items: center;"> <div style="background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 40px; margin-right: 14px; width: 40px;"></div> <div style="display: flex; flex-direction: column; flex-grow: 1; justify-content: center;"> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 100px;"></div> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 60px;"></div></div></div><div style="padding: 19% 0;"></div> <div style="display:block; height:50px; margin:0 auto 12px; width:50px;"><svg width="50px" height="50px" viewbox="0 0 60 60" version="1.1" xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g transform="translate(-511.000000, -20.000000)" fill="#000000"><g><path d="M556.869,30.41 C554.814,30.41 553.148,32.076 553.148,34.131 C553.148,36.186 554.814,37.852 556.869,37.852 C558.924,37.852 560.59,36.186 560.59,34.131 C560.59,32.076 558.924,30.41 556.869,30.41 M541,60.657 C535.114,60.657 530.342,55.887 530.342,50 C530.342,44.114 535.114,39.342 541,39.342 C546.887,39.342 551.658,44.114 551.658,50 C551.658,55.887 546.887,60.657 541,60.657 M541,33.886 C532.1,33.886 524.886,41.1 524.886,50 C524.886,58.899 532.1,66.113 541,66.113 C549.9,66.113 557.115,58.899 557.115,50 C557.115,41.1 549.9,33.886 541,33.886 M565.378,62.101 C565.244,65.022 564.756,66.606 564.346,67.663 C563.803,69.06 563.154,70.057 562.106,71.106 C561.058,72.155 560.06,72.803 558.662,73.347 C557.607,73.757 556.021,74.244 553.102,74.378 C549.944,74.521 548.997,74.552 541,74.552 C533.003,74.552 532.056,74.521 528.898,74.378 C525.979,74.244 524.393,73.757 523.338,73.347 C521.94,72.803 520.942,72.155 519.894,71.106 C518.846,70.057 518.197,69.06 517.654,67.663 C517.244,66.606 516.755,65.022 516.623,62.101 C516.479,58.943 516.448,57.996 516.448,50 C516.448,42.003 516.479,41.056 516.623,37.899 C516.755,34.978 517.244,33.391 517.654,32.338 C518.197,30.938 518.846,29.942 519.894,28.894 C520.942,27.846 521.94,27.196 523.338,26.654 C524.393,26.244 525.979,25.756 528.898,25.623 C532.057,25.479 533.004,25.448 541,25.448 C548.997,25.448 549.943,25.479 553.102,25.623 C556.021,25.756 557.607,26.244 558.662,26.654 C560.06,27.196 561.058,27.846 562.106,28.894 C563.154,29.942 563.803,30.938 564.346,32.338 C564.756,33.391 565.244,34.978 565.378,37.899 C565.522,41.056 565.552,42.003 565.552,50 C565.552,57.996 565.522,58.943 565.378,62.101 M570.82,37.631 C570.674,34.438 570.167,32.258 569.425,30.349 C568.659,28.377 567.633,26.702 565.965,25.035 C564.297,23.368 562.623,22.342 560.652,21.575 C558.743,20.834 556.562,20.326 553.369,20.18 C550.169,20.033 549.148,20 541,20 C532.853,20 531.831,20.033 528.631,20.18 C525.438,20.326 523.257,20.834 521.349,21.575 C519.376,22.342 517.703,23.368 516.035,25.035 C514.368,26.702 513.342,28.377 512.574,30.349 C511.834,32.258 511.326,34.438 511.181,37.631 C511.035,40.831 511,41.851 511,50 C511,58.147 511.035,59.17 511.181,62.369 C511.326,65.562 511.834,67.743 512.574,69.651 C513.342,71.625 514.368,73.296 516.035,74.965 C517.703,76.634 519.376,77.658 521.349,78.425 C523.257,79.167 525.438,79.673 528.631,79.82 C531.831,79.965 532.853,80.001 541,80.001 C549.148,80.001 550.169,79.965 553.369,79.82 C556.562,79.673 558.743,79.167 560.652,78.425 C562.623,77.658 564.297,76.634 565.965,74.965 C567.633,73.296 568.659,71.625 569.425,69.651 C570.167,67.743 570.674,65.562 570.82,62.369 C570.966,59.17 571,58.147 571,50 C571,41.851 570.966,40.831 570.82,37.631"/></g></g></g></svg></div><div style="padding-top: 8px;"> <div style=" color:#3897f0; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:550; line-height:18px;"> View this post on Instagram</div></div><div style="padding: 12.5% 0;"></div> <div style="display: flex; flex-direction: row; margin-bottom: 14px; align-items: center;"><div> <div style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(0px) translateY(7px);"></div> <div style="background-color: #F4F4F4; height: 12.5px; transform: rotate(-45deg) translateX(3px) translateY(1px); width: 12.5px; flex-grow: 0; margin-right: 14px; margin-left: 2px;"></div> <div style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(9px) translateY(-18px);"></div></div><div style="margin-left: 8px;"> <div style=" background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 20px; width: 20px;"></div> <div style=" width: 0; height: 0; border-top: 2px solid transparent; border-left: 6px solid #f4f4f4; border-bottom: 2px solid transparent; transform: translateX(16px) translateY(-4px) rotate(30deg)"></div></div><div style="margin-left: auto;"> <div style=" width: 0px; border-top: 8px solid #F4F4F4; border-right: 8px solid transparent; transform: translateY(16px);"></div> <div style=" background-color: #F4F4F4; flex-grow: 0; height: 12px; width: 16px; transform: translateY(-4px);"></div> <div style=" width: 0; height: 0; border-top: 8px solid #F4F4F4; border-left: 8px solid transparent; transform: translateY(-4px) translateX(8px);"></div></div></div></a> <p style=" margin:8px 0 0 0; padding:0 4px;"> <a href="https://www.instagram.com/p/BrMU2RBFdxg/?utm_source=ig_embed&amp;utm_campaign=loading" style=" color:#000; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none; word-wrap:break-word;" target="_blank">⚡My #fec_fukuoka Lightening Talk - #RGSoC : 3 months journey of contributing to babel.js - is here. https://sujinleeme.github.io/fec-fukuoka-rgsoc/ Thank #fec_fukuoka &amp; #RailsGirlsSoC for the opportunity to share my OSS experience and encourage contribution in the #babel community. 💖</a></p> <p style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;">A post shared by <a href="https://www.instagram.com/sujinleeme/?utm_source=ig_embed&amp;utm_campaign=loading" style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px;" target="_blank"> Sujin Lee 이수진  李秀珍</a> (@sujinleeme) on <time style=" font-family:Arial,sans-serif; font-size:14px; line-height:17px;" datetime="2018-12-10T03:59:15+00:00">Dec 9, 2018 at 7:59pm PST</time></p></div></blockquote>
<script async src="//www.instagram.com/embed.js"></script></figure><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Neat hoodie from <a href="https://twitter.com/hashtag/fec_fukuoka?src=hash&amp;ref_src=twsrc%5Etfw">#fec_fukuoka</a> ✌ <a href="https://t.co/Cy0FxzdqCz">pic.twitter.com/Cy0FxzdqCz</a></p>&mdash; Sujin Lee 👩‍🦰 (@sujinleeme) <a href="https://twitter.com/sujinleeme/status/1073407438424948736?ref_src=twsrc%5Etfw">December 14, 2018</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure>]]></content:encoded></item><item><title><![CDATA[Talk: PyConKR 2018 Keynote: Committing to diversity and inclusion]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>I was honored to be invited to the <a href="https://www.pycon.kr/2018/program/keynote/">PyConKR 2018</a> as a keynote speaker. The theme of PyCon 2018 was &quot;Dive into Diversity&quot; and I delighted to deliver my stories and movement to improve diversity in python community on the final stage.</p>
<h4 id="slides">Slides</h4>
<div>
<iframe src="https://noti.st/sujinleeme/E2xuCw/embed" frameborder="0" width="960" height="540" allowfullscreen></iframe>
</div>
<h4 id="video">Video</h4>
<div>
<iframe width="560" height="315" src="https://www.youtube.com/embed/QgFnmeDLuno" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
</div>
<h3 id="description">Description</h3>
<p>The PyCon along</p>]]></description><link>https://sujinlee.me/committing-to-diversity-and-inclusion/</link><guid isPermaLink="false">5f55e80e150a1a7ae0eae850</guid><category><![CDATA[talk]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Wed, 22 Aug 2018 11:02:09 GMT</pubDate><media:content url="https://sujinlee.me/content/images/2018/08/diveintodiversity.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://sujinlee.me/content/images/2018/08/diveintodiversity.jpg" alt="Talk: PyConKR 2018 Keynote: Committing to diversity and inclusion"><p>I was honored to be invited to the <a href="https://www.pycon.kr/2018/program/keynote/">PyConKR 2018</a> as a keynote speaker. The theme of PyCon 2018 was &quot;Dive into Diversity&quot; and I delighted to deliver my stories and movement to improve diversity in python community on the final stage.</p>
<h4 id="slides">Slides</h4>
<div>
<iframe src="https://noti.st/sujinleeme/E2xuCw/embed" frameborder="0" width="960" height="540" allowfullscreen></iframe>
</div>
<h4 id="video">Video</h4>
<div>
<iframe width="560" height="315" src="https://www.youtube.com/embed/QgFnmeDLuno" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
</div>
<h3 id="description">Description</h3>
<p>The PyCon along with its staff, participants and speakers all around the world stands by its Python Software Foundation's diversity statement, and all our members are outspoken diversity advocates. Diversity awareness is fundamental not only for open source projects, tech communities, companies, and their products but also in our everyday lives. We believe that inclusion is the key to building a society that respects diversity in which everyone can achieve growth and be accepted for who they are. Inclusion involves maintaining a professional and productive atmosphere where everyone can feel valued and appreciated.<br>
In this talk, I will present how Python communities, such as Django Girls, are helping an under-represented group in tech to speak out more, and thereby promoting diversity and inclusion in many practical ways. I'd like to share my personal story with you, and I hope it could give you an idea of how we can work together in real life towards the same goal.</p>
<h3 id="aboutsujinlee">About Sujin Lee</h3>
<p>Sujin jumped into the programming world with Python language when she was studying music composition in university. Since she started hosting workshops for Django Girls Seoul in 2015, she has been promoting diversity by supporting women to advance in the IT industry and tech communities. Currently, she is leading Women Who Code Seoul network to help women excel in tech careers and growth. She is a graduate student at Seoul National University and pursuing her passion in music and programming with equal vigor. Her goal is to inspire the world with creative works; she is developing interactive music applications at Music and Audio Research Group of Seoul National University.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[[RGSoC] #1. Prologue: Introduce  Team #RGSoCSunshine and Babel.js]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>My history with open source has just started from this July! I am honored to have been invited to participate in this <a href="https://railsgirlssummerofcode.org/">Rails Girls Summer of Code(RGSoC) </a>, a global fellowship program for women and non-binary coders in this year. RGSoC students receive a three-month scholarship to work on existing</p>]]></description><link>https://sujinlee.me/rgsoc-prologue/</link><guid isPermaLink="false">5f55e80e150a1a7ae0eae84a</guid><category><![CDATA[opensource]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Wed, 18 Jul 2018 18:21:32 GMT</pubDate><media:content url="https://sujinlee.me/content/images/2018/07/rgsoc-thank-you-board-making-of-04.jpg" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://sujinlee.me/content/images/2018/07/rgsoc-thank-you-board-making-of-04.jpg" alt="[RGSoC] #1. Prologue: Introduce  Team #RGSoCSunshine and Babel.js"><p>My history with open source has just started from this July! I am honored to have been invited to participate in this <a href="https://railsgirlssummerofcode.org/">Rails Girls Summer of Code(RGSoC) </a>, a global fellowship program for women and non-binary coders in this year. RGSoC students receive a three-month scholarship to work on existing Open Source projects and expand their skill set. This article was published in Rails Girls Summer of Code's official blog.</p>
<hr>
<h1 id="whoweare">Who we are</h1>
<p><img src="https://sujinlee.me/content/images/2018/07/coding_rgsoc.jpg" alt="[RGSoC] #1. Prologue: Introduce  Team #RGSoCSunshine and Babel.js"></p>
<p>안녕하세요(annyeonghaseyo)! We are Sujin and Gyujin from 'Sunshine' team in Seoul, S.Korea. We are working for <a href="https://babeljs.io/">Babel.js</a>, an open source JavaScript transpiler that converts cutting-edge JavaScript into plain old ES5 JavaScript that can run in any browser.</p>
<p>We are full stack JavaScript developers who have common interests in UI development and interactive interfaces. Before jumping into the programming world, we were artists: Gyujin was a designer and Sujin was a musician. We love art, creativity and technology. Based on both sides of programming and art experiences, we say the process of making graphic design or composing a music piece is very similar to computer programming.</p>
<p><a href="https://twitter.com/MarinaGJCho">Gyujin</a> is a junior frontend engineer at <a href="https://ridibooks.com">Ridibooks</a>, South Korea's leading e-reading services company. Staring with ES6, she has used jQuery, Vue.js, React.js, and TypeScript last two years for company product.</p>
<p><a href="https://twitter.com/sujinleeme">Sujin</a> is a graduate student in Seoul National University and she has pursued her passions of music and programming with equal vigor, aiming to make creative work that inspires. Nowadays she is strongly interested in making interactive music applications using machine learning. She leads the Women Who Code Seoul Network and is trying to build a more diverse and inclusive tech community here in Korea. Everywhere she goes she always has a pair of running shoes with her to run.</p>
<h1 id="howwemet">How we met</h1>
<p>Last february, as soon as Sujin came across a news about the opening of the Rails Girls Summer of Code Applications, it just stuck in her mind. Sujin was confident that she could get a great opportunity to contribute to <strong>a real open source project</strong> as a member of Open Source ecosystem. After that Sujin searched for a like-minded team mate in Django Girls community in Seoul and Gyujin that's how we got on the same boat. We asked close senior engineers to be our team coaches and everyone eagerly accepted the request and this is how we formed Team Sunshine to start our open source journey.</p>
<h1 id="whatbringsustojoinrgsoc">What brings us to join RGSoC</h1>
<p>It is certain that the numbers for women engineers in the tech industry are already pretty bleak, <a href="https://www.wired.com/2017/06/diversity-open-source-even-worse-tech-overall/">but the situation is even worse as far as participation in open source projects is concerned.</a> Additionally, we thought there was no room for the entry level of developers in open source projects and we weren't familiar to open source community until we met RGSoC. Unfortunately, we couldn’t meet any woman open source contributor who works the world's leading repository of open-source code, in Korea. We were highly motivated by RGSoC's initiatives to bring more diversity into open source and last RGSoC fellows's successful stories boosted our confidence. And we couldn't be more thrilled to think that our code could be running on millions of computers! Yes, RGSoC is definitly worth it.</p>
<p>During our preparation for the RGSoC application, we had to pick candidate two projects. Among other projects, <a href="https://babeljs.io/">Babel</a> was the best fit for our needs. We had already used ES6 features for mordern web development, experienced React for SPAs and moveover we have a passionate concern for the next generations of ES.</p>
<p>We also read a recent <a href="https://medium.freecodecamp.org/were-nearing-the-7-0-babel-release-here-s-all-the-cool-stuff-we-ve-been-doing-8c1ade684039">Henry’s writing</a> published in free code camp medium. We were touched by the warmth of his welcome to Babel open source community. He says that &quot;Babel is in a great position to be an educational tool for programmers so they can continue to learn how JavaScript works&quot;. We thought that  through contributing to the project, they can learn about Javascript core concepts such as ASTs, compilers, language specification.</p>
<h1 id="tadawhatasurprise">Ta-da! What a surprise!</h1>
<p>One day in April, we had a video call from Ana and Ramón from the RGSoC team. We felt a little nervous at the beginning of about 5 min, however, it fell from us very quickly because these two warmhearted supervisors made us feel very comfortable. Actually, we didn't think that we were successful in making them enthusiastic about us so that any of us were expecting to be able to get in.</p>
<p>Two weeks later, Ana and Vaishali requested a second round interview because the selection process had been a bit more demanding than previous editions. It ended up being a surprise call were we learned that we were accepted in RGSoC! 🎉 We were thrilled with the good news and it was a moment we will never forget.</p>
<blockquote class="twitter-tweet" data-cards="hidden" data-lang="en"><p lang="en" dir="ltr">Meet the RGSoC 2018 Teams! This year we surprised them with the good news and made a clip with their reactions! 💛 💜 💚 💙 💖<br>📢 But there&#39;s more: We extended the crowdfunding campaign to fund more teams! Watch the full video and read all about it here:<a href="https://t.co/EAcn22m1KD">https://t.co/EAcn22m1KD</a> <a href="https://t.co/cQHQ32mAzL">pic.twitter.com/cQHQ32mAzL</a></p>&mdash; RGSoC 2018 (@RailsGirlsSoC) <a href="https://twitter.com/RailsGirlsSoC/status/987757077614514177?ref_src=twsrc%5Etfw">April 21, 2018</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<div>
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">Congrats to <a href="https://twitter.com/sujinleeme?ref_src=twsrc%5Etfw">@sujinleeme</a> and <a href="https://twitter.com/MarinaGJCho?ref_src=twsrc%5Etfw">@MarinaGJCho</a> on ☀️Team Sunshine ☀️! The <a href="https://twitter.com/babeljs?ref_src=twsrc%5Etfw">@babeljs</a> team is super excited to work with ya&#39;ll this summer for <a href="https://twitter.com/RailsGirlsSoC?ref_src=twsrc%5Etfw">@RailsGirlsSoC</a> 😊<a href="https://t.co/JaKjUiHlfE">https://t.co/JaKjUiHlfE</a></p>&mdash; Henry Zhu (@left_pad) <a href="https://twitter.com/left_pad/status/988019997023920128?ref_src=twsrc%5Etfw">April 22, 2018</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</div>
<h2 id="ourprojectbabeljs">Our Project: Babel.js</h2>
<center>
<img alt="[RGSoC] #1. Prologue: Introduce  Team #RGSoCSunshine and Babel.js" src="https://cdn-images-1.medium.com/max/1600/1*XmHUL5DeySv_dGmvbPqdDQ.png" width="50%" align="middle">
</center>
<p>Babel is an open-sourced JavaScript compiler that leader companies like Facebook, Netflix, and Spotify and countless others adopt to ship software for the web. Babel not only allows developers to use the latest syntax in older browsers, but even has its role in shaping the future of the language itself due to its adoption in the community. The SPA frameworks like Angular, React, Vue, Ember also use Babel. It's downloaded more than 14 million times a month on npm. Isn't it amazing? ✨</p>
<p>In Bable offical website, there is <a href="https://babeljs.io/repl/">Babel REPL</a>, a playground for ES6; where user can write ES6 code, check the relevant code in ES5; and also the output.</p>
<p><img src="https://sujinlee.me/content/images/2018/08/repl-example.png" alt="[RGSoC] #1. Prologue: Introduce  Team #RGSoCSunshine and Babel.js"></p>
<p>Currently, the <a href="https://babeljs.io/team">Babel core team</a> is going to replace CodeMirror to <a href="https://twitter.com/CompuIves">Ives'</a> <a href="https://codesandbox.io/">CodeSandBox</a>, the new code online editor. An end goal is to show a different view of the output code: <a href="https://astexplorer.net/">AST</a>, output code, or <a href="https://github.com/babel/babel-time-travel">time travel</a>. We have just started to work for integration time travel in REPL and are going to contribute for UI development with help of our mentors and coaches.</p>
<p><img src="https://sujinlee.me/content/images/2018/07/Screen-Shot-2018-07-17-at-11.06.38-PM.png" alt="[RGSoC] #1. Prologue: Introduce  Team #RGSoCSunshine and Babel.js"></p>
<h2 id="howwework">How we work</h2>
<p>We are part-time sponsored team and we meet from 6pm to 10pm at <a href="https://www.coworker.com/south-korea/seoul/peachtree">PeachTree</a>, a co-working space that supports startups via its networks, spaces, amenities and various programs. The name 'PeachTree' comes from the tale of 'Romance of the Three Kingdoms'. Just like how Yubi, Gwanwoo and Jangbi pleged their brotherhood under the peach tree. We hope to cooperate in harmony each other in its space under the peach tree.</p>
<p>We have regular meeting with all members. Our team has 6 different time zones; Seoul, Lisbon, NY and more!</p>
<div>
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">☀️<a href="https://twitter.com/hashtag/RGSoC2018?src=hash&amp;ref_src=twsrc%5Etfw">#RGSoC2018</a> <a href="https://twitter.com/hashtag/RGSoCSunshine?src=hash&amp;ref_src=twsrc%5Etfw">#RGSoCSunshine</a> DAY8 <a href="https://twitter.com/MarinaGJCho?ref_src=twsrc%5Etfw">@MarinaGJCho</a> <a href="https://twitter.com/sujinleeme?ref_src=twsrc%5Etfw">@sujinleeme</a><br>✅ We had two 📞; with <a href="https://twitter.com/AnaSofiaPinho?ref_src=twsrc%5Etfw">@AnaSofiaPinho</a>, and  <a href="https://twitter.com/existentialism?ref_src=twsrc%5Etfw">@existentialism</a> <a href="https://twitter.com/left_pad?ref_src=twsrc%5Etfw">@left_pad</a> <a href="https://twitter.com/CompuIves?ref_src=twsrc%5Etfw">@CompuIves</a> <br>✅ Checked Sandpack PR to REPL <br>✅ Ready to review code of current babel-time-travel <br>👉 Read More? <a href="https://t.co/M7oEjz8ygA">https://t.co/M7oEjz8ygA</a> <a href="https://t.co/ZraU9wJzCR">pic.twitter.com/ZraU9wJzCR</a></p>&mdash; Sujin Lee (@sujinleeme) <a href="https://twitter.com/sujinleeme/status/1017263419802521600?ref_src=twsrc%5Etfw">July 12, 2018</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</div>
<h2 id="meetshiningoursupervisormentorsandcoaches">Meet shining our supervisor, mentors, and coaches!</h2>
<img alt="[RGSoC] #1. Prologue: Introduce  Team #RGSoCSunshine and Babel.js" src="https://sujinlee.me/content/images/2018/07/team-sunshine_first-week-calls.jpg" width="80%">
<p>We are honored to have the opportunity to join open source community and collaboration with the experts. We are pleased to introduce our supervisor, mentors, and coaches!</p>
<h3 id="supervisorrgsoc">Supervisor: RGSoC</h3>
<ul>
<li><a href="https://twitter.com/AnaSofiaPinho">Ana Sofia Pinho</a><br>
Ana lives in Coimbra, Portugal. She has been involved with RGSoC as an organizer and supervisor since 2016. She handles the marketing and social media related areas of RGSoC.<br>
As a supervisor Ana is our RGSoC contact for the whole summer. She keeps an eye on the general well-being of the project's progress and assists in the non-coding aspects of the Rails Girls Summer of Code.</li>
</ul>
<h3 id="mentorsbabeljs">Mentors: Babel.js</h3>
<ul>
<li>
<p><a href="https://twitter.com/left_pad">Henry Zhu</a><br>
Henry is a developer in NYC who left his job at Behance this past March to maintain Babel full time with the support by backers on Patreon and Open Collective. He's interested in living out the parallels of digital communities and faith through open source.</p>
</li>
<li>
<p><a href="https://twitter.com/existentialism">Brian Ng</a><br>
Brian is a developer and startup advisor living in Houston, Texas. He helps maintain Babel and contributes to other open source projects in his free time.</p>
</li>
<li>
<p><a href="https://twitter.com/loganfsmyth">Logan Smyth</a><br>
Logan is a Bay Area-based developer who has been involved with Babel for the past 3 years helping with development efforts, overall maintenance efforts, and user support. He is interested in open source, software language design, and writing good code.</p>
</li>
</ul>
<h3 id="coacheslocal">Coaches: Local</h3>
<ul>
<li>
<p><a href="https://twitter.com/YoonByungjune">Byungjune Yoon</a><br>
Byungjune is a frontend engineer at <a href="https://danoshop.net/">DANO</a>, a health-care startup. He has a desire to make a better world with technology and loves that open source allows him to do just that. He is also interested in social, political, and gender issues. He worked on <a href="https://voteforkorea.org">voteforkorea.org</a> project, the national voting lottery a.k.a, as an incentive, giving to the people who participated in the national election a chance to win a lottery.</p>
</li>
<li>
<p><a href="https://twitter.com/incleaf">Hyeonsu Lee</a><br>
Hyeonsu is a frontend engineer at <a href="https://ridicorp.com">Ridi</a>, an e-book startup. He loves to learn the new things to get out of the comfort zone. He is familiar with JavaScript and React. He is serving in the military at the moment as a software engineer.</p>
</li>
<li>
<p><a href="https://twitter.com/wagurano">Seongjun Kim</a><br>
Seongjun is a prominent rubyist writing code for world peace, however, he has used java for various type of products in his company. He contributed to make social campaign web platform, voteaward.com &amp; ansim.me by ruby on rails and tries to boost local ruby communities in Korea.</p>
</li>
<li>
<p><a href="https://twitter.com/adhrinae">Dohyung Ahn</a><br>
Dohyung is a frontend engineer who has been working on React-Typescript based projects recently. He is in <a href="https://www.protopie.io/">protopie.io</a> now. He has a huge enthusiasm for contributing to open source culture and he is happy to help programmers to not only improving their technical skills, but also products and projects.</p>
</li>
</ul>
<h2 id="whatdowewanttoachievebytheendofthesummer">What do we want to achieve by the end of the summer?</h2>
<p>RGSoC is a shot at life. Again, it is a pleasure to join for Rails Girls Summer of Code program and meet fellow women developers in open source community again. Our primary focus is continuing to learn advanced level of Javascript and growing up to be open source developers! We want to make people recognize the contributions women are making and inspire more women through our lines of code, PRs, and more during RGSoC. If we have a chance, we are willing to give a talk about our open source activities.</p>
<h2 id="followrgsunshineontwitter">Follow #RGSunshine on twitter</h2>
<p>We share our team news, daily working report, funny moments, achievements, compliments and more using <a href="https://twitter.com/hashtag/RGSoCSunshine?src=hash">#RGSoCSunsine</a> on Twitter! 💖</p>
<blockquote>
<p>Keep your face to the sun and you will never see the shadows. - Helen Keller</p>
</blockquote>
<p>Sunshine is the vital and main source of energy for life on earth. So if we focus on the <strong>sunshine</strong> - work, process, habit - it will bring us positivity in life and we will never feel frustration and negativity.</p>
<p>Just like our team name, we will always look on the bright side of life with a positive and optimistic energy. 🌞!</p>
<img src="https://sujinlee.me/content/images/2018/07/rgsoc-campaign-2018-header-1.png" width="100%" alt="[RGSoC] #1. Prologue: Introduce  Team #RGSoCSunshine and Babel.js">
<hr>
<p>main photo credit. <a href="http://anasofiapinho.com/work/work-rgsoc.html">Ana Sofia Pinho's website</a></p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[[번역] 깊이 있는 리액트 개발 환경 구축하기]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>브라질 출신 풀스택 개발자인 <a href="https://www.linkedin.com/in/esausilva/">에수 실바(Esau Silva)</a>의 <a href="https://esausilva.com/2018/01/13/learn-webpack-for-react/">How to use Webpack with React: an in-depth tutorial</a> 튜토리얼을 번역한 글이다. 리액트에 익숙한 경험자를 대상으로 웹팩(webpack)과 바벨(babel)을 사용해 리액트 개발 환경을 만드는 과정을 소개한다. 완성된 코드는 <a href="https://github.com/esausilva/react-starter-boilerplate-hmr">react-starter-boilerplate-hmr</a>에서 확인할 수 있다.</p>
<hr>
<p>리액트 라우터(React Router), 핫</p>]]></description><link>https://sujinlee.me/webpack-react-tutorial/</link><guid isPermaLink="false">5f55e80e150a1a7ae0eae849</guid><category><![CDATA[react]]></category><dc:creator><![CDATA[Sujin Lee]]></dc:creator><pubDate>Sat, 16 Jun 2018 15:48:16 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1514428631868-a400b561ff44?ixlib=rb-0.3.5&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=1080&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ&amp;s=68ee32be5d3a0962b0e84f9369554064" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://images.unsplash.com/photo-1514428631868-a400b561ff44?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=68ee32be5d3a0962b0e84f9369554064" alt="[번역] 깊이 있는 리액트 개발 환경 구축하기"><p>브라질 출신 풀스택 개발자인 <a href="https://www.linkedin.com/in/esausilva/">에수 실바(Esau Silva)</a>의 <a href="https://esausilva.com/2018/01/13/learn-webpack-for-react/">How to use Webpack with React: an in-depth tutorial</a> 튜토리얼을 번역한 글이다. 리액트에 익숙한 경험자를 대상으로 웹팩(webpack)과 바벨(babel)을 사용해 리액트 개발 환경을 만드는 과정을 소개한다. 완성된 코드는 <a href="https://github.com/esausilva/react-starter-boilerplate-hmr">react-starter-boilerplate-hmr</a>에서 확인할 수 있다.</p>
<hr>
<p>리액트 라우터(React Router), 핫 모듈 리플레이스먼트(HMR, Hot Module Replacement), 경로(Route)와 벤더(Vendor)로 코드 분할 및 배포 설정 구성까지, 웹팩(Webpack)과 리액트(React) 사용법의 모든 것을 알아보자.</p>
<p>아래는 앞으로 사용할 라이브러리 목록이다.</p>
<ul>
<li>React 16</li>
<li>React Router 4</li>
<li>Semantic UI : CSS 프레임워크</li>
<li>Hot Module Replacement (HMR)</li>
<li>CSS Autoprefixer</li>
<li>CSS Modules</li>
<li>Stage 1 Preset</li>
<li>Webpack 4</li>
<li>Route과 Vendor로 코드 분할</li>
<li>Webpack Bundle Analyzer</li>
</ul>
<h3 id>준비 사항</h3>
<ul>
<li><a href="https://yarnpkg.com/">yarn</a>과 <a href="https://nodejs.org/en/">Node.js</a> 설치가 되어 있는지 확인한다.</li>
<li>리액트(React)와 리액트 라우터(React Router) 기초 지식이 필요하다.</li>
<li>역자: 리액트 기초지식이 없다면, 이 튜토리얼을 멈추고 <code>create-react-app</code>으로 간단한 리액트 앱을 만들어보는 것을 추천한다. <code>create-react-app</code>은 리액트 개발 도구와 환경 설정이 이미 세팅되어 있기 때문에 애플리케이션 구현에만 신경쓰면 된다. <a href="http://www.realhanbit.co.kr/books/87">리액트 도움닫기 - create-react-app</a> 장부터 읽는 것을 추천한다.</li>
</ul>
<p>📌 <code>yarn</code> 대신 <code>npm</code>을 사용할 수 있으나 명령어가 다를 수 있으니 유의하길 바란다.</p>
<h2 id="1">1. 의존성 초기화</h2>
<p>제일 먼저 새 디렉터리를 만들고 그 안에 <code>package.json</code> 파일을 만든다.</p>
<pre><code>mkdir webpack-for-react &amp;&amp; cd $_
yarn init -y
</code></pre>
<p><code>package.json</code> 파일은 아래와 같을 것이다.</p>
<pre><code>{
  &quot;name&quot;: &quot;webpack-for-react&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;license&quot;: &quot;MIT&quot;
}
</code></pre>
<p>초기 프로덕션 의존성(production dependencies) 과 개발 의존성(development dependencies)을 설치한다. 개발 의존성은 개발 단계에서만 사용되는 의존 라이브러리이며, 프로덕션 의존성은 배포 단계에서 사용되는 라이브러리를 말한다.</p>
<pre><code>yarn add react react-dom react-prop-types react-router-dom semantic-ui-react

yarn add babel-core babel-loader babel-preset-env babel-preset-react babel-preset-stage-1 css-loader style-loader html-webpack-plugin webpack webpack-dev-server webpack-cli -D
</code></pre>
<p>📌 볼드체는 변경된 코드를 뜻한다. 의존성 버전은 다를 수 있다.</p>
<pre>
<code>
{
  "name": "webpack-for-react",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
   <b> 
  "dependencies": {
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "react-prop-types": "^0.4.0",
    "react-router-dom": "^4.2.2",
    "semantic-ui-react": "^0.77.1"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.3",
    "babel-preset-env": "^1.6.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-1": "^6.24.1",
    "css-loader": "^0.28.10",
    "html-webpack-plugin": "^3.0.4",
    "style-loader": "^0.19.1",
    "webpack": "^4.0.0",
    "webpack-cli": "^2.0.14",
    "webpack-dev-server": "^3.0.0"
  } </b>
}
</code>
</pre>
<p>설치된 라이브러리를 간략히 알아보자.</p>
<ul>
<li>react: 리액트</li>
<li>react-dom: 브라우저 DOM 메서드를 제공한다.</li>
<li>react-prop-types: React props 타입을 체크한다.</li>
<li>react-router-dom: Provides routing capabilities to React for the browser</li>
<li>semantic-ui-react: CSS 프레임워크</li>
<li>babel-core: Babel 핵심 의존성 라이브러리이다. Babel(바벨)은 자바스크립트 ES6를 ES5로 컴파일하여 현재 브라우저가 이해할 수 있도록 변환하는 도구다.</li>
<li>babel-loader: babel과 webpack을 사용해 자바스크립트 파일을 컴파일한다.</li>
<li>babel-preset-env: ES2015, ES2016, ES2017 버전을 지정하지 않아도 바벨이 자동으로 탐지해 컴파일한다.</li>
<li>babel-preset-react: 리액트를 사용한다는 것을 바벨에게 말해준다.</li>
<li>babel-preset-stage-1: TC39에서 검토 중인 Stage 1 스펙을 사용한다. (stage-0부터 3까지는 EcmaScript 스펙 중에서 비공식 실험적인 기술들을 사용할 수 있게 해주는 프리셋으로 Stage 2와 Stage 3도 사용 가능하다.)</li>
<li>css-loader: <code>import/require()</code>처럼 <code>@import</code>와 <code>url()</code> 해석한다.</li>
<li>html-webpack-plugin: 애플리케이션을위한 HTML 파일을 생성하거나 템플릿을 제공한다.</li>
<li>style-loader: <code>&lt;style&gt;</code> 태그를 삽입하여 CSS에 DOM을 추가한다.</li>
<li>webpack: 모듈 번들러(Module bundler)</li>
<li>webpack-cli: Webpack 4.0.1 이상에서 필요한 커맨드라인 인터페이스다.</li>
<li>webpack-dev-server: 애플리케이션 개발 서버를 제공한다.</li>
</ul>
<h2 id="2babel">2. Babel 설정</h2>
<p>최상위 디렉터리 <code>webpack-for-react</code>에 바벨 설정 파일을 만든다.</p>
<pre><code>touch .babelrc
</code></pre>
<p><code>.babelrc</code> 파일을 열어 아래 코드를 추가한다.</p>
<pre><code>{
  &quot;presets&quot;: [&quot;env&quot;, &quot;react&quot;, &quot;stage-1&quot;]
}
</code></pre>
<p>바벨이 프리셋(preset) 플러그인을 사용할 수 있게 됐다. 나중에 Webpack에서 <code>babel-loader</code>를 호출할 때 어떤 역할을 하는지 이해하게 될 것이다.</p>
<h2 id="3webpack">3. Webpack 설정</h2>
<p>지금부터 본격적으로 시작해보자. Webpack 설정 파일을 만들어보자.<br>
터미널에서 아래 명령어를 입력해  <code>webpack.config.js</code>을 만든다.</p>
<pre><code>touch webpack.config.js
</code></pre>
<p><code>webpack.config.js</code> 파일을 열고 아래 코드를 작성한다.</p>
<pre><code class="language-javascript">const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const port = process.env.PORT || 3000;

module.exports = {
  // webpack 설정 부분
};
</code></pre>
<p>웹팩 기본 설정을 마쳤다. 다음으로 <code>webpack</code>과 <code>html-webpack-plugin</code>이 필요하다. 환경 변수 PORT가 없으면 기본 포트를 제공하고 모듈을 내보내는 일을 한다.</p>
<p><code>webpack.config.js</code> 파일을 다시 열어 아래 코드를 추가한다.</p>
<pre><code class="language-javascript">...
module.exports = {
  mode: 'development',
};
</code></pre>
<p>설정 사항이 개발 환경(<code>development</code>)인지 프로덕션(<code>production</code>)인지를 알려줬다.</p>
<blockquote>
<p>&quot;개발 모드는 속도와 개발자 경험에 최적화되어 있다. 프로덕션 모드는 애플리케이션을 배포와 관련된 유용한 집합을 제공한다.&quot; <a href="https://medium.com/webpack/webpack-4-mode-and-optimization-5423a6bc597a">웹팩 4: mode and optimization</a> 에서 발췌</p>
</blockquote>
<pre><code class="language-javascript">...
module.exports = {
  ...
  entry: './src/index.js',
  output: {
    filename: 'bundle.[hash].js'
  },
};
</code></pre>
<p>웹팩 인스턴스를 실행하기 위해 <code>entry</code>, <code>output</code>, <code>filename</code>, <code>devtool</code>, <code>module</code>, <code>rules</code> 값을 설정해보자.</p>
<ul>
<li><code>entry</code> - 애플리케이션의 진입점(entry point)이다. 리액트 앱이 있는 위치와 번들링 프로세스가 시작되는 지점이다. (<a href="https://webpack.js.org/concepts/entry-points/">웹팩 공식 문서 - entry point</a>)</li>
</ul>
<p>웹팩4에서는 웹팩3과 반대로 <code>entry</code>를 생략할 수 있다. <code>entry</code>가 없으면 웹팩은 시작점이 <code>./src</code> 디렉토리 아래에 있다고 가정한다. 그러나 이 튜토리얼에서는 <code>entry</code>를 설정해 시작점을 분명하게 표시하기로 한다. 나중에 이 부분을 삭제해도 된다.</p>
<ul>
<li><code>output</code> - 컴파일된 파일을 저장할 경로를 알려준다.</li>
<li><code>filename</code> - 번들된 파일 이름을 말한다. <code>[hash]</code>는 애플리케이션이 수정되어 다시 컴파일 될 때마다 웹팩에서 생성된 해시로 변경해주어 캐싱에 도움이 된다.</li>
</ul>
<pre><code class="language-javascript">...
module.exports = {
  ...
  devtool: 'inline-source-map',
};
</code></pre>
<p><code>devtool</code>은 <a href="https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Use_a_source_map">소스 맵(source maps)</a>을 생성해 애플리케이션 디버깅을 도와준다. 소스 맵에는 여러 가지 유형이 있으며 그 중 inline-source-map은 은 개발시에만 사용된다. (이외 옵션은 <a href="https://webpack.js.org/configuration/devtool/">공식 문서</a>를 참고한다.)</p>
<pre><code class="language-javascript">...
module.exports = {
  ...
  module: {
    rules: [

      // 첫 번째 룰
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      },

      // 두 번째 룰
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              camelCase: true,
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
};
</code></pre>
<ul>
<li><code>module</code> - 애플리케이션 내 포함되는 모듈을 정의한다. 우리의 경우 ESNext(바벨), CSS 모듈에 해당한다.</li>
<li><code>rules</code> - 각 모듈을 처리하는 방법을 설정한다.</li>
</ul>
<h4 id>첫 번째 룰</h4>
<p><code>node_modules</code> 디렉터리를 제외한 자바스크립트 파일을 찾은 다음 <code>babel-loader</code>를 통해 바벨을 사용해 바닐라 자바스크립트로 변환한다. 바벨은 <code>.babelrc</code> 파일에서 설정 내용을 읽는다.</p>
<h4 id>두 번째 룰</h4>
<p>css 파일을 찾고 <code>style-loader</code>와 <code>css-loader</code>로 css를 처리한다. 그 다음 <code>css-loader</code>에게 CSS 모듈, 카멜 케이스(camel case), 소스 맵을 사용할 것을 지시한다.</p>
<h3 id="css">CSS 모듈과 카멜 케이스</h3>
<p>이제 <code>import Styles from ‘./styles.css</code> 또는 <code>import { style1, style2 } from './styles.css'</code>와 같이 구조 해체 문법으로 스타일 정의를 할 수 있다.</p>
<pre><code class="language-javascript">...
&lt;div className={Style.style1}&gt;Hello World&lt;/div&gt;
// 또는 구조해체 문법을 사용할 수 있다.
&lt;div className={style1}&gt;Hello World&lt;/div&gt;
...
</code></pre>
<p>아래와 같은 CSS 클래스가 있다면,</p>
<pre><code class="language-css">.home-button {...}
</code></pre>
<p>아래 코드와 같이 카멜 케이스를 정의해 CSS 클래스를 가져온다.</p>
<pre><code class="language-javascript">import { homeButton } from './styles.css'
</code></pre>
<p>이제 플러그인을 구성해보자.</p>
<p><code>html-webpack-plugin</code>은 다른 옵션을 가진 객체를 받는다. HTML 템플릿과 favicon을 지정한다. 이후 Bundle Analyzer 및 HMR 용 플러그인을 추가할 것이다.</p>
<p>📌<a href="https://github.com/jantimon/html-webpack-plugin#configuration">웹팩 공식 문서 - 플러그인 설정</a></p>
<p><code>webpack.config.js</code> 파일에 아래 코드를 추가하자.</p>
<pre><code class="language-javascript">module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    })
  ],
};
</code></pre>
<p>마지막으로 개발 서버를 설정한다.</p>
<pre><code>...
module.exports = {
  ...
  devServer: {
    host: 'localhost',
    port: port,
    historyApiFallback: true,
    open: true
  }
};
</code></pre>
<p><code>host</code>는 localhost로, <code>port</code>는 기본 port로 할당했다. 현재 기본 port 번호는 3000이다. <code>historyApiFallback</code>을 <code>true</code>로, <code>open</code>을 <code>true</code>로 설정한다. 서버를 실행하면 브라우저가 자동으로 열리고 <a href="http://localhost:3000">http://localhost:3000</a> 에서 애플리케이션이 자동으로 실행된다.</p>
<p>📌 <a href="https://webpack.js.org/configuration/dev-server/">웹팩 공식 문서 - 개발 서버 설정</a></p>
<p>완성된 웹팩 설정 코드는 아래와 같다.</p>
<pre><code class="language-javascript">const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const port = process.env.PORT || 3000;

module.exports = {
  mode: 'development',  
  entry: './src/index.js',
  output: {
    filename: 'bundle.[hash].js'
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              camelCase: true,
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    })
  ],
  devServer: {
    host: 'localhost',
    port: port,
    historyApiFallback: true,
    open: true
  }
};
</code></pre>
<h2 id="3">3. 리액트 애플리케이션 만들기</h2>
<p>간단한 리액트 애플리케이션을 만들어보자. home, page not found, dynamic 각 세 웹 페이지 경로를 만들고 비동기로 로딩하게 만들 것이다.</p>
<p>📌리액트와 리액트 라우터 기초 지식이 있다고 생각하고 설명한다.</p>
<p>현재 프로젝트 구조는 아래와 같다.</p>
<pre><code>|-- node_modules
|-- .babelrc
|-- package.json
|-- webpack.config.js
|-- yarn.lock
</code></pre>
<p>터미널을 열고 <code>public</code> 폴더를 만들고 <code>index.html</code>을 만든다.</p>
<pre><code>mkdir public &amp;&amp; cd $_
touch index.html
</code></pre>
<p><code>public</code> 디렉터리 안에 favicon도 추가한다. <a href="https://github.com/esausilva/react-starter-boilerplate-hmr/blob/master/public/favicon.ico">이 곳</a>에서 기본 리액트 favicon 샘플 이미지 파일을 다운받을 수 있다.</p>
<p><code>index.html</code> 파일을 열고 아래 코드를 붙여 넣는다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;

&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
  &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot;&gt;
  &lt;link rel=&quot;stylesheet&quot; href=&quot;//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.13/semantic.min.css&quot;&gt;&lt;/link&gt;
  &lt;title&gt;webpack-for-react&lt;/title&gt;
&lt;/head&gt;

&lt;body&gt;
  &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
&lt;/body&gt;

&lt;/html&gt;
</code></pre>
<p>간단한 HTML 템플릿이다. Semantic UI 스타일시트 URL를 추가하고 <code>div</code>도 추가했다. 이 부분이 리액트 앱이 렌더링되는 곳이다.</p>
<p>다시 터미널로 돌아와 명령어로 <code>index.js</code> 파일을 만든다.</p>
<pre><code>cd ..
mkdir src &amp;&amp; cd $_
touch index.js
</code></pre>
<p><code>index.js</code> 파일을 열고 아래 코드를 넣는다.</p>
<pre><code class="language-javascript">import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';

ReactDOM.render(&lt;App /&gt;, document.getElementById('root'));
</code></pre>
<p>터미널에서 명령어로 리액트 컴포넌트 파일을 만든다.</p>
<pre><code>mkdir components &amp;&amp; cd $_
touch App.js Layout.js Layout.css Home.js DynamicPage.js NoMatch.js
</code></pre>
<p>프로젝트 구조가 아래와 같을 것이다.</p>
<pre><code>|-- node_modules
|-- public
    |-- index.html
    |-- favicon.ico
|-- src
    |-- components
        |-- App.js
        |-- DynamicPage.js
        |-- Home.js
        |-- Layout.css
        |-- Layout.js
        |-- NoMatch.js
    |-- index.js
|-- .babelrc
|-- package.json
|-- webpack.config.js
|-- yarn.lock
</code></pre>
<p>리액트 라우터로 페이지 경로와 컴포넌트를 연결할 차례다.<br>
<code>App.js</code> 파일을 열고 아래와 같이 수정한다.</p>
<pre><code class="language-javascript">import React from 'react';
import { Switch, BrowserRouter as Router, Route } from 'react-router-dom';

import Home from './Home';
import DynamicPage from './DynamicPage';
import NoMatch from './NoMatch';

const App = () =&gt; {
  return (
    &lt;Router&gt;
      &lt;div&gt;
        &lt;Switch&gt;
          &lt;Route exact path=&quot;/&quot; component={Home} /&gt;
          &lt;Route exact path=&quot;/dynamic&quot; component={DynamicPage} /&gt;
          &lt;Route component={NoMatch} /&gt;
        &lt;/Switch&gt;
      &lt;/div&gt;
    &lt;/Router&gt;
  );
};

export default App;
</code></pre>
<p>이제 Layout 컴포넌트 스타일과 컴포넌트를 정의하자.</p>
<p><code>Layout.css</code> 파일을 열고 아래 코드를 넣는다.</p>
<pre><code class="language-css">.pull-right {
  display: flex;
  justify-content: flex-end;
}

.h1 {
  margin-top: 10px !important;
  margin-bottom: 20px !important;
}
</code></pre>
<p><code>Layout.js</code> 파일을 열고 아래 코드를 작성한다.</p>
<pre><code class="language-javascript">import React from 'react';
import { Link } from 'react-router-dom';
import { Header, Container, Divider, Icon } from 'semantic-ui-react';

import { pullRight, h1 } from './layout.css';

const Layout = ({ children }) =&gt; {
  return (
    &lt;Container&gt;
      &lt;Link to=&quot;/&quot;&gt;
        &lt;Header as=&quot;h1&quot; className={h1}&gt;
          webpack-for-react
        &lt;/Header&gt;
      &lt;/Link&gt;
      {children}
      &lt;Divider /&gt;
      &lt;p className={pullRight}&gt;
        Made with &lt;Icon name=&quot;heart&quot; color=&quot;red&quot; /&gt; by Esau Silva
      &lt;/p&gt;
    &lt;/Container&gt;
  );
};

export default Layout;
</code></pre>
<p>웹 사이트의 레이아웃을 만들었다. CSS Modules를 통해 <code>layout.css</code>에서  <code>pullRight</code>, <code>h1</code> 두 CSS 룰을 가져왔다. 카멜 케이스로 가져와야 한다.</p>
<p><code>Home.js</code>를 열고 아래 코드를 작성한다.</p>
<pre><code class="language-javascript">import React from 'react';
import { Link } from 'react-router-dom';

import Layout from './Layout';

const Home = () =&gt; {
  return (
    &lt;Layout&gt;
      &lt;p&gt;Hello World of React and Webpack!&lt;/p&gt;
      &lt;p&gt;
        &lt;Link to=&quot;/dynamic&quot;&gt;Navigate to Dynamic Page&lt;/Link&gt;
      &lt;/p&gt;
    &lt;/Layout&gt;
  );
};

export default Home;
</code></pre>
<p><code>DynamicPage.js</code>를 열고 아래 코드를 작성한다.</p>
<pre><code class="language-javascript">import React from 'react';
import { Header } from 'semantic-ui-react';

import Layout from './Layout';

const DynamicPage = () =&gt; {
  return (
    &lt;Layout&gt;
      &lt;Header as=&quot;h2&quot;&gt;Dynamic Page&lt;/Header&gt;
      &lt;p&gt;This page was loaded asynchronously!!!&lt;/p&gt;
    &lt;/Layout&gt;
  );
};

export default DynamicPage;
</code></pre>
<p><code>NoMatch.js</code>를 열고 아래 코드를 작성한다.</p>
<pre><code class="language-javascript">import React from 'react';
import { Icon, Header } from 'semantic-ui-react';

import Layout from './Layout';

const NoMatch = () =&gt; {
  return (
    &lt;Layout&gt;
      &lt;Icon name=&quot;minus circle&quot; size=&quot;big&quot; /&gt;
      &lt;strong&gt;Page not found!&lt;/strong&gt;
    &lt;/Layout&gt;
  );
};

export default NoMatch;
</code></pre>
<p>필요한 모든 컴포넌트를 만들었다. 마지막으로 애플리케이션을 실행하는 스크립트를 <code>package.json</code>에 정의한다.</p>
<pre>
<code>
{
  "name": "webpack-for-react",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
   <b>"scripts": {</b>
    <b>"start": "webpack-dev-server"</b>
  <b> },</b>
  "dependencies": {
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "react-prop-types": "^0.4.0",
    "react-router-dom": "^4.2.2",
    "semantic-ui-react": "^0.77.1"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.3",
    "babel-preset-env": "^1.6.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-1": "^6.24.1",
    "css-loader": "^0.28.10",
    "html-webpack-plugin": "^3.0.4",
    "style-loader": "^0.19.1",
    "webpack": "^4.0.0",
    "webpack-cli": "^2.0.14",
    "webpack-dev-server": "^3.0.0"
  }
}
</code>
</pre>
<p><code>scripts</code> 와 <code>start</code> 키(key)를 추가했다. 이제 웹팩 개발 서버와 리액트가 함께 실행된다. 환경 설정 파일을 지정하지 않으면 <code>webpack-dev-server</code>는 <code>webpack.config.js</code> 파일을 최상단(루트) 디렉터리 기본 구성 항목으로 찾는다.</p>
<p>터미널에서 직접 확인해보자! 프로젝트 최상단 디렉터리로 가서 <code>yarn</code> 명령어로 서버를 실행한다.</p>
<pre><code>yarn start
</code></pre>
<img src="https://cdn-images-1.medium.com/max/800/1*jTtUShYfxBJfa9HUyTAxcQ.gif" alt="[번역] 깊이 있는 리액트 개발 환경 구축하기">
<p>이제 리액트 앱은 웹팩으로 작동한다. 번들된 자바스크립트 해쉬인 <code>bundle.d505bbab002262a9bc07.js</code>로 파일명이 표시되는 것을 확인할 수 있다.</p>
<h2 id="4hotmodulereplacementhmr">4. Hot Module Replacement (HMR) 설정</h2>
<p>터미널로 돌아가, <a href="https://github.com/gaearon/react-hot-loader">react-hot-loader</a> 라이브러리를 개발 의존성으로 설치한다.</p>
<pre><code>yarn add react-hot-loader -D
</code></pre>
<p><code>.babelrc</code> 파일을 열고 볼드체로 된 부분을 추가한다. 2번째 줄 끝에 (,)를 추가하는 것도 잊지 말자.</p>
<pre>
<code>
{
  "presets": [<b>["env", { "modules": false }]</b>, "react", "stage-1"],
  <b>"plugins": ["react-hot-loader/babel"]</b>
}

</code>
</pre>
<p>코드를 간결하게 만들기 위해 기존에 입력한 코드는 생략했다.</p>
<pre><code class="language-javascript">...
module.exports = {
  entry: './src/index.js',
  output: {
    ...
    publicPath: '/'
  },
  ...
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    ...
  ],
  devServer: {
    ...
    hot: true
  }
};
</code></pre>
<p>추가한 부분을 살펴보자.</p>
<ul>
<li><code>publicPath: ‘/’</code> — Hot reloading 은 중첩된 경로에서 동작하지 않는다.</li>
<li><code>webpack.HotModuleReplacementPlugin</code> — HMR 업데이트시 브라우저 터미널에 표시해 알아보기 쉽게 한다.</li>
<li><code>hot: true</code> — 서버에 HMR 작동을 허락한다.</li>
</ul>
<p><code>index.js</code> 파일을 열고 <code>&lt;AppContainer&gt;</code>으로 애플리케이션 전체 컴포넌트를 감싼 다음<code>module.hot</code>을 체크하도록 설정한다.</p>
<pre><code class="language-javascript">import { AppContainer } from 'react-hot-loader';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';

const render = Component =&gt;
  ReactDOM.render(
    &lt;AppContainer&gt;
      &lt;Component /&gt;
    &lt;/AppContainer&gt;,
    document.getElementById('root')
  );

render(App);

// Webpack Hot Module Replacement API 부분
if (module.hot) module.hot.accept('./components/App', () =&gt; render(App));
</code></pre>
<p>이제 HMR을 테스트해보자! 터미널로 다시 돌아와 서버를 재 실행해보자.</p>
<pre><code>yarn start
</code></pre>
<p>새로고침 없이 앱이 업데이트 되는 것을 볼 수 있다.</p>
<img src="https://cdn-images-1.medium.com/max/1600/1*Twg3ovg3MFeG2uzXCgs-WQ.gif" alt="[번역] 깊이 있는 리액트 개발 환경 구축하기">
<p>브라우저에서 변경 사항을 표시하려면 크롬 개발자 도구(Chrome DevTools)에서 <strong>Rendering -&gt; Paint flashing</strong> 을 선택 후 변경된 페이지마다 녹색 부분으로 강조 표시되는 것을 볼 수 있다. 터미널에서도 웹팩이 브라우저에 전송한 변경 사항을 확인할 수 있다.</p>
<h2 id="5">5. 코드 분할</h2>
<p><a href="https://webpack.js.org/guides/code-splitting/">코드 분할(code splitting)</a>을 사용하면 큰 번들 대신에 비동기 또는 병렬로 로드되는 여러 개의 번들을 만들 수 있다. 또한 Vendor 코드를 분리시켜 앱에서 로딩 시간을 줄일 수 있게 된다.</p>
<h3 id>경로 지정</h3>
<p>경로 별로 코드 분할을 수행할 수 있는 방법은 많다. 이 중 우리는 <a href="https://github.com/theKashey/react-imported-component">react-imported-component</a>를 사용할 것이다.</p>
<p>사용자가 다른 경로로 이동할 때 로드 스피너를 보여주어 새 페이지가 로드될 때까지 대기하는 동안 사용자가 빈 화면을 보지 않게 만들어보자. 이를 위해 로딩 컴포넌트(Loading component)를 만들 것이다.</p>
<p>그러나 새 페이지가 실제로 아주 빨리 로딩되는 경우, 사용자는 몇 밀리 초 동안 깜빡이는 로드 스피너를 보지 못하게 되므로 300 밀리 초 정도 로딩되는 컴포넌트를 지연시키고자 한다. 이를 위해 <a href="https://github.com/theKashey/react-imported-component">react-delay-render</a> 라이브러리를 사용할 것이다.</p>
<p>먼저 터미널에서 아래 명령어를 입력해 두 의존성을 추가하자.</p>
<pre><code>yarn add react-imported-component react-delay-render
</code></pre>
<p>이제 Loading 컴포넌트를 만들어보자.</p>
<pre><code>touch ./src/components/Loading.js
</code></pre>
<p><code>Loading.js</code> 파일을 열고 아래 코드를 입력한다.</p>
<pre><code class="language-javascript">import React from 'react';
import { Loader } from 'semantic-ui-react';
import ReactDelayRender from 'react-delay-render';

const Loading = () =&gt; &lt;Loader active size=&quot;massive&quot; /&gt;;

export default ReactDelayRender({ delay: 300 })(Loading);
</code></pre>
<p>이제 Loading 컴포넌트를 만들었으니, <code>App.js</code> 파일을 열어 아래와 같이 수정한다.</p>
<pre><code class="language-javascript">import React from 'react';
import { Switch, BrowserRouter as Router, Route } from 'react-router-dom';
import importedComponent from 'react-imported-component';

import Home from './Home';
import Loading from './Loading';

const AsyncDynamicPAge = importedComponent(
  () =&gt; import(/* webpackChunkName:'DynamicPage' */ './DynamicPage'),
  {
    LoadingComponent: Loading
  }
);
const AsyncNoMatch = importedComponent(
  () =&gt; import(/* webpackChunkName:'NoMatch' */ './NoMatch'),
  {
    LoadingComponent: Loading
  }
);

const App = () =&gt; {
  return (
    &lt;Router&gt;
      &lt;div&gt;
        &lt;Switch&gt;
          &lt;Route exact path=&quot;/&quot; component={Home} /&gt;
          &lt;Route exact path=&quot;/dynamic&quot; component={AsyncDynamicPAge} /&gt;
          &lt;Route component={AsyncNoMatch} /&gt;
        &lt;/Switch&gt;
      &lt;/div&gt;
    &lt;/Router&gt;
  );
};

export default App;
</code></pre>
<p>이로서 DynamicPage 컴포넌트, NoMatch  컴포넌트, 기본 애플리케이션, 각각 하나의 번들 또는 청크(chunck)가 만들어진다.</p>
<p>이제 번들 파일 이름을 변경해보자. <code>webpack.config.js</code>를 열고 아래와 같이 수정한다.</p>
<pre><code class="language-javascript">...
module.exports = {
  ...
  output: {
    filename: '[name].[hash].js',
    ...
  },
}
</code></pre>
<p>이제 앱을 실행해 경로를 기준으로 코드 분할이 잘 실행되는지 확인해보자.</p>
<pre><code>yarn start
</code></pre>
<img src="https://cdn-images-1.medium.com/max/1600/1*dcnAMR9QQpliwlUN0Gybog.gif" alt="[번역] 깊이 있는 리액트 개발 환경 구축하기">
<p>위 이미지 파일에 웹팩으로 생성된 세 가지 청크(chunk) 파일을 확인해보자. 앱이 실행되면 주요 번들만 로드된다는 것을 알 수 있다. 마지막으로 동적 페이지로 이동을 클릭하면 이 페이지에 해당하는 번들이 비동기적으로 로딩되는 것을 확인할 수 있다.</p>
<p><em>404 not found: 페이지를 찾을 수 없음</em> 페이지는 번들이 로드되지 않기 때문에 사용자 대역폭(user bandwith)을 절약할 수 있게 된다.</p>
<h3 id="vendor">vendor</h3>
<p>vendor로 애플리케이션 코드를 쪼개보자. <code>webpack.config.js</code> 파일을 열고 아래와 같이 파일을 변경한다.</p>
<pre><code class="language-javascript">...
module.exports = {
  entry: {
    vendor: ['semantic-ui-react'],
    app: './src/index.js'
  },
  ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'initial',
          test: 'vendor',
          name: 'vendor',
          enforce: true
        }
      }
    }
  },
  ...
};
</code></pre>
<p>각 설정 부분을 살펴보자.</p>
<ul>
<li><code>entry.vendor: [‘semantic-ui-react’]</code> — 메인 앱에서 특정 라이브러리를 빼내어 vendor로 만든다.</li>
<li><code>optimization</code> — 이 부분을 생략하면 Webpack은 vendor로 에플리케이션을 분할할 것이다. 이 부분을 추가하면 큰 번들 용량이 대폭 줄어든다. (<a href="https://github.com/webpack/webpack/issues/6357">참고: CommonsChunkPlugin -&gt; Initial vendor chunk</a>)</li>
</ul>
<p>📌 웹팩3에서는 <code>CommonsChunkPlugin</code>을 사용해 vendor와 common으로 코드 분할을 했지만, 웹팩4에서는 삭제된 기능이다. 따라서 <code>CommonsChunkPlugin</code>을 제거했다. 캐싱 제어가 필요할 수도 있기에 <code>optimization.splitChunks</code>를 추가했다. 자세한 내용은 <a href="https://gist.github.com/sokra/1522d586b8e5c0f5072d7565c2bee693">이 곳</a>을 참고하길 바란다.</p>
<p>터미널에서 다시 앱을 실행하자.</p>
<pre><code>yarn start
</code></pre>
<img src="https://cdn-images-1.medium.com/max/1600/1*_Mu3ndi_cs9ShJvvnD0VIg.gif" alt="[번역] 깊이 있는 리액트 개발 환경 구축하기">
<p>터미널에서 이전 세 청크와 더해진 새 vendor 청크를 표시했다. HTML을 보면 vendor와 앱 청크 둘다 모두 로드 되었음을 알 수 있다.</p>
<p>지금까지 웹팩 구성을 업데이트한 <code>webpack.config.js</code> 코드는 아래와 같다.</p>
<pre><code class="language-javascript">const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const port = process.env.PORT || 3000;
module.exports = {
  mode: 'development',
  entry: {
    vendor: ['semantic-ui-react'],
    app: './src/index.js'
  },
  output: {
    filename: '[name].[hash].js'
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              camelCase: true,
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'initial',
          test: 'vendor',
          name: 'vendor',
          enforce: true
        }
      }
    }
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    })
  ],
  devServer: {
    host: 'localhost',
    port: port,
    historyApiFallback: true,
    open: true,
    hot: true
  }
};
</code></pre>
<h3 id>배포 설정</h3>
<p><code>webpack.config.js</code>  파일 이름을 <code>webpack.config.development.js</code>로 바꾸고 <code>webpack.config.production.js</code> 파일을 새로 만든다.</p>
<pre><code>mv webpack.config.js webpack.config.development.js
cp webpack.config.development.js webpack.config.production.js
</code></pre>
<p>새로 개발 의존성 라이브러리인 <a href="https://github.com/webpack-contrib/extract-text-webpack-plugin">Extract Text Plugin(텍스트 추출 플러그인)</a>을 설치한다. 공식 문서에서 &quot;엔트리 청크(entry chunks)의 모든 필수 <code>*.css</code> 모듈을 별도 CSS 파일로 이동시킨다. 스타일은 JS에 번들링되지 않고 별도의 CSS 파일(<code>styles.css</code>)로 인라인된다. 스타일 시트 파일 용량이 크더라도 CSS 번들이 JS 번들과 병렬로 로드되기 때문에 더 빠르게 로드되는 장점이 있다.&quot;라고 말하고 있다.</p>
<pre><code>yarn add extract-text-webpack-plugin@next -D
</code></pre>
<p>📌 웹팩4 사용을 위해 extract-text-webpack-plugin 선 배포판을 설치해야 한다.</p>
<p><code>webpack.config.production.js</code> 파일을 열고 아래와 같이 변경한다.</p>
<pre><code class="language-javascript">const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
  mode: 'production',
  entry: {
    vendor: ['semantic-ui-react'],
    app: './src/index.js'
  },
  output: {
    // 'static' 디렉터리 안에 자바스크립트 번들을 생성한다.
    filename: 'static/[name].[hash].js',
    // 출력 디렉터리는 'dist' 이다.
    // '__dirname' 은 현재 디렉터리의 절대 경로를 제공하는 Node 변수다.
    // 'path.resolve'로 디렉터리를 합친다.
    // 웹팩4는 출력 경로를 './dist'로 가정하기 때문에 이부분을 그대로 둘 수 있다.
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/'
  },
  // 프로덕션 소스맵
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      },
      {
        test: /\.css$/,
        // 'Extract Text Plugin' 구성한다.
        use: ExtractTextPlugin.extract({
          // CSS가 추출되지 않으면 loader가 실행된다.
          fallback: 'style-loader',
          use: [
            {
              loader: 'css-loader',
              options: {
                modules: true,
                // @import(ed) 리소스에 css-loader를 적용하기 전 로더를 구성한다.
                importLoaders: 1,
                camelCase: true,
                // CSS 파일을 위해 소스 맵을 생성한다.
                sourceMap: true
              }
            },
            {
              // css-loader 전에 PostCSS이 실행되어 압축(minify)하고 CSS 룰을 적용하고 
              // 자동 전처리(autoprefixer)를 실행한다.
              // 자동 전처리 단계에서 최신 브라우저 2 사양을 사용한다.
              loader: 'postcss-loader',
              options: {
                config: {
                  ctx: {
                    autoprefixer: {
                      browsers: 'last 2 versions'
                    }
                  }
                }
              }
            }
          ]
        })
      }
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'initial',
          test: 'vendor',
          name: 'vendor',
          enforce: true
        }
      }
    }
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    }),
    
    // 'styles' 디렉터리 내 스타일시트를 생성한다.
    new ExtractTextPlugin({
      filename: 'styles/styles.[hash].css',
      allChunks: true
    })
  ]
};
</code></pre>
<p>port 변수, HMR 및 <code>devServer</code>와 관련된 플러그인을 제거했다. 또한 프러덕션 구성에 PostCSS를 추가했다. postcss-loader를 설치하고 새 설정 파일을 만든다.</p>
<pre><code>yarn add postcss-loader -D
touch postcss.config.js
</code></pre>
<p><code>postcss.config.js</code> 파일을 열고 아래 코드를 입력한다.</p>
<pre><code class="language-javascript">module.exports = {
  plugins: [require('autoprefixer')]
};
</code></pre>
<p>Postfix가 autoprefixer 플러그인을 사용하도록 했다. (자세한 옵션은 <a href="https://webpack.js.org/loaders/postcss-loader/">공식 문서</a>를 참고한다.)</p>
<p>배포 빌드하기 전 마지막으로 <code>package.json</code>에 <code>build</code> 스크립트를 추가해야 한다.</p>
<p>파일을 열고 <code>scripts</code> 부분에 아래 내용을 추가한다.</p>
<pre><code class="language-json">...
&quot;scripts&quot;: {
  &quot;dev&quot;:&quot;webpack-dev-server --config webpack.config.development.js&quot;,
  &quot;prebuild&quot;: &quot;rimraf dist&quot;,
  &quot;build&quot;: &quot;cross-env NODE_ENV=production webpack -p --config webpack.config.production.js&quot;
},
...
</code></pre>
<p>스크립트 명령어에서 <code>start</code>를 <code>dev</code> 로 수정했고, <code>prebuild</code>와 <code>build</code> 두 항목을 추가했다.</p>
<p>마지막으로 개발 및 배포 시 어떤 설정 구성을 따라야 하는지를 알려줬다.</p>
<ul>
<li>
<p><code>prebuild</code> — <a href="https://github.com/isaacs/rimraf">rimraf</a> 라이브러리를 사용해 build 스크립트 전에 실행하여 마지막 프로덕션 빌드로 생성된 <code>dist</code> 디렉토리를 삭제한다.</p>
</li>
<li>
<p><code>build</code> - 먼저 윈도우 환경에서 <a href="https://github.com/kentcdodds/cross-env">cross-env</a> 라이브러리를 사용하고 <code>NODE_ENV</code>으로 환경 변수를 설정한다. 그 다음 웹팩을 <code>-p</code> 플래그로 호출해 프로덕션을 위해 빌드 최적화를 지시하고 마지막으로 프로덕션 구성을 지정한다.</p>
</li>
</ul>
<p><code>package.json</code>에 명시된 두 의존성을 추가하자.</p>
<pre><code>yarn add rimraf cross-env -D
</code></pre>
<p>프로덕션 빌드 생성 전, 새 프로젝트 구조를 살펴보자.</p>
<pre><code>|-- node_modules
|-- public
    |-- index.html
    |-- favicon.ico
|-- src
    |-- components
        |-- App.js
        |-- DynamicPage.js
        |-- Home.js
        |-- Layout.css
        |-- Layout.js
        |-- Loading.js
        |-- NoMatch.js
    |-- index.js
|-- .babelrc
|-- package.json
|-- postcss.config.js
|-- webpack.config.development.js
|-- webpack.config.production.js
|-- yarn.lock
</code></pre>
<p>빌드 명령어로 프로덕션 번들을 생성할 수 있다.</p>
<pre><code>yarn build
</code></pre>
<img src="https://cdn-images-1.medium.com/max/1600/1*aw89SCBqWe3FoQ3WsEsSHw.gif" alt="[번역] 깊이 있는 리액트 개발 환경 구축하기">
<p>위 이미지에서 확인할 수 있듯이, <code>build</code> 스크립트가 실행 후 웹팩은 프로덕션이 준비된 애플리케이션이 들어있는 <code>dist</code> 디렉터리를 만든다. 그리고 생성된 파일을 검사해 압축된 파일인지를 확인하고 원본 소스가 있는지 확인한다. <code>PostCSS</code>는 CSS 파일에 자동 접두어(autoprefixing)를 추가한다.</p>
<p>이제 프로덕션 파일을 가져와 노드 서버를 실행해 서비스되는 화면을 볼 수 있다.</p>
<img src="https://cdn-images-1.medium.com/max/1600/1*_yT6DfUBQ053y1qC5PElAQ.gif" alt="[번역] 깊이 있는 리액트 개발 환경 구축하기">
<p>📌프로덕션 파일 실행을 위해 <a href="https://github.com/esausilva/quick-node-server">quick-node-server</a>을 사용했다.</p>
<p>개발 구성 웹팩과 프로덕션 구성 웹팩 두 가지가 있다. 그러나 두 파일 설정 항목이 비슷하여 서로 공유 가능하게 만들어보자. 이를 위해 다음 단계에서 웹팩 구성을 다시 수정해보자.</p>
<h2 id="6">6. 웹팩 구성</h2>
<p>이 부분은 션 라킨(Sean Larkin)의 <a href="https://webpack.academy/">웹팩 아카데미(Webpack Academy)</a>을 바탕으로 작성됐다. 웹팩 아카데미에서 리액트와 웹팩에 대해 자세히 배우기를 권한다.</p>
<p><a href="https://github.com/survivejs/webpack-merge">webpack-merge</a>와 <a href="https://github.com/chalk/chalk">Chalk</a>를 개발 의존성으로 설치하자.</p>
<pre><code>yarn add webpack-merge chalk -D
</code></pre>
<p><code>build-utils/addons</code> 디렉터리를 만들고 새 파일을 추가한다.</p>
<pre><code>mkdir -p build-utils/addons
cd build-utils
touch build-validations.js common-paths.js webpack.common.js webpack.dev.js webpack.prod.js
</code></pre>
<p>현재 프로젝트 폴더 구조는 아래와 같을 것이다.</p>
<pre><code>|-- build-utils
    |-- addons
    |-- build-validations.js
    |-- common-paths.js
    |-- webpack.common.js
    |-- webpack.dev.js
    |-- webpack.prod.js
|-- node_modules
|-- public
    |-- index.html
    |-- favicon.ico
|-- src
    |-- components
        |-- App.js
        |-- DynamicPage.js
        |-- Home.js
        |-- Layout.css
        |-- Layout.js
        |-- Loading.js
        |-- NoMatch.js
    |-- index.js
|-- .babelrc
|-- package.json
|-- postcss.config.js
|-- webpack.config.development.js
|-- webpack.config.production.js
|-- yarn.lock
</code></pre>
<p><code>common-paths.js</code> 파일을 열고 아래 코드를 작성한다.</p>
<pre><code class="language-javascript">const path = require('path');
const PROJECT_ROOT = path.resolve(__dirname, '../');

module.exports = {
  projectRoot: PROJECT_ROOT,
  outputPath: path.join(PROJECT_ROOT, 'dist'),
  appEntry: path.join(PROJECT_ROOT, 'src')
};
</code></pre>
<p>파일명에서 유추할 수 있듯이 웹팩 구성의 공통 경로를 정의했다. <code>PROJECT_ROOT</code>는 <code>build-utils</code> 디렉터리 (프로젝트의 실제 최상단(root) 경로에서 한 레벨 아래에 위치) 내에서만 작업하는 것과 같이 단일 디렉터리만 확인해야 한다.</p>
<p><code>build-validations.js</code> 파일을 열고 아래 코드를 작성한다.</p>
<pre><code class="language-javascript">const chalk = require('chalk');
const ERR_NO_ENV_FLAG = chalk.red(
  `You must pass an --env.env flag into your build for webpack to work!`
);

module.exports = {
  ERR_NO_ENV_FLAG
};
</code></pre>
<p>앞으로 <code>package.json</code>을 수정할 때 스크립트에 <code>-env.env</code>플래그가 필요하다. 플래그가 있는지를 유효성 판단한다. 플래그가 없으면, 오류를 발생시킨다.</p>
<p>다음으로 웹팩 환경 설정 파일을 개발과 프로덕션 공통, 개발, 프로덕션 세 파일로 나눠보자.</p>
<p>먼저 <code>webpack.common.js</code> 파일을 열고 아래 코드를 입력한다.</p>
<pre><code class="language-javascript">const commonPaths = require('./common-paths');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const config = {
  entry: {
    vendor: ['semantic-ui-react']
  },
  output: {
    path: commonPaths.outputPath,
    publicPath: '/'
  },
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      }
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'initial',
          test: 'vendor',
          name: 'vendor',
          enforce: true
        }
      }
    }
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    })
  ]
};
module.exports = config;
</code></pre>
<p><code>webpack.config.development.js</code>와 <code>webpack.config.production.js</code>에서 공통 항목을 빼내서 <code>common-paths.js</code> 파일을 만들었다. 이를 위해 <code>output.path</code> 경로를  <code>common-paths.js</code>로 설정했다.</p>
<p><code>webpack.dev.js</code> 파일을 열고 개발 설정 내용을 떼어 옮긴다.</p>
<pre><code class="language-javascript"> commonPaths = require('./common-paths');
const webpack = require('webpack');
const port = process.env.PORT || 3000;
const config = {
  mode: 'development',
  entry: {
    app: `${commonPaths.appEntry}/index.js`
  },
  output: {
    filename: '[name].[hash].js'
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              camelCase: true,
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    host: 'localhost',
    port: port,
    historyApiFallback: true,
    hot: true,
    open: true
  }
};
module.exports = config;
</code></pre>
<p><code>webpack.prod.js</code> 파일을 열고 프로덕션 설정 내용을 떼어 옮긴다.</p>
<pre><code class="language-javascript">const commonPaths = require('./common-paths');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const config = {
  mode: 'production',
  entry: {
    app: [`${commonPaths.appEntry}/index.js`]
  },
  output: {
    filename: 'static/[name].[hash].js'
  },
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: [
            {
              loader: 'css-loader',
              options: {
                modules: true,
                importLoaders: 1,
                camelCase: true,
                sourceMap: true
              }
            },
            {
              loader: 'postcss-loader',
              options: {
                config: {
                  ctx: {
                    autoprefixer: {
                      browsers: 'last 2 versions'
                    }
                  }
                }
              }
            }
          ]
        })
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin({
      filename: 'styles/styles.[hash].css',
      allChunks: true
    })
  ]
};
module.exports = config;
</code></pre>
<p>이제 모든 것을 통합할 차례다. 프로젝트 루트 디렉터리에서 기존 웹팩 구성을 삭제하고 새로운 웹팩 구성을 만든다. 새 파일인 <code>webpack.config.js</code>로 만든다.</p>
<pre><code>cd ..
rm webpack.config.development.js webpack.config.production.js
touch webpack.config.js
</code></pre>
<p><code>webpack.config.js</code>을 설정하기 전에, <code>package.json</code> 파일을 열어 <code>scripts</code> 부분을 업데이트한다.</p>
<pre><code class="language-json">...
&quot;scripts&quot;: {
  &quot;dev&quot;: &quot;webpack-dev-server --env.env=dev&quot;,
  &quot;prebuild&quot;: &quot;rimraf dist&quot;,
  &quot;build&quot;: &quot;cross-env NODE_ENV=production webpack -p --env.env=prod&quot;
},
...
</code></pre>
<p><code>-config</code> 플래그를 제거했기 때문에 웹팩은 <code>webpack.config.js</code>에서 기본 구성을 찾는다. 이제 <a href="https://webpack.js.org/guides/environment-variables/">-env</a> 플래그를 사용해 웹팩 환경 변수를 전달하고 개발 환경에서는 <code>env=dev</code>를, 프로덕션 환경에서는 <code>env = prod</code>를 전달한다.</p>
<p><code>webpack.config.js</code> 파일을 열고 아래 내용을 입력한다.</p>
<pre><code class="language-javascript">const buildValidations = require('./build-utils/build-validations');
const commonConfig = require('./build-utils/webpack.common');

const webpackMerge = require('webpack-merge');

// 애드온(addon)으로 웹팩 플러그인을 추가할 수 있다. 
// 개발할 때마다 실행할 필요가 없다.
// '번들 분석기(Bundle Analyzer)'를 설치할 때가 대표적인 예다.
const addons = (/* string | string[] */ addonsArg) =&gt; {
  
  // 애드온(addon) 목록을 노멀라이즈(Normalized) 한다.
  let addons = [...[addonsArg]] 
    .filter(Boolean); // If addons is undefined, filter it out

  return addons.map(addonName =&gt;
    require(`./build-utils/addons/webpack.${addonName}.js`)
  );
};

// 'env'는 'package.json' 내 'scripts'의 환경 변수를 포함한다.
// console.log(env); =&gt; { env: 'dev' }
module.exports = env =&gt; {

  // 'buildValidations'를 사용해 'env' 플래그를 확인한다.
  if (!env) {
    throw new Error(buildValidations.ERR_NO_ENV_FLAG);
  }

  // 개발 또는 프로덕션 모드 중 사용할 웹팩 구성을 선택한다.
  // console.log(env.env); =&gt; dev
  const envConfig = require(`./build-utils/webpack.${env.env}.js`);
  
  // 'webpack-merge'는 공유된 구성 설정, 특정 환경 설정, 애드온을 합친다.
  const mergedConfig = webpackMerge(
    commonConfig,
    envConfig,
    ...addons(env.addons)
  );

  // 웹팩 최종 구성을 반환한다.
  return mergedConfig;
};
</code></pre>
<p>리액트 개발 환경 구축을 위해 설정할 내용이 너무 많아보이지만 장기적으로는 매우 유용한 방법이다.</p>
<p>애플리케이션을 실행해 프로덕션 파일이 빌드되면서 모든 기능이 예상대로 잘 작동되는지 확인하자.</p>
<pre><code>yarn dev
yarn build
</code></pre>
<h2 id="6webpackbundleanalyzer">6. 보너스: 웹팩 번들 분석기 (Webpack Bundle Analyzer) 설치하기</h2>
<p>웹팩 번들 분석기(Webpack Bundle Analyzer)를 꼭 설치할 필요는 없지만 손쉽게 번들을 최적화할 수 있어 편리하다.</p>
<p><a href="https://github.com/webpack-contrib/webpack-bundle-analyzer">webpack-bundle-analyzer</a>를 설치하고 설정 파일을 만든다.</p>
<pre><code>yarn add webpack-bundle-analyzer -D
touch build-utils/addons/webpack.bundleanalyzer.js
</code></pre>
<p><code>webpack.bundleanalyzer.js</code> 파일을 열고 아래 코드를 입력한다.</p>
<pre><code class="language-javascript">const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
  .BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'server'
    })
  ]
};
</code></pre>
<p>플러그인 부분을 뺴서 웹팩 번들 분석기에 추가했다. <code>webpack-merge</code>이 플러그인을 결합하여 마지막으로 웹팩 구성 설정인 <code>webpack.config.js</code>로 전달한다.</p>
<p>마지막으로 <code>package.json</code> 파일을 열어 새 스크립트를 추가한다.</p>
<pre>
<code>
"scripts": {
  "dev": "webpack-dev-server --env.env=dev",
  <b>"dev:bundleanalyzer": "yarn dev --env.addons=bundleanalyzer",</b>
  "prebuild": "rimraf dist",
  "build": "cross-env NODE_ENV=production webpack -p --env.env=prod",
  <b>"build:bundleanalyzer": "yarn build --env.addons=bundleanalyzer"</b>
},
</code>
</pre>
<ul>
<li><code>dev:bundleanalyzer</code> — <code>dev</code> 스크립트를 불러 새 환경 변수인 <code>addons=bundleanalyzer</code>를 전달한다.</li>
<li><code>build:bundleanalyzer</code> — <code>build</code> 스크립트를 불러 새 환경 변수인 <code>addons=bundleanalyzer</code>를 전달한다.</li>
</ul>
<p>이제 웹팩 번들 분석기를 실행해보자.</p>
<pre><code>yarn dev:bundleanalyzer
</code></pre>
<img src="https://cdn-images-1.medium.com/max/1600/1*RcDi6r3fMQA9ergCHA2bug.gif" alt="[번역] 깊이 있는 리액트 개발 환경 구축하기">
<p>웹팩 번들 분석기 외에도 많은 플러그인이 있다. 여러 플러그인을 웹팩 구성 애드온(addon)에 추가하여 유용하게 사용할 수 있다.</p>
<h2 id>맺는 말</h2>
<p>여기까지 오느라 정말 수고했다. 그리고 여러분은 모두 해냈다! 🎉 리액트 개발을 위한 웹팩 설정 기초에 대해 배웠다. 앞으로 심화된 테크닉과 기술들을 탐험하고 사용할 수 있을 것이다. 이 튜토리얼 코드는 <a href="https://github.com/esausilva/react-starter-boilerplate-hmr">깃허브</a>에서 다운받을 수 있다.</p>
<p>이 튜토리얼을 읽으면서 재미를 느꼈길 바란다. 질문이나 제안은 언제나 환영한다. <a href="https://medium.com/@_esausilva">미디엄</a>, <a href="https://twitter.com/_esausilva">트위터</a>, <a href="https://github.com/esausilva/">깃허브</a>로 언제든지 연락하길 바란다.</p>
<!--kg-card-end: markdown-->]]></content:encoded></item></channel></rss>