어색하지 않게 한국어로 소개하고 싶었지만 가능하지 않을 것 같은(위키피디아에서 초본문이라고 소개하는) 하이퍼텍스트.

하이퍼텍스트는 문서 안의 하이퍼링크(참조)를 통해 다른 문서로 즉시 접근할 수 있는 문자를 말한다. html의 ht가 바로 하이퍼텍스트이며 앞서 설명한 내용 보다 html 단어를 봤을 때, 그 의미 파악이 더 쉬웠을 것이다.

1960년대에 테드 넬슨이 하이퍼텍스트라는 개념을 처음 고안했다는 글들이 많이 보이지만 이 개념은 우리가 광복을 맞이하던 해, 1945년, 베네바 부시가 기고한 ’As We Think’에서 메멕스(=MEMory EXtender) 시스템 제안과 함께 시작 되었으며 그것을 테드 넬슨이 하이퍼텍스트라는 단어를 사용하며 정리했다고 보는게 맞을 것 같다. 당연하게도 테드 넬슨은 부시에게서 영감을 얻었다.

테드 넬슨은 미래에는 전 세계가 네트워크로 연결이 되고 사용자 인터페이스를 통해 상호작용이 가능할 것이라 예견하였다. 이와 함께 어느 곳에서나 임의적으로 원하는 정보에 접근할 수 있는, 하이퍼텍스트라는 용어를 만들어 사용했다.

html 외에도 이러한 개념을 찾을 수 있는게 또 있다. 화석들은 당연히 알고 있을 하이퍼픽션(=하이퍼텍스트픽션)이다. 당연히 난 모르던 것이다. (*하이퍼픽션을 소개한 이전 기사)

하이퍼텍스트의 특성을 이용, 여러 갈래를 가진 이야기를 출판할 수 있는 전용 프로그램으로 소설을 제작한다. 하이퍼픽션 최초의 작품으로 인정받고 있는 마이클 조이스의 ‘오후: 이야기’는 1990년 이스트게이트 시스템스(프로그램 이름이 아니라 회사 이름이다)를 통해 쓰여진지 3년만에 퍼블리싱 된다.

  • 주의 1. 이 글은 소속 회사의 입장을 대변하지 않습니다.
  • 주의 2. 이 글에는 늘 그렇듯이 현명함 보다는 멍청함이 묻어 있습니다.
  • 주의 3. 선택한 방법과 주장하는 바가 옳지 않을 수 있습니다.
  • 주의 4. 위의 이유로 예고없이 삭제될 수 있습니다.
  • 주의 5. 수정할 사항이 있다면 댓글에 남겨주세요. 검증 후 본문을 업데이트 하겠습니다.


배경

가끔 개발자들이 완벽한 프로덕트를 위해 무언가를 늦춰야 한다고 이야기합니다. 맞습니다. 가끔은 시간 보다는 품질이 중요할 때가 있습니다. 하지만 대부분의 사업은 충분한 시간이 오히려 적이 되는 경우도 많습니다.(소속 회사의 사정을 말하는게 아닙니다) 프로덕트, 또는 서비스는, 그 가치가 지속 가능하기 위해서 이윤을 남겨야 하거나 그것을 벌어들이는 도구일 수 밖에 없습니다. 그런데 가끔은 그런 것들을 망각하는 경우도 많은 것 같습니다.
 
 
시간이 많지 않은 상황에서 우리는 두개의 센터를 연달아 가동시켜야 할 상황이 되었고 저와 우리 팀, 전체의 조직이 해당 목표를 달성하기 위하여 힘차게 뛰어야 했습니다. 지금은 두개의 센터가 잘 기능하고 있습니다. (너무 감사하게도... 아직 보완할 점은 엄청 많지만...) 그러면 어떻게 동시에 진행할 수 있었을까요?!


섣부른 추상화 대신 아주 작은 교집합 찾아내기

제가 회사에서 수행하는 업무는 물류 도메인에 필요한 프론트엔드 개발을 지원하는 것입니다. 이번 센터 오픈 지원을 진행하면서 새롭게 개발이 필요한 영역은 '피킹 프로세스'였습니다. 피킹 프로세스는 물류 센터에 보관하고 있는 상품을 패킹(출고할 수 있는 상태로 만드는 절차)할 수 있도록 만드는 전처리라고 생각해주시면 됩니다. 할당된 물품들을 물류 창고에 보관되어 있는 곳들을, 동선에 따라 지나가며 토트(상품을 담는 바구니)에 지정된 상품을 담아서 약속된 장소로 이동시켜주는 절차입니다. 이를 위해 작업자들은 스캐너 기능이 있는 PDA 기기를 이용하며, 우리 팀은 해당 기기에 사용할 어플리케이션을 개발하는 업무를 맡게 되었습니다.
 
 
그런데 왜 그런 비슷한 일을 하는 시스템을 별도로 개발하게 됐을까요?! 앞서 설명한 핵심 요건은 같았지만 그 절차가 매우 달랐기 때문입니다. 이럴 때 우리는 많은 고민을 하지만 저와 팀은 한개의 어플리케이션을 만들어 서로 다르게 사용하는 것이 위험할 수 있다는 판단을 했습니다.

  1. 실무에 투입되기 전 섣부른 추상화를 하여 더욱 복잡도를 향상시키는 행위를 하지말자.
  2. 사용성이 비슷해지면 어느 하나를 말라죽이고 이관하는 행위가 더 쉬울 것이다.

위의 두가지가 가장 큰 이유였습니다. 많은 개발자 분들이 효율성을 위해 하나를 만들어 여러 곳에서 사용합니다. 재사용성을 프로그래밍의 미덕으로 생각하고 아직 현장에서 사용하지도 않은 로직을 하나로 개발하여 여러 분기를 만들어냅니다. 물론 이는 중요합니다. 그리고 이것이 나쁘다는 의미는 아닙니다. 하지만 이미 사용중인 어플리케이션에 사용자의 피드백이 어느 정도 예측되는 기능을 넣는것과 아직 사용하지 않은, 앞으로 현장에서 많은 피드백을 수용해야 할 어플리케이션에 개발자들이 예측한 기능을 포함시키는 행위는 많이 다르다는 것을 경험했습니다.
 
 
어플리케이션 사용을 시작하며 개발자들이 예측한 사용자들의 사용 방법이나 프로세스는 늘 예상을 빗나가거나 교묘히 다르기 마련입니다. 두개의 어플리케이션을 따로 개발하면서 저와 팀은 사용자들의 요구사항이 오픈 전과 그 후가 매우 다를것이라 판단한 이유입니다. 만약 우리의 예측이 다르면 적정한 시기에 하나를 닫고 다른 하나를 확장하는 것이, 하나의 어플리케이션을 모든 분기 처리의 다른 부분을 포함한 또 다른 어플리케이션으로 분리하는 것 보다 고통스러운 부분이 덜하다고 판단하였습니다.
 
 
그래서 우리는 더이상 쪼갤 수 없는 단위의 기능들을 열거하고 그 기능들을 만들고 또 로그인 페이지와 같이 공통으로 사용할 수 밖에 없는 단위들을 찾아내며 컴포넌트들의 조합을 만들었습니다. 라이브러리 제작을 하기로 결정했습니다.

아토믹 디자인 패턴

글의 시작 부분에서 언급했듯이 동시에 하나의 목적을 가진 두개의 어플리케이션을 개발할 수 밖에 없는 상황이었기에 저는 효율적인 방법을 찾아보았습니다. 그래서 별개의 절차에서 아주 작은 단위로 재사용이 될수밖에 없는 컴포넌트들을 제작하여 그것을 별개의 어플리케이션에서 가져다 쓰고 나머지는 어플리케이션의 구현부에서 다름을 장착하기로 결정했습니다.
 
 
완벽하고 또 베스트 프랙티스라고 할만한 수준은 아니지만, 목적에 합당한 수단으로 아토믹 디자인 패턴을 이용하기로 하였고 그것을 결정하고 약속하기 위해 팀원들과 많은 회의를 거쳤습니다.
 
 
여러가지 스캔과 입력상황을 대비하여 많은 수의 input 컴포넌트들을 만들었고 그것들을 각각의 page에 잘 조합하거나 또 꼭 동시에 쓸만한 것들은 조금 더 큰 단위의 컴포넌트를 만들어 개발을 진행했습니다.
 
 
비록 아직 추가해야되거나 지원해야 할 기능들이 많지만 우리는 적기에 어플리케이션을 완성할 수 있었고 각 센터의 요구사항에 충족되도록 수정하거나 확장할 수 있게 되었습니다.

마무리

솔직히 포스팅한지 너무나 오랜 공백이 있었고, 또 기억에 남는 일이었기에 글을 남기는거여서 여러분들이 읽기 재밌거나 유익한 내용이 아닐수는 있습니다. 하지만 간단하지만 복잡해지는 로직들을 보면서 우아한 방법으로 여러 곳에서 사용하게 만드는 것 보다 때로는 단순하지만 안전하게 코드베이스를 관리하는 방법도 있다는 말씀을 전달하고 싶었습니다.
 
 
현장의 피드백을 충분히 적용하고 그 후에 발견된 공통된 규칙으로도, 우리는 우아하게 시스템을 관리할 수 있습니다.(물론 제가 많이 부족하여 내린 결론일 수 있습니다.)

if (a == 0 && a == 1)

며칠 전 꽤 흥미로운 짤을 접했습니다.

그냥 지나가는 짤이라 생각하고 별 생각이 없었지만 해당 게시물에 페친분들의 댓글이 계속해서 달리면서 위의 난해한 코드를 직접 구현해 보게 되었습니다.

 

자바스크립트를 다뤄본 개발자라면 위 구문의 'a'는 '변수'라고 자연스레 가정할 것입니다. 하지만 일반적인 변수로는 위의 평가를 통과할 수 있는 구문을 작성할 수 없습니다. 그렇다면 어떻게 접근해야 할까요?? 이와 비슷한 기능을 하는 것은 무엇일까요?? 만약 a가 일반적으로 프론트엔드 개발자들이 담는 예측할 수 있는 값이 아니라 프로퍼티라면??

 

아마 getter와 setter를 활용하여 해당 값을 담고 반환할 수 있겠죠?! 그렇다면 window의 프로퍼티로 1. 일반적인 변수 형식으로 2. getter에 변화를 주어 반환하게 한다면 가능하지 않을까요??

 

다음은 위의 개념을 증명하기 위해서 테스트를 진행한 코드입니다.

Object.defineProperty(window, 'a', {
    get: function () {
        return window.b++;
    },
    set: function (val) {
        window.b = val;
    }
});
Window { parent ... }                 // 시스템 출력
a = 0;
0;                                    // 시스템 출력
a == 0 && a == 1
true                                // 시스템 출력

const, let과 달리 var를 사용한 변수는 자동적으로 window의 프로퍼티가 되며 window는 생략 가능합니다. 이러한 부분을 고려하여 위에서 정의된 'a'를 'window.a'로 만들면 자연스레 'a'로 접근과 사용이 가능합니다. 그러면 위의 '1. 일반적인 변수 형식'은 만족합니다.

 

동치연산자는 좌변과 우변의 값을 대조하여 평가(이때 여러가지 일이 벌어지지만 해당 부분은 별도로 찾아 보시면 좋을것 같습니다)합니다. 이때 값을 가져오면서 해당 프로퍼티의 값을 평가하게 됩니다. 이 말은 getter가 실행된다는 의미입니다. 위의 '2. getter엔 변화를 주어 반환'한다는 조건에 맞아 떨어집니다.

 

이제 'a'는 위의 두 조건을 모두 만족하여 변수처럼 할당한 값이, 평가되는 순간 변이가 일어나게 되며, 'a == 0 && a == 1'도 true로 평가됩니다.

 

...

...

 

그렇다면 이것은 과연 좋은 코드일까요??

좋은 코드는 무엇인가??

좋은 코드는 무엇일까요?? 클린 코드?? 오류없는 코드?? 요구사항이 잘 반영된, 납기일을 만족하는 코드??

 

좋은 코드는 각자의 상황과 역할에 따라서 정의하는 것이 매우 다를것 같습니다. 좋은 코드를 말하기 위해서, 저는 나쁜 코드는 무엇인가를 매우 한정적으로 정의를 내리고 그것의 반대되는 개념을 찾게 되었습니다.

 

제가 생각하는 나쁜 코드는,

  1. 목적이 불분명한 것들의 조합
  2. 성장하지 않는(리팩토링 되지 않는) 코드
    입니다.

매우 한정적이지만 많은 것을 포함하고 있습니다.

if (a == 0 && a == 1)은 무엇인가??

만약 이 게시물을 만들게 된 평가식을 우리가 관리하고 있는 프로덕트에서 마주하게 된다면 '기 작성된 코드'는 프로퍼티로 식별할 수 있게 반드시 수정되어야 합니다.

 

전역객체를 오염시킬 뿐 아니라 함께 작업을 하고있는 많은 협업자들을 혼란에 빠지게 할 수 있습니다. 구문을 이해하기 위하여 파편화 된 전역 설정을 찾아봐야 할 것이고, 'a'가 도대체 어디서 어떻게 이용되고 있는지 수정 혹은 개선하기 전 모든 스펙과 사용처를 확인해야 합니다.

 

자바스크립트의 전문가 중 한분은 이러한 코드도 '명확한 의도를 가지고 기능하도록 만들었기에' 문제가 없다고 피드백 주시는 분도 계셨습니다. 하지만 우리는 실제 프로덕트를 '그루'로 불리는 전문가들로 구성된 lab에서 개발하지 않습니다. 우리는 함께 일하고 있는 '누구나', '언제든', 해당 코드를 찾고 개선할 때, 명확한 방법과 영향을 알고 사용해야 합니다.

좋은 코드는 나쁜 코드의 반대, 그리고...

사람은 항상 실수를 합니다. 사람이 작성한 코드는 실수를 유발할 수 있습니다. 우리는 이것을 각종 약속과 규약으로 줄이기 위해 노력합니다.

 

코드컨벤션, 현재의 문제점 공유, 코드리뷰, 정적 분석 모두가 실수를 줄이기 위하여 하는 행위(꼭 순수한 목적은 아닙니다)들 입니다.

 

실수를 줄이고, 실수를 할 수 있는 부분을 없애가며, 그 경험을 공유하는 것은 좋은 코드를 향해 나아가는 좋은 자세가 될것이라 믿습니다.

 

조직이나 개인의 여건에 의하여 언제나, 늘, 좋은 품질의 코드를 만들어낼 수는 없을 것입니다. 하지만 그것을 계속해서 묵혀두고 알고만 있다면 훗날 더 큰 어려움을 가져올 수 있다는 것도 인지하여야 합니다.

  • 주의 1. 이 글은 async / await와 아무런 관련이 없습니다.
  • 주의 2. 이 글에는 멍충함이 묻어있을 수 있습니다.
  • 주의 3. 테스트 방법이 잘못됐을 수 있습니다.
  • 주의 4. 위의 이유로 예고없이 삭제될 수 있습니다.
  • 주의 5. 수정할 사항이 있다면 댓글에 남겨주세요. 검증 후 본문을 업데이트 하겠습니다.

 

의심

큰 프로젝트를 앞두고 PoC를 진행하다 기본적인 부분을 돌아보는 시간을 갖게 되었습니다. 어플리케이션에서 전반적으로 사용하는 기능을 개발해야하는데 평소에 알고있다 생각했던 로딩과 실행에 관한 기본적인 것들을 확인하던 중, 제가 알고있던 사실을, 제가 검증한 적이 없다는 것을 깨닫고 스펙을 확인한 후 나름의 환경을 구성하여 테스트를 진행했습니다.

script의 async / defer 요소

제가 검증하려던 부분은 스크립트의 로딩과 실행이었습니다. 모듈, 의존되는 기능 실행, DOM 핸들링에 관하여 테스트를 실행하다 보니 자연스럽게 async와 defer를 테스트하게 되었습니다. 우선 script의 async와 defer는 무엇인지, 어떤 특징과 차이를 가지는지 알아보겠습니다.

 

출처 / MDN

 

async | HTML5

For classic scripts, if the async attribute is present, then the classic script will be fetched in parallel to parsing and evaluated as soon as it is available.
For module scripts, if the async attribute is present then the scripts and all their dependencies will be executed in the defer queue, therefore they will get fetched in parallel to parsing and evaluated as soon as they are available.
This attribute allows the elimination of parser-blocking JavaScript where the browser would have to load and evaluate scripts before continuing to parse. defer has a similar effect in this case.
This is a boolean attribute: the presence of a boolean attribute on an element represents the true value, and the absence of the attribute represents the false value.
See Browser compatibility for notes on browser support. See also Async scripts for asm.js.

 

defer

This Boolean attribute is set to indicate to a browser that the script is meant to be executed after the document has been parsed, but before firing DOMContentLoaded.
Scripts with the defer attribute will prevent the DOMContentLoaded event from firing until the script has loaded and finished evaluating.
This attribute must not be used if the src attribute is absent (i.e. for inline scripts), in this case it would have no effect.
The defer attribute has no effect on module scripts — they defer by default.
Scripts with the defer attribute will execute in the order in which they appear in the document.
This attribute allows the elimination of parser-blocking JavaScript where the browser would have to load and evaluate scripts before continuing to parse. async has a similar effect in this case.

(*한글 번역이 매끄럽지 않아 영문으로 가져왔습니다.)

 

MDN과 '노란책'으로 통용되는 '프론트엔드 개발자를 위한 자바스크립트 프로그래밍'에서 async와 defer에 관하여 언급한 내용을 요약하면 async는 HTML5에 등장, 즉시 다운로드 후 사용이 가능한 시점에 바로 실행, defer는 HTML 4.01에서 소개, 즉시 다운로드하지만 문서가 준비되었을 때 실행한다 입니다.

 

출처 / async vs defer attributes

 

 

 

위의 설명과 이미지를 보면 스크립트의 async와 defer 다운로드와 실행 순서는 async는 다운로드 된 이후, 다큐먼트가 준비된 것과는 별개로 실행 가능한 시점에 바로 실행이 될것이고 defer는 다운로드 후 문서의 콘텐트가 모두 완료된 후, DOMContentLoaded 전에 실행되리라 짐작할 수 있습니다. 그런데 꼭 그렇지만은 않았습니다.

환경 구축

예제파일

 

위의 링크에서 예제파일을 다운로드 및 설치 후 npm run start를 입력하시면 간단한 테스트베드에서 실제로 동작하는 모습을 콘솔창을 통해 확인할 수 있습니다. index.html을 확인하면 head의 script 구문들과 body 안에서 inline으로 작동하는 스크립트 확인이 가능하며, head의 script 작성 순서는 다음과 같습니다.

 

  • index.js / global function definition
  • async.js / async option
  • defer.js / defer option
  • default / no options

 

우리가 앞서 살펴본 내용으로 추론하자면,

1) index.js는 다큐먼트 구문 분석을 멈추고 실행

2) async.js는 다운로드 즉시 실행되므로 async.js에서 정의한 색상(red)의 div는 페이지에서 확인이 불가

3) defer.js는 다큐먼트가 준비된 후 실행되므로 마지막에 실행

4) default.js는 문서 작업을 멈춘 후 바로 실행될 것이므로 async.js와 동일하게 페이지에서 확인이 불가

 

할 것으로 예상했습니다.

검증

파이어폭스, 사파리, 크롬으로 해당 테스트를 진행했습니다. (IE는 여건상 할 수 없었습니다.) 아래는 테스트를 진행한 결과의 스크린샷 입니다.

 

 

예상했던 대로 테스트 결과, 브라우저 별로 작동하는 방식이 달랐습니다. 하지만 너무나 다릅니다. index.js를 제외한 스크립트의 실행 순서는 다음과 같았습니다.

 

파이어폭스
ASYNC -> DEFAULT -> INLINE / head -> INLINE / body -> INLINE / after header -> INLINE / after section -> INLINE / after footer -> DEFER -> DOMContentLoaded -> load

 

사파리

DEFAULT -> INLINE / head -> INLINE / body –> INLINE / after header -> INLINE / after section –> INLINE / after footer -> DEFER -> DOMContentLoaded –> ASYNC -> load

 

크롬

DEFAULT -> INLINE / head -> INLINE / body -> INLINE / after header -> INLINE / after section -> INLINE / after footer -> ASYNC -> DEFER -> DOMContentLoaded -> load

 

추가적으로 모든 브라우저에서 계속 동일한 결과를 주지는 않았습니다. 가끔은 실행 순서가 바뀌고 캐시를 삭제하지 않은 페이지 재진입은 크롬에서 꽤 높은 확률로 async가 defer 보다 뒤에서 실행되었습니다. 이것은 단어를 어떻게 이해했느냐와 관련된 문제일 수도 있겠지만 너무나 당연시 여기던 저의 '상식'이 잘못되었음을 깨닫게 되었습니다. 여기서 확실히 다짐한 점은, '절대 스크립트 로딩과 실행을 option 값으로만(async, defer) 판단하고 사용하지 말자'입니다.

결론

'알고있는 내용이 알고있던 것과 다를 수 있음을 기억하자'

 

파이어폭스 조차 페이지 리로드를 계속해서 하다보면 '빨간 박스'가 페이지에 노출될 때가 많습니다. 물론 스크립트 파일을 내려받고 실행하는 것이 동일한 기능을 한다해도 늘 같은 속도를 보장하지는 않겠지만 이를 의식하지 않고 암기한 내용을 사용하면 큰 낭패를 볼 가능성이 보입니다. 해당 문제에 관한 내용은 추후 포스팅을 통해서 그 이유를 추적하고자 합니다. (물론, 언제할지는 미정입니다만...)

 

무엇이든 신뢰하지 말자는게 아니라 확실히 알고 사용해야 한다에 초점을 두고 알고있던 기능도 한번쯤은 이렇게 테스트 해보는 계기가 되었으면 합니다.

리액트를 배워 보자!!!

 

프론트엔드 개발자로서의 삶을 살아가면서 아직도 접해보지 않았던 이름이 있었습니다. 10에 4명 이상은 리액트로 프로젝트를 진행하는 요즘에도 조직과 개인적 이유로 리액트 스터디를 미뤄오다 우연치 않은 기회에 [리액트를 다루는 기술]을 획득(?!)하여 간단한 후기를 남기게 되었습니다.

 

* 현재까지의 스터디 분량으로 리뷰를 진행하며 책의 전체 리뷰는 완독 후 해당 포스트에 업데이트 할 예정입니다.

 

저는 마크업과 UI를 개발하다 AngularJS를 접하며 프론트엔드 개발자로 성장 하였습니다. 처음 접하게 된 프레임워크와 프론트엔드 개발이 이유가 된건지 AngularJS (지금은 사용하지 않습니다... 다 까먹었어요...) / Angular에 대한 애착이 있었고 토이 프로젝트 혹은 프로토타이핑은 Angular를 주로 이용했지만 지금의 회사에서는 프레임워크를 통일하여 사용하고 있기에 Vue.js로만 모든 개발업무를 진행하고 있습니다.

 

리액트에 대한 개발자로서의 호기심과 좋은 평판을 들으며 리액트를 스터디 하려고 계획만 세워놓고 스터디 모임, 컨퍼런스, 밋업, 취미생활, 게임, 음주... 등 계속해서 업무와 개인적인 일련의 일들을 진행하다 보니 자연스레 계획했던 공부는 늘 '내일하자'로 마음 속 한 구석에 처박아 놓고 잊어버리고 있었습니다.

 

개발자 리뷰어를 모신다는 '길벗' 출판사의 광고를 보고나서, 구매해 보고 싶었던 책을 얻게되면 그걸 (반강제적이지만 지극히 자발적인) 동기부여의 기회로 삼아 공부를 할 수 있겠다는 생각이 들어 신청을 했는데, 왠걸!!! 됐습니다!!! 선정되었습니다!!! 그리하여 오늘은 [리액트를 다루는 기술] 그것도 무려 300페이지 쯤 더 두꺼워진 개정판의 스터디(still ...ing) 후기를 남겨보려 합니다.

(자세한 책의 정보는 아래의 링크에서 얻으실 수 있습니다.)

 

* 길벗, 리액트를 다루는 기술

 

리액트를 다루는 기술(개정판)

입문부터 대규모 애플리케이션까지 한 권으로!

www.gilbut.co.kr

 

이 책은 친절하지만은 않다.

리액트에 관심이 있는 프론트엔드 개발자라면 김민준(이라 적고 벨로퍼트라고 읽음) 저자를 한번쯤은 들어보셨을 겁니다. 여러 커뮤니티에서 저자의 게시물과 작업들로 도움을 받으신 분들이 많이 계실겁니다. 여러 경로를 통해 개발자들에게 많은 도움을 주셨던 분이지만 이 책이 한없이 친절한 것은 아닙니다.

 

* 김민준 저자 블로그

 

책의 초반은 리액트의 간단한 개념으로 배경지식을 조금씩 설명하고 JSX가 무엇이며 장점은 무엇인지, 어떻게 코드를 작성하는지 린트를 소개하고 적용하는 방법 등을 소개하며 리액트의 구성요소를 하나씩 차근차근 설명해줍니다.

 

하지만 자바스크립트를 어느 한 라이브러리만 사용해서 개발을 진행했던, 예를 들어(아니, 콕 짚어) jQuery에 의존하여 개발을 진행했거나 JS의 객체지향 패턴 비구조화 할당, 화살표 함수, this의 바인딩과 스코프 (함수 / 블록 스코핑) 등의 전반적인 지식이 부족하거나 문법이 낯설다면 저자의 친절한 설명 글들이 그리 친절하게만 보이지 않을 수도 있습니다.

 

만약 마크업 개발만을 담당하다 프론트엔드 개발자로서 발돋움하기 위해서 책을 펼친다면 이 책은 종국에는 굉장히 큰 도움이 될 거라 확신하지만 다른 얇은 기본서와 ES6에 대한 서적을 빠르게 읽어보시는게 좋을 것 같습니다. (물론, 제 입장에서 바라보는 이야기입니다.)

 

그럼에도 불구하고 이 책은 정말 친절하다.

하지만 이 책은 굉장히 친절하게도 몇가지의 프로젝트를 진행하며 실무에서 고민하고 맞닥뜨려야 하는 여러가지 문제를 자연스럽게 익힐 수 있도록 도와줍니다. 비록 챕터가 지날수록 조금씩 난이도가 높아지는 느낌은 있지만 (현재 기준으로는) 친절하게 설명을 이어가고 독자가 잘 이해할 수 있는 가능한 많은 방법 (노트, 패턴별 코드 비교 및 분석, 개념 설명)을 동원하여 독자가 이해할 수 있는 길로 가도록 잘 이끌어 줍니다.

 

한쪽만 다루지는 않는다.

많은 기술 서적은 본연의 책임과 역할을 잘 수행하기 위하여 한가지만을 중점적으로 설명합니다. 이 이야기는 반대로 한가지만 설명하고 그것을 지탱하고 수행하는 부분의 어쩌면 핵심이 될 수도 있는 부분은 버린다는 이야기가 될것입니다. 하지만 해당 서적은 SPA만 집중적으로 설명하는 것을 넘어서 SSR(서버사이드 렌더링), Node.js, Koa, 거기다 JWT를 통한 인증 시스템 구축까지 실무에서 당연히 마주할 부분을 커버합니다. 그렇다고 많은 부분을 보여주기 위해 소흘한 부분이 있는 것은 아닌 것 같습니다. (아직 완독하기 전이라 섣불리 뭐가 부족하다는 말은 삼가하겠습니다.) 상태관리나 새롭게 추가된 핵심 기능, 함께 사용할 법한 라이브러리 등의 소개 역시도 꼼꼼히 소개해줍니다.

 

대상독자라면 충분히 기본을 넘어설 수 있다.

포스팅을 하면서 앞서 언급한 것과 같이 이 책은 새롭게 프론트엔드 개발을 배우기 위한 입문서적은 될 수 없습니다. 역시나 자바스크립트에 대한 아주 기초적인 지식만으로는 병행해야 하는 별도의 '공부'가 필요할 수 있습니다. 하지만 이 책과 저자가 염두해 둔 [대상독자]라면 이 책은 아주 훌륭한 입문서이자 레퍼런스가 될 수 있을 것 같습니다.

 

* 책에대한 리뷰는 읽어 나가는대로 이 곳에 업데이트할 예정입니다.

** 이 책의 대상독자가 아닌 분들을 위해 입문서로 활용할 아주 얇은서적이 있다면 다른 분들을 위해 댓글로 정보공유 부탁드립니다.

*** 해당 리뷰는 서적 자체를 제외한 별도의 금전적 지원은 받지 않았으며 어떠한 가이드나 지침으로 작성된 글이 아님을 밝힙니다.

 

 

 

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

오늘은 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;
  }
}

 

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

[LIFO / Stack class]

Last In, First Out!!!

스택 구조는 bottom에서부터 top으로 쌓이는 자료형 입니다. 그렇기 때문에 추가와 삭제가 제일 마지막 원소에서 이뤄집니다.

이는 자바스크립트에서 제공하는 내장 메서드 중에서 push와 pop이랑 같은 역할을 합니다.

아래는 WeakMap을 이용한 Stack 클래스입니다.

// 스택 클래스

const items = new WeakMap;
class Stack {
  constructor () {
    items.set(this, []);
  }

  push (element) {
    const s = items.get(this);
    s.push(element);
  }

  pop () {
    const s = items.get(this);
    const r = s.pop();
    return r;
  }

  peek () {
    const s = items.get(this);
    return s[s.length - 1];
  }

  size () {
    const s = items.get(this);
    return s.length;
  }

  isEmpty () {
    const s = items.get(this);
    return s.length === 0;
  }

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

  toString () {
    const s = items.get(this);
    return s.toString();
  }
}
  • WeakMap은 이곳에서 아주 자세히 소개하고 있으니 링크를 따라가 보세요. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/WeakMap

  • 부수적이지만 items를 WeakMap의 인스턴스로 만들면 스택 안의 데이터를 임의로 수정하거나 삭제할 수 없습니다. Symbol()을 활용 및 참조하여 private하게 만들 수도 있지만 그렇게 되면 데이터를 들여다보고 수정 및 삭제가 가능하므로 WeakMap을 사용합니다.

  • const items = Symbol('items'); this[items] = []의 형태로 사용이 가능합니다.

[Vue.js Style Guide 적용기]


이런저런 이유로 1년 가까이 블로그 활동을 접었다가 다시 시작합니다.


그동안 이직도 했고 개인적인 이유로 공부는 하면서도 그에대한 정리가 없었습니다. 이제 연말이 가까워지니 다시금 프론트엔드 개발자, 조직과 사회의 구성원으로서 삶(?!)을 정리해 보며 게을러진 저를 채찍질하기 위해서 운동과 블로그 활동을 재개하려 합니다.

물론 언제까지 포스팅 하려는 의지가 계속될지 모르겠지만... 다시 시작하는 의미로 게시물과 카테고리 정리를 하고나니 블로그가 너무 볼것이 없어 이번 프로젝트에서 코드 리팩토링을 하며 적용한 Vue.js 스타일 가이드에 대해 간단한 수행기를 포스팅합니다.


 Angular와 다른 프레임워크, 언어와는 다르게 (자바스크립트 제외) Vue.js의 공식 사이트에서는 강력한 한글 지원을 확인할 수 있습니다. 비록 업데이트 되는 내용이 모두 반영되는 것은 아니지만 한국 개발자들에게는 무척이나 반가워 할 일입니다.

스타일 가이드는 혼자서 개발할 때에는 그렇게까지 중요한 부분이 아니라고 여겨질 수 있지만 프로젝트를 진행하며 볼륨이 커져갈 때에는 그 필요성이 절실하게 느껴질 때가 생깁니다. 

서로가 작성한 코드의 가독성을 높이고 명시적인, 또는 암묵적인 룰을 지켜 운영과 유지보수를 제대로 수행하기 위해서 스타일 가이드를 준수하면 보다 수월하게 프로젝트를 진행할 수 있을 것입니다.

아직은 Vue.js 자체에서 강제하는 정도의 수준은 아니지만 밑의 문구를 보면 조만간 ESLint와 다른 자동화 프로세스를 걸쳐 스타일 가이드를 적용할 수 있는 팁을 제공할 예정이라고 하니 미리 그 내용에 대해 알아볼 필요가 있을것 같습니다.

Soon, we’ll also provide tips for enforcement. Sometimes you’ll simply have to be disciplined, but wherever possible, we’ll try to show you how to use ESLint and other automated processes to make enforcement simpler.

공식 사이트에서 제공하는 문서를 보면 Vue.js의 스타일 가이드는 4가지의 우선순위를 A, B, C, D의 레벨로 나눠 세부 규칙을 명시하고 있습니다.

A - 필수,
B - 강력 권고, 
C - 권고, 
D - 사용시 주의

(* 번역본이 있지만 최대한 원문을 번역하여 포스팅 합니다. 원문의 느낌이 개인차가 있기 때문이지 번역이 잘못된 것은 절대 아닙니다.)

공식 사이트의 스타일 가이드를 읽어내려 가던 중 습관처럼 사용했던 네이밍 규칙이 필수로 규정되어있어 조금 놀랐습니다. (다른 분들이 안그러셨다면 저의 게으름을 반성합니다.) 문서를 보면 명시 되어있지만 A 레벨오류 방지 차원에서 컴포넌트의 네이밍 규칙을 필수로 권하고 있습니다. 

root App 컴포넌트를 제외하고 컴포넌트의 이름은 반드시 다수의 단어로 사용을 해야합니다. 

이것은 해당 문서의 링크를 따라가보면 그 이유를 자세히 알수 있습니다. 요약하자면 현재, 그리고 앞으로 생성될 HTML 요소들의 이름이 단수의 단어로 규정 되었거나 규정되기 때문에 커스텀 엘리먼트인 컴포넌트의 이름을 다수의 단어로 강제하여 커스텀 엘리먼트와 네이티브 엘리먼트 사용 오류를 방지하기 위함 입니다.

자세한 것은 링크에서 확인하시기 바랍니다.


쭈욱 읽어가다 보면 너무나 당연한 규칙들, 예를 들어 new 키워드를 사용한 Vue 인스턴스 생성이 아니라면 팩토리 함수를 사용하여 data를 사용해야한다는, 이미 너무나도 잘 알고있는 당연한 내용부터, 이런것도 있었는지 잘 모르던 내용까지, 여러가지 스타일 가이드 예제가 간단하지만 이해하기 쉽게 잘 설명되어 있습니다.


이 중에서, 스타일 가이드를 확인하고 적용하면서 처음 알게된 내용 세가지만 간단하게 정리하겠습니다. (다른 분들께 공유와 저의 망각을 위함 입니다...) 


1. Props definitions


Parent에서 Child로 데이터를 넘겨줄 때 사용하는 pops는 단순히 item의 이름만 명시하여 사용하고 있었지만 프로토타이핑인 경우를 제외하고는 가능한 구체적으로, 해당 데이터의 타입(type)과 필수여부(required), 값 반환 혹은 검증에 필요한 함수 제공을 규정하고 있습니다. 


위의 규칙은 필수이지만 성능 이슈나 향후 오류 방지의 효과를 기대하기 때문도 있지만 데이터의 타입이나 필수값, 기본값 등을 체크하여 자체적인 데이터 검증 등을 통해 해당 데이터를 이름만으로 추측하는 것이 아니라 사용 목적을 분명히 하기 위함으로 보입니다. (물론 뇌피셜입니다.) 추가적으로 해당 문서에서 설명이 조금 빠져있지만 props의 default 혹은 validator 사용을 찾아보시길 권합니다. (* 참조 링크)


2. Avoid v-if with v-for

Vue.js에서는 v-if 사용시 v-for와 함께 사용하는 것을 엄격히 금하고 있습니다. (물론 저는 사용했습니다...) 명시된 문서에는 두가지의 일반적인 예를 들어 각각의 경우 상황에 맞게 대체하여 코딩할 수 있는 예시를 제공합니다.

1. 리스트 안의 반복되는 아이템을 필터링하기 위해서 사용하였다면 해당 리스트 자체를 computed 프로퍼티로 계산하여 리스트를 반환하고 반복 아이템을 렌더링 한다.

2. 어떠한 조건을 충족하지 못할 때 리스트의 아이템 렌더링을 회피하기 위한 목적일 경우, v-if 문을 해당 리스트의 컨테이너 엘리먼트로 옮긴다.

이렇게 제시하는 이유는 해당 설명에 접힌채로 명시 돼 있습니다. 자세한 설명을 자세하지 않게 간단히 설명하자면 v-for가 v-if 문 보다 우선순위 우위에 있기 때문에 해당 조건문의 값이 변경이 되거나 그러하지 않은 경우에도 리스트 구문이 렌더링 될 때마다 v-for의 내부 함수가 실행되기 때문입니다.

이를 지키지 않으면 매번 연산되는 횟수(step)가 늘어나고, 곧 성능 저하로 이어지기 때문에 스타일 가이드 상의 무엇보다 더 엄격히 지켜져야 할 것입니다. 보다 자세한 설명은 스타일 가이드를 참조하시기 바랍니다.

3. v-if / v-else-if / v-else without key

이번 규칙은 '레벨 D'지만 많이들 그냥 넘어가는 부분이라 (물론, 그 '많이'에 제가 포함되어 있습니다.) 정리합니다. v-조건문 사용에 있어서 key 어트리뷰트는 필수는 아니지만 Vue.js에서 v-if문을 처리하는 방식 때문에 같은 요소(예를 들면 v-if와 v-else가 동일한 요소인, div 엘리먼트에 각각 명시되어 있는 경우)에서 v-조건문을 사용할 경우 key 어트리뷰트를 사용하여 두 엘리먼트를 key 값으로 분리할 것을 권고하고 있습니다. 


By default, Vue updates the DOM as efficiently as possible. That means when switching between elements of the same type, it simply patches the existing element, rather than removing it and adding a new one in its place.


위에서 설명하듯 Vue.js는 DOM을 가능한 가장 효율적인 방법으로 업데이트하기 때문에 만약 v-조건문이 명시된 엘리먼트와 같은 엘리먼트가 이미 존재한다면 그것을 node 상에서 지우거나 새롭게 추가하는 것이 아니라 단순하게 패치(patch)하여 의도하지 않은 사이드 이펙트를 불러올 수 있습니다. 그러니 낮은 단계의 권고 수준이라도 v-조건문을 같은 엘리먼트에 사용할 때에는 주의를 기울여야 할 것입니다.


위의 세가지는 스타일 가이드를 적용하며 수정 하였던 많은 규칙들 중 저에게 중요한 부분이라 정리한 것이고 성능과 가독성을 위해서, 또는 향후 발생할 수 있는, 의도치 않는 오류를 피하기 위하여 반드시 스타일 가이드를 일독해 보시기를 권합니다.

+ Recent posts