올 한해 여러가지 프로젝트를 진행했지만, 정말 나의 피, 땀, 눈물이 들어간 프로젝트를 마무리하며 회고글을 적어보고자 한다.
사건의 발단
: 현재 다노에서는 매주 or 2주 단위로 1on1 이라는 것을 진행하고 있다. CTO님 혹은 팀장님과 현재 자신이 겪고 있는 개인적인 이슈 혹은 문제점들을 공유하면서 함께 싱크를 맞추어 나가는 자리이다. 올 6월쯤 1on1을 진행하면서, 현재 다노샵 서버를 하고 있지만 다음에 기회가 되면 프론트엔드 쪽도 한번 해보고 싶다 라는 이야기를 했었고, CTO님께서는 다음에 다노앱안에 위치하고 있는 매거진이라는 탭을 리뉴얼할 예정이니(언제가 될 진 모르지만) 기회가 되면 다음에 한 번 해보겠냐고 하셨다. 그때 당시에는 아무 생각 없이 "예" 라고 대답을 하였다. 물론 그게 언제가 될 진 모르니깐... (많은 일들이 이렇게 기억 속으로 묻어져가니 이것도 그렇겠지라고 생각하며..)
사건의 시작
: 9월이 시작되면서 CTO님이 갑자기 주말에 슬랙을 주셨다. 다노 매거진 개편 관련된 디자인이 다 되었는데, 병욱님이 한번 프론트 쪽 개발을 해보겠느냐는 이야기였다. 순간 머리 속이 하애졌다. '갑자기 나에게 왜 이런 이야기를 하실까?' 라는 생각도 잠시 6월의 김병욱이 저질렀던 일이 떠올랐다...아아악... 아무튼 내가 뱉은 말이었고, 먼저 가능할지에 대한 일정을 체크했다. 아무튼 기존에 하던 회사 업무 외에 진행해야 하는 업무였고(외주로 진행), 회사 내 개편 업무다 보니 DUE도 어느정도는 명확하게 걸린 업무였다.
때마침 코로나가 심해져서 주말마다 하던 일도 없어졌고, 하여 이번 기회에 좀 쉬어야지 하던 차였다. 일단 주말 일정은 확보, 그리고 있는 추석 연휴. 한글날 등등 휴일도 넉넉했다. 평일 퇴근 후에 업무를 진행하고 휴일을 모두 넣으면 어떻게든 하면 되지 않을까라고 생각했다. (이 일을 하기 전에 프론트엔드를 해본 것은 html, css, 간단한 javascript로 ajax를 써본 것이 다였다.)
그렇게 나는 6월의 내가 했던 말에 대한 책임을 지기로 했다. 물론 그 동안 프론트엔드를 공부해야지 생각만 하던 나에게 아주 좋은 자극이 될 수 있을거라고 생각했다. 그렇게 2달 간의 기나긴 프로젝트가 시작되었다.
프로젝트의 시작 (9월 초 ~ 9월 중순)
: 정확히 9월 7일날 백엔드 인턴분이 입사하셔서 매거진의 백엔드 부분을 맡아서 개발해주시기로 하셨고, 나 역시도 9월 7일부로 개발을 시작하게 되었다. React라는 것을 한번도 써본적이 없어서, 먼저 공부부터 해야 하였다.
다양한 수업들이 많은데, 내가 들은 것은 유튜브 클론 코딩 그리고 해당 수업을 듣기 전에 들으면 좋다고 참고자 올려놓으신 boile-plate? 수업이었다. boile-plate 수업을 모두 듣고 유튜브 클론 코딩을 절반까지 들었다. 그렇게 일주일이 훅 지나갔다. ㅠㅠ 나에게는 시간이 그렇게 많지 았다. 아 일단 이렇게 하는 거구나 느낌을 잡고, 일단 프로젝트 start를 끊었다. (시작이 절반이다...)
사내 프론트 개발자분에게 구조에 대한 피드백을 받은 뒤에, 컴포넌트들을 생성하기 시작했다. 그렇게 9월 중순 나의 첫 제대로 된 프론트엔드 프로젝트가 시작되었다.
지금 다시 생각해봐도.. 정말 답이 없는 시간이었다.
프로젝트의 초기(9월 중순 10월 초)
: 어렵게만 느껴졌단 프로젝트는 생각보다 순탄히 진행되었다. 컴포넌트들을 만들어서 쌓는다는 개념은, 굉장히 참신하면서 그동안 html, css만을 써서 만들던 나에게는 매우 직관적으로 다가왔다. (이래서 사람들이 react, react하는 구나라고 잠깐 생각할 수 있었다.) 그리고 이미 잘 만들어져 있는 라이브러리 들이 많았다. (특히 슬라이더 및 롤링 배너, 스크롤에 따라서 특정 부위를 따라다니게 하는 sticky 기능은 짱이었다.)
그리고 무엇보다 중요한, 업무시간을 확보하였다. 현재 다노에서는 출근 시간을 조정할 수 있어서 2가지 패턴으로 시간을 확보해보았다. 먼저 8시까지 출근해서 9시 30분까지 매거진 업무를 진행하고 9시 30분부터 6시 30분까지는 회사 업무, 그리고 식사 이후에 다시 10시까지 매거진 업무를 진행하는 1가지 형태와 그냥 8시부터 오후 5시까지 회사 업무를 진행하고 쭉 이어서 10시까지 매거진 업무를 진행하는 2가지 형태였다.
결론적으로는 2번째 형태(일찍 회사 업무를 마무리하고 이후에 매거진 업무를 진행하는 형태)가 더 업무가 잘되어서 2번째 형태로 시간 확보를 하였다.
이건 뭔가 나의 고질적인 문제점이기도 한데, 일단 앞에 목표가 있게 되면 그 이외의 부분들은 크게 신경을 쓰지 않게 되는 것 같다. 그렇게 나에게 이 매거진을 끝낼 때까지 다른 일들은 모두 2순위로 밀어두었다.
프로젝트의 위기
: 생각보다 초기에 쭉쭉 잘나가던 프로젝트였는데 (나중에 안 사실이지만, 그냥 화면에 보이게 만드는 것보다는 디테일한 부분이 훨씬 시간이 오래 걸리고 어려운 것이었다. 이때까지만 하더라도 화면에 그리면 되겠지라고 생각만 하고 있었다.) 생각지도 못하게, 탈장이라는 병에 걸리게 되었다. 탈장은 말 그대로 장기가 약해진 피부벽을 뚫고 나온 것인데, 수술 이외에는 치료법이 없다고 한다.
처음에는 그냥 아래배 쪽이 불룩하게 튀어나와서 "뭐지 염증인가" 하고 편하게 생각하고 있었는데, (마침 추석 연휴라서 병원에도 가지 못했다.) 추석 연휴 이후에 병원에 가보니 탈장이라고 하였다. 그 동안 아래배 쪽에 불룩하게 튀어나와 있었던 게 장기였다니!!!! 진단을 받자마자 급격하게 아파지기 시작하였다.ㅠㅠ(그 동안도 아프긴 하였지만 참을만 하였는데, 진단을 받고나니 갑자기 10배는 더 아파진 느낌이었다.)
그렇게 급하게 수술 날짜를 잡고, 수술을 진행하였다.(아악 안돼 내 휴가.. 흑흑) 생각보다 탈장이라는 병에 걸리는 사람들이 많았고, 그렇게 큰 병도 아니었지만 문제는 일상 생활이 매우 불편해진다는 것이었다. 일단 오랜 시간 앉아 있는게 굉장히 힘들어졌다. 일단 업무외에 시간을 투자해서 해당 프로젝트를 진행해나가야 했기에 나에게 오랜 시간 앉아 있을 수 있는 것은 매우 중요했는데, 그게 어려워지다보니 프로젝트도 시간을 내기 어려웠다.
자연스럽게 컨디션도 급격하게 나빠지기 시작했다. 이미 한 달 정도의 시간을 퇴근 이후의 시간들 그리고 주말, 추석 연휴까지 모두 시간을 쏟고 있어서 정신적으로나 신체적으로나 매우 힘들었지만, 아무튼 내가 하겠다고 하였고 나는 거기에 책임을 지고 싶었다.
프로젝트의 Detail
: 전체적인 그림을 그리는 것은 한 달정도 안에 마무리를 했던 것 같은데, 생각보다 디테일한 부분에서 신경 쓸 부분이 많다. 그 동안은 서버가 진행되는 동안 Mocking api를 활용하여 대부분 get요청으로만 화면을 뿌려주었다. (post man에서 간단하게 mocking api를 만들 수 있다.) 그러다가 서버가 붙게 되면서 여러 가지 신경써야 할 부분이 많았다. 사전에 맞추었지만 변수명이 틀린 부분들도 있었고, 우리 생각처럼 잘 작동하지 않았다.
그리고 디테일하게 신경쓸 부분이 필요 했던 게 많았는데, 바로 상단 카테고리바와, 검색창이었다.
사실 저렇게 화면에 보이게 하는 것은 어렵지 않았다. 다만 개발 요구사항은 쉽지 않았다. "일주일 이내의 새로운 글이 있을 때는 빨간 버튼이 보였으면 좋겠어요. 하지만 만약에 고객이 해당 카테고리를 클릭한 적이 있으면 빨간점이 사라져야 해요. 근데 또 만약에 고객이 클릭하고 난 다음에 새로운 글이 또 올라오면 빨간점이 또 보여야 해요"
즉 우리가 생각하던대로 새로운 글(일주일 이내)이 있으면 빨간버튼이 있어야 하고, 그 새로운 글이 우리가 해당 카테고리를 클릭하게 되면 더 이상 새로운 글이 아니다라는 것이다. 너무나도 자연스러운 액션인데, 이걸을 개발하기 위해서는 말로 정리가 되어야하고, 정리가 되어야 코드로 작성할 수 있는데, 쉽지 않았다.
이것을 해결하기 위해서는 해당 사용자가 언제 각 카테고리들을 클릭했는지 저장시켜놔야했다. 일주일 이내의 글이 있을 수 있지만, 그 글이 사용자가 그 카테고리를 클릭했을 당시에 있었던 글인지 아닌지는 또 판단이 필요했다. 이것을 해결하기 위해 서버에서 일주일 이내의 글이 있는지에 대한 정보와, 최신 발행글의 시간을 가지고 왔다.
그리고 프론트에서는 고객이 각 카테고리를 클릭한 정보들을 localstorage에 저장시켜놓고 비교하였다. 기본적으로 일주일 이내의 글이 없다고 하면 아예 빨간 버튼을 보이지 않게 하였고, 만약에 일주일 이내의 글이 있다면, 고객이 해당 카테고리를 마지막에 누른 시간과, 그 카테고리의 최신글의 시간을 비교하였다. 그래서 만약에 해당 카테고리를 누른 시간이 더 늦다면, 해당 고객이 그 카테고리를 눌렀을 당시 이미 그 글은 보였으니 빨간점을 붙이면 안되었고, 그렇지 않다면 빨간점을 붙이게 하여 해결하였다. (항상 서버만 로직을 짜면 된다고 생각했는데... 정말 이번 기회에 많은 반성을 하였다. 프론트엔드도 복잡한 로직이 굉장히 많다.)
그리고 또 하나 카테고리의 요청사항이 있었다. "각 카테고리를 눌렀을 때 그 카테고리를 보던 위치로 갔으면 해요" 이것도 너무나 당연하게 볼 수 있는데, 이것을 구현하기 위해서는 각 카테고리마다 마지막 보던 위치를 기억하고 있어야 했다. 그래야 그 카테고리를 눌렀을 때 해당 위치로 이동할 수 있었다. 앱에서는 이게 그렇게 어렵지 않게 구현이 가능하다고 하는데, 웹에서는 각 카테고리를 누를 때 마다 다시 api를 호출하고 랜더링을 다시 해줘야하기 떄문에, 다른 방법이 없었다. (아마 있겠지만 찾아봐도 보이지 않았다.) 그래서 각 카테고리마다 마지막 보던 위치를 저장시켜 놓았다. 그래서 현재 매거진에는 각 카테고리를 누를 때마다 마지막에 보던 위치를 찾아서 이동한다.
그리고 정말 쉽지 않았던 검색창 interaction
처음 검색창을 눌렀을 때, 그리고 입력하기 위에 커서를 위에 눌렀을 때, 최근 검색어 저장, 검색 결과 등 등 다양한 컴포넌터들이 상황에 맞게 랜더링이 되어야 했다.
그리고 또 검색하다가 해당 검색어를 지웠을 때, 등등 우리가 그 동안 자연스럽게 사용하던 부분이 막상 구현을 하려고 하니 그렇게 막막할 수 없었다.
처음 검색창을 눌렀을 때는 인기검색어가 나와야하고, 검색창을 클릭하면 최근 검색어가, 그리고 검색을 하면 해당 검색어가 최근 검색어에 저장되어야 하고, 검색어를 삭제했을 때는 다시 인기검색어로 나오는 형태로 구현이 되어야 했다.
3개의 컴포넌트가 상황에 따라서 나와야 하다보니 복잡한 부분이 많았지만, 적절하게 어떻게 분기를 태워서 해결해냈다.
사실 이 밖에도 신경써서 구현한 부분이 많았지만 그것은 모든 개발자들이 겪는 어려움일거라고 생각한다. 그래도 조금 더 자랑해보자면,
등이 있다. 조금 더 디테일한 부분들은 추후에 하나씩 다뤄보고자 한다.
아 그리고 정말 쉽지 않았던 데이터심는 작업ㅠㅠ 정말 데이터 심는 작업이 너무 쉽지 않았다. 오히려 요건이 까다로울 때는 개발보다 더 어려운 부분도 있었다.
분명 2시간~ 3시간정도면 끝나는다는 CTO님의 말을 철석같이 믿고 있었는데... 주말 온전히 2틀을 사용했는데도, 끝내지 못했다. 예를 들면 이런 요건도 있었다. "슬라이딩 배너에 상품들이 여러가지 있는데 각각의 상품들이 50% 이상 1초 이상 보였을 때 이벤트를 보내주세요" ㅎㅎ 구현을 떠나 말로도 어려운 이벤트 심는 작업이었다. ㅠㅠ (회사내에 이미 구현되어 있던 것들을 많이 이용하였다...ㅎㅎ)
프로젝트의 마무리
: 그래서 이 프로젝트가 과연 끝났을까? 처음 프로젝트를 시작할 때, 너무나도 막막했던 시간이었다. 그리고 항상 새롭게 오셔서 바로 매거진 서버를 맡게 된 나온님과 이야기 했던 것은 어떻게든 이 비둘기를 날게 하자 였다.
그렇게 2달이 지나 우리 비둘기를 날았다. 그리고 정말 많은 분들의 도움으로 생각보다 멋진 모습으로 날았다.
새롭게 매거진을 디자인 해주신 디자이너분들과 새롭게 리뉴얼된 매거진에 따라서 다시 컨텐츠를 일일이 만들어주신 컨텐츠 마케터분들, 정말 귀찮은 질문을 계속해서 받아주신 주변의 많은 개발자분들, 데이터를 잘 심고, 트레킹 하기 위해 노력해주신 데이터사이언티스트분들, 앱 안에 들어가는 것이다보니 많은 추가 작업을 해주신 앱 개발자분들, 안정적으로 서비스가 돌아갈 수 있도록 도와주신 인프라 개발자분, 지치지 않고 옆에서 정신 수양을 도와주신 CTO님까지
정말 정말 많은 분들의 도움이 없었다면, 이 모든 것들이 불가능 했으리라 생각한다. 이번 작업을 하면서 내가 개인적으로 가지고 있던, 다른 분야에 대한 진입장벽도 깨진 것 같다.
개발자는 프로젝트를 하면서 성장한다는 말이 있다. 나도 이번 기회를 통해서 한 단계 더 성장했기를 기대해본다.
저 역시도 29살의 나이에, 처음으로 개발이라는 것을 배우면서 무엇을 해야 할지 몰라 많은 시행착오를 겪었습니다.
그리고 현재는 잘 성장하여, 3년차 백엔드 개발자로 일하고 있습니다. 새롭게 개발 시작하시는 분들이 저와 같은 시행착오를
겪지 않았으면 좋겠다는 생각으로 개발자가 된 이후로 지속적으로 강의를 해왔었는데, 항상 시간이 부족해서 아쉬움이 가득이었습니다.
그래서, 정말 큰 맘 먹고 올해 1월부터 책을 적기 시작했습니다. 회사 출근 전 퇴근 후 최대한 시간을 내서(피,땀, 눈물 ㅠㅠ), 제가 알고 있는, 그리고 부족한 것은 주변 개발자분들에게 물어가며 정말 열심히 적었습니다!
그리고 10개월이 지난, 이제서야 이 책이 정말 곧 빛을 보려고 합니다. 개발 시작하시는 분들이 정말 꼭 읽고 시작했으면 좋겠다는 생각에 용기내어 이렇게 글을 적습니다. 최소 2개월은 save하실 수 있으실거에요! (책 팔아서 돈을 번다는 것은 정말... 너무 어려운 일이고,그러고 싶지도 않습니다.)
정말 이 책이 정말 개발 시작하려는 분들에게는 조금이라도 도움이 되었으면 좋겠습니다. 그것이면, 전 정말 만족할 것 같습니다.
사실 나의 수업을 듣는 가장 많은 사람들이 하는 질문이기도 하다. "개발자가 되면 실제 생활은 어떤가요? 만족도는 어떠신가요?"
개발자에 대한 좋은 이야기들로 개발을 시작하려고 하는데, 막상 또 검색을 해보면 좋지 못한 이야기들이 많고, 혹시라도 개발자가 된 이후에 개발자가 잘 맞지 않으면 어쩔까에 대한 걱정이 많은 것도 같다. (그리고 그런 환상들을 깨주는 글들도 너무 많다.)
사실 이것에 대한 이야기도, 본인이 어떤 곳에서 처음 개발을 시작하고 있는지 혹은 현재 어디서 일을 하는지에 따라서 굉장히 큰 영향을 미칠 것 같다.
일단 나에 대해서 이야기 해보면, 나는 자체 서비스를 하는 회사에서 일을 하고 있고, 그렇다 보니 일반 고객들과 바로 마주하는 서비스의 서버를 개발한다.
이것의 장점은 내가 개발한 것을 사람들이 바로바로 이용할 수 있다는 장점이 있고, 관리자 페이지 쪽을 개발할 때는 운영단에서의 이슈를 해결해주어서 실제 관리자들분들이 만족해하시는 모습들을 보며 보람을 찾는다.
반대로 단점도 있다. 24시간 언제든지 에러에 대응해야 하고, 실제 고객들이 활용하는 서비스에 배포를 진행한다는 것은 매번 배포를 진행할 때마다 손에 땀을 쥐게 하는... 일들의 연속이다.ㅠㅠ
하지만 그럼에도 불구하고, 개인적인 생각으로는 개발자에 대한 업계의 대우, 주어진 업무들을 스스로 개발해나가면서 느끼는 일의 만족도 등은 다른 직군에 대해 높은 편이라고 할 수 있을 것 같다. (과거 페이퍼 작업을 할 때는, 나의 쓸모에 대해서 매우 많이 고민도 했었다.)
어떤 분들은 그렇게 이야기한다.
"비전공자가 개발자 시작했다가 1년도 못 채우고 돌아간 것을 수도 없이 봤다." "개발자가 잘 맞지 않으면 금방 다시 돌아갈 거니깐 적성 먼저 파악해"
사실 나는 이런 이야기들에 대해서 조금은 반대적인 생각을 가지고 있다.
내가 회사에 1년 넘게 있으면서도 비전공자로 개발자로 시작하신 분들을 많이 보았고, 그중에서 개발자를 포기하신 분은 한 분도 없었다.
그리고 적성이 잘 맞는지에 대한 유무는... 생각보다 개발자로 생활을 해봐도 파악하는 게 쉽지 않다. 반대로 우리가 다른 직업을 하고 있다면, 그 직업이 정말 적성에 잘 맞아서 하고 있는 사람들은 과연 얼마나 될까 생각해본다.
그런 의미에서, 결국 본인이 현재 있는 곳에 따라서 보고 느끼고 생각하게 된다. 그 말은 개개인마다 생각이 모두 다를 수 있다는 것이다. 그러므로 개발자가 되기도 전에 다른 사람들의 이야기를 너무 많이 참고하는 것은 큰 도움이 되지 못하는 것 같다.
개발자가 되서 행복한 게 아니라, 본인이 생각한 직업을 해볼 수 있다는 것에 의의를 두더라도, 100세 인생에서 한 번쯤은 개발자라는 직업을 한번 꼭 도전해봐도 괜찮다고 생각한다.
그리고 무엇보다, 내가 생각한 것들을 바로 만들어 볼 수 있다는 것은, 개발자로서의 최고의 장점이 아닐까 한다!
저 역시도 29살의 나이에, 처음으로 개발이라는 것을 배우면서 무엇을 해야 할지 몰라 많은 시행착오를 겪었습니다.
그리고 현재는 잘 성장하여, 3년차 백엔드 개발자로 일하고 있습니다. 새롭게 개발 시작하시는 분들이 저와 같은 시행착오를
겪지 않았으면 좋겠다는 생각으로 개발자가 된 이후로 지속적으로 강의를 해왔었는데, 항상 시간이 부족해서 아쉬움이 가득이었습니다.
그래서, 정말 큰 맘 먹고 올해 1월부터 책을 적기 시작했습니다. 회사 출근 전 퇴근 후 최대한 시간을 내서(피,땀, 눈물 ㅠㅠ), 제가 알고 있는, 그리고 부족한 것은 주변 개발자분들에게 물어가며 정말 열심히 적었습니다!
그리고 10개월이 지난, 이제서야 이 책이 정말 곧 빛을 보려고 합니다. 개발 시작하시는 분들이 정말 꼭 읽고 시작했으면 좋겠다는 생각에 용기내어 이렇게 글을 적습니다. 최소 2개월은 save하실 수 있으실거에요! (책 팔아서 돈을 번다는 것은 정말... 너무 어려운 일이고,그러고 싶지도 않습니다.)
정말 이 책이 정말 개발 시작하려는 분들에게는 조금이라도 도움이 되었으면 좋겠습니다. 그것이면, 전 정말 만족할 것 같습니다.
GraphQL은 페이스북에서 만든 데이터 질의어이며, 'gql'이라고 한다. gql은 서버에 작성된 쿼리를 통해 데이터를 조회하는 방식이 아니라, 클라이언트에서 쿼리를 작성하여 필요한 데이터만 조회하는 방식을 제공한다. 또한, 하나의 EndPoint를 가지기 때문에 개발 규모에 따라 EndPoint의 복잡도가 증가하는 REST API보다 개발이 간편하다. 여러 데이터 집합에서 데이터를 조회하는 경우 gql은 하나의 쿼리로 조회가 가능하지만, REST API는 Request를 여러번 시도해야 한다. REST API도 한번의 Request로 처리가 가능하지만, 매번 여러개의 데이터 집합을 조회하기 때문에 자원의 낭비를 초래한다.
GraphQL은 데이터의 구조를 정의하는 스키마(Schema)와 데이터 조회를 위한 쿼리(Query), 데이터 위한 뮤테이션(Mutation), 조회 결과에 대한 구현을 위한 리졸버(Resolver)로 구성되며, 이 외에 API명세서의 기능을 하는 인스로펙션(Instropection)으로 구성된다.
혼자서 공부하며 내린 정의 :
기존 rest api에서 발생하는 문제는 크게 2가지 였습니다.
overfetch와 underfetch
그 중 overfetch는
user의 정보를 요청하는 api를 보냈을 때, 내가 필요한 정보보다 훨씬 많은 정보들이 넘어오는 것이었습니다.
보통 프론트에서는 현재 user의 name만 필요로 하겠지만, 보통 rest api에서 고객 정보를 요청하면 해당 고객의 모든 정보를 보내주도록 구현되어 있어서, 더 많은 리소스 낭비로 이어졌습니다.
그리고 underfetch의 경우,
만약 고객의 장바구니 정보, 혹시 좋아요한 물품 정보를 모두 함께 보고 싶다고 했을 경우,
user/cart/ 뿐만 아니라 user/wish/ 등 여러가지 api를 요청해야 하는 이슈가 있습니다. 무엇보다 중요한 것은 이게 점점 서비스의 규모가 커져감에 따라서 관리해야 하는 endpoint들이 기하급수적으로 늘어날 수 있다는 것입니다. 이는 개발자나 클라이언트에게 부담이 될 수 있습니다.
GRAPHQL은 위에서 설명한 것처럼 REST API의 한계를 극복하기 위해 나왔습니다.
endpoint는 통상 1개만 생성하고, 클라이언트에서 필요한 데이터는 클라이언트에서 직접 쿼리를 작성하여 호출 반환하도록 합니다.
웹브라우저와 관련된 객체들의 집합을 브라우저 객체 모델(BOM: Brower Object Model)이라고 부른다. 이 브라우저 객체 모델을 이용하여 Brower와 관련된 기능들을 구성한다. DOM은 BOM 중의 하나이다. DOM은 document object model의 약자이다. 문서 객체 모델인데, 문서 객체란 <html> < body>와 같은 html문서의 태그들을 javascript가 이용할 수 있는 개체로 만드는 것. 그것을 문서 객체라고 한다.
또 뒤에 model이 붙어 있는데 여기서는 문서 객체를 인식하는 방식정도로 해석하면 좋을 것 같다.
즉 웹브라우저가 html 페이지를 인식하는 방식이다.
보통은 데이터가 변화하게 되면 양방향 바인딩으로 처리
변화(Mutation) => 특정 변화가 있으면 모델에 변화를 일으키고, view에 로직을 만들어준다. 그리고 화면에 다시 띄어준다.
페이스북에서는 다른 생각
만약에 변화가 일어나야 하면 mutation하지말고 기존에 있던 view를 날려버리고 새로 만들어버리면 어떨까? => 브라우저는 돔기반으로 작동하기 떄문에, 성능적으로 엄청난 문제가 있을 수 있다.
그래서 virtual dom이 나왔다.
가상의 돔이다.
변화가 일어나면 브라우저의 돔에 새로운 것을 넣는 것이 아니라 javascript로 이루어진 가상의 돔에 랜더링을 하고 기존의 돔과 비교를 하고 정말 비교가 필요한 부분만 업데이트 한다
도메인 네임서버는 IANA(IANA(Internet Assigned Numbers Authority)는 인터넷 할당 번호 관리기관의 약자로 IP 주소, 최상위 도메인 등을 관리하는 단체이다. 현재 ICANN이 관리하고 있다. 처음에는 서던캘리포니아 대학교정보 과학 학회의 존 퍼스텔이 서던캘리포니아 대학교 정보 과학 학회와 미국 국방부 간에 맺어진 계약 아래 관리했으며, 퍼스텔은 미국 상무부 계약으로 ICANN이 설립 될 때까지 이 업무를 수행했다.)
라는 곳에서 실제 도메인 주소를 가지고 오나
이러한 요청을 계속 주게 되면 네트워크상에 부하가 가중되기 때문에,
한 번 가져온 주소는 DNS server에 저장하고
호스트의 요청시 그 안에 정보를 가지고 온 후 반환해준다.
test
궁금증 :
그럼 해당 Address로도 접근이 가능해야 하는데 왜 접근이 불가능할까?
=> 원래는 접근 가능해야 하나, 요즘은 보안상의 이슈로 직접 IP접속을 차단한 경우가 많아,
현재 다노에서 다노샵 서버 개발을 하고 있는 대구올빼미입니다.(올빼미지만 밤 10시에 자는 건 비밀)
그리고 현재 다노샵 서버는 python을 베이스로 django 프레임워크를 활용하여 구성되어 있습니다. 많은 분들이 아시다시피 Django는 정말 좋은 기능들을 많이 제공해줍니다.
그중에서 초보 개발자에게 정말 좋은 것은 바로 ORM...!! ORM은 객체(Object)와 관계(Relation)을 연결(Mapping)해주는 개념입니다(이라고 합니다). 객체와 관계형데이터베이스를 변형 및 연결해주는 작업이라고 말할 수 있습니다. 즉 개발하는 데 있어서 관계형 데이터베이스의 제약을 최대한 받지 않으면서, 객체를 클래스로 표현하는 것과 같이 관계형 데이터베이스를 객체처럼 쉽게 사용할 수 있도록 해줍니다.
# 30세 이상 멤버 정보 가져오기 in sql
select * from member where age >= 30;
# 30세 이상 멤버 정보 가져오기 in django orm
Member.objects.filter(age__gte=30)
여러 프레임워크에서도 이 ORM을 제공해 줍니다.
그렇다 보니, 실제 서비스에서도 보통 ORM을 통해서 개발을 많이 진행합니다. 하지만 이 ORM의 가장 무서운 점은 실제 안에서 어떤 쿼리문을 발생시켜서 저렇게 쉽게 쓸 수 있도록 해주는지 알 수 없다는 것입니다.
그동안 제가 했던 작업선에서는 평상시에는 전혀 성능적으로 이슈가 크게 되지 않다 보니 너무나도 편하게 ORM을 사용해왔습니다. (사실 ORM이 아직도 너무 좋습니다 흑....)
실제 ORM 쿼리가 어떻게 요청이 되는지, 사수분이 여러 번 이야기해주셨지만 직접적으로 와 닿지가 않아 그렇게 넘어갔습니다. (죄송합니다... 반성합니다ㅠㅠ)
그리고 그 문제는 생각보다 크리티컬 하게 일어났습니다.
최근에 다노샵에서는 옵션별 할인 프로모션과, 기존에 있었던 대용량 할인 정보를 함께 표시해주도록 하는 업무를 진행하게 되었습니다. 그리고 그동안에 잘 사용하던 ORM에서 드디어 문제를 일으키고 말았습니다.
기존에는 각각의 제품 단위로 프로모션 할인이 들어가게 되었고, 제품 단위로 할인율을 구해주면 되었지만, 이제는 제품 하위의 옵션 단위로, 그리고 해당 옵션이 대용량 할인을 하고 있는지 일일이 계산하여, 옵션별로 할인율을 표기해주어야 하였습니다.(그만큼 기존보다 더 많은 로직을 타야 했고, 제품에 대한 할인 정보이다 보니 굉장히 많은 곳에서 해당 부분을 사용하고 있었습니다.)
해당 작업을 진행하며, DUE도 짧았지만 나름 정말 몰입하여(심지어 아침에 운동할 때도 이 생각만 하고), 구조를 짜고 코드를 구성하였습니다. (물론 ORM을 그동안도 잘 사용해왔기에, 성능은 신경 쓰지 않았습니다.)
그리고 그 결과는 아주 처참하게 나타났습니다.
테스팅을 할 때는 잘 작동하는 것에 만족하면서 테스팅을 진행하였고(현재는 QA시스템이 도입되었습니다.), 빠듯하게 실제 live가 되었을 때 곧 웹사이트가 느려진 것을 알게 되었습니다. 그리고 다급하게 New Relic(New Relic은 SaaS 기반의 APM(Application Performance Management) 서비스를 제공하는 회사)을 살폈습니다.
Newrelic의 web transaction time( 사용자가 웹 페이지를 클릭했을 때, 그 페이지에 대한 응답을 받는데까지의 시간)을 보았을 때 기존보다 적게는 2배에서 많게는 5배까지 느려진 지표를 보여주었다.
이때서야 저는 ORM이 얼마나 무서운지 깨닫게 되었습니다. 그동안 ORM을 아무 생각 없이 사용하였던 나 자신에 대한 깊은 후회와, 반성이 밀려들어왔습니다. 다급하게 전체 캐시를 적용해주어 성능적인 이슈는 막고 있었지만, 이번 프로모션 기간이 끝나기 전에 해당 부분을 개선해주어야 하는 2번째 미션이 주어졌습니다. (즉 일을 2번 하게 되었습니다.)
그렇게 반성을 하며 해당 부분에 대한 리팩토링을 시작하였습니다. 크게 저는 4가지의 방법을 통해 성능을 개선시켰습니다. 지금부터 그 이야기를 해보려고 합니다.
먼저 ORM에서 실제 발생시키는 쿼리를 보지 못하면 절대 개선을 할 수 없습니다. 따라서 ORM에 대해서 잘 모를 때는 무조건 실행 쿼리문을 볼 수 있는 창을 따로 띄어놓고 개발하시기를 추천드립니다.(네... 물론 저는 그렇지 않았습니다. ㅠㅠ)
별첨 : ORM에서 실제 발생시키는 쿼리문 보기 (Django + mysql)
# mysql client에서
show processlist; # 현재 mysql에서 실행 중인 process 보기
show variables like 'general_log%'; # 현재 general_logs의 상태를 볼 수 있습니다.
set GLOBAL general_log='ON'; #general_log를 켤 때
set GLOBAL general_log='OFF'; #general_log를 끌 때
set GLOBAL gnenral_logs = 'ON'으로 하고 나면 어디에 로그를 쌓는지 보이게 됩니다.
이제 docker(아마 docker 환경에서 개발할 것으로 생각됩니다.)에서 해당 경로로 가서 해당 파일의 log를 tail로 보게 되면 실행되는 쿼리들을 볼 수 있습니다.
첫 째 : 그 동안 크게 생각하고 있지 않던 select_related에 대해 다시 공부하여 적용해주었습니다. (1+N쿼리문제 해결)
관계형 데이터베이스의 장점은 Foreign Key를 묶을 수 있고 해당 Foreign key로 다른 테이블에도 접근할 수 있다는 것입니다. 하지만 이렇게 Foregin Key로 묶인 데이터를 ORM에서 가지고 올 때 주의해야 할 부분이 있습니다.
간단한 예시를 들어보겠습니다.
class Member(models.Model):
name = models.CharField(max_length=32)
age = models.SmallIntegerField()
detail = models.ForeignKey('MemberDetail', on_delete=SET_NULL)
class Meta:
db_table = "member"
class MemberDetail(models.Model):
address = models.CharField(max_length=32)
class Meta:
db_table = "member_detail"
가장 쉽게 볼 수 있는 DB 모델을 하나 만들었습니다. 고객의 중요한 정보를 Member 모델에 넣고, 추가 정보들을 MemberDetail로 따로 저장하도록 모델을 구성하였습니다.
나이가 30살 이상인 고객들의 address를 가지고 오고 싶으면 어떻게 해야 할까요?
def get_member_address_list():
result = []
member_list = Member.objects.filter(age__gte=30)
for member in member_list:
member_address = member.detail.address
result.append(member_address)
return result
간단한 함수를 하나 만들고 실행시켜보면, 굉장히 놀랄 수도 있습니다. (물론 위의 ORM에 의해 발생하는 로그들을 보고 있을 때입니다.) 왜냐하면 member.detail.address 이 부분에서 Foregin key에 묶인 MemberDetail 정보를 가지고 오기 위해 새로운 쿼리를 발생시키는데, 해당 for문을 돌 때마다 1번씩 발생시키기 때문입니다. (해당하는 고객이 1만 명이라면, 1만 번 발생하게 됩니다.)
숫자가 적을 때는 괜찮지만 숫자가 많아지게 되면 어마어마한 쿼리를 발생시키게 됩니다. (물론 컴퓨터는 굉장히 빠르기 때문에, 평소에는 잘 느끼지 못합니다.)
이것을 1+N 쿼리 문제라고 합니다.
이 부분을 개선해주기 위해서는 어떻게 해야 할까요? 바로 select_related를 써주어 처음 member_list를 query 해올 때, 함께 Foregin Key로 묶인 MemberDetail 정보를 함께 쿼리 해오는 것입니다. (filter를 적었다고 쿼리문이 발생하지 않습니다. 위에서는 for문을 돌아야 하는 시점에 해당 filter의 query가 실행되게 됩니다.)
해당 코드를 select_related를 통해 개선해보겠습니다.
def get_member_address_list():
result = []
member_list = Member.objects.select_related('detail').filter(age>30)
for member in member_list:
member_address = member.detail.address
result.append(member_address)
return result
ORM에서 filter를 해올 때 select_related를 통해 Foreign key 묶인 것을 한 번에 query를 해오면 해당 Foregin key가 필요할 때 굳이 또 query문이 생성되지 않아도 된다는 것입니다. 해당 함수를 실행시키면, 딱 1회 query 문이 실행되는 것을 볼 수 있습니다. Member에서 age가 30 초과인 사람들을 filter 해올 때 해당하는 사람들의 detail에 묶인 정보도 함께 query 해와 메모리에 저장해놓고 이후에 지속적으로 사용하게 됩니다. (select_related 이외에서 prefetch_related도 있으나 여기서는 다루지 않겠습니다.)
둘 째 : 매개변수를 넘겨줄 때 객체 형태로 넘겨주면 또 한 번의 쿼리를 줄일 수 있습니다.
보통 우리는 매개변수에 값을 전달해줄 때, 정수를 넘겨주는 경우가 많다. 예를 들면 member_id를 넘겨주는 것이다. 이것도 예시를 먼저 들어보면 좋을 것 같다.
이번에는 특정 멤버의 address를 가지고 오는 함수를 생성하였다.
class GetMemberInfo:
# 특정 나이보다 큰 멤버들의 주소 정보를 가지고 오는 함수
def get_member_address_list_from_age(age):
result = []
member_list = Member.objects.filter(age>30)
for member in member_list:
member_address = self.get_member_address(member.id)
result.append(member_address)
# 특정 멤버의 주소 정보를 가지고 오는 함수
@staticmethod
def get_member_address(member_id):
member = Member.objects.get(id=member_id)
member_address = member.detail.address
return member_address
위의 get_member_address 함수를 보면 member_id라는 argument를 받아서, 그 안에서 member_id를 통해 member 정보를 다시 가지고 오고, 그리고 묶인 Foreign Key를 통해 그 멤버의 address를 뽑아내고 있습니다.
이렇게 되면 get_member_address 함수를 실행할 때마다 member에 대한 쿼리 1번과 member의 Foreign key로 묶인 detail(Member_Detail)에 대한 쿼리 1번 총 2번의 쿼리가 실행되게 됩니다. 이것을 1번으로 줄이는 것은 위의 select_related를 사용해주면 1번으로 줄일 수 있습니다. (member = Member.objects.select_related('detail').get(id=member_id)
그럼 1번은 꼭 어쩔 수 없이 실행되어야 하는 걸까요?
이 쿼리 1번도 아예 줄여 버릴 수 있습니다. 그 방법은 바로 argument로 member_id를 넘겨주는 대신 객체 자체를 넘겨주는 방법입니다. 위의 함수를 좀 수정해보도록 하겠습니다.
class GetMemberInfo:
# 특정 나이보다 큰 멤버들의 주소 정보를 가지고 오는 함수
def get_member_address_list_from_age(age):
result = []
member_list = Member.objects.select_related('detail').filter(age>30)
for member in member_list:
member_address = self.get_member_address(member)
result.append(member_address)
# 특정 멤버의 주소 정보를 가지고 오는 함수
@staticmethod
def get_member_address(member):
member_address = member.detail.address
return member_address
member_id를 argument로 넘겨주어서 다시 한번 get_member_address 함수에서 쿼리를 실행시키는 게 아니라, 애초에 argument로 쿼리 되어 뽑힌 객체 자체를 넘겨주는 것입니다. 그리고 추가적으로 member_list를 filter 해오는 단계에서 select_related를 통해 detail 정보를 함께 쿼리 해오게 되면, 객체 정보를 넘겨줄 때 Foreign key로 묶인 detail에 대한 정보도 함께 넘겨지게 되고, 놀랍게도 get_member_address 함수에서는 아예 쿼리가 발생하지 않게 됩니다. 그럼 쿼리는 member_list에 대한 쿼리 딱 1번으로 마무리할 수 있습니다.
ORM을 사용하여 아무 생각 없이 코드를 작성하다 보면 쿼리의 효율이 굉장히 나빠질 수 있습니다. (또 한 번 반성합니다..)
셋 째 : 필요한 부분에는 캐시를 적용해 줍니다.
현재 다노에서는 Redis를 캐시 DB로 활용하고 있습니다. 필요한 적재적소의 부분에 캐시를 적용해주면 성능을 굉장히 높일 수 있습니다. 위의 과정들을 통해 성능을 개선하였다면, 더 비약적으로 성능 개선을 위해서는 캐시 설정하는 것만큼 극단적인 효과를 보기가 쉽지 않습니다. 물론 캐시 역시 만들어서 특정 데이터베이스에 저장해 놓는 것이기 때문에 데이터에 접근하기 위한 시간이 소모되긴 하지만 적절한 위치에 캐시를 적용해 놓기만 하면 훨씬 개선된 성능을 볼 수 있습니다.
이것도 예를 한번 들어보겠습니다. 제품에 대한 정보를 가지고 올 때, 단지 제품에 대한 정보만 가지고 오면 될까요? 사실 제품에는 묶여 있는 것들이 많습니다. 제품에 대한 정보뿐만 아니라 제품 옵션들 각각에 대한 정보, 그리고 배송에 관한 정보, 재고에 관한 정보, Q&A에 대한 정보, 후기에 대한 정보 등등 굉장히 다양한 데이터 테이블들이 묶여서 우리가 보는 제품에 대한 페이지를 구성합니다.
그럼 필연적으로 해당 테이블들을 모두 쿼리 해올 수밖에 없습니다. 하지만 사실 재고에 대한 정보를 제외하고는 제품에 대한 정보, 그리고 옵션에 대한 정보, 배송에 대한 정보 등은 한번 설정을 하고 나면 잘 바뀌지 않습니다.(물론 리뉴얼 등으로 바뀔 수도 있습니다.)
이렇게 바뀌지 않는 정보들에 대한 것은 모두 모아 캐시로 설정해놓고, 재고에 대한 정보만을 쿼리 해와서 함께 붙여주기만 하면 성능이 훨씬 개선될 수 있습니다.
여기서 캐시 설정에 주의할 점들이 있습니다. 먼저 해당 캐시의 시간을 적절하게 세팅해주는 것(너무 긴 시간을 설정해주면, 정보가 변경되었지만 반영이 안 되게 되고, 너무 짧은 시간 설정해주면 의미가 없게 됩니다.) 그리고 적절한 위치에 캐시를 설정해주고 (잘 변경되지 않는 데이터들), 또 특정한 순간(상품에 대한 정보가 바뀔 때 등)에 캐시를 깨 주는 로직을 추가해주어야 합니다.
넷 째 : 원시 SQL문 적용해주기
django에서도 원시 SQL?을 사용하여 쿼리를 요청할 수 있습니다. 방법은 크게 2가지가 있습니다. 1. Manger.raw ()를 사용하여 원시 쿼리를 수행하고 모델 인스턴스를 반환하는 것 2. 모델 레이어를 완전히 파괴하고 사용자 정의 SQL을 직접 실행시키는 것
2가지의 방법을 통해 직접 SQL 문을 작성하여 실행시킬 수 있습니다. 현재 다노에서도 많은 데이터를 쿼리 해와야 할 때는 직접 SQL을 작성하여 DB에 connection을 뚫어서 직접 SQL을 요청하고 있습니다.
다만 해당 부분은 ORM을 직접적으로 사용하는 방법이 아니므로, 이번 글에서는 자세히 다루지 않겠습니다.
이번에 잘못된 부분들에 대해서 리팩토링을 진행하면서는 크게 위의 1,2,3번의 요소들을 반영해 주었습니다. 그리고 실제 개선된 부분들에 대해서 이야기를 해보고자 합니다.
과연 그 결과는 ...!! 두둥
기존 결과 값과 비슷하거나 더 개선된 수치들도 존재하였다. (로직이 많이 추가된 것에 비해서 많은 부분 개선되었다고 판단하였습니다!) 다시 리팩토링을 하여서 비록 작업을 2번 하게 되었지만, 이번 기회를 통해 정말 정말 정말... ORM의 무서움을 직접 몸으로 겪게 되었고, 현재는 위의 규칙들을 잘 생각하며 코딩하고 있습니다..
ORM은 너무 편하고 좋지만, ORM의 실제 쿼리들이 어떻게 생성되는지 초창기에는 창을 함께 띄어놓고 보시면서 개발을 하시면 저와 같은 시행착오를 겪지 않으실 거라 생각하고, 개발하시는데도 많은 도움이 되실 거라 생각합니다.