프로그레시브 웹 앱은 이미 수년전부터 큰 화두였습니다.

오늘은 PWA를 적용하며 느끼고 고민했던 것들 + 약간의 팁(...이라고 말하기 민망한 주의사항)을 공유하고자 합니다.

 

* 글에 앞서 해당 내용은 주관적인 생각이 20,000% 포함되어 있으며 이 글의 내용이 개발 가이드는 절대 아님을 밝힙니다. 수정 보완해야 할 내용이 있다면 댓글에 공유 부탁드립니다. (_ _)

 

* 이어서 간단한 소개를 할 것이지만, 혹시나 PWA를 처음 들어보신다면, 아래의 링크들을 참고 해보세요. 많은 도움이 되실 것 같습니다.

 

Progressive Web Apps

 

- 웹 펀더멘털

 

여러분의 첫 Progressive Web App  |  Web Fundamentals  |  Google Developers

Pete is a Developer Advocate 소개 웹 앱, 프로그레시브 웹 앱은 무엇입니까? 프로그레시브 웹 앱은 웹을 통해 직접 구축 및 제공되는 데스크톱 및 모바일에서 설치 가능하고 앱과 유사한 환경을 제공합니다. 빠르고 신뢰할 수있는 웹 앱입니다. 가장 중요한 것은 모든 브라우저에서 작동하는 웹 앱입니다. 오늘 웹 앱을 제작한다면, 이미 프로 그레시브 웹 앱을 구축하는 길에 서 있습니다. 신속하고 신뢰할 수있는 모든 웹 경험은 빠르며, 특히

developers.google.com

 

- 조은님의 PWA 소개 포스팅

 

PWA를 구성하는 기술들

지난 글 만으로 코드랩을 진행하고자 하였으나 막상 준비를 하다보니 사람들이 PWA는 어떤 기술들로 구성되어있는 지 궁금해할까 하여 이 글을 작성한다. 우선 명확하게 말하자면 그 어떤 기술도 PWA를 위해 반드시 사용할 필요는 없다.

medium.com

- MDN 프로그레시브 웹 앱 소개

 

프로그레시브 웹 앱 소개

이 문서는 프로그레시브 웹 앱(PWA)의 소개입니다. PWA가 무엇이고 일반 웹 앱에 어떤 이점을 가져다주는지 설명합니다.

developer.mozilla.org

 

PWA는??

처음 PWA를 접하는 분들은 이 용어에 많은 혼란을 느낍니다. 이것은 어떠한 언어 혹은 프레임워크일까요? 정확히 말하자면 PWA는 일반적인 웹 앱을 네이티브 앱 수준에 가깝게 점진적으로 발전시키려는 목표를 가지고 2015년 '알렉스 러셀'이 고안한 '개념'입니다. 그렇기 때문에 PWA는 그 개념에 관한 이해가 상당히 중요하며 어떠한 플러그인 혹은 기술로 PWA가 certificated(인증)되는 것은 아닙니다.

 

그렇다면 PWA의 개념을 만족시켜주기 위해서는 무엇이 필요할까요?? (여기서는 4가지의 핵심 키워드가 아닌 3가지 키워드로만 설명합니다.)

 

Progressive Web Apps are user experiences that have the reach of the web, and are:

  • Reliable - Load instantly and never show the downasaur, even in uncertain network conditions.
  • Fast - Respond quickly to user interactions with silky smooth animations and no janky scrolling.
  • Engaging - Feel like a natural app on the device, with an immersive user experience.

This new level of quality allows Progressive Web Apps to earn a place on the user's home screen.

 

위의 글은 구글 개발자 사이트의 Progressive Web Apps 메뉴의 소개입니다. 바로 이 세가지(혹은 네가지)가 PWA의 핵심 개념이며 이러한 노력과 기술들이 들어있어야 PWA로 인정받을 수 있습니다.

 

신뢰성

'불안정한 네트워크 조건에서도 페이지 로딩이 즉각적이고 공룡(downasaur: 크롬 에러페이지 주인공입니다.)을 보여서는 안된다'

어떠한 형태로든 네트워크 의존일 수 밖에없는 웹 앱을 네트워크 상황에 맞게 사용자가 사용할 수 있도록 만들어야 함을 뜻합니다.

 

신속성

'사용자 상호작용에 부드러운 애니메이션 효과와 버벅임 없는 스크롤로 즉각적으로 반응해야한다.'

사용자가 스크롤이나 터치, 클릭 등의 상호작용을 하였을 경우 그에 대한 응답이 빠르게 처리되어야 함을 의미합니다.

 

참여성

'몰입형 사용자 경험(immersive user experience)으로 디바이스의 원래 앱처럼 느끼게 한다.'

참여성(Engaging)이라는 말이 어렵게 느껴질지 모르지만 현재의 웹 앱이 단순히 웹의 어느 곳에서 서비스되는 것처럼 느끼게 하는 것 보다 마치 실제 데스크탑의 프로그램처럼, 혹은 스마트폰에 직접 설치한 앱처럼 느껴지게 해야함을 표현합니다.

 

위의 세가지를 지향하고 이를 적극적으로 반영한 웹 앱이 바로 PWA입니다.

 

Vue.js + PWA + Prerender

이번 프로젝트 역시도 웹뷰에서 서비스 되기 때문에, 조금더 네이티브 앱에 가깝게 구현하기 위한 노력들을 하게 되었고 자연스럽게 PWA를 도입하기로 결정하였습니다. 이와 함께 SEO, 공유기능 등의 이유로 Prerender역시 함께 도입했습니다.

 

Vue-cli를 사용하여 PWA 플러그인을 추가하였고  Prerender SPA Plugin을 사용하여 해당 프로젝트를 진행하였습니다. PWA를 프로젝트에 추가하면 일반적인 Vue.js  프로젝트에 추가적으로 manifest.jsonregisterServiceWorker.js 파일이 설치됩니다.

 

manifest.json은 일반적인 웹 앱을 확장하여 설치가능한 네이티브 앱처럼 만들게 도와줍니다. 흔히 이 파일에는 앱의 이름과 디스플레이 모드, 방향(orientation), 스플래시 스크린의 설정 값들이 들어가게됩니다. 물론 브라우저에 따라서 제대로 동작되지 않는 것들이 많습니다. 이와 함께 설치된 registerServiceWorker.js는 Service worker를 등록하게 도와주는 파일입니다.

 

서비스 워커를 모두 소개하기에는 무리가 있지만 간단히 설명하자면 브라우저의 백그라운드에서 작동하는 워커오프라인에서 웹 페이지를 보여줄 수 있는 방법을 고민하다 나오게 되었습니다. 기본적인 registerServiceWorker.js 외에도 서비스워커를 등록할 수 있는 방법은 여럿이며 서비스워커는 '캐싱전략'과 아주 밀접한 관계를 가지고 있으므로 이는 직접 찾아보시면서 학습하셔야 할 필요가 있습니다.

 

추가적으로 전달드리고 싶은 점은 서비스워커를 도입하면 디바이스의 푸시 알람을 보낼 수 있다는 부분과 업데이트가 일반적인 웹 앱과는 다르다는 것입니다. (iOS는 지원하지 않으며 웹뷰에서는, 저희 내부적인 보안상 이유 때문인지는 몰라도, 작동이 안됩니다.) 무작정 PWA를 도입했다가 캐싱과 업데이트 이슈 때문에 빠르게 삭제해 본 경험이 있어서 그런지(이번이 두번째 PWA 도입) 우선은 어떻게 사용자에게 앱에 새로운 변화를 알릴지 (iOS 포함) 그리고 어떻게 해야 엔드유저의 이탈을 막을 수 있을지 여러가지를 다양한 부분에서 신경써야 된다고 말씀드리고 싶습니다.

 

위의 두가지에 대한 전략과 셋팅이 어느 정도 진행되었다면 이제 PWA 본연의 철학을 위해 내부적으로 진행해야 할 리스트들이 필요할 것입니다. 저 같은 경우에는 혼자서 모든 마크업과 프론트엔드 개발을 진행하기에 바이크쉐딩이 없었지만 어떠한 전략도 완벽한 것은 없으니 가장 적게 잃는 것 (성능, 심미적 아름다움, 사용성 등)을 선택하시길 바랍니다.

 

저의 최우선 과제는 성능과 앱과 같은 UI/UX였습니다.

 

처음 디자인을 받았을 때에는 위와 같은 UI가 아니였습니다. 조금 더 모바일 웹 같은 모습이였고 단순한 스크롤 이벤트로 각각의 리스트 아이템들을 살펴볼 수 있었기 때문에 제가 원하던 형태의 앱과 같은 UI/UX는 아니었습니다. 이를 해소하기 위하여 여러 앱을 벤치마킹하였고 디자인팀과 회의를 통해 현재의 Card 형태의 디자인과 스와이프 제스쳐를 사용한 디자인이 완성되었습니다.

 

디자인이 완료된 후에는 최우선 과제였던 성능적인 부분이 걱정되었습니다. 기존의 스와이퍼 라이브러리들은 제가 원하던 효과를 지원하지 않았고 이를 해결하기 위해서 외부 라이브러리를 수정하기에는 예측과 방어가 어려운 side-effect가 우려되었기 때문에 저희만의 UI/UX를 위한 기능을 직접 제작하기로 결정하였습니다.

 

마우스 커서를 이용한 시연이기에 샘플로 보여지는 gif에서는 그 느낌이 잘 살진 않지만 제법 앱과 같은 부드러운 터치 제스쳐 느낌을 줄 수 있었고 필요한 기능만 간단하게, 예를 들어 물리적인 느낌을 주기 위해서 자바스크립트로 ease-in 등을 구현하지 않고(사실 삼각함수 바보인 문과 출신인지라...) 최대한 CSS를 활용하여, 작은 사이즈로 원하던 효과를 얻을 수 있었습니다.

 

이렇게 직접 구현이 필요한 부분과 외부 라이브러리 사용을 검토하는 것은 개발 및 운영상 큰 이점이 있습니다. 커뮤니티가 활발한 라이브러리를 사용한다면 1. 에러가 발생할 경우 집단 지성을 통해 이슈 대응이 수월하고 2. 그러므로 비교적 안정적인 서비스 운영을 할 수 있습니다. 직접 제작할 경우에도 1, 2의 장점을 모두 가질 수 있겠지만 경험상으로는 잘 만들어진 라이브러리를 적절하게 활용하는 것이 개발 생산성을 높이는데 도움이 많이 되었습니다.

 

직접 구현이 필요하다는 판단은 개발자들 마다 다른 관점의 다양한 이유가 존재하겠지만 저에게는 크게 1. 구현하고자 하는 효과 / 연산 / 퍼포먼스가 기대치 이하일 때, 2. 근사치에 가까운 라이브러리가 존재하지만 수정하는 것이 위험하다고 판단될 때 입니다. 물론 선택과 도입은 개발자 본인의 몫입니다.

 

 

프리렌더링은 처음 시도해 보는 것이기도 했고 레퍼런스가 적었기 때문에 개발 초기부터 꾸준히 이터레이션 하면서 배포 과정 중에 확인 및 수정하며 작업을 진행했습니다. 프리렌더링은 미리 정적인 파일을 만들어 deploy하는 것으로 routing 룰과 많은 관계가 있습니다. 예를 들어 /some_dir을 사용하는 path에서 보여지는 페이지를 프리렌더링 하고 싶다면 해당 파일을 번들링 시에 만들어 내도록 vue.config.js에 routes를 등록하고 해당 routes 룰을 직접 수정 및 보완해야 합니다. 여기서 페이지에 어떠한 사용자 상호작용 혹은 일정 시간이 필요하다면 renderer 프로퍼티에 별도의 옵션들을 설정하여 스냅샷을 찍을 때 원본에 가깝도록 만들어줘야 합니다. renderAfterTime 같이 물리적인 시간을 설정하여 기다린 후 파일을 생성하거나 혹은 어떠한 이벤트가 발생한 후 캡쳐하도록 하는 방법들이 Github에 자세히 설명되어있으며 이를 적극 활용하시기 바랍니다.

 

이렇게 개발 막바지에 들어서고 웹뷰가 아닌 '홈 스크린에 추가'된 웹 앱으로 스플래시 스크린, 아이콘 사이즈 등을 테스트 하다 중요한 사실을 하나 확인할 수 있었습니다. 네이티브 앱과 달리 사용자의 방향(orientation) 전환 이슈가 발생하였고 이를 방지하기 위한 추가적인 조치가 필요하다는 것이었습니다.

 

UI/UX적으로도 해당 orientation을 대응할 수 없었기에 방향전환 이벤트가 발생하면 정상적인 사용을 유도하기 위한 기능을 넣게 되었습니다. 물론 웹에서 접근하는 경우가 극히 적지만 이를 방어하고 불필요한 에러 혹은 그와 같은 경험을 제거하기 위하여 꼭 필요한 조치였습니다.

 

아직 오프라인에 대한 대응을 어떻게 해야할지, 그리고 PC 대응 등의 여러 태스크들이 존재하지만 PWA가 지향하는 바와 근접하게 개발하기 위하여 여러가지를 다양하게 고민해 본 프로젝트였습니다.

 

도입후

확실히, 글에서 언급한 것 외에도, 여러가지 고민을 한 결과 만족할만한 결과물을 얻을 수 있었습니다. 다만 모든 선택이 다 장점만 얻을 수 없듯이 프리렌더링에 대한 아쉬움은 조금 큰 편입니다. 모든 데이터를 자동적으로 반영하여 물리적인 document를 생성하는 것은 불가능하며 이를 통해 검색엔진에서 비교적 정확, 신속한 정보를 얻어내기에는 무리가 있을 것 같습니다. 만약 포스팅한 내용과 비슷하게 서비스를 개발하시려고 한다면 잘 참고하시기 바랍니다.

Test-Driven Development
"테스트 주도 개발(Test-driven development TDD)은 매우 짧은 개발 사이클을 반복하는 소프트웨어 개발 프로세스 중 하나이다. 개발자는 먼저 요구사항을 검증하는 자동화된 테스트 케이스를 작성한다. 그런 후에, 그 테스트 케이스를 통과하기 위한 최소한의 코드를 생성한다. 마지막으로 작성한 코드를 표준에 맞도록 리팩토링한다. 이 기법을 개발했거나 '재발견' 한 것으로 인정되는 Kent Beck은 2003년에 TDD가 단순한 설계를 장려하고 자신감을 불어넣어준다고 말하였다." - 위키피디아

 

테스트 주도 개발 (=TDD)은 테스트 코드를 먼저 작성하고, 작성된 테스트 케이스들을 이용하여 반복적으로 테스트하며 개발하는 것을 말합니다. 처음 TDD를 도입하려고 했을 때에는 시간이 부족하다는 혼자만의 판단으로 '나중에, 나중에...'를 계속 되새겼지만 실제 TDD를 도입해본 결과 분명히 일정한 양의 반복적인 재발비용이 발생하는 것은 사실이나 업무의 양이 두배, 혹은 수배나 되는 많은 시간이 (물론 익숙해지기 까지의 일련의 과정과 많은 삽질이 필요합니다 ㅡ_ㅡ;;) 가중되는 것은 아니었습니다.

 

 

 

TDD Cycle Red Green Refactor

처음 TDD를 진행하며 가장 막연했던 것은 Sanity Test가 끝난 후 '무엇을 테스트 하며 어떻게 구성하냐'였습니다. 제가 레퍼런스로 삼았던 Testing Vue.js Applications.js (Manning | Edd Yerburgh ) 책에서는 여기에 관하여 경험자로서 그리고 Vue.js 코어팀의 Test 관련 담당자로서 자신의 철학을 잘 설명해주고 있습니다. (아직 번역서는 없습니다.)

 

출처: Testing Vue.js Applications.js

 

여기서 저자는 프론트엔드 개발 시 이상적인 테스트 스위트의 구조를, 다음 세 단계의 Unit tests, Snapshot tests, e2e tests의 피라미드 형태로 보여줍니다. 전체의 테스트 스위트 중 해당 테스트가 차지하는 비율은 60% / 30% / 10%입니다.

 

이 세가지의 테스트는 동일하게 어플리케이션을 테스트하는 것이지만 관심사나 테스트 방식, 동작이 전혀 다릅니다.

 

유닛 테스트컴포넌트 컨트랙트로 동작해야 하는 기능들을 테스트하며 테스트 스위트 중 가장 많은 비율을 차지합니다. 이 유닛 테스트 과정에서 서버에 요청을 보내고 응답을 받는 일련의 과정은 테스트에 포함시키지 않았습니다. 가장 큰 이유 중 하나는 테스트 실행시간과 프론트엔드 개발의 관심사를 벗어난 부분이기 때문입니다. 제공된 API는 검증의 책임이 백엔드 서비스 / 로직에 있습니다. 그와 함께 현재의 네트워크 연결상태와 응답까지 걸리는 시간테스트 때마다 늘 증가하는 것은 생산성을 매우 많이 떨어뜨리게 만드는 행위이므로 이는 테스트 대상에서 제외합니다.

 

스냅샷 테스트는 말 그대로 컴포넌트 / 레이아웃의 현재의 상태를 저장(스냅샷)하여 다음번 테스트 실행시 원래의 스냅샷과 비교하여 오류 상태를 알려줍니다. 이전의 테스트가 존재하지 않다면 첫 실행시 스냅샷은 단순 저장만 합니다. 그러한 이유로 컴포넌트, 레이아웃의 모든 개발과정이 끝난 후 mannual testing을 거친 후 테스트 코드를 작성 해야합니다. 그렇지 않다면 계속해서 업데이트 되는 항목들이 생겨 올바른 테스트가 되질 않고 불필요한 시간을 소비하게 됩니다.

 

마지막 e2e 테스트는 모든 유닛 테스트와 스냅샷 테스트를 마친 후 어플리케이션 프로세스의 여정을 가상으로 테스트합니다. 말이 조금 생소하게 다가올지 모르나 엔드유저가 어플리케이션에 체류하면서 어떻게 사용할것인지 예상되는 방법들을 정리하여 브라우저를 띄운 상태에서(이는 자동 실행됩니다.) 해당 절차대로 어플리케이션을 동작시키는 테스트입니다.

 

간단히 TDD가 무엇이며 어떻게 구성되었는지 알아보았습니다. 이렇게 열거해놓고 보니 꽤나 복잡하게 느껴지실 겁니다. 하지만 TDD에 있어서 가장 중요한 것은 따로 있습니다.

 

일단 제멋대로 구현하고 있는 자신의 손과 sanity test, 즉!!! 컴포넌트를 정상적으로 테스트 프레임워크에서 동작시키도록 만드는 것이 가장 중요합니다. 앞서 설명한대로 TDD는 우선 테스트 코드를 작성하는 것으로 시작합니다. 많은 TDD 관련 서적과 정보를 찾아보면 아래와 같은 문구를 발견하실 수 있습니다.

 

'반드시 실패하는 테스트 코드를 먼저 작성한다.'

 

그렇다고 성공하는 테스트 코드를 실패하게끔 작성하라는 뜻은 아닙니다. 위의 문구는 구현부가 없기 때문에 반드시 실패하는 테스트 코드가 처음 작성될 것이라는 의미를 강조한다고 생각하시면 됩니다. TDD의 시작은 테스트 코드부터 작성을 하고 그것을 성공하게 만든 후 리팩토링을 하는 과정을 반복적으로 수행하는 것입니다. 위의 과정을 통해서 해당 테스트 코드로 다듬어진 구현부가 완성되는 것입니다.

 

그런데 테스트 주도 개발에 익숙하지 않은 개발자라면 우선 손가락이 먼저 움직이게 됩니다. 저 역시도 구현부를 만들다 도중에 테스트 코드를 작성하기도 하고 구현이 끝난 후 테스트 코드를 작성해가며 애를 먹었습니다. 우선은 많이 익숙해지기 전까지는 테스트 코드를 먼저 작성하는 습관을 들이는 것이 중요합니다.

 

그러한 습관이 들기 전에 마주할 커다란 벽은 해당 컴포넌트를 테스트할 환경을 구성하는 일입니다. 컴포넌트를 jest라는 프레임워크에 mount 하고 외부 라이브러리를 사용한다면 그에 대한 처리를, 해당 컴포넌트가 다른 컴포넌트의 하위 컴포넌트로 마운트 시점부터 propsData를 가지고 있어야 한다면 해당 데이터를 세팅해주어야 정상적으로 컴포넌트의 테스트 코드를 작성할 수 있습니다. 이런 일련의 과정이 처음에는 굉장히 무의미해 보이고 소비하는 시간 대비 생산성이 떨어질 때왜 이런 소모적인 일을 해야하는지, 이걸 반복적으로 해야되나하는 의구심과 불만이 커지게됩니다. 저 역시도 마찬가지였지만 지금 두개의 프로젝트에서 TDD를 도입하여 개발을 진행해본 결과,

 

'그것은 가치있다!'

 

로 노선을 바꾸게 되었습니다. 우선은 이렇게 함으로서 외부 라이브러리 동작방식에 대하여 생각하고 공부하는 시간이 많아졌습니다. 컴포넌트에 필요한 것을 생각하는 시간이 늘었고 동작방식이 이상하거나 개선해야 할 사항을 다시 한번 점검하게 되는 계기를 마련해 주었습니다.

 

테스트 주도 개발을 하며 늘어난 것 중 하나는 예외처리였습니다. 예상밖의 응답값 혹은 동작이 발생하였을 때 예외처리를 어떻게 할 것인가, 또 사용성 유지를 위하여 에러를 발생시키는 것이 나은지 아니면 프로세서의 처음으로 돌려보내는 것이 맞을지, 아니면 알림으로 엔드 유저에게 해당 부분의 처리를 가이드해줄지, 이러한 예외처리에 대한 부분이 코드 곳곳에 늘어나게 되었습니다.

 

한가지 더 늘어난 것 중 하나는 항상 유닛 테스트를 하기 때문에 회귀에러를 많이 줄일 수 있다는 것입니다. 물론 회귀에러가 줄어든 것이지 없어지지는 않습니다.

 

마지막으로 클래스 / 함수 / 메서드 등의 구현부가 조금 더 SOLID하게 변하게 되었습니다. 각각의 테스트를 진행하며 각각의 디스크립션을 작성하다 보면 해당 기능의 역할과 책임이 분명해집니다. 물론 개발을 하기 전에 기획서 혹은 요구사항 명세를 보겠지만 해당의 지시 혹은 문서는 말 그대로 서비스적인 관점에서 작성됩니다. (절대적이지 않으며 단순히 저의 경험에서 나온 결론입니다.) 엔드 유저 관점에서는 관심이 없는 부분이지만 개발할 때에는 꼭 지켜져야 하는 부분, 예를 들면 해당 데이터의 응답 혹은 연산이 비용이 많이 드는 것이라면 메모이제이션으로 해당 값을 캐싱한다는 계획 같은 것은 개발자가 구현하며 추가해야 하는 기능들입니다. 물론 경험 많은 개발자들은 기획 리뷰를 하며 이러한 부분들을 바로 캐치할 수 있겠지만 테스트 코드를 작성하며 반복적인 리팩토링 작업을 하다보면 해당 기능을 구현하는 부분의 문제점과 반드시 극복해야하는 개발적인 이슈들이 눈에 잘 띄게 됩니다.

 

출처: pinterest

그렇다면 앞으로 모든 프로젝트는 TDD를 실행한다?!

 

그것은 프로젝트의 성격과 개발자 및 조직의 선택사항입니다. TDD가 훌륭한 하나의 개발 방법론이지만 이는 각자의 상황과 환경, 시간적인 부분이 충분히 고려되어야 할 것입니다. 위에서도 말씀 드렸지만 개발 시간은 일정부분 증가할 수 밖에 없습니다. 모델 인터페이스가 변경되거나 컴포넌트의 중요한 로직이 변경된다면 무조건 테스트 코드의 수정이 필요합니다.

 

한가지의 추가 사항 혹은 수정사항이 생길 때 최악의 경우, 그와 관련된 거의 대부분의 테스트를 손봐야 하는 경우도 생깁니다. 그렇기 때문에 TDD를 도입할 때에는 여러가지 상황이 고려되어야 할 것입니다. 아직 무엇도 확정된 것이 없고 주요 기능만 러프하게 주어진 상황에서 프로토타이핑을 할 경우 TDD를 도입하는 것이 맞는가를 생각해 본다면, 물론 답은 정해지지 않았지만, 저는 그렇지 않다라고 얘기할 것 같습니다.

 

어찌됐건 TDD는 좋은 개발 방법이자 코드베이스를 조금 더 깔끔하고 우아하게 관리하는 습관인 것 같습니다.

 

 

 

 

[자료 구조 활용] 최근 검색어 기능 구현 예제

 

* 해당 예제는 복기의 일부 입니다. 소개되는 소스를 그대로 적용하면 여러분의 프로젝트에 많은 허들을 만들 수 있습니다.

 

이전 포스팅에서 자료구조 중 스택에 대해 알아봤습니다. 하지만 스택이나 큐 외에도 실제 프로젝트에서는 해당 스펙(기획안)에 따라서 그에 맞는 자료구조와 기능 들이 필요합니다.

 

만약 최근 검색어를 담아두는 기능을 구현한다고 가정해 봅시다. 이의 구현을 위해서는 다음과 같은 기능이 필요할 것입니다.

 

1. 사용자가 어플리케이션을 처음 사용하지 않고 검색한 내용이 있다면 그 내용을 바탕으로 초기화가 이루어져야 한다.

2. 최근 검색어는 모든 내용을 저장하지 않고 반드시 주어진 길이에 맞는 갯수만큼의 데이터만을 가지고 있는다.

3. 이미 검색된 내용을 다시 한번 선택 / 저장한다면 그 내용이 가장 최신으로 이동되어야 한다.

4. 해당 데이터를 이용하기 위하여 배열 형태로 자료를 받아야 한다.

 

이 외에도 여러가지 기능들이 필요하겠지만(ex. 해당 검색어의 만료시점을 파악하여 삭제 등) 가장 기본적인 기능들을 열거하면 위의 네가지 정도의 핵심 기능이 필요할 것입니다.

 

위의 내용을 종합해보면 데이터는 집합이어야 하며 가장 최신의 데이터데이터의 가장 처음 인덱스에 위치해야만 하며 중복된 기존 검색어는 삭제되며 가장 최신의 데이터가 되어야 합니다. 더불어 데이터의 최대 크기를 초과하면 가장 마지막 데이터(가장 먼저 생성된 데이터)는 삭제되어야 합니다.

 

이를 구현하기 위하여 앞으 예제를 조금 다듬어 클래스를 제작하겠습니다.

const keywords = new WeakMap();
const max = new WeakMap();

class SearchedKeywords {
  constructor (cached, len) {
    if (len === undefined || len === 0) {
      throw new Error('최근 검색어의 최대값은 필수 매개변수 입니다. 검색어 저장 최대값을 설정해주세요.');
    }
    keywords.set(this, (arguments[0] === undefined || arguments[0] === null) ? [] : cached);
    max.set(this, len);
  }
}

 

오... 아름다운 템플릿... 새로운 기능을 영접합니다. 발행해 놓으니 스타일이 달라지네요 ㅡ_ㅡ 아시는 분은 댓글 좀...

 

먼저 WeakMap을 이용하여 해당 클래스의 프라이빗 멤버를 설정한 후, SearchedKeywords 클래스의 내부에서 멤버변수로 사용합니다. 이 멤버들은 private합니다.

 

그러면 이번 포스팅에서 제법 귀찮은 기능을 구현하겠습니다. 바로 unshift입니다. 자료를 새롭게 저장해야하는데 문제는, 중복 데이터가 허용이 안되는 '집합'이며 기존의 중복된 원소가 있다면 새롭게 저장되어야 한다는 것입니다.

 

unshift (element) {
  const arr = keywords.get(this);
  const num = max.get(this);
  const duplicatedIdx = arr.findIndex(el => {
    return el === element;
  })
  if (duplicatedIdx > -1) {
    const tmp = arr.slice();
    this.clear();
    tmp.forEach(el => {
      if (el !== element) {
        arr.push(el)
      }
    });
  }
  arr.unshift(element)
  if (arr.length > num) {
    arr.pop();
  }
}

 

이 메서드에서는 데이터의 최대값이 필요하며 중복된 값을 체크하여 해당 원소를 제거한 후 가장 처음에 삽입해야 합니다. ㅡㅡ;; 뭔가 말만으로는 복잡하지만 위의 구현부를 확인하시면 이해하시기에 무리는 없을겁니다.

 

해당 인스턴스의 배열과 최댓값을 변수에 담고 중복값이 있는지 체크합니다. 만약 중복값이 존재한다면 임시 저장소(=tmp)에 해당 데이터를 담고 중복값이 아닌 원소들만 원래의 배열(=arr)에 담습니다. 그렇지 않다면 (중복값이 없다면) 단순히 배열에 새로운 데이터를 담고 최대값을 체크하여 필요시 삭제해줍니다.

 

나머지는 이전 포스팅의 예제와 비슷합니다.

 

다음은 이번 예제의 스펙에 맞게 구현한 클래스의 전체 코드입니다.

 

const keywords = new WeakMap();
const max = new WeakMap();

class SearchedKeywords {
  constructor (cached, len) {
    if (len === undefined || len === 0) {
      throw new Error('최근 검색어의 최대값은 필수 매개변수 입니다. 검색어 저장 최대값을 설정해주세요.');
    }
    keywords.set(this, (arguments[0] === undefined || arguments[0] === null) ? [] : cached);
    max.set(this, len);
  }
  // toString, toArray
  unshift (element) {
    const arr = keywords.get(this);
    const num = max.get(this);
    const duplicatedIdx = arr.findIndex(el => {
      return el === element;
    })
    if (duplicatedIdx > -1) {
      const tmp = arr.slice();
      this.clear();
      tmp.forEach(el => {
        if (el !== element) {
          arr.push(el)
        }
      });
    }
    arr.unshift(element)
    if (arr.length > num) {
      arr.pop();
    }
  }

  pop () {
    const arr = keywords.get(this);
    const ret = arr.pop();
    return ret;
  }

  peek () {
    if (this.isEmpty()) return undefined;
    const arr = keywords.get(this);
    return arr[arr.length - 1];
  }

  size () {
    const arr = keywords.get(this);
    return arr.length;
  }

  isEmpty () {
    const arr = keywords.get(this);
    return arr.length === 0;
  }

  clear () {
    while (!(this.isEmpty())) {
      this.pop();
    }
  }

  toString () {
    const arr = keywords.get(this);
    return arr.toString();
  }

  toArray () {
    const arr = keywords.get(this);
    return arr;
  }
}

 

요즘은 프레임워크를 사용하여 해당 컴포넌트에서 모든것을 처리하는 일이 많습니다. 하지만 관심사가 동일한, 여러 부수적인 기능이 필요한 부분이 있다면 하나의 클래스로 관리하는 것이 더 나을 것입니다.

+ Recent posts