Namgung Jong Min

토끼굴을 개척하는 개발자

지금까지 패스트캠퍼스 X 야놀자 부트캠프가 어떤식으로 진행되어 가는지를 소개했었는데요, 오늘은 부트캠프의 강사분들의 약력과 함께 실제 강의를 들어본 입장에서 후기를 전해드리고자 합니다. 또한 현재 진행되고 있는 멘토님과의 멘토링이 어떻게 도움이 되고있는지도 이야기 해볼게요.

부트캠프 강사진

박영웅 강사님

부트캠프에서 가장 먼저 강의를 통해 만나뵙게 된 박영웅 강사님! 프론트엔드의 기초라고 할 수 있는 HTML과 CSS, 그리고 Javascript에 대한 온라인 강의를 통해 만나뵈었습니다.

특히 Javascript의 경우에는 앞으로 배울 현업에서 쓰이는 중요한 기술들의 토대가 되기 때문에 확실히 공부해야한다고 생각했었습니다. 박영웅 강사님의 강의 같은 경우 우선적으로 이론에 대해 자세하게 정리하신 후 저희가 자주 접할 수 있는 사이트들을 실습으로 코딩하면서 이론들이 어떻게 실제 개발과정에서 쓰이는지를 학습할 수 있었습니다.

실습 강의로 API를 활용한 영화검색 사이트 만들기를 진행해 주셨는데요, 단순히 자바스크립트를 이용한 동적 구현뿐만 아니라 컴포넌트의 개념까지 설명해주시면서 실제 프로젝트의 적용할 수 있는 디렉토리 구성과 라우팅까지 학습할 수 있는 정말 좋은 강의라고 생각합니다.

사실 이 강의를 볼때마다 아쉬웠던 점이 하나 있어요. 저희가 두번째 과제로 Javascript를 활용한 사이트를 만들었었는데, 과제 시점이 이 실습 강의를 듣는 시점 이전이었기 때문에 많이 헤맸습니다. 과제를 제출한 뒤에 강의를 듣고는 ‘아 강의를 듣고 과제를 했다면 정말 수월하게 할 수 있었겠다.’라는 생각이 들었습니다.

안재원 강사님

안재원 강사님은 리액트와 여러가지 프레임 워크들을 강의해주셨습니다. 온라인 강의와 더불어 실시간 강의를 통해서 여러가지 실습들을 통해 다양한 개발 경험을 할 수 있도록 해주셨어요.

현업에서 현재 가장 많이 사용하고 있는(취업에 제일 중요하다고 해요) React와 프레임워크들을 강의해주셨어요. 특히 실습위주의 강의이다보니 매 강의마다 시간이 훌떡훌떡 갔습니다.

중요한 점은 실습을 통한 코드 따라치기를 벗어나 다시 한번 학습한 내용에 대해 되돌아보는 시간이 매우 중요하다는 생각을 하게 됬어요. 실습을 통해 개발을 매끄럽게 진행하시는데 따라치다보면 어느새 완성되어가는 결과물들이 내 실력이라는 착각을 할 수 있겠다고 생각했습니다.

막상 혼자 무에서부터 개발을 시작하려다보면 무엇을 해야할지 감이 안잡히고 헤매는 경우가 생기곤 해요. 그래서 그러지 않도록 어떻게 접근하셨고, 왜 이것부터 하셨지?, 왜 이 메소드를 사용하셨을까와 같은 점들을 바로바로 질문하면서 메모해두려고 노력을 많이 했습니다.

나동빈 강사님

나동빈 강사님은 패스트캠퍼스 X 야놀자 부트캠프의 알고리즘 강의를 맡아주셨어요. 특히 저는 그룹스터디를 통해 매주 알고리즘 주제를 정하고 해당 알고리즘의 대표 문제들을 풀어오고 있었는데 도움을 많이 받았습니다.

이번 주의 주제가 BFS라면 해당 주제에 대한 나동빈 강사님의 녹화강의를 듣고 정리한 뒤에 문제를 풀면서 머리로 이해한 개념들을 문제 풀이에 적용하는 방식으로 공부했습니다.

특히 단순한 개념 강의만이 아니라 어떤 문제들을 만났을 때 해당 알고리즘을 떠올려야 하는지, 알고리즘이 문제마다 어떤식으로 구현되는지를 여러 예시 문제들을 풀어주시면서 자연스럽게 코딩테스트 문제들을 익힐 수 있도록 강의하시는게 정말 좋았습니다.

나동빈님은 사실 부트캠프를 시작하기 이전에도 유튜브를 통해 몇번 뵌적이 있었어요. 그때마다 어떤식으로 개발 공부를 할지, 코딩테스트 공부는 어떻게 준비해야하는지 등에 대해 많은 조언이 담긴 영상들을 통해 큰 도움을 받았다고 생각했는데 이번 부트캠프에서 강사진으로 만나뵙게 되니 너무 반가웠습니다.

그룹 7조 멘토님

서정현 멘토님

저희 그룹에서 멘토링을 해주시는 서정현 멘토님! 현재 현업에서 근무하고 계십니다. 따라서 개발 외적인 부분들에 대해서도 질문을 많이 드리고 도움이 되는 답변을 많이 얻어가는 것 같아요.

예를들어, 스타트업에 취직을 하게 된다면 어떤 기업들을 선택해야 하는지, 취업 프로세스 등과 같은 것들도 물어봅니다. (현업에서 근무하시면서 실제 팀원들을 뽑는 위치시다보니 취업관련 질문들에 대해서도 도움이 되는 답변들을 많이 해주세요!)

부트캠프가 진행되면서 개발와중 생기는 궁금점들을 모두 질문하기보다는 웹서칭을 통해 스스로 해결하게 되는 경우가 많아지면서 저희 조의 질문들이 점점 적어지는 것 같아요. 하지만 매 멘토링 시간마다 최대한 많은 도움을 주기 위해서 노력해주시는게 느껴집니다.

요즘에는 질문이 적어 시간이 남을 때는 면접관련 도움을 주시기로 해주셨어요. 실제 팀원들을 선별하는 경험과 취업을 위한 면접을 둘다 경험해보셨기 때문에 주시는 면접 질문들 하나하나 소중히 기록하고 답변할 수 있도록 준비하려고 노력하고 있습니다.

마치며

오늘 포스팅에서 패스트캠퍼스 X 야놀자 부트캠프의 강사분들과 멘토님을소개하고 부트캠프 과정 중 느꼈던 여러가지 점들을 적어보았습니다. 이 포스팅을 보시는 분들이면 아마 국비지원이나 부트캠프 등에 관심이 있으신 분들이라고 생각됩니다. 저는 패스트캠퍼스에서 운영하는 부트캠프가 좋은 강사분들과 커리큘럼을 가지고 있다고 생각해요. 제 포스팅들이 여러분의 선택에 도움이 되었으면 좋겠다고 생각합니다.

저번 포스팅에서 그룹스터디 팀 결성과 진행 방식 그리고 후기를 알려드렸었어요. 패스트캠퍼스 X 야놀자 부트캠프에서는 각 팀 별 그룹스터디만 진행되는 것이 아니라 주기적으로 워크샵을 통해 부트캠프를 함께 하는 동기생들이 어떤 방식으로 공부를 이어나가고 있는지 이야기를 나눠볼 수 있는 시간이 있었습니다. 오늘은 두번에 걸쳐 진행된 워크샵 후기와 함께 저번에 말씀드렸던 패스트캠퍼스 X 야놀자 부트캠프의 장점이라고 언급했던 멘토링 후기에 대해서 이야기하려 합니다.

1차 부트캠프 워크샵

7월 24일에 그룹스터디 워크샵을 가졌습니다. 이번 워크샵에서는 각 그룹스터디 별로 어떻게 진행되어 가는지, 느낀점들이나 공유하고 싶은 내용등을 자유롭게 이야기하는 시간이었습니다. 각 그룹스터디 별로 스터디 주제나 진행 방식들이 차이가 있어서 좋은점들은 우리 스터디에도 적용하고 싶은 마음에 정말 열심히 들었던 것 같습니다. 뿐만아니라 우리 스터디에서 잘 적용되고 있는 방식들을 자랑하고 싶은 마음도 있었던 것 같아요!

저희 조는 노션을 통해 발표자료를 준비해 워크샵을 준비했습니다. 저희 조원들 소개와 스터디 진행방식 그리고 느낀점들을 발표자료로 준비했습니다.

저번 포스팅에서 다뤘던 코딩테스트 스터디에 대한 발표 자료네요. 백준을 통해 각 알고리즘별로 이론 공부와 더불어 대표 문제들을 학습했습니다.

이제는 알고리즘 별 대표문제들을 쭉 훑고나서 프로그래머스에서 기출문제들 위주로 코딩테스트 스터디를 진행하고 있어요!! 확실히 알고리즘별로 학습을 탄탄히 한 뒤에 기출문제 풀이로 넘어가니 문제를 접근하는 방식이나 풀이에 있어서 도움을 많이 받는 것 같습니다.

다음으로는 자바스크립트 딥다이브 스터디에 대한 발표 자료인데요, 각자 발표를 준비한다는 마음으로 책을 정독하고 뽑기로 걸린 팀원이 주제들에 대해 설명한다는 방식은 다른 많은 팀들에게 좋은 호응을 얻었습니다! 그분들 스터디에도 적용하는 것을 고민한다는 이야기도 들었어요.

사실 900쪽이 넘어가는 이 두꺼운 책을 팀원들과 언제 다 읽을 수 있을까 걱정이 많았는데 지금 이 글을 쓰는 지금은 벌써 완독을 눈앞에 두고 있답니다.

저희 그룹 뿐만아니라 다름 스터디그룹에서도 정말 열심히 스터디를 진행하고 계신거 같더라구요. 저희와 다른 방식을 선택한 그룹이나 저도 꼭 해보고 싶은 주제를 선정해 스터디를 진행하는 팀들도 있었습니다. 특히 그룹원들끼리 토이프로젝트를 진행하는 팀이 흥미로워서 주기적으로 팀 노션을 눈팅해왔는데, 이번 워크샵을 통해 어떤 결과물들을 만들고 있는지 자세히 소개해주셔서 너무 재밌었던 것 같아요.

2차 부트캠프 워크샵

8월 25일에는 두번째 워크샵이 진행되었습니다. 이전 그룹스터디 워크샵과는 다르게 각 개인이 그 동안 부트캠프를 진행하면서 느낀점이나 배운점들 또는 공유하고 싶은 주제들에 대해서 이야기하는 시간이었습니다. 추가로 진행된 ‘천하제일 내 꿀통 자랑대회’ !!!
주제를 선정해 발표하고 참여자 전원 상품까지 얻어갈 수 있는 좋은 기회였습니다. 한치의 고민도 없이 바로 참여했습니다 😁

발표자 분들이 생각보다 많이 적었어요. 저를 포함해 네분이 발표를 진행하셨습니다. 아마 발표에 대해 부담감이 있으신 분들이 있으셨던 것 같아요. 그 동안 눈팅해온 동기생분들 중에는 정말 실력자분들이 많으셔서 기대했는데 아쉬웠습니다 ㅠㅠ.

천하제일 내 꿀통 자랑대회 발표

저는 웹 성능과 성능 지표를 활용한 최적화라는 주제로 발표를 했습니다.

우선 성능에 대한 전반적인 설명에 앞서 필요한 지식인 렌더링 과정에 대해서 이야기하고, 성능 개선의 필요성을 설명한 뒤 다양한 성능 지표에 대해 소개하고 그 지표를 기반으로한 최적화 방안에 대해 이야기했습니다.

성능 개선의 필요성을 엘레베이터 닫기 버튼을 누르는 우리의 모습과 연관지어 설명해보았는데 참 괜찮은 예시였던 것 같아요^^.

많이 떨리기도 했지만 한편으로는 설렘 가운데서 발표를 잘 마무리할 수 있었습니다.

마지막 발표인데다 다른 분들에 비해 긴 시간의 발표여서 더 떨렸던 것 같아요. 시간이 너무 길까 말도 너무 빨랐던 것 같고… 경험삼아 다음번엔 더 잘할 수 있겠죠?

다른 분들의 발표 중에 특히 어승준님의 optimistic ui 와 pasimistic ui에 대한 발표는 정말 유익했습니다. 부트캠프의 장점! 동기생들과의 연결. 바로 DM을 드려 이야기를 나누고 발표자료들을 교환한 뒤 주말에 공부했습니다. 너무 너무 유익했습니다.

워크샵이 잘 마무리되고 상품도 잘 받았답니다. 아! 저는 ‘개발자의 글쓰기’라는 책을 선택해서 받았어요!

그룹스터디 멘토링

패스트캠퍼스 X 야놀자 부트캠프의 큰 장점이라고 저번 포스팅에서 이야기 했던 멘토링입니다. 공부를 하면서 모르는 내용들 궁금한 내용들을 그때마다 요청서에 적어두면 현직 멘토님의 답변과 함께 일주일에 한번 있는 멘토링 시간에 이야기를 나눠볼 수 있었습니다.

사실 처음에는 멘토링이라는 것이 이 정도로 좋을지 몰랐어요. 우리가 공부를 하다가 궁금한 것이 생기면 질문하고 답변을 기다리기 보다는 구글링등을 통해 바로바로 알고 해결하려고 하잖아요. 그런데 꼭 개발 지식에 관련된 내용이 아니더라도 직접 취업 후 경험을 통해 알 수있는 내용들이나 질문하기 어려운 추상적인 것들도 대화를 통해 충족시킬 수 있다는 점이 너무나도 좋았습니다.

질문이 없을 때 이 귀중한 멘토링 시간을 어떻게 써야할지는 항상 고민이었습니다. 그래서 멘토님과의 이야기를 통해 남는 시간은 저희 면접 준비를 도와주시기로 하셨어요! 멘토님께서 직접 경험하셨던 면접 질문들이나 실제 팀원을 뽑으실 때 물어볼 것 같은 질문들을 저희에게 제시해주시고 답변을 들어주시며 교정해주시는데 너무나 유익한 것 같습니다.

마치며

패스트캠퍼스 X 야놀자 부트캠프를 진행하면서 부트캠프로 이곳을 선택한 것이 정말 확실한 선택이었다는 것을 요즘 계속해서 느끼고 있습니다. 과정 중 진행되는 앞서 소개한 여러가지들 중 도움이 되지 않는 것이 단 하나도 없는 것 같아요. 제가 쓰는 후기들이 국비지원이나 부트캠프를 고려하는 다른 학우분들께 도움이 되셨으면 좋겠습니다.

그룹스터디 팀 결성

패스트캠퍼스 X 야놀자 부트캠프의 시작과 함께 그룹이 배정되었습니다!!. 성후님, 혜민님, 홍규님, 수연님, 영민님, 그리고 저 이렇게 6명의 그룹원들이 한팀이 되었어요. 그룹의 팀장은 성후님께서 맡아주시기로 결정되었습니다.

저희 팀이 7조라 팀명을 ‘마 니 개발좀7나’로 결정하였습니다. 재밌지 않나요?😂 이후엔 회의를 통해 앞으로 우리가 어떤 방식으로 그룹스터디를 운영해나갈지, 무엇을 공부할지에 대해 이야기를 나누었습니다.

그룹스터디 운영

패스트캠퍼스 X 야놀자 부트캠프의 그룹스터디는 자유도가 매우 높은 스터디입니다. 그룹스터디 진행 방식과 목적, 목표 그리고 공부할 내용까지도 팀원과의 협의를 통해 결정하고 팀별로 알아서 이루어지게 됩니다.

그룹스터디의 진행사항은 노션을 통해 정리하게 되었습니다. 패스트캠퍼스 X 야놀자 부트캠프의 노션에 저희 그룹스터디 페이지가 생성이 되었네요!

저희가 그룹스터디를 통해 이루고자하는 목표는 두가지로 잡았습니다.

  1. 코딩테스트에 합격할 수 있는 수준의 알고리즘 역량을 기른다.
  2. JavaScript Deep Dive 책을 완독하고, 각 주제를 서로에게 설명해줄 수 있다.

따라서 그룹스터디에서는 코딩테스트 준비로 알고리즘 공부와 Javascript 이론에 대한 공부가 함께 진행되기로 했습니다.

1) 알고리즘 공부 및 코딩테스트 준비

코딩테스트 준비는 각 주마다 알고리즘 주제를 선정하고 해당 알고리즘을 사용하여 풀 수 있는 코딩테스트 문제를 백준에서 선택하여 풀어옵니다. 이후에는 사다리타기를 통해 문제별 발표자를 선정하고 해당 발표자가 어떻게 문제를 접근했고, 어떻게 풀어나갔는지를 설명합니다. 발표 이후에는 조원들끼리 서로의 풀이과정을 공유하며 코드 리뷰를 하는 시간을 갖게 됩니다.

Brute Force 알고리즘을 공부한 스터디 내용을 살펴볼까요?

주제 알고리즘에 대한 설명과 관련된 코딩테스트 문제들, 그리고 문제들을 맡은 발표자들이 정리되어 있네요!

그 다음에는 해당 문제에 대한 담당자의 풀이와 그것을 비교한 팀원들의 코드리뷰 내용들이 정리되어있습니다.

개인별 문제풀이 코드들을 팀원 모두가 함께 확인하고 자신의 코드와 비교하면서 다른 사람의 문제 접근 방식과 코드 구현 방식을 확인할 수 있었고, 이후 코드리뷰를 통한 토론을 통해 어떤 방식이 더 좋은 방식이고, 좋은 코드인지 이야기를 나눠보면서 JavaScript 코드의 구현 역량을 기를 수 있었습니다.

2) JavaScript Deep Dive 스터디

JavaScript Deep Dive 스터디의 경우 러버덕 방식으로 진행하기로 결정되었습니다. 미리 정한 범위까지 책을 정독하고, 발표자를 선정하여 해당 주제에 대한 내용에 대해 발표하고, 즉각적으로 내용에 대한 이해 여부를 검증하면서 이해하지 못한 문장들에 대해서도 이야기를 나누었습니다. 이 방식을 통해 다음과 같은 이점들을 얻을 수 있었습니다.

  1. 해당 주제에 대한 완벽한 이해

저희는 다른 사람에게 설명할 수 있어야 비로소 자신의 지식이라고 생각했습니다. 해당 주제에 대해 전체적으로 설명할 수 있어야 내가 완벽히 이해한 내용이고, 또한 그렇게 설명을 염두에두고 공부하는 과정에서 주제에 대해 깊이있는 공부가 가능할 것이라고 생각했습니다.

  1. 면접 준비

다른사람에게 설명하는 것을 염두에 둔 공부와 실제 발표는 해당 주제에 대해 자신감을 얻게 될 것이라 생각했습니다. 또한 내 지식을 다른 사람에게 말하는 것을 반복하게 되면서 자연스럽게 면접 준비가 되겠다고 생각했습니다.

이렇게 러버덕을 통한 공부 이후에는 스터디 때 오간 내용들을 종합하여 정리된 내용을 토대로 블로그 포스팅을 통해 해당 주제에 대한 지식을 온전히 자신의 것으로 만들 수 있게 하였습니다.

3) 그룹스터디 멘토링

그룹스터디 진행이 서로의 의견 교환을 통해 이루어진다는 것은 스스로가 생각할 기회가 많다는 장점이 있지만, 한편으로는 팀원끼리의 소통만으로는 올바른 판단인지를 결정하기 힘들다는 단점이 있었습니다. 모두 공부하는 입장이기 때문에 오고간 이야기들을 객관적으로 검증할 방법이 필요했습니다.

이를 위해 패스트캠퍼스 X 야놀자 부트캠프에서는 주 1회 그룹별 멘토링 시간을 제공해주었습니다. 멘토님께 질문하면서 현재 스터디 방향이 맞는지 계속 확인하고, 팀원들끼리 판단하지 못했던 여러 주제들에 대해 멘토링을 받을 수 있었습니다.

스터디를 진행하면서 궁금한 점들이 생길 때마다 멘토링 사전요청서에 작성하여 해당 질문에 대한 답들을 멘토님께 들을 수 있었고, 추가로 구글밋 등을 활용해 매주 멘토님과 직접 이야기하면서 궁금한 점들을 해결할 수 있었습니다.

그룹스터디 후기

그룹스터디의 가장 큰 효과는 자기 자신을 객관화 할 수 있다는 점이었습니다. 내가 어느정도의 실력을 가지고 있는지, 스스로를 팀원들과 비교하면서 파악할 수 있었고 자신의 장점과 부족한점들 또한 배울 수 있는 기회가 되었습니다.

또한 혼자 공부를 진행해 가는 것은 매너리즘에 빠질 우려가 있습니다. 사람이 항상 의욕적일 수는 없고 어느 순간 나태해질 수 있는데, 확실한 목표 설정과 계획을 통해 강제적으로 공부해야되는 부분이 정해지다보니 항상 일정한 공부량을 채울 수 있게 되었고 하루에 공부하는 공부의 총량또한 늘어난 것이 느껴졌습니다.

기존에 다른 그룹스터디를 경험해보았지만 패스트캠퍼스 X 야놀자 부트캠프의 그룹스터디의 차별화 된 강점이 느껴진 부분은 지속적인 관리가 이루어진다는 점이었습니다. 그룹스터디 운영방식에 대해서는 자유를 부여하지만 멘토링 시스템의 도입과 운영진 분들의 진행사항 확인 등을 통해 각 그룹별 스터디가 잘 이루어지고 있는지를 지속적으로 검증할 수 있었고, 공부 방향에 대한 확신또한 얻을 수 있었습니다.

이대로 꾸준히 진행되었을 때 개발자로서의 역량을 키울 수 있다는 것에 확신이 생겼고, 부트캠프를 수료했을 때 얼마나 스스로가 성장해있을지를 기대하게 됩니다.

패스트캠퍼스 X 야놀자 프론트엔드 과정의 ‘패스트러너’로 선발되었습니다. 🎺 (짝짝짝)
부트캠프의 처음 들어온 OT를 시작으로 앞으로 진행해나갈 주요 커리큘럼들의 후기들을 공유해보겠습니다. 해당 OT는 2023년 7월 10일 진행되었습니다.

야놀자 테크스쿨

‘아기다리고기다리’던 패스트캠퍼스X야놀자 부트캠프가 7월 10일 드디어 개강했습니다. 첫날이니 만큼 OT를 통해 앞으로의 부트캠프 진행에 대한 설명과 Q&A, 그리고 이 기간동안 어떻게하면 개발자로서의 역량을 끌어올릴 수 있을지에 대한 멘토님들의 특강이 있었습니다.

저는 야놀자 테크스쿨 이전에 한번의 부트캠프를 이미 경험했었습니다. 시작은 노베이스의 사람들이 아예 기초부터 배울 수 있는 학원에 등록했었는데, 점점 개발에 흥미를 붙이고 컴퓨터 앞에 있는 시간이 늘어갈수록 눈에 보이는 변화가 나타났습니다. 코딩테스트 점수는 0.4점에서 만점을 받을 정도로 오르게 되었고, 여러 과제와 토이 프로젝트들도 성공적으로 마무리하여 우수수강생으로 선정되어 강남에서 오프라인 교육을 받을 수 있게 되었습니다.

특히 그곳에서는 ‘모던 자바스크립트 딥 다이브’의 저자인 이웅모 강사님께 자바스크립트 이론을 배웠는데 러버덕을 통해 내가 이해한 내용을 다른 사람에게 설명하는 방식의 공부법을 통해 내가 이해한 내용을 다른 사람에게 설명하는 방식의 공부법은 저에게 기초 지식에 대한 큰 자신감을 주었습니다. 여러 어플리케이션 구현과 한번의 팀 프로젝트를 경험한 뒤에 느낀 것은 성장했다는 기쁨과 그 이상의 갈증이었습니다.

‘내가 이전에 따라가기만에도 벅찼던 과정들을 성장한 나로서 다시 한번 경험해보고 싶다’

한번의 프로젝트는 너무나도 부족했습니다. 실제 현업에서 어떻게 프로젝트가 진행되는지를 간접적으로 느끼기에도 부족했고, ‘좀 더 준비된 상태의 나였다면 더 많은 것을 얻을 수 있지 않았을까?’라는 아쉬움도 크게 남았습니다. 결국 저는 다시 한번 부트캠프를 통해 개발자로서의 나를 더욱 성장시키고자 결심했습니다.

다수의 프로젝트 경험을 제공하는 부트캠프

여러 부트캠프 모집요강을 보면서 ‘패스트캠퍼스 X 야놀자 테크스쿨’은 프로젝트 기반으로 성장할 수 있는 기회를 마련해주는 부트캠프라는 생각이 들었습니다. 과정 중 지속적인 토이프로젝트와 팀 프로젝트 그리고 무엇보다 기업연계 & 협업 프로젝트라는 것은 꼭 경험해보고 싶은 것이었습니다.

또한 프로젝트에서 다양한 협업 관계자와 논의하며 프로젝트를 진행하는 경험을 제공한다는 특징또한 저의 마음을 사로잡았습니다.

이러한 이유들로 ‘패스트캠퍼스 X 야놀자 부트캠프’에 지원했고 자기소개서와 면접등을 치룬 뒤에 당당히 합격하게 되었습니다. 약 7개월 간의 여정 이후에 개발자로서 더욱 성장해있을 나를 기약하면서 하루하루 설렘속에 부트캠프를 이어나가고 있습니다.


Orientation

OT에서 전체적인 커리큘럼 소개와 부트캠프 진행방식, 그리고 멘토님들의 특강이 진행되었습니다.

커리큘럼은 주어진 대로 학습을 이어나가기만 한다면 개발자로서의 역량을 충분히 단단하게 다질 수 있겠다는 생각이 들었습니다. 뿐만아니라 7개월이라는 긴 기간동안 부트캠프를 이어가다보면 매너리즘에 빠질 수 있고, 나태해지기 마련인데 운영자님들을 통한 지속적인 관리와 매주 있는 멘토님과의 멘토링을 통해 더욱 더 과정에 몰입할 수 있겠다는 생각을 했습니다.

개발하면서의 Mind Set

특강의 주제로 앞으로 학습을 이어나가면서 가져야할 마인드 셋에 대하여 멘토님께서 알려주셨습니다. 어떻게 공부를 한 사람이 아웃풋을 높게 가져갔는지를 알려주시면서 개발 공부 중 지켜야 할 점들에 대해 설명해주셨습니다.

무엇을 통해 성장할까?

1) 강의를 통한 성장

우선 강의만 들어서는 성장이 보장되지 않음을 강조하셨습니다. 실력 향상은 수강 시간에 비례하지 않고 고민의 총량에 비례한다는 것을 강조하셨습니다.

강의의 예제코드를 그대로 따라치는 수강을 하지말 것
항상 ‘왜?’에 집중하여 고민하는 습관을 들이기
코드의 의미 새기기, 비판적으로 코드 바라보기, 다르게 구현해보기

2) 오류를 통한 성장

오류 메시지의 원인과 해결을 기억하려는 습관 또한 성장하는 공부라고 하셨습니다. 왜 문제가 발생했고, 어떻게 해결했는지 기록하고, 시행착오를 반복하는 것으로 성장할 수 있다고 하셨습니다.

스스로 직면한 오류를 완전히 이해했는지에 대한 척도를 알려주셨습니다.

  1. 같은 오류 메세지를 발생시키는 다른 잘못된 코드를 작성 가능한가?(재현 가능한가)
  2. 다른 코드에서 같은 오류 메세지가 발생했을 때 해결 가능한가>?
  3. 이 오류의 원인과 해결을 아무것도 참고하지 않은 상태에서 ‘설명’가능한가?

3) TIL / 블로그를 통한 성장 (기록의 중요성)

채용 과정에서 과연 프로젝트의 코드만을 보여주는 것이 전부인지 물어보셨습니다. 그리고 채용 과정에서 우리가 보여줄려고 노력해야 하는 것은 성장, 성실함, 흡수력이라고 하셨습니다.

잘쓴 TIL과 블로그는 그 자체로 무기라고 표현하시고 꾸준한 글 작성을 통해 업무 성향, 성실도, 협업 가능성, 성장 과정을 보여줄 수 있다고 하셨습니다.

TIL / 블로그 작성 방법

추가로 모범적인 TIL과 블로그 작성 방법에 대해서도 조언해주셨는데 ‘문시해알’ 이라는 단어를 통해 설명해주셨습니다.

문: 어떤 문제가 있었나
시: 내가 무얼 시도해봤지?
해: 어떻게 해결을 했나
알: 뭘 새롭게 알게 되었나

4) 토이프로젝트를 통한 성장

포트폴리오를 목적으로 한 토이 프로젝트는 기술적 개념을 다른 사람에게 설명할 수 있는 정도의 이해가 동반되어야 한다고 하셨습니다. 토이프로젝트를 구성하는 코드 중 어디를 가리키더라도 잘 설명할 수 있도록 스스로의 프로젝트에 대해 충분히 이해하면서 개발을 진행해야 한다고 하셨습니다.

5) 커밋 로그를 통한 성장

다른 사람의 코드와 커밋 메시지를 통해 다른 개발자들이 어떻게 코드를 쓰는지를 종종 확인하는 것도 스스로의 성장에 매우 큰 도움이 된다고 하셨습니다.

어떻게 빠르게 성장할 수 있을까?

1) 시간 배분하기

강의 수강은 항상 자신의 코드로 복습하는 과정으로 이어져야 합니다. 반드시 하루 이내, 되도록 한 시간 이내로 실습하며 복습하는 해야 한다고 강조하셨습니다.

2) 질문을 활용하기

패스트캠퍼스 X 야놀자 부트캠프의 경우는 항시 답변을 해주시는 멘토님이 배정됩니다. 따라서 충분한 고민 이후에 스스로 해결하지 못한 문제나 더 좋은 방안에 대한 질문을 멘토님에게 질문하면서 성장을 촉진할 수 있다고 하셨습니다.

여기서 중요한 점은 질문에도 좋은 질문과 나쁜 질문이 있다는 것입니다. 좋은 질문을 해야 성장에 도움이 된다고 하셨습니다.

좋은 질문이란?

  1. 정돈된 질문 : 내가 무엇을 모르는지 어떠한 부분을 어떻게 모르는지 정확하게 아는 질문
  2. 충분한 고민이 반영된 질문
  3. 과거의 답변 / 강의 /구글링 / 공식 문서를 통해서 해결하려고 했으나 실패한 질문

추가적으로 질문과 답변은 항시 기록하는 습관을 들여야 온전히 자신의 지식이 된다고 하셨습니다.

OT 후기

오리엔테이션이 진행될수록 제가 ‘패스트캠퍼스 X 야놀자 부트캠프’를 고른 것이 정확한 선택이었음을 확신할 수 있었습니다. 각 커리큘럼에 대한 명확한 목적과 기대 효과를 저희에게 설명해주었고, 지속적인 수강생 관리와 일정한 간격으로 진행되는 멘토링, 그리고 언제나 자신의 고민들을 털어놓을 수 있는 환경임을 확인할 수 있었습니다.

OT에서 멘토님이 하셨던 말씀처럼 단지 이 커리큘럼을 온전하게 따라가는 것만으로도 내가 원하는 성장을 이룰 수 있겠구나라는 자신감을 불어넣어주는 부트캠프 소개였고, 스스로 마음을 다잡고 노력해나가야 겠다는 생각을 심어주는 시간이었습니다.

현재 2023년 9월 8일, 이 글을 작성하는 시점에서 OT를 되돌아본다면 정말 OT에서 소개한 그대로 과정이 진행되고 있습니다. 지금까지 총 2개의 사이트 구현을 한 뒤 현재 팀빌딩 이후 토이프로젝트를 진행하고 있으며 각 과정마다 얻어가는 것들이 너무나도 많은 것 같습니다. 항상 저희들을 관리해주시고 매번 과정이나 강의가 끝날 때마다 의견을 여쭤보시며 향상된 부트캠프 경험을 제공하려고 노력하는 운영진분들 모두에게 감사의 말씀을 올립니다.

AJAX는 자바스크립트를 사용하여 브라우저가 서버에게 비동기 방식으로 데이터를 요청하고, 서버가 응답한 데이터를 수신하여 웹페이지를 동적으로 갱신하는 프로그래밍 방식입니다.

전통적인 웹페이지는 html 태그로 시작하여 html로 끝나는 완전한 HTML 문서를 서버로부터 전송받아 웹페이지 전체를 처음부터 다시 렌더링하는 방식으로 동작했습니다. 따라서 화면이 전환되면 서버로부터 새로운 HTML 문서를 전송받아 웹페이지를 처음부터 다시 렌더링 했습니다.


전통적 웹페이지의 단점

  1. 이전 웹페이지와 차이가 없어서 변경할 필요가 없는 부분들까지 새롭게 완전한 HTML을 전송받아야하기 때문에 불필요한 리소스가 발생합니다.

  2. 변경할 필요가 없는 부분을 처음부터 렌더링하기 때문에 페이지에 깜빡임 현상이 발생합니다.

  3. 클라이언트와 서버와의 통신이 동기 방식으로 동작하기 때문에 서버로부터 응답이 있을 때까지 추가적인 처리가 불가능합니다.

AJAX의 등장으로 서버로부터 웹페이지에 변경이 필요한 부분만 데이터를 전송받아 렌더링하는 것이 가능해졌고, 이로 인해 데스크탑에서 애플리케이션과 유사한 빠른 퍼포먼스와 부드러운 화면 전환이 가능해졌습니다.


XMLHttpRequest 객체를 활용한 Http 요청

자바스크립트의 전통적인 Http 요청 방식은 XMLHttpRequest 객체를 이용한 방법이었습니다. XMLHttpRequest를 이용한 방식에서는 비동기 처리를 위한 패턴으로 콜백 함수를 사용해야 했습니다. 이러한 콜백 함수를 이용한 패턴은 콜백 헬로 인해 가독성이 나쁘고 비동기 처리 중 발생한 에러의 처리가 곤란하며, 여러 개의 비동기 처리를 한번에 처리하는 데에도 한계가 있었습니다.

아래 코드를 통해 전통적인 AJAX 이용에서 콜백 패턴이 필수적이었던 이유를 확인해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let todos;

const get = (url) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();

xhr.onload = () => {
if (xhr.status === 200) {
console.log(JSON.parse(xhr.response));
todos = JSON.parse(xhr.response);
} else {
console.error(xhr.status);
}
};
};

get("https://jsonplaceholder.typicode.com/posts/1");
console.log(todos); // output: undefined

서버로부터 응답이 도착하면 xhr 객체에서 load 이벤트가 발생합니다. 이때 xhr.onload 핸들러 프로퍼티에 바인딩한 이벤트 핸들러가 즉시 실행되지 않습니다. xhr.onload 이벤트 핸들러는 load 이벤트가 발생하면 일단 태스크큐에 저장되어 대기하다가, 콜 스택이 비면 이벤트 루프에 의해 콜 스택으로 푸쉬되어 실행됩니다.

따라서 기존 콜스택이 전부 비워져야만 우리가 원하는 코드 (todos에 값을 할당하는 코드)가 실행되기 때문에 기대한 대로 동작하지 않습니다.

이처럼 비동기 함수는 비동기 처리 결과를 외부에 반환할 수 없고, 상위 스코프의 변수에 할당할 수도 없습니다. 따라서 비동기 함수의 처리 결과에 대한 후속처리는 비동기 함수 내부에서 수행되어야 합니다. 이를 위해 비동기 함수 내부에 비동기 처리 결과에 대한 후석 처리를 수행하는 콜백 함수를 전달하는 것이 일반적입니다.

전통적 Http 요청 방식의 문제점

1) 콜백 헬

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const get = (url, callback) => {
const xhr new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();

xhr.onload = () => {
if (xhr.status === 200) {
callback(JSON.parse(xhr.response));
} else {
console.error(xhr.status);
}
};
};

const url = 'https://jsonplaceholder.typicode.com';
get(`${url}/post/1`, ({userId})=>{
console.log(userId); // output: 1

get(`${url}/users/${userId}`, userInfo => {
console.log(userInfo); // output: {id: 1, name: jongmin, username: Eric}
})
})

위 코드를 보면 GET요청을 통해 서버로부터 id가 1인 post를 취득하고 이 데이터를 사용하여 또 다시 GET 요청을 합니다. 이처럼 비동기 통신 내부에서 후속 처리를 위한 코드를 사용하는 콜백 패턴은 가독성을 저해하며 실수를 유발하는 원인이 됩니다.

만약 비동기 통신으로 취득한 단계적 데이터들에 의존한 코드가 몇겹씩 쌓이다보면 정말 말 그대로 지옥같은 코드가 생성됩니다.

1
2
3
4
5
6
7
8
9
get('step1', a => {
get(`step2/${a}`, b => {
get(`step3/${b}`, c => {
get(`step4/${c}`, d =>{
...
});
});
});
});

2) 에러 핸들링

1
2
3
4
5
6
7
try {
setTimeout(() => {
throw new Error("Error");
}, 1000);
} catch (e) {
console.error(e); // 에러 캐치 불가
}

전통적 Http 요청 방식에서 사용되는 콜백 패턴의 가장 큰 문제점은 에러처리가 곤란하다는 점입니다. 비동기 코드의 동작 원리를 다시 생각해보면 setTimeOut 함수의 콜백 함수는 런타임 환경에서 모든 코드가 실행된 이후 콜 스택이 완전히 비워진 이후에야 이벤트 루프를 통해 태스크 큐에서 콜 스택으로 푸쉬됩니다. 이 때 try catch 문은 종료된 시점이기 때문에 에러가 캐치되지 않습니다.


Promise의 등장

전통적 Http 요청을 위한 콜백 패턴은 콜백 헬이나 에러 처리의 문제가 있었습니다. 이를 극복하기위해 ES6에서 Promise가 도입되었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const promiseGet = url => {
return new Promise((resolve, reject) => {
const xhr new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();

xhr.onload = () => {
if (xhr.status === 200) {
callback(JSON.parse(xhr.response));
} else {
console.error(xhr.status);
}
};
})
};

promiseGet('http://jsonplaceholder.typicode.com/posts/1')
.then(v => console.log(v), e => console.log(e));

Promise는 후속처리 메서드를 통해 에러 핸들링을 할 수 있습니다. then의 첫번 째 콜백 함수로 비동기 처리 결과가 성공했을 때의 후속 조치를 할 수 있고, 두 번째 콜백 함수로 비동기 처리 결과가 실패했을 때의 후속 조치도 가능합니다. 또한 catch메서드를 통해 전체적인 에러 핸들링또한 가능합니다.

또한 Promise는 체이닝을 통해 전통적 통신 방식에서의 콜백 헬도 해결이 가능합니다.

1
2
3
4
promiseGet(`${url}/posts/1`)
.then(({ userId }) => promiseGet(`${url}/users/${userId}`))
.then((userInfo) => console.log(userInfo))
.catch((err) => console.error(err));

then, catch finally 후속 처리 메서드는 콜백 함수가 반환한 Promise를 반환합니다. 또한 콜백 함수가 Promise가 아닌 값을 반환하더라도 그 값을 암묵적으로 resolve 또는 reject하여 프로미스를 생성하여 반환합니다. 그렇게 반환한 Promise를 이용하여 체이닝을 통해 후속 처리를 하기 때문에 전통적 방식의 콜백 헬을 방지했다는 점은 긍정적이지만 결국 Promise도 콜백 패턴을 사용하기 때문에 가독성을 저해할 수 있다는 점에서 문제가 있습니다.


async / await를 이용한 후속 처리

기존 콜백 패턴을 이용한 비동기 통신의 단점을 ES8에 도입된 async/await를 통해 해결할 수 있습니다. async/await를 사용하면 Promise의 후속 처리 메서드 없이 동기 처리처럼 프로미스가 처리 결과를 반환하도록 구현할 수 있습니다.

1
2
3
4
5
6
7
8
9
(async () => {
try {
const { userId } = await promiseGet(`${url}/posts/1`);
const userInfo = await promiseGet(`${url}/users/${userId}`);
console.log(userInfo);
} catch (err) {
console.error(err);
}
})();

async/await는 Promise를 가독성 좋게 사용할 수 있는 방법입니다. 비동기 코드를 동기 코드 처럼 동작하도록 구현할 수 있습니다. 이것은 에러처리에서 기존 비동기적 특성 때문에 사용하지 못했던 try/catch 문 또한 사용할 수 있다는 것을 의미합니다.

▪︎ 적용할 정규식 개념

▫︎ 검색 기준 패턴

^문자열 특정 문자열로 시작합니다. (시작점)

문자열$ 특정 문자열로 끝납니다. (종착점)

▫︎ 갯수 반복 패턴

  • 최소 한개 or 여러개

▪︎ 숫자 앞 뒤의 반복되는 9를 제거하기

▫︎ 숫자 앞의 반복되는 9 제거

1
2
3
4
const num = 99193;
const changedNum = Number(String(num).replace(/^9+/, ""));

console.log(changedNum); // output: 193

▫︎ 숫자 뒤의 반복되는 9 제거

1
2
3
4
const num = 19399;
const changedNum = Number(String(num).replace(/9+$/, ""));

console.log(changedNum); // output: 193

배열을 조작하여 원하는 값을 도출할 때 배열을 조작하는 로직의 시간복잡도 때문에 고민하는 경우가 있습니다. 예를들어 O(N)의 시간복잡도로 구현해야하는 문제에서 순회문 안에서 배열의 앞의 원소를 추가/삭제하거나 배열에서 가장 크거나 작은 값을 뽑아오는 경우가 그렇습니다. 저번 포스트에서는 배열의 앞쪽 원소를 조작할 때의 시간복잡도를 연결리스트 자료구조로 줄일 수 있다는 것을 확인하였습니다. 이번 포스트에서는 힙 자료구조를 통해 우선순위큐를 구현하여 배열 원소들 중 최대/최소값을 O(logN)의 시간복잡도로 처리하는 방법을 알아보겠습니다.

▪︎ 힙 (Heap)

image.png

힙은 완전 이진트리 구조로 되어있는 자료 구조입니다. 완전 이진트리란 각각의 노드가 최대 두 개의 자식 노드를 가지는 형태에서 마지막 레벨을 제외한 모든 노드가 완전히 채워진 것을 말합니다. 힙은 직접 연결된 부모-자식 노드 간의 크기를 비교하여 정해진 우선순위에 맞게 구성되어있습니다. 루트 노드에 가장 우선 순위가 높은 데이터를 위치시키고 각 노드를 연결하고 조정하여 모든 노드가 자식 노드에 비해 우선 순위가 크거나 같게 구현하면 됩니다. 이 때 값이 낮을 수록 우선순위가 높은 힙을 최소 힙(Min Heap)이라 하고, 값이 높을수록 우선순위가 높은 힙을 최대 힙(Max Heap)이라고 합니다.

▫︎ 데이터 삽입

힙에 데이터를 삽입하는 과정입니다. 최소힙을 예시로 들었으나 최대힙 또한 우선순위 설정의 차이만 있을 뿐 같은 과정을 거칩니다. 이렇게 데이터를 추가하고 아래에서부터 힙을 재정렬하는 방식을 Heapify Up 이라 합니다.

image.png

1. 힙의 마지막 위치에 삽입할 데이터를 추가합니다.

image.png

2. 추가된 데이터와 부모노드의 값을 비교하여 우선순위가 높다면 자리를 바꿉니다.

image.png

3. 추가된 데이터가 루트로 이동하거나 부모 노드보다 우선순위가 낮을 때까지 ‘2’를 반복합니다.

▫︎ 데이터 삭제

힙에 데이터를 삭제하는 과정입니다. 최소힙을 예시로 들었으나 최대힙 또한 우선순위 설정의 차이만 있을 뿐 같은 과정을 거칩니다. 이렇게 데이터를 삭제하고 마지막 데이터를 루트데이터에 위치시킨 후, 위에서부터 힙을 재정렬하는 방식을 Heapify Down 이라 합니다.

image.png

1. 루트 노드의 데이터를 삭제하고 그 자리에 힙의 마지막 노드를 가져옵니다.

image.png

2. 루트로 가져온 데이터와, 그 자식 노드들 중 우선순위가 더 높은 자식 노드를 비교하여 가져온 데이터가 우선순위가 낮다면 자리를 바꿉니다.

3. 마지막 노드에서 가져온 데이터가 트리의 종단으로 이동하거나 비교한 자식 노드보다 우선 순위가 높을 때까지 ‘2’를 반복합니다.

▪︎ 우선순위 큐 (Priority Queue)

image.png

image.png

우선순위 큐는 힙 자료구조를 이용해 만든 추상적인 개념의 자료구조입니다. 기본적인 큐가 First In First Out으로 동작하는 것과는 다르게 큐에 입력된 데이터들 중 우선순위가 높은 데이터를 먼저 호출합니다.

힙을 통한 우선순위 큐는 자바스크립트에서는 배열을 이용하여 추상적으로 구현할 수 있습니다. 힙을 클래스로 선언하고 배열을 정의합니다. 그리고 배열의 각 인덱스를 통해 트리를 추상화 합니다. 배열의 첫 인덱스부터 루트 노드, 이후엔 왼쪽 자식 노드, 오른쪽 자식 노드 순으로 트리의 depth를 따라 순차적으로 배열에 저장됩니다. 이후 해당 배열을 조작하는 클래스 메소드를 통해 힙을 재정렬하게 됩니다.

▪︎ 자바스크립트로 우선순위 큐 구현

자바스크립트에서 힙의 구현은 다음과 같은 단계로 나눌 수 있습니다. 위에 서술한 힙의 데이터 추가/삭제에 관련한 예시와 함께 보시면 코드를 이해하기가 편합니다. 수월한 코드 구현을 위해 부모 노드와 자식 노드간의 위치에 대한 값은 외워두는게 좋습니다.

  • 부모 노드 - 자식 노드 간 인덱스
    부모 노드 (자식 노드 기준) : Math.floor((childIndex - 1) / 2)
    왼쪽 자식 노드 (부모 노드 기준) : parentIndex * 2 + 1
    오른쪽 자식 노드 (부모 노드 기준) : parentIndex * 2 + 2

1. 힙 클래스 생성

2. 힙 정렬에 필요한 메서드 추가 (heapifyUp, heapifyDown)

3. 힙 조작 메서드 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 1. 최소힙 클래스 생성
class MinHeap {
constructor() {
this.heap = [];
this.length = 0;
}

// 3. 힙 조작 메서드 추가
push(value) {
this.heap.push(value);
this.heapifyUp();
this.length++;
}
delete() {
const root = this.heap[0];

if (this.heap.length === 0) return null;
else if (this.heap.length === 1) this.heap = [];
else {
this.heap[0] = this.heap.pop();
this.heapifyDown();
}

this.length--;
return root;
}

// 2. 힙 정렬에 필요한 메서드 추가
getLeftChildIndex(parentIndex) {
return parentIndex * 2 + 1;
}
getRightChildIndex(parentIndex) {
return parentIndex * 2 + 2;
}
getParentIndex(childIndex) {
return Math.floor((childIndex - 1) / 2);
}
swap(index1, index2) {
[this.heap[index1], this.heap[index2]] = [this.heap[index2], this.heap[index1]]; // 두 인덱스에 위치한 데이터의 위치를 서로 변경
}
heapifyUp() {
let cIdx = this.heap.length - 1; // 새로 추가된 데이터의 위치를 변수에 저장하고 추적

while (cIdx > 0) {
const parentIdx = this.getParentIndex(cIdx); // 추가된 데이터와 비교할 부모 노드의 위치

if (this.heap[cIdx] < this.heap[parentIdx]) {
this.swap(cIdx, parentIdx);
cIdx = parentIdx;
} else break;
}
}
heapifyDown() {
let cIdx = 0; // 루트 노드로 이동된 힙의 마지막 노드의 위치를 변수에 저장하고 추적

while (this.getLeftChildIndex(cIdx) < this.heap.length) {
const leftChildIndex = this.getLeftChildIndex(cIdx);
const rightChildIndex = this.getRightChildIndex(cIdx);
// 두 자식 노드 중 우선순위가 높은 자식 노드의 위치
// 자식 노드가 하나일 경우엔 반드시 왼쪽 노드에 위치하기 때문에 그 점을 고려하여 삼항연산자 작성
const targetIndex = this.heap[leftChildIndex] > this.heap[rightChildIndex] ? rightChildIndex : leftChildIndex;

if (this.heap[cIdx] > this.heap[targetIndex]) {
this.swap(cIdx, targetIndex);
cIdx = targetIndex;
} else break;
}
}
}

같은 방식으로 최대힙도 만들 수 있습니다. 구성을 동일하나, heapifyUp과 heapifyDown에서 두개의 노드를 비교하는 식만 변경하면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class MaxHeap {
constructor() {
this.heap = [];
this.length = 0;
}

push(value) {
this.heap.push(value);
this.length++;
this.heapifyUp();
}

delete() {
const root = this.heap[0];

if (this.heap.length === 0) return null;
else if (this.heap.length === 1) this.heap = [];
else {
this.heap[0] = this.heap.pop();
this.heapifyDown();
}

this.length--;
return root;
}

getLeftChildIndex(parentIndex) {
return parentIndex * 2 + 1;
}
getRightChildIndex(parentIndex) {
return parentIndex * 2 + 2;
}
getParentIndex(childIndex) {
return Math.floor((childIndex - 1) / 2);
}
swap(index1, index2) {
[this.heap[index1], this.heap[index2]] = [this.heap[index2], this.heap[index1]];
}
heapifyUp() {
let cIdx = this.heap.length - 1;

while (cIdx > 0) {
const parentIdx = this.getParentIndex(cIdx);

if (this.heap[cIdx] > this.heap[parentIdx]) {
this.swap(cIdx, parentIdx);
cIdx = parentIdx;
} else break;
}
}
heapifyDown() {
let cIdx = 0;

while (this.getLeftChildIndex(cIdx) < this.heap.length) {
const leftChildIndex = this.getLeftChildIndex(cIdx);
const rightChildIndex = this.getRightChildIndex(cIdx);
const targetIndex = this.heap[leftChildIndex] < this.heap[rightChildIndex] ? rightChildIndex : leftChildIndex;
// 자식노드가 하나만 있을 때에는 왼쪽거만 있을테니 조건문이 undefined일 때는 왼쪽걸 선택하도록 주의해야함.

if (this.heap[cIdx] < this.heap[targetIndex]) {
this.swap(cIdx, targetIndex);
cIdx = targetIndex;
} else break;
}
}
}

▪︎ Example of Apply

image.png

image.png

위 문제는 그리디 문제입니다. 두 개의 카드 묶음을 합치면서 하나로 합쳐질 때까지의 비교 횟수의 최솟값을 출력하기 위해서는 매번 카드묶음들 중 가장 작은 두 묶음을 비교해야합니다. 이 때 가장 작은 두 묶음을 찾기 위해 for문 등으로 순회할 경우 100,000 _ (100,000 - 1) _ (100,000 - 2) * … 2 의 시행 횟수를 가집니다 (시간복잡도는 O(N^2)). sort 메소드를 이용하여 정렬을 하는 경우에도 O(N^2log(N))의 시간복잡도이므로 시간제한을 통과할 수 없습니다.

따라서 조건에 맞게 문제를 풀기 위해서는 주어진 카드 묶음에서 가장 크기가 작은 카드 묶음을 O(N)이하로 가져와 O(N^2) 이하로 해결해야 합니다. 최소힙을 적용한 우선순위 큐를 이용하면 가장 크기가 작은 카드 묶음을 O(logN)의 시간복잡도로 가져올 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
class MinHeap {
constructor() {
this.heap = [];
this.length = 0;
}

push(value) {
this.heap.push(value);
this.heapifyUp();
this.length++;
}

delete() {
const root = this.heap[0];

if (this.heap.length === 0) return null;
else if (this.heap.length === 1) this.heap = [];
else {
this.heap[0] = this.heap.pop();
this.heapifyDown();
}

this.length--;
return root;
}

getLeftChildIndex(parentIndex) {
return parentIndex * 2 + 1;
}
getRightChildIndex(parentIndex) {
return parentIndex * 2 + 2;
}
getParentIndex(childIndex) {
return Math.floor((childIndex - 1) / 2);
}
swap(index1, index2) {
[this.heap[index1], this.heap[index2]] = [this.heap[index2], this.heap[index1]];
}
heapifyUp() {
let cIdx = this.heap.length - 1;

while (cIdx > 0) {
const parentIdx = this.getParentIndex(cIdx);

if (this.heap[cIdx] < this.heap[parentIdx]) {
this.swap(cIdx, parentIdx);
cIdx = parentIdx;
} else break;
}
}
heapifyDown() {
let cIdx = 0;

while (this.getLeftChildIndex(cIdx) < this.heap.length) {
const leftChildIndex = this.getLeftChildIndex(cIdx);
const rightChildIndex = this.getRightChildIndex(cIdx);
const targetIndex = this.heap[leftChildIndex] > this.heap[rightChildIndex] ? rightChildIndex : leftChildIndex;

if (this.heap[cIdx] > this.heap[targetIndex]) {
this.swap(cIdx, targetIndex);
cIdx = targetIndex;
} else break;
}
}
}

const fs = require("fs");
const input = fs.readFileSync("dev/stdin").toString().trim().split("\n");
const arr = input.slice(1).map((v) => +v);
const minHeap = new MinHeap();

if (arr.length === 1) {
console.log(0);
return;
}
arr.forEach((el) => minHeap.push(el));
let answer = 0;

while (minHeap.length > 1) {
const sum = minHeap.delete() + minHeap.delete();
minHeap.push(sum);
answer += sum;
}

console.log(answer);

과제를 진행하며 느낀점

지난 HTML/CSS 과제가 우리가 눈으로 보는 페이지 디자인 자체를 만들어내는 것이 목적이었다면 이번
JavaScript 과제는 기능들을 동작하도록 하는 것이 중점이었다. 가장 크게 느꼈던 점은 기능이 동작하도록 하는 것에 있어서
수많은 방법들이 있다는 것이었다. 뿐만아니라 같은 기능이라도 유저의 입장에서 고민한다면 더욱 나은 방향으로 구현하는 것 또한
가능했다. 따라서 단지 실제 사이트와 같은 모습만을 만들면 되는 지난 과제보다 훨씬 어렵게 다가왔던 것 같다.

1) Firebase을 처음으로 사용하면서

이번 과제의 필수 요건 중 하나는 데이터를 따로 저장하여 관리할 수 있는 서비스인 AWS S3나 Firebase같은 기능을 이용하는 것이었다.
커리큘럼 내 강의에서 접해보지 못한 부분이었기 때문에 관련 정보들을 검색하고 공식 문서를 사용해 하나하나 익혀가면서 기능을 구현했다.
이 과정에서 두 가지의 긍정적인 경험을 할 수 있었다.

첫 째, 내가 처음 접하는 기술이라도 공식 문서를 통해 익혀가면서 문제들을 해결해나갈 수 있다는 자신감을 얻게 되었다. 강의로 배운 내용들이
아닌 내가 스스로 문서를 읽고 시행착오를 겪어가며 마주치는 문제들을 해결하다보니 이제는 어떻게 새로 기술들을 배울 때 문서들을 참고해야할지
경험할 수 있었고, 공식 문서의 내용이 내 생각 이상으로 자세하고 친절하다는 것을 깨달았다. 새로운 것을 배울 때 유튜브와 인프런 등의
강의들을 뒤지며 어떻게 배워나가야할까 고민하던 내가 이번 경험으로 강의만이 정답이 아니라는 것을 알았고, 문서를 통해 학습하는 것 또한
빠르게 기능을 구현할 수 있는 방법이라는 것을 알게되었다.

둘 째, 데이터를 어떤 방식으로 저장할지 어떻게 사용할지를 고민하게 되면서 그간 프론트엔드에서 경험할 수 없던 것들을 직접 경험할 수 있었다.
프론트엔드 과정의 특성 상 실제 유저들이 접하는 페이지와 UI들에 대해 고민하게 되는 경우는 많아도 실제로 어떻게 데이터를 줄 것인지, 어떻게
저장할 것인지 생각해볼 기회가 없었다. 하지만 이번에 과제에서는 그러한 것들을 직접 해봄으로써 실제 사이트들이 어떻게 돌아가고 있는지에 대한
감을 익힐 수 있었다.

2) 동작에만 집중하는 코드는 목표한 기능에 불필요한 동작을 추가시킬 수 있다.

구현하고자 하는 기능에 대해 정확히 뭐가 필요한지 그것을 해내려면 무엇을 알아야 할지 파악하고 그것을 바탕으로 작업을 해야된다는 것을 느꼈다.

자바스크립트는 자유로운 언어다. 그만큼 내가 원하는 것들을 다양한 방법으로 구현할 수 있고, 그 과정에서 꼭 필요하지 않은 작업을 하는 경우가 있었다.
멘토님께서 지적해주신 placeholder에 관한 부분이었는데, html의 input요소의 특성상 placeholder 속성에 값을 부여해주면 blur처리 될 때 input value에 따라 자동으로 placeholder를 다시 보여준다. 하지만 나는 ‘그렇게 동작해야지’ 라는 생각에만 사로잡혀 이미 그렇게 구현되어있는 기능들까지
하나하나 자바스크립트를 통해 동작을 제어하였다. 동작만에 생각이 매몰되어 정작 전체적인 그림을 보지 못했던 것이었다. 단순히 이런 동작을 하게
해야지에 집중하는 것이 아니라 전체적인 기능에 있어 내가 무엇을 제어해야되고, 어떤 것이 필요한지를 제대로 생각하고 코드를 작성해야겠다고
생각했다.

3) 여러 방법들 중에 선택한 이유가 있어야 한다.

멘토님의 멘토링 과정에서 지속적인 함수호출에 관련하여 debounce라는 기능에 대해 설명을 들었고, 이번 과제에서 활용해보고자 하였다. 로그인 기능에서
이메일, 패스워드 입력값을 받아 validation 할 때, 매 input값의 변경마다 validation을 하는 것이 비효율적이라 느껴서 적용한 것이었는데 과연
내가 그 동작을 하도록 하는 방법에 대해 고민했는가를 생각해보았다. 분명 비슷한 동작을 하는 다른 방법들이 있었을 것이다. 그러나 고민이 없었기에
그저 사용하여 기능을 구현하고 만족했다.

리팩토링을 하면서 이에 대해 생각하면서 debounce를 고민할 때에는 비슷한 동작을 하는 throttle도 함께 고민했어야 한다고 생각했다. 결과적으로는
throttle보다 debounce를 통해 구현하는 것이 원래 목적에 맞았지만, 그저 결과적으로 옳은 방법을 선택한 것과 둘을 고민해서 왜 그게 옳은 것인지를
확인하고 넘어가는 것에는 큰 차이가 있다. 이제는 동작에 대해 고민할 때 가능한 여러 방법들을 생각하고 타당한 이유와 함께 선택하여 코드를 작성해야겠다.

멘토님의 코드리뷰

1) 비동기 코드에 대한 전반적인 이해와 예외처리의 부족

분명 이론적으로 학습할 때에는 알고있다고 생각한 내용들이었지만 실제 과제에서 적용하면서 왜 이것들이 필요한지, 어떤 방식으로 사용해야 하는지에 대해
깊게 생각하지 않고, 이걸 적용해야 기능이 동작하네!라는 것에만 초점을 맞췄던 것 같다. 그러다보니 불필요한 async/await 사용과 예외처리 부분에서
아쉬운 코드가 되었던 것 같다.

1
2
3
const $list = document.getElementById('member-list');

const render = async (members, isSignin = false) => {

async/await의 경우 비동기 함수를 마치 동기처럼 코드 내에서 처리하기 위한 방법이다. 따라서 비동기 코드들의 순서에 의존하는 코드들이 아니라면
굳이 사용할 필요가 없었다.

1
2
3
4
5
6
7
8
// 기존 코드
const [person, division, email, password, contact, picture] = $form;
// 사진 firebase 올리고 profileUrl 할당
const profileUrl = await uploadImage(email.value, picture.files[0]);

await addData(person.value, email.value, contact.value, division.value, profileUrl);

location.replace('main.html');

위 코드를 보면 비동기 코드가 많은 것을 확인할 수 있다. 개발 시 에러가 발생했을 때 어떤 함수에서 문제가 있었는지를 수월하게 파악하기 위해서 try/catch를 통해 예외처리 하는 것을 습관해야한다고 멘토님께서 말씀해 주셨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 리팩토링 코드
const [person, division, email, password, contact, picture] = $form;
// 사진 firebase 올리고 profileUrl 할당
const profileUrl = email => {
try {
uploadImage(email.value, picture.files[0]);
} catch (err) {
console.error(err);
}
};

await addData(person.value, email.value, contact.value, division.value, profileUrl(email));

location.replace('main.html');

2) apikey를 환경변수로 관리

로컬에서 개발할 때와 서버에 배포할 때 DB연결, 포트 설정 등 관련된 부분을 매번 수정해서 배포하는 것은 쉽지 않다. 또한 유출되면 안되는 secret key
는 public으로 배포하면 안된다는 것을 다시 한번 상기하게 되었다. firebase를 사용하면서 내 데이터베이스에 접근할 수 있는 key들을 그대로 오픈해놨는데
지금처럼 학습용으로 작은 사이트를 만들 때에는 상관이 없더라도 기업에서 일을 할 때 매우 크게 잘못될 수 있으니 이러한 것들을 환경변수로 관리하는 것을
습관하해야겠다.

1
2
3
4
5
6
7
8
9
10
// 기존 firebase.js 안에 apikey를 노출
const firebaseConfig = {
apiKey: 'AIzaSyCy7QYDgKpkn-0eH42AmxAki6u4DH1oGZ0',
authDomain: 'members-dev-d3512.firebaseapp.com',
projectId: 'members-dev-d3512',
storageBucket: 'members-dev-d3512.appspot.com',
messagingSenderId: '277572932709',
appId: '1:277572932709:web:070862b87f629ee329a28c',
measurementId: 'G-3GBJM8LJ01',
};
1
2
3
// dotenv를 활용하여 환경변수로 관리
# .env
f_Apikey=AIzaSyCy7QYDgKpkn-0eH42AmxAki6u4DH1oGZ0
1
2
3
4
5
6
7
8
9
10
11
12
13
require('dotenv/config');

const apiKey = process.env.f_apikey;

const firebaseConfig = {
apiKey,
authDomain: 'members-dev-d3512.firebaseapp.com',
projectId: 'members-dev-d3512',
storageBucket: 'members-dev-d3512.appspot.com',
messagingSenderId: '277572932709',
appId: '1:277572932709:web:070862b87f629ee329a28c',
measurementId: 'G-3GBJM8LJ01',
};

3) 성능을 생각하여 코드를 구성

1
2
3
4
5
6
7
8
9
10
window.addEventListener('DOMContentLoaded', async e => {
if (JSON.stringify(localStorage.getItem('isSignin')) === 'null') {
location.replace('signin.html');
return;
}

await getAllMembers(state.members);

await render(state.members);
});

위 코드에서 페이지에 적용되는 render함수는 getAllMembers함수가 반드시 실행된 이후에 동작하게 된다. 처음 이렇게 코드를 구성한 이유는
데이터를 추가/삭제 할 때 데이터베이스에 먼저 반영하고 그 데이터를 토대로 페이지에 렌더하려는 것이 목적이었다. 그러나 리스트를 삭제하고
추가할 때마다 데이터베이스를 먼저 거치고 페이지에 반영하는 것은 사용자 입장에서 더 느린 응답을 받는 결과로 이어진다는 것을 생각하지 못했다.

최초 데이터를 불러올 때에만 getAllMembers를 먼저 실행하여 페이지에 반영하고 그 이후에는 자바스크립트에서 미리 객체를 선언해서 동시에 관리하면
훨씬 빠른 응답을 줄 수 있을 것이라 생각하였다. 객체를 수정하여 먼저 페이지에 반영한 뒤에 그 결과를 데이터베이스에 전달하는 것이 사용자 경험에서
효율적일 것이라 생각했다.

▪︎ 연결리스트 (Linked List)

image.png

연결리스트는 각 노드가 데이터와 포인터를 가지고 한 줄로 연결되어있는 방식으로 데이터를 저장하는 자료 구조입니다. 이름처럼 데이터를 담고있는 노드들이 연결되어 있는데, 노드의 포인터가 다음이나 이전의 노드와의 연결을 담당하게 됩니다.

연결리스트는 배열과 비교하여 구조를 변경하고 데이터를 삽입 삭제하는데에 있어 효율적인 처리가 가능합니다. 예를들어 배열의 앞쪽에 새로운 데이터를 추가하는 push 메서드, 배열의 앞쪽의 데이터를 삭제하는 shift 메서드는 인덱스의 변경으로 인해 O(N)의 시간복잡도로 처리됩니다. 그러나 연결리스트에서 같은 동작을 수행한다면 새로운 노드를 생성하고 그 노드의 포인터만 앞쪽 노드로 가리켜주면 되기 때문에 O(1)의 시간복잡도로 처리가 가능합니다.

▫︎ 연결리스트의 구현

자바스크립트에서 연결리스트의 구현은 3단계로 구분할 수 있습니다.

  1. 노드 클래스 생성
  2. 연결리스트 클래스 생성
  3. 연결리스트 메서드 추가

이 때 노드를 정의하면서 다음 노드의 위치를 가리키는 포인터만을 정의한다면 단방향 연결리스트, 이전 노드의 위치를 가리키는 포인터를 추가로 정의한다면 양방향 연결리스트를 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 노드 클래스 생성
class Node {
constructor(value) {
this.value = value;
this.next = null;
// this.prev = null;
}
}
// 연결리스트 클래스 생성
class LinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}

// 필요한 연결리스트의 메서드들을 추가
add(value) {
const node = new Node(value);

if (this.head) {
this.tail.next = node;
// node.prev = this.tail;
} else {
this.head = node;
}

this.tail = node;
this.length++;
return node;
}

remove() {
this.head = this.head.next;
// this.head.prev = null;
this.length--;
}

getHead() {
return this.head.value;
}
}

▪︎ Example of Apply

image.png

image.png

▫︎ 일반적인 배열 조작으로 풀이

1
2
3
4
5
6
7
8
9
10
11
12
13
const fs = require("fs");
const input = fs.readFileSync("linkedlist.txt").toString().trim();
const cards = Array(+input)
.fill(0)
.map((_, idx) => idx + 1);
// cards = [1,2,3,4,5,6]

while (cards.length > 1) {
cards.shift();
cards.push(cards.shift());
}

console.log(...cards); // output: 4

위 코드의 while문 한번의 시행에서 배열의 원소 중 하나만이 삭제되기 때문에 N-1번의 시행 횟수를 가집니다. 또한 while문 안에서 카드의 앞 요소를 삭제하는 shift 메서드는 배열의 길이만큼 순회하게됩니다. 따라서 O(N^2)의 시간복잡도를 가지며 이것은 입력에 따라 시간제한 2초 (2*10^8)를 초과할 수 있으므로 조건에 맞는 풀이가 아닙니다. 따라서 O(N)의 시간복잡도로 풀 수 있도록 연결리스트 자료구조를 적용해보겠습니다.

▫︎ 연결리스트 자료구조를 적용하여 풀이

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const fs = require("fs");
const input = fs.readFileSync("linkedlist.txt").toString().trim();

class Node {
constructor(value) {
this.value = value;
this.next = null;
this.prev = null;
}
}

class LinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}

add(value) {
const node = new Node(value);

if (this.head) {
this.tail.next = node;
node.prev = this.tail;
} else {
this.head = node;
}

this.tail = node;
this.length++;
return node;
}

remove() {
this.head = this.head.next;
this.head.prev = null;
this.length--;
}

getHead() {
return this.head.value;
}
}

const cards = new LinkedList();
for (let i = 1; i <= +input; i++) {
cards.add(i);
}

while (cards.length > 1) {
cards.remove();
cards.add(cards.getHead());
cards.remove();
}

console.log(cards.getHead());

이번 포스팅에서는 가장 기본적인 자료구조인 스택과 큐에 대해 알아보고, 우리가 자바스크립트로 개발하면서 어떻게 적용되고 있었는지 확인해보겠습니다. 가볍게 이런식으로 사용되고 있었구나 정도만 확인하고 정확한 동작원리는 따로 자바스크립트 포스팅에서 자세히 다루겠습니다.

▪︎ 스택 (Stack)

image.png

스택은 가장 마지막에 저장된 데이터가 먼저 실행되는 후입 선출 (Last In First Out, LIFO) 구조입니다. 스택은 데이터의 삽입과 삭제 (실행)가 O(1), 탐색에는 O(N)의 시간복잡도를 가집니다.

▪︎ 큐 (Queue)

image.png

큐는 삽입된 순서되로 삭제 (실행)되는 선입 선출 (First In First Out, FIFO) 구조입니다. 큐와 마찬가지로 삽입과 삭제에는 O(1), 탐색에는 O(N)의 시간복잡도를 가집니다.

▪︎ 자바스크립트의 실행 컨텍스트 스택

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const a = 1;
const b = 2;
const c = 3;

console.log(a);

const sum = (...element) => {
return element.reduce((acc, cur) => acc + cur);
};

const minus = (element1, element2) => {
return element1 - element2;
};

const result1 = sum(a, b, c);
const result2 = minus(b, a);

console.log(result1);
console.log(result2);

image.png

우리가 자바스크립트로 짠 코드가 실행되는 런타임에서 코드를 읽어가면서 함수를 호출할 때마다 스택에 실행컨텍스트를 쌓고, 실행이 완료되면 삭제하는 과정을 거칩니다. 위 예시를 통해 이해해보겠습니다. 먼저 자바스크립트가 실행되면 (런타임) 전역 실행 컨텍스트를 스택에 등록하고 해당되는 코드들을 실행하기 시작합니다. 그렇게 코드들을 실행하다가 함수의 호출부를 만나게 되면 진행되던 코드 실행을 잠시 멈추고 해당 함수에 대한 실행 컨텍스트를 스택에 삽입하게 됩니다. 그리고 함수를 실행하고 삭제하는 과정을 거친 뒤, 다시 전역 실행 컨텍스트의 코드 실행을 진행하며 모든 코드가 실행되었을 때, 전역 실행컨텍스트를 삭제하게 되면서 자바스크립트 실행이 끝나게 됩니다.

▪︎ 자바스크립트의 태스크 큐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const a = 1;

console.log(a);

const consoleAfter1 = (element) => {
setTimeout(console.log(element), 1000);
};

const consoleAfter2 = (element) => {
setTimeout(console.log(element), 2000);
};

consoleAfter1(a);
consoleAfter2(a);

console.log(a);

image.png

위 코드에서 우리에게 익숙한 setTimeOut 함수가 보입니다. 우리는 setTimeOut을 막연하게 설정한 시간(ms) 이후에 실행되는 함수라고 알고 있습니다. 그렇다면 위 코드에서 consoleAfter1, consoleAfter2 함수가 호출되기 전 10초가 걸릴 정도의 코드가 작성되었다 가정해봅시다. 그럼 console.log가 어떤 순서로 실행될까요? 여전히 1 → 2 → consoleAfter1 → consoleAfter2 의 순서로 실행될 것입니다.

즉 정확히 말하면 setTimeOut 함수는 ‘정해진 시간 뒤에 실행해라’가 아닌 ‘정해진 시간 뒤에 태스크 큐에 콜백함수를 집어넣어라!’라는 동작을 하는 함수입니다. 태스크큐는 이렇듯 비동기 함수들이 실행컨텍스트 스택 내에서 실행되면 순서대로 태스크 큐에 함수들을 쌓게 됩니다. 그리고 실행컨텍스트들이 모두 실행되어 실행컨텍스트 스택이 비게 되면, 비로소 태스크큐에 삽입된 순서대로 실행되게 됩니다.

0%