이 과정은 브라우저의 렌더링 파이프 라인을 파악하여 고성능 웹 응용 프로그램을 쉽게 만들 수 있도록 도와줍니다.
오늘날 디바이스들은 초당 60번 화면을 다시 그립니다.
서비스 사용자들은 이 프레임 중 하나가 늦어진다는 것을 쉽게 알아챕니다. (화면이 버벅대는 현상)
이런 결과는 서비스의 평가를 낮출 뿐만 아니라 실제로 서비스가 멈출 수 있는 가능성까지 있습니다.
1000ms당 60프레임을 렌더링하려면 단일 프레임을 렌더링하기 위해선 약 16ms(1000/60)가 필요합니다.
즉, 유려하고 버벅대지 않는 자연스러운 화면을 만들기 위해서는 16ms 미만으로 프레임이 구성되도록 작업이 되어야 합니다.
우선 프레임이 구축되는 순서를 살펴보겠습니다.
처음 브라우저는 서버에 GET 요청을 하고 서버는 응답으로 HTML을 보냅니다.
이때, 브라우저는 해당 HTML을 파싱하는데 이 단계를 Chrome Dev Tools 에서는 Parse HTML로 나타내며, CSS 파싱은 Parse Stylesheet, DOM과 CSS 결합은 Recalculate Styles로 표현하고 있습니다.
이렇게 DOM과 CSS가 결합하면 Render Tree를 구축합니다.Render Tree는 Dom Tree와 비슷해 보이지만 CSS를 통해 보이지 않는 요소는 제거됩니다.즉, 페이지에 실제로 보이는 요소만 Render Tree에 표시됩니다.
브라우저가 요소에 적용되는 규칙(공간을 얼마나 차지하고, 어디에 있는지 등)을 알게 되면, 레이아웃 계산을 시작할 수 있습니다.이는 Layout으로 표현하고 있습니다. 다음 해당 픽셀에 색을 채우는 Rasterizer 단계인데 이는 Paint로 표현됩니다.
브라우저는 레이어라는 불리는 여러 표면을 만들고 그것들을 개별적으로 그릴 수 있습니다. 이러한 개별적인 레이어를 합치는 단계는 Composite로 표현합니다.
이렇게 브라우저는 렌더링 파이프라인을 가지고 있습니다.
Javascript로 위의 파이프라인을 제어할 수 있는 경우는 3가지의 경우있습니다.
첫째, Javascipt로 요소에 변형을 가했을 경우 Style, Layout, Paint, Composite단계가 모두 발생합니다.
둘째, 색상만 변경했을 경우에는 Layout이 발생하지 않고 Style, Paint, Composite만 발생합니다.
셋째, 위치 및 색상 모두 변경되지 않는 것을 수정 했을 경우에는 Style, Composite만 발생합니다. (transform, opacity는 요소가 레이어를가지고 있을경우 Composite에서 만 처리됩니다.)
https://developers.google.com/web/fundamentals/performance/rendering/
웹 앱 라이프 사이클은 Load, Idle, Animation, Response로 4가지의 영역이 존재합니다.
사용자는 언제나 페이지가 빠르게 Load 되기를 원합니다. 이때에는 처음 페이지가 보이기 위해 꼭 필요한 것들만 필요로 합니다. 모든 방법을 써서 1초 안에 이 과정을 완료되도록 최적화하는 것은 매우 중요합니다. (파일 크기 축소, CDN 활용 등)
앱이 완전히 Load 된 후는 보통 Idle 상태입니다. 이것은 사용자가 상호작용하기를 기다린다는 의미로, 이때는 Load 시간을 1초로 맞추기 위해 뒤로 미뤄두어야 했던 작업들을 수행할 수 있습니다. 예들 들자면, 곧바로 보여게 될 수도 있는 이미지, 비디오, 다른 섹션의 내용을 호출하는 동작들입니다.
이후 사용자의 상호작용을 기다리는데 이때 1장에서 알아본 것처럼 16ms 미만의 시간 안에 처리해야 유려하게 보일 수 있습니다. 이때 opacity, transform을 사용할 경우 composite만 trigger 되기 때문에 다른 layout이나 paint가 발생하여 다시 렌더링을 해야 하는 것을 막을 수 있습니다.
즉, 요약하자면. 사용자에게 유려한 페이지를 제공하는 방법은 아래와 같습니다.
어떤 시점에서 무엇을 할 수 있는지, 언제 할 수 있는지를 알면 성능개선에 도움이 됩니다.
Chrome Dev Tools에서 Performance 탭에서 변화를 레코드 할 수 있습니다.
이는 초당 프레임 수와 프레임마다 어떤 작업이 포함되었는지 알려줍니다.
여기에는 단계별로 색상 코드가 존재합니다.
파란색은 Parse HTML입니다.
보라색은 Recalculate style과 Layout입니다.
녹색은 Paint와 Composite입니다.
초당 60프레임이 넘어가는 작업을 확인하고 개선할 수 있습니다.
확인 전에 현재의 값을 측정하는 작업은 중요합니다.
만약 측정을 하지 않고 수정한다면 수정 후 실제로 얼마나 개선되었는지 알 수 없습니다.
차이를 비교하기 위해 사전에 값을 측정해 놓는 것은 매우 중요합니다.
Performance 탭에서 빨간색 삼각형을 성능에 문제가 있을 수 있는 부분에서 경고해줍니다.
해당 영역을 클릭하면 어디에서 시간을 오래 소비하고 있는지 코드 위치까지 찾아볼 수 있습니다.
]]>첫 직장에서의 목표는 배운 지식이 실제 사회에서 어떻게 사용되는지를 알고자 하는 것이 가장 큰 목표였다. 아무래도 학문과 실제는 차이가 있기 때문이다. 그 결과 목표보다 더 많은 것들을 얻었다. 사회생활, 좋은 동료들, 나만의 가치관을 얻었으며 자연스럽게 나의 두 번째 목표를 가지게 되었다.
처음엔 혼자 이런저런 시도를 하면서 개발을 하는 것이 나만의 무언가를 만든다는 생각에 재미있고 만족스럽지만, 시간이 흐르면 혼자 아둥바둥한다는 느낌과 함께 막막하다는 생각을 하게 된다. 이때, 가장 많이 하는 생각 중에 하나는 “이게 맞는 건가?” 라는 것이다. 이런 문제에 대한 해결방법은 사람마다 다양하다. 오픈소스를 찾아보면서 자신을 고취하는 사람도 있을 테고, 아예 이런 생각을 하지 못할 환경에 놓여 있을 수 있다. 하지만 나는 같은 고민을 나눌 동료가 필요했다.
3개월 동안 지내보면서 이곳에는 같은 목표를 가진 동료들도, 같은 고민을 나눌 동료들도, 동기부여를 주는 동료들도 많다. 이런 환경에서 일할 수 있다는 점만으로 목표 중 하나는 이루었다. 이제는 이러한 긍정적인 환경을 잘 흡수할 내 노력만 남았다.
작은 회사에서 알지 못하는 것 중 하나는 많은 사용자들을 대응하는 방법이다. 소수의 사용자들을 대응하는 방법은 그리 많지 않다. 하지만 사용자 수가 많다면 얘기가 달라진다. 실제 서비스 구현 이외에도 네트워크, 브라우저의 렌더링 속도, 리소스 관리 등에도 더 많은 관심을 기울여야 한다. 그러한 부분을 제어 하는 부분에서 많은 니즈를 느끼고 있었다.
현재는 구현은 기본이고 이런 부분들을 같이 의논하고 피드백을 주고 받고며, 전혀 몰랐던 부분에 대하여 정보를 얻고 익히는 자리가 자연스럽게 생기고 있어서 좋다.
마지막은 프로다움이다. 이제 개발을 업무로써 4년 정도 하고 있는데 아직도 프로다운 태도에는 많은 궁금증을 가지고 있다. 프로다운 것은 무엇일까? 모르는 것이 없고, 계획에 빈틈이 없고 철두철미함을 말하는 걸까? 지금까지 여러 가지 본보기가 있었지만, 동료가 많은 이곳에서는 그 답을 좀 더 명확히 얻을 수 있으리란 기대가 있다.
이곳에 있으면서 지금까지 짧은 기간 이지만 여러 가지를 알게 되었는데, 그중에 기억하고 싶은 것은 키워, 네이밍, 설계이다.
개발 용어, 줄임말, 업무용 언어 등 다양한 용어들이 쏟아졌다. 용어를 모르니까 전체적인 그림이 그려지지 않았기에 그것들을 하나씩 익히는데 많은 시간과 노력을 기울였다. 비록 아직 완전히 커버하지는 못해서, 아직 빈틈이 많지만 하나씩 그 퍼즐들을 맞춰가면서 적응하고 있는 게 재미있기만 하다.
알지 못했던 개발 용어를 배우는 건 정말 좋다. 용어를 아는 것 자체가 사고의 확장이기 때문이다. 모두 놓치지 말고 내것으로 만들고 싶다.
네이밍의 중요성은 익히 알고 있다. 하지만 내가 알고 있던 부분은 가독성에만 영향을 미친다는 작은 부분만을 생각하고 있었다. 네이밍은 가독성을 포함하여 시스템 전체, 기능, 구조에도 영향을 미칠 정도로 중요하다. 이름에는 그 이름이 포함하고 있는 여러 가지 의미가 있기에 아주 작은 단위의 이름이 아니라면, 그 이름이 가지고 있는 의미를 신중히 생각하지 않으면 코드의 의미가 혼란스러울 가능성이 있다. (Event, Bridge, Listener, Emit, Action, Request … )
지금까지 나는 프로젝트 설계에 있어서 패턴과 및 구현에 집중했었다. 특별한 제약이 있지 않았다. 하지만 지금의 환경에는 다양한 제약사항들 있다. 제약이 많은 환경에서 훌륭하게 돌아가는 설계를 하는 것은 또 다른 도전이다. 비록 이 과정에서 동료분들의 많은 도움을 받았지만 스스로 얻은것도 정말 많았다.
이곳에서는 내 노력 여하에 따라 많은 기회가 주어진다. 그것을 내 것으로 만들려면 준비가 되어있어야 한다. 여전히 영어, 개발실력, 업무 디테일이 부족하지만, 충분히 성장시킬 수 있고 반드시 그렇게 할 것이다. 그리고 기회를 잡을 것이다. 스스로 자신 있게 나를 믿고 조금씩, 꾸준히 진행해보자.
]]>사용자들이 서비스를 선택할 때에는 여러 가지 기준이 있습니다. 그중에 하나는 부드러운 인터렉션 입니다. 오늘날의 기기들은 그러한 시각적인 효과를 위해 초당 60번 화면을 다시 그립니다. 그러므로 우리는 이 60개의 화면(프레임) 안에서 시각적인 효과를 표현해야 합니다.
사용자들은 이 프레임 중에 하나라도 놓치면 그것을 쉽게 알아챕니다. 초당 60개의 프레임을 렌더링하려면 1개의 단일 프레임은 16ms(1,000ms/60frame)안에 수행해야 합니다. 즉, 초당 60개의 프레임을 부드러운 속도로 보여주기 위해서는 약 16ms 미만으로 프레임을 유지하는 것이 좋습니다.
브라우저가 화면에 무언가를 그리기까지는 여러 단계가 존재합니다.
애니메이션 및 기타 작업들을 수행하는 Javascript, CSS 규칙을 어떤 요소에 적용할지 계산하는 프로세스인 Style, 브라우저가 요소에 어떤 규칙을 적용할지 알게 되면 화면에서 얼마의 공간을 차지하고 어디에 배치되는지 계산하는 프로세스인 Layout(reflow), 픽셀을 채우는 프로세스인 Paint(redraw), 이전의 작업들이 개별적인 레이어에서 진행되고 이를 합치는 프로세스인 Composite 로 진행됩니다.
때에 따라 다르지만 기본적으로 한번에 그림을 그리기 위해서는 위와 같은 랜더링 파이프라인을 호출하게 됩니다. 우리는 흔히 애니메이션을 수행하기 위해 setTimeout() 또는 setInterval()을 사용합니다. 하지만 이와 같은 함수들은 주어진 시간내에 동작을 할 뿐 위에서 언급한 프레임을 전혀 고려하지 않습니다.
그래서 종종 프레임이 누락되고 사용자게 버벅거리는 인터렉션을 제공할 수 있습니다.
화면에서 변화가 발생할 때 개발자는 브라우저에서 정확한 시간(프레임 시작 시)에 작업을 수행해야 매끄러운 움직임을 수행할 수 있습니다.
이 메소드는 실제 화면이 갱신되어 표시되는 주기에 따라 함수를 호출해주기 때문에 자바스크립트가 프레임 시작 시 실행되도록 보장합니다.
보통 1초에 60회 정도 실행이 되지만 대부분의 브라우저는 W3C 권장사항에 따라 디스플레이 주사율과 일치하도록 실행됩니다.
setTimeout(), setInterval()은 보이지 않은 곳에서도 수행되지만, requestAnimationFrame()는 현재 창에 표시 되지 않으면 애니메이션을 중지하여 배터리 수명과 성능향상에 도움이 됩니다
즉, requestAnimationFrame()을 사용하면 브라우저가 리소스 사용을 더욱 최적화하고 애니메이션을 더욱 부드럽게 만들 수 있습니다.
]]>참고 :
REQUEST ANIMATION FRAME
MDN - window.requestAnimationFrame()
udacity
돌이켜보면 가장 난감한 경우는 의존성이 고려되지 않은 프로젝트를 였습니다. 의존성이 고려되지 않고 작업 된 프로젝트는 유지보수에 많은 시간이 필요했습니다.
이에 못지않게 또 한 가지 어러운 일 중의 하나는 바로 데이터 변경에 따른 뷰 변경 작업이라고 생각합니다. 데이터가 많고 복잡할수록 뷰 또한 많은 변화가 있어 일일이 대응하기가 쉽지 않습니다.
그래서 이러한 문제들을 개선해보고자 MVC 및 옵저버 패턴을 도입해 해당 문제들을 개선한 프로젝트를 진행하기로 했습니다.
MVC 패턴은 담당하려는 역할별로 나누어서 각각에 대한 의존성을 낮출 수 있는 패턴입니다. 우선 패턴에 대한 학습 및 래펀런스로 아래 자료에서 도움을 많이 받았습니다.
해당 기능에서 사용되는 데이터에 대한 참조는 모두 Model에서 담당합니다. Model은 다른 것들과 다르게 의존성이 전혀 존재하지 않는 독립적인 개체입니다.
View는 Model 기반으로 렌더링할 DOM을 구축하는 역할을 맡습니다. 추가로 View에서 조금 더 복잡한 요구될 경우 하위에 Template
을 두어서 복잡도를 낮추도록 하였습니다. DOM이 있기 때문에 직접 이벤트를 바인딩하는 부분도 포함됩니다.
Controller는 View와 Model들에 접근하여 앱을 이어주는 단일 컨트롤러로 두었습니다. 초기 세팅, 기능 간의 로직 처리, View에 바인딩 될 이벤트 핸들러를 관리합니다.
MVC 패턴에서 객체를 여러 개로 만드는 경우도 있고 하나만 만드는 경우도 있기 때문에 싱글톤 처리가 필요합니다. ES6에서 제공되는 코어객체 WeackMap
를 사용해 싱글톤 객체를 생성합니다.
1 | // err = (v = 'invalid') => { throw v } |
WeackMap
은 객체를 키로 해서 값을 가질 수 있습니다. WeackMap
에서 제공되는 has, get, set의 직접 사용을 막아서 WeackMap
처럼 사용될 수 없게 합니다. getInstance()를 통해 키로 넘어온 객체의 생성자를 기준으로 사용하여 클래스당 인스턴스가 한 개씩 생성되도록 합니다.
Singleton 객체를 활용해서 Model, View, Controller의 에서 getInstance()를 활용해 싱글톤 객체를 얻을 수 있도록 합니다.
1 | const singleton = new Singleton() |
이 부분까지는 참고한 자료를 기반으로 적용할 수 있었습니다. 하지만 제가 작업하는 환경에서는 아래와 같은 에러가 발생하며 babel로 컴파일된 코드에는 동작하지 않았습니다.
Uncaught TypeError: Constructor WeakMap requires ‘new’
그래서 답을 찾다가 다음과 같이 설정 파일을 변경하였는데 정상적으로 동작했습니다. 서버로 사용하고 중인 node 버전을 targets으로 설정하면 에러가 발생하지 않았습니다.
1 | { |
하지만 새로 추가한 코드 때문에 전체 코드가 관장되는 설정 파일을 수정할 수가 없었기에 다시 원래 설정으로 돌렸습니다.
1 | { |
그래서 newWeakMap을 new로 생성한 인스턴스를 사용할 수 있는 클래스를 새로 만들어 사용하는 식으로 수정하였습니다.
1 | class newWeakMap { |
덕분에 설정 파일 변경 없이 싱글톤 객체를 사용할 수 있었습니다.
옵저버 패턴을 지원하기 위해 모델에서는 ES6의 Set
을 상속받아서 사용했습니다.
1 | // is = (t, p) => t instanceof p |
Set
과 배열의 차이점은 중복검사를 할 필요가 없다는 점입니다. Set
은 들어오는 값에 대하여 중복 값은 무시합니다.
기본적인 옵저버 패턴을 위해 Controller를 Set에 추가, 삭제하고 notify 할 수 있는 기능을 추가합니다.
WeakMap
과 마찬가지로 같은 문제가 발생해서 새로운 ‘Set’을 생성하여 문제를 해결합니다.
1 | class newSet { |
view는 렌더링을 담당하는 역할로 그 기능은 render 함수가 수행합니다.
주입받은 model과 해당 view를 렌더링할 위치를 주입받아 해당 위치에 DOM을 추가합니다.
controller에는 이벤트 핸들러가 있어서 필요한 DOM에 이벤트를 바인딩하여 사용합니다.
1 | class view extends View { |
1 |
|
컨트롤러에는 notify에 대응하는 Listen 함수를 생성하였고, 그 안에는 맞는 모델에 맞게 다시 view를 그릴 수 있도록 설정합니다.
그리고 controller에는 이벤트 핸들러 및 서비스 로직이 추가됩니다.
많은 부분이 패턴을 적용하기 이전보다 편리한 점이 있었습니다. 역할별로 기능들을 나눈 덕분에 코드가 더 명확하고 심플해 졌다는 것을 느낄 수 있었습니다. 또한, 옵저버 패턴을 활용해서 Model 변경으로 View를 렌더링하면서 세세한 View 컨트롤이 없어서 너무나 편리했습니다.
하지만 마냥 좋은 점만 있던 건 아니었습니다. 모든 작업이 그렇듯이 예외적인 케이스들이 있어서 그런 예외 대응하면서 패턴이라는 구조에 맞게 작업을 하는 건 쉽지가 않았습니다.
제가 머릿속에 처음 구상된 MVC는 아래와 같은 형태였습니다.
하지만 단일 컨트롤러에서 여러 모델과 뷰를 관리하고 그것들이 서로서로를 참조하여 모델을 변경하는 부분에서는 아래와 그림과 같은 상황이었습니다.
제가 진행한 프로젝트 Model과 View의 쌍이 6개였는데 만약 더 큰 규모의 프로젝트였으면 이러한 문제에 대하여 충분히 대응이 없다면 그 프로젝트도 결과 유지보수가 쉬운 코드가 될 수는 없다는 생각이 들었습니다.
이번 프로젝트를 통해 패턴에 대한 장점을 익히게 되어서 좋았지만, React와 View 같은 UI 라이브러리들이 얼마나 잘 만들어지고 편리한지 또한 알게 되었던 것 같습니다.
]]>기존 프로젝트에서 아쉬웠던 부분과 새롭게 추가해보고 싶은 것들을 정리했습니다.
너무 많은 목표를 두는 게 아닌가 생각은 했지만, 목표들이 어느 정도 포함관계가 있어서 지금 해두는 게 이후에 적용하는 것보다 덜 복잡할 것 같다는 생각을 해서 이번 기회에 추진하기로 했습니다.
올 초에 프로젝트를 진행하면서 리엑트로 프로젝트를 변경하는 것을 중점으로 작업하다 보니 그 이외에 부분을 신경 쓰지 못했습니다. 그중에 한 부분은 바로 빌드 프로세스입니다.
기존에는 번들 파일이 bundle.js
와 같은 형태로 생성되었습니다. 이러한 형태는 파일이 수정되어도 브라우저는 알지 못할 수 있습니다. 브라우저는 기본적으로 리소스를 브라우저캐시에 두고 파일 이름의 차이로 인해 변경사항을 파악합니다. 그래서 아래처럼 작성하여 브라우저 캐시를 갱신합니다.
1 | { |
하지만 이렇게 번들링된 파일은 파일 변경 시 매번 hash 값이 달라지니 파일 이름도 같이 달라져서 HTML에 sciprt를 추가할 수가 없습니다. 번들링 된 파일 이름을 알아내기 위해 webpack에 webpack-manifest-plugin 을 적용했습니다.
1 | new ManifestPlugin({ |
빌드를 통해 나온 manifest.json
파일을 통해 번들링된 파일 이름을 얻을 수 있고 리소스를 HTML에 추가할 수 있었습니다.
1 | { |
웹팩의 설정파일은 webpack.config.dev.js
, webpack.config.prod.js
와 같이 나누어져 있었습니다. 여러 설정이 추가되면서 설정파일이 커지면서 공통된 부분을 별도로 나누는 것이 좋다고 생각했습니다.
공통 부분은 webpack.common.js
를 생성해서 공통모듈을 생성하였습니다. 해당 파일로 분리된 설정파일을 각각 하나의 dev
와 prod
병합할 때는 webpack-merge을 사용했습니다.
1 | const merge = require('webpack-merge') |
기존에는 단일페이지라서 별도로 router를 세팅할 필요가 없지만, 새롭게 추가될 페이지를 위해 react-route를 적용하기로 하였습니다.
해당 프로젝트는 앞서 언급했던 것처럼 SSR이 지원되어야 했습니다. 그러므로 리엑트에서 적용했던 ReactDOM.render
과 ReactDOMServer.renderToString
처럼 SSR과 CSR을 위한 다른 Wrapper 메서드가 필요했는데 역시나 App을 Wrapping 하여 사용하는 react-router
도 그러한 메서드가 존재했습니다. CSR에는 BrowserRouter
를 사용하고, SSR에는 StaticRouter
를 사용하였습니다.
App 내부에서는 <Switch/>
로 감싼 <Route/>
들을 설정하였습니다.
1 | // SSR |
리엑트가 v16을 배포한 지 약 10개월 정도 지났는데, 이번 기회에 v16 중 필요한 기능들을 적용해 보았습니다.
Context API는 주로 전역 데이터가 필요할 때 사용합니다. 이러한 기능은 제가 redux를 도입한 목적과 같아 충분히 교체 가능했습니다.
redux를 이해하고 적용하는데 많은 시간이 든다고 생각됩니다. 저도 필요한 부분은 적용하며 사용했지만, 모든 부분을 자세히 알고 있다고는 생각할 수 없습니다. 전역 데이터를 위해 사용했지만, redux의 사용이 좀 과하다는 생각이 남아있긴 했습니다.
Context API를 적용하기 위해 많은 정보를 찾았고, 그중 많은 부분을 velopert
의 블로그에서 참고하여 작업을 진행하였습니다. 이유는 redux를 사용할 때도 store 내부에 크게 2개로 나누어 구분해서 사용했기 때문입니다. 그래서 Context API를 적용하려는 상황에서도 다중 context가 필요하다고 생각이 들었습니다. 이와 관련한 내용이 친절하게 설명이 되어 있었습니다.
하지만 모든 부분을 이전과 같은 컴포넌트 구조로 교체하지 못했습니다. 하나의 컴포넌트에서 여러 context가 필요한 경우였습니다. redux에는 store가 하나였기에 이런 부분에는 문제가 없었습니다. Context API를 활용하는 컴포넌트를 Hoc 형태로 만들었기에 하나의 컴포넌트에 2개 이상의 context를 주입받아야 할 때 컴포넌트를 겹겹이 감싸는 형태는 좋아 보이지 않았습니다.
그래서 이러한 경우가 발생한 컴포넌트들은 다시 context 기준으로 컴포넌트를 분리하였습니다. context 기준으로 새롭게 컴포넌트를 생성하다 보니 이전보다 더 명확한 목표를 가진 컴포넌트가 자연스럽게 생성된 결과를 볼 수 있었습니다.
작업하면서 “이 데이터가 이 context에 들어가는 게 맞나?”라는 고민을 참 많이 했던 것 같습니다. 구현 전에 데이터 모델링이 중요하다는 사실을 다시 한 번 느꼈습니다. 하지만 설계단계가 아닌 개발 중간마다 이러한 경우를 맞이하는 것이 아직은 더 많은 공부가 필요한 것을 상기시켜 주는 것 같습니다.
redux를 제거하게 되면서, redux관련 모듈인 react-redux, redux-thunk, redux-form 들을 제거할 수 있었습니다. 그 중 redux-form은 다른 모듈로 대체되어야 했는데 그 역할은 formik
였습니다. 사용이 간편하고 필요한 기능들을 충족했기 때문이었습니다.
사실 submit action이나 validation은 작업의 편의성을 위해 라이브러리를 주로 사용합니다. 하지만 단순히 값을 체크하는걸 넘어서 자동으로 다른 폼의 값을 채우거나, 비동기로 값을 체크하거나, 특정 액션에 다른 view에 영향을 미치는 등 예외의 경우를 조작하는 것은 간단하지만은 않았습니다.
이럴 거면 그냥 라이브러리 사용하지 않고 처음부터 만들 걸 그랬나 생각도 들었지만, 기본기능들을 그냥 새로 만드는 건 비효율적일 것 같지 않아 끈기있게 붙들고 적용하였습니다.
redux의 의존하지 않는다는 점에서 충분히 작업의미가 있었습니다. 처음엔 redux-form
과 formik
가 많이 다르다고 생각했지만, 결과적으로 바뀐 코드를 보면 역시 프론트엔드의 컴포넌트 형태의 개발은 크게 다르지 않다는 점을 알 수 있었습니다.
v16을 적용하면 ReactDOM.render()
를 ReactDOM.hydrate()
로 변경하라는 경고를 볼 수 있습니다. 이건 v16에서 나온 새로운 렌더링 메서드이며 ReactDOM.render()
은 v17에서는 사용되지 않을 예정이라고 합니다.
ReactDOM.hydrate()
SSR을 지원하는 앱에서 서버 렌더링 된 마크업이 있는 노드에서 호출하면 리엑트는 이벤트 핸들러만 연결해서 성능을 시켜준다고 합니다.
What’s the difference between hydrate() and render() in React 16?
복잡한 데이터 구조는 버그를 발생시키는데 한몫합니다. 여러 프로젝트를 하면서 가장 많은 버그를 생성한 곳 역시 복잡한 데이터를 주고받는 부분이었습니다. 이러한 부분에 도움을 받을 만한 것이 바로 타입스크립트(typescript)
라고 생각을 했고 적용해보기로 했습니다. 규모가 작은 프로젝트에는 오버엔지니어링이라고 생각될 수 있지만, 이번 기회에 타입스크립트를 적용해보면서 느낀 점도 있어서 결과적으로 “적용해보길 잘했다.”라는 생각을 했습니다.
공식페이지 React & Webpack에 언급된 awesome-typescript-loader를 사용하기로 했습니다. 해당 loader를 설치한 후에 ‘webpack.config.js’ 설정했습니다.
1 | { |
타입스크립트를 설정한 파일의 확장자가 .tsx
인 것을 확인했고, 그 파일에만 타입스크립트가 적용되기 때문에 .jsx
파일을 하나씩 .tsx
로 변경하면서 기능을 확인한 후 넘어가는 식으로 변경 작업을 진행했습니다.
타입스크립트에서는 타입을 선언하는 방식이 많은데 저는 주로 가장 간단한 interface
키워드를 사용하여 타입을 생성했습니다. 우선 선언한 타입을 기준으로 리엑트 컴포넌트를 생성하는 방법입니다.
1 | import * as React from "react" |
1 | import * as React from "react" |
공통된 구조의 타입은 interface를 extends로 상속하여 사용했습니다. 사실 컴포넌트 간의 구조를 생각하면서 하기도 쉽지 않은데 interface 간에 구조도 생각하려니 머리가 아팠습니다. 그러나 결과만 놓고 보면 기본적은 Props의 타입 검사는 만족했으며, 컴포넌트에서 어떠한 구조의 Props를 사용하고 있다는 것을 더욱 가시적으로 확인할 수 있다는 점이 정말 좋았습니다.
하지만 좋은 점만 있는 게 아니었습니다. 1의 일에 1.5배를 추가로 시간을 투자해야 했습니다. 게다가 강하게 타입체크를 하다 보니 쉬이 넘길 수 있는 부분에 추가로 코드가 들어가야 하는 걸 보면 답답함을 느끼곤 했습니다. 하지만 이런 부분은 기존 작업방식에서 빈틈이었던 부분이니 보완해야 할 부분이 맞아 보였습니다.
여러 목표를 갖고 개선 프로젝트를 했는데도, 새로운걸. 적용하고 알게 되면서 또 다른 새로운 게 보이고 더 나은 방법이 보이는 건 어쩔 수 없는 것 같습니다. 하지만 이러한 경험 덕분에 다른 것들을 적용해볼 수 있는 환경 및 정보들을 얻었으니 좋은 시도였다고 생각이 듭니다. 다음 개선에는 또 다른 걸 시도해 보고 싶은데 그중에서 하나는 렌더링 성능 관련인데 충분히 더 알아보고 도전해봐야겠습니다. 읽어 주셔서 감사합니다.
]]>우선, Iteration & Generator를 알아보기에 앞서 Interface에서 대하여 알아보도록 하겠습니다.
우선, Javascript에서의 Interface는 고유명사입니다.
다른 언어에서 말하는 Interface와는 아무 상관이 없습니다.
ES6에서의 Interface의 정의
http://www.ecma-international.org/ecma-262/6.0/#sec-iteration
예를 들어 Interface ‘TEST’를 만들어 보겠습니다.
Interface ‘TEST’의 요구 사항
1 | const obj = { // TEST'Interface 를 만족하는 object의 형태 |
덕 타이핑(duck typing)
class Test가 test Interface 조건만 만족한다면 test Interface의 구상체로 볼 수 있습니다.
덕 타이핑을 쓰는 이유는 런타임에는 컴파일타임의 타입체크가 되지 않기 때문입니다.
즉, Javascript에서 말하는 Interface는 덕 타이핑으로 구현을 약속한 프로토콜 같은 것이라고 볼 수 있습니다.
Javascript 스펙에는 미리 규정된 Interface들이 있습니다.
그 중 Iterator Interface를 살펴보겠습니다.
Iterator Interface의 요구 사항
iteratorResultObject
를 반환한다.iteratorResultObject
는 value
와 done
이라는 키를 갖고 있다.(interface)done
은 계속 반복할 수 있을지 없을지에 따라 Boolean 값을 반환한다.1 | const iterator = { // iterator Interface를 만족하는 object의 형태 |
이렇게 Iterator Interface을 적용하여 iterator object를 만들어 보았고,
반복수행을 해보았습니다. 다음은 Iterable Interface에 대하여 알아보겠습니다.
Iterable Interface의 요구 사항
1 | const iterable = { // iterable Interface를 만족하는 object의 형태 |
iterator는 한번 반복 후에는 수명을 다해서 없어집니다.
그래서 원본을 그대로 둔 상태에서 계속 반복 가능한 객체를 새롭게 얻기 위해 iterable을 사용합니다.
ES6에서는 ES5에서 제공되는 기본 기능들에도 변화가 생겼습니다.
1 | const a = { |
ES5으로 for .. in은 순서보장이 지원되지 않았는데,
ES6에서는 숫자, 알파벳, 심볼로 순서로 정렬되서 결과를 나옵니다.(생성은 순서대로 진행된다.)
1 | let arr = [1,2,3,4]; |
while문과 iterator객체가 매우 유사하다는 사실을 알 수 있습니다.
차이점으로는 while은 loop를 ‘문’으로 돌아서 제어가 불가능 하지만 iterator는 ‘값’으로 돌기 때문에 제어할 수 있습니다. 또한 next() 함수로 실행하기 때문에 지연실행이 가능합니다.
또한 while은 반복을 엔진이 해주지만 iterator는 외부에 실행기를 두어야 합니다.
즉, iterator object는 반복 자체를 하지는 않지만, 외부에서 반복하려고 할 때 반복에 필요한 조건과 실행을 미리 준비해둔 객체 입니다.
이를 통해 반복행위와 반복을 위한 준비를 분리했다고 볼 수 있습니다.
1 | // 사용자 반복처리기 (직접 Iterator 반복 처리기를 구현) |
loop라는 반복기를 통해 iterator object를 순회합니다.
1 | const [a,...b] = iter; |
실제로 destructuring은 배열을 해체하는 게 아니라 iterable을 해체하는 것입니다.
배열(문자열)을 넣었을 때도 destructuring이 정상작동하는 원리는 배열(문자열)에도 [Symbole.iterator]가 존재하다는 것을 알 수 있습니다.
이처럼 기본에 알고 있는 데이터의 구조도 ES5와는 차이가 있습니다.
1 | console.log(typeof ""[Symbol.iterator]); // true |
새롭게 추가된 iterator를 소비하는 Loop로 기존 제어문이 소비하는 형태의 Loop 입니다.
1 | // 제곱을 요소로 갖는 가상 컬렉션 |
데이터 자체가 외부의 반복기에 의존하지 않고 원하는 조건에 의하여 데이터를 생성할 수 있게 되었습니다.
Generator를 가장 쉽게 사용하는 예로는 iterator generator로써 사용하는 것입니다.
Iterator generator라는걸 풀어서 생각해보면 iterator 생성기라는 말인데,
위에서 같이 보신 것처럼 지금까지 iterator 생성기의 역할은 iterable이 하고 있었습니다.
iterator를 만든다는 입장에서 generator와 iterable은 같다고 볼 수 있다.
Iterable이 iterator를 만드는 방식Symbol.iterator
호출하는 것에 의해서 그 반환 값으로 호출 당할 때마다 iterator를 만들어 낸다.
Generator는 Generator를 호출할 때 마다 그 결과로 iterator를 만들어 낸다.
그럼 실제로 우리는 iterable, iterator 인터페이스를 모두 알아야만 사용 가능할까요?
그건 너무 불친절한 일입니다. 그래서 그걸 몰라도 사용할 수 있는 문법적 장치가 바로 generator
입니다.
1 | const generator = function*(max) { |
iterable, iterator은 ES6에서 완전히 새로 나온 개념이라서 이해하기가 쉽지 않았습니다. 하지만 반면에 새로운 기능이라서 기존의 기능을 확장해서 알아야 할 필요는 없다는 생각으로 이해하려고 노력했습니다.
평소 프로그래밍을 하다 보면 당연하게 데이터를 반복하려면 당연히 반복기가 필요하고, 그 반복기로 순회를 할 때는 멈출 수 없다는 게 당연한 생각이었습니다.
그래서 ES6에서의 generator가 이를 개선해줄 거라고 나왔을 때도 반가워하며 찾아봤지만 이해하기가 쉽지 않았습니다. 하지만 이 기회에 interface를 기반으로 이해하려니까 더 잘 이해가 되는 것 같습니다.
ES6의 데이터가 ES5의 데이터와 다르게 iterator의 interface를 갖고 있다는 것만으로도 많은 이해가 됐습니다.
이제는 generator를 활용하며 익숙해져야겠습니다. :)
]]>
객체 생성 패턴에 대한 간략한 정리이며, 하단부분에는 저의 생각을 작성했습니다. 읽는데 참고하시면 좋겠습니다. “자바스크립트 패턴과 테스트” 책을 참고하였습니다.
모듈 패턴은 데이터 감춤이 주목적인 함수
가 모듈 API
를 이루는 객체
를 반환하게 하는 구조이며, 두가지 기반의 모듈 생성 방식이 있습니다.
1 | var MyApp = MyApp || {}; |
API 반환하는건 임의 모듈과 같지만, 외부 함수를 선언하자마자 실행하는 방법입니다.
반환된 API는 이름공간을 가진 전역 변수에 할당된 후 해당 모듈의 싱글톤 인스턴스가 됩니다.
1 | var MyApp = MyApp || {}; |
외부 함수는 애플리케이션 기동 코드의 실행과 상관없이 코드가 작성된 지점에서 즉시 실행되기 때문에 함수 (즉시) 실행시 의존성을 가져오지 못하며 외부 함수에 주입할 수 없습니다.
1 | function Marsupial(name, nocturanl) { |
Marsupial함수는 주어진 인자를 내부적으로 생성할 인스턴스의 프로퍼티에 할당한다. 다음 줄에서 maverick, slider라는 이름으로 생성한 두 Marsupial 인스턴스는 각자 고유한 프로퍼티 값을 가진다.
자바스크립트 언어는 Marsupial 함수를 생성자 함수(new 키워드와 함께 사용하려고 작성한 함수)로 사용하라고 강요하지 않는다. 즉, new 키워드 없이 생성자 함수를 사용해도 이를 못하게 막을 보호 체계가 없다. 그래서 더러 개발자들은 파스칼 표기법(PascalCase)으로 생성자 함수를 따로 표기하여 구분하기도 한다.
1 | function Marsupial(name, nocturanl) { |
생성자 함수의 프로토타입에 함수를 정의하면 객체 인스턴스를 대량 생성할 때 함수 사본 개수를 한개로 제한하여 메모리 점유율을 낮추고 성능까지 높이는 추가 이점이 있습니다.
구조화를 위해 데이터의 객체화에 대한 필요성을 느끼기 시작했습니다. 그래서 흔히들 말하는 모듈패턴과 생성자 함수 프로토 타입을 익히는데 노력했습니다.
그리고는 특별히 이상한 점을 느끼지 못하고는 이리저리 패턴들을 적용하기에 이르렀습니다. 하지만 자바스크립트가 아시다시피 모든걸 묵인하고 인용하며 에러는 주지않고 “너 하고싶은대로 하거라” 라는 부처님 같은 녀석이라서 이것에 대한 오용과 남용(?)에 대해 잘 인지하지 못하고 있었습니다.
결국 전 이런 코드까지 쓰게 된거죠. 끔찍한 혼종을 만들어 버리고 맙니다.
1 | function Car() { |
당연하지만 생각처럼 동작지 않습니다. 두 경우 모두 setOil
을 찾아볼 수 없습니다. 모듈 패턴에서는 당연히 myCar가 Car의 인스턴스가 아니기 때문이며, 생성자 패턴에서는 return의 대상이 변경되어서 더 이상 생성자의 역할을 하지 못하기 때문입니다.
이를 통해 알게 된 점은 목적에 맞는 방법으로 객체를 생성해야 한다는 점입니다. 그러니 모듈 패턴은 유틸과 같은 한 개의 모듈로 동작하게 되는 경우가 적당하며, 여러 개의 인스턴스가 필요한 경우에는 생성자 패턴을 적용해야 합니다.
이름에서 그 패턴의 목적이 잘 녹아있음에도 불구하고, 자바스크립트의 특성에 의해 전혀 생각지도 못하게 쓰고 있었던 것 같습니다.
비록 실제 서비스가 Car
와 같이 쉽게 객체를 명확히 나눌 수 있는 것은 아닙니다. 객체 자체를 어떻게 구조조화 할건지는 참 어려운 일 중 하나인 것 같습니다. 하지만 이제 스스로도 객체를 생성할 때 좀 더 명확한 판단의 기준을 가지고 효율 적인 객체를 생성하는데 큰 도움이 될 것 같습니다. 😃
프로젝트는 서버사이드 렌더링이 필요했습니다. SSR(Server Side Rendering)이란 말 그대로 렌더링 되어야 할 파일들이 미리 서버에서 렌더링이 되어서 내려오는 것을 의미합니다.
리엑트는 Webpack을 통해 JSX로 형태로 구성된 파일들을 컴파일해서 브라우저가 읽을 수 있는 번들 파일을 만들어 화면을 렌더링하는 CSR(Client-side Rendering) 방식으로 렌더링하는 방식이 보통의 방식으로 사용되고 있습니다. 그러면 어떻게 리엑트가 SSR이 되도록 할까요?
리엑트 사용 시 컴포넌트를 렌더링 하기 위해 ‘react-dom’을 import 하는데, 이때 ‘react-dom’에는 renderToString
란 메서드가 존재합니다. 이는 리엑트 컴포넌트를 단순 문자열로 변환시켜서 서버에서 바로 사용할 수 있게 해줍니다.
서버는 Node.js express 구성되어있으며, 아래와 같이 기본 html 형태를 템플릿 리터럴 형태로 내려주었습니다. 이때 renderToString
에 리엑트 엘리먼트를 넘긴 형태를 root 엘리먼트의 자식으로 넘겨주는 방식으로 사용할 수 있습니다.
1 | import { renderToString } from 'react-dom/server' |
이렇게 작업한 ‘server.js’파일을 node server.js
가 아닌 babel-node server.js
로 Run하여 서버를 띄웠습니다. 또한, webpack을 통해 얻은 ‘bundle.js’를 추가해 상호 작용이 가능하도록 했습니다. 여기에서 preloadedState
는 서버에서 직접 받는 데이터로써 해당 데이터를 props로 전달해 component에서 활용합니다.
이 프로젝트는 크기가 크지 않아 처음에는 리덕스를 사용하지 않으려 했습니다. 하지만 앱의 기능 중 하나로 페이지 전체의 ‘언어변경’기능이 있었는데 이는 언어 변경 시 전체 페이지의 언어가 다른 국가의 언어로 변경되어야 했습니다. 그렇다는건 모든 텍스트가 직접 컴포넌트에 작성되어 있는 게 아니라 레퍼런스 값이어야 컨트롤이 가능하다는 것을 의미했습니다. 매 컴포넌트 작업 시 언어정보를 root에서 받아서 사용하는 건 매우 비효율적인 작업이 아닐 수 없었습니다.
컴포넌트에서 즉시 값을 사용할 수 있게 리덕스를 적용해보았습니다. ‘redux’의 createStore
를 사용해 서버에서 받은 값인 ‘preloadedState’와 store 를 컨트롤 하기 위한 ‘reducers’를 넘겨서 store를 생성합니다. 생성된 store를 react와 연결하는 역할을 하는 ‘react-redux’의 Provider
사용해 리엑트에 주입합니다.
1 | import { renderToString } from 'react-dom/server' |
이제 컴포넌트들은 store를 통해 값을 받아 사용할 수 있습니다. 랜더링 이후에도 ‘bundle.js’에도 동일하게 store를 사용해야하기 때문에 브라우저의 전역 변수인 window에 임의값(__PRELOADED_STATE__
)에 preloadedState를 넣어 주도록 합니다.
webpack-hot-middleware
를 사용하기로 했습니다. webpack-hot-middleware
는 webpack-dev-middleware
에 의존적이기 때문에 두가지가 설치되어야 합니다.1 | entry: [ |
server.js에 webpack-dev-middleware
와 webpack-hot-middleware
를 사용하도록 설정합니다.1
2
3
4
5
6
7
8
9
10var webpack = require('webpack')
var webpackConfig = require('./webpack.config')
var compiler = webpack(webpackConfig)
app.use(require("webpack-dev-middleware")(compiler, {
noInfo: true,
publicPath: webpackConfig.output.publicPath
}))
app.use(require("webpack-hot-middleware")(compiler))
store값 변경에도 hmr이 감지하도록 reducers를 바라보도록 합니다.1
2
3
4
5
6
7
8
9import reducers from './reducers'
const store = createStore(reducers)
if (process.env.NODE_ENV == 'development' && module.hot) {
module.hot.accept('./reducers', () => {
store.replaceReducer(require('./reducers').default);
})
}
위에 보시면 process.env.NODE_ENV == 'development'
가 있습니다. 이는 개발 환경에서만 이를 사용한다는 의미입니다. 이렇게 분기를 준 이유는 HRM은 일종의 가상 웹서버를 띄워서 그 웹서버를 제어해 컴포넌트 변화 시 화면을 리렌더링하는 방식입니다. 하지만 서버사이드 프로젝트로 다른 서버를 통해 렌더링 되게 프로젝트를 구성한 현재의 방식에는 적합하지 않습니다. 그렇기 때문에 랜더링 되는 부분을 개발 모드와 프로덕션 모드로 나눠줍니다.
1 | if (process.env.NODE_ENV === 'development') { |
개발 모드에서는 renderToString
을 사용하는 SSR이 아닌 CSR을 이용하도록 해서 HMR이 동작하도록 합니다.
Live Reload / Hot Module Replacement with Webpack Middleware
Setting Up Webpack Dev Middleware in Express
리엑트는 컴포넌트 방식으로 코드를 모듈화합니다. 그럼 컴포넌트란 무엇을 의미할까요? 저는 html, CSS, JS가 하나로 뭉쳐있어서 그것만 다른 곳에 놓더라도 바로 사용할 수 있는 것이라고 생각합니다. 하지만 현재의 상태로는 html과 JS는 합쳐 있지만, CSS는 그렇지 않습니다. 여전히 CSS는 다른 파일에서 작업 된 후 적용이 되어야 하죠. 그렇기 때문에 styled-component를 사용하기로 했습니다.
기본적으로 컴포넌트에서 styled-components를 import 해서 사용 할 수 있지만, 서버사이드 렌더링 시에는 추가로 작업해줘야 할 부분이 있습니다.
‘styled-components’의 ServerStyleSheet
를 통해 얻은 sheet 인스턴스의 collectStyles메서드로 렌더링 될 앱에 넘겨주어야 하며 sheet 인스턴스의 getStyleTags()
를 통해 렌더링 될 컴포넌트의 CSS를 얻어 head에 넣어줍니다.
1 | import { renderToString } from 'react-dom/server' |
어느 정도 환경은 세팅이 되었다는 판단으로 관련 파일들을 어떻게 구성해야 할지 고민이 많았습니다. 대략적인 구조를 오픈소스들을 참고하며 구조를 잡아 보았습니다.
redux-thunk
를 활용해서 비동기 처리를 합니다.맨 처음 프로젝트를 설정할 때 babel-node server.js
로 서버를 Run 한다는 생각으로 프로젝트를 구성하였습니다. 하지만 babel-node가 ‘프로덕션 모드에 사용돼서는 안 된다’는 사실을 알게 되었습니다. 오픈소스 프로젝트들이 production 모드에서 babel-node를 설정한 부분을 정확히 확인하지 않고 그대로 적용한 저의 실수였습니다.
오픈소스를 조사하면서 서버사이드 렌더링 시 서버를 Run 하는 방법은 크게 두 가지로 나눌 수 있었습니다.
1번 항목은 프로덕션 배포를 앞둔 저에게는 답이 될 수 없었습니다. 그리고 2번 항목은 저에게는 맞지 않았습니다. 해당 프로젝트는 API Server가 따로 없이 express 서버에 API가 같이 존재했으며, 해당 백엔드 작업은 제가 작업하지 않았거니와 백엔드를 잘 모르는 저에게 그 코드를 번들링해버리는 건 무책임한 행동이라고 생각이 들었습니다.
그래서 저는 생각 끝에 다음과 같은 방식으로 변경을 시도했습니다.
commonjs
형태로 번들링 되도록 설정합니다.결과적으로 제가 원하는 대로 server는 건드리지 않고 렌더링 되는 부분만 번들링해서 사용할 수 있게 되었습니다.
해당 글에는 언급되지 않았지만 이를 제외하고도 많은 문제가 있었고 해결하지 못한 문제들도 많았습니다. 솔직히 말하면 생각했던 방향대로 작업 되지 않아 굴복하고 동작하게끔만 설정해놓은 부분도 더러 있었습니다. 하지만 목표대로 서비스가 되는 프로젝트를 정상적으로 리엑트로 변경하는 작업은 성공이라고 생각됩니다. 시간이 될 때마다 작업한 프로젝트를 손보면서, 새로운 프로젝트에는 또 다른 방식 혹은 더 나은 방식으로 구성해서 작업 해봐야겠습니다. 글이 생각보다 길어져 작업 후 느낀점 같은 부분은 제 기술 블로그가 아닌 개인 블로그에 따로 정리를 해야 할 것 같습니다. 읽어주셔 감사합니다.
]]>자바스크립트에서 빠지지 않은 개념 중 하나인 클로저에 대해 알아봅시다.
자바스크립트를 사용한지 꽤 지났음에도 불구하고 다시 한번 클로저에 대한 포스팅을 올린다는것은 아직도 내가 클로저를 완벽하게 이해하지 못한다는 것을 의미하고 있는 것 같습니다.
누군가 나에게 클로저에 대하여 묻는다면, 똑부러지게 대답할 수 있을까요? ‘접근하려고 하는 함수의 생명주기가 종료됬지만, 내부함수가 참조 하고 있어서 그 함수에 접근할 수 있는 함수’입니다.
더 설명하라고 하면 아직 뭐라해야 할지 모르겠습니다. 다른 사람에게 설명할 수 있을 정도가 되야 아는 거라도 했던가요? 여러번 봤지만 오늘도 또 한번 공부해보려고 합니다. 같은 개념이지만 공부할때마다 이해하는 범위가 조금씩 확장되는것 같습니다. 이러한 이유로 또 한번 클로저를 공부해보려고 합니다.
예전에는 클로저는 무엇이다라는 것에 대해서만 집중했지만, 클로저를 잘 이해하기 위해서는 몇가지 개념들을 사전에 이해해야 한다고 생각합니다.
제가 생각하는 클로저를 알기 전에 이해해야할 몇 가지 키워드들입니다.
프로그래밍 언어에서 Scope는 변수와 매겨변수의 접근성 및 생존 기간을 제어하고, 이름 충돌 문제 및 메모리 관리를 해주는 중요한 개념 중 하나입니다.
대표적으로 C언어와 같은 언어는 블록 유효범위(Block Scope)
가 있습니다.
그러나 자바스크립트는 블록 유효범위
가 아닌 함수 유효범위(Function Scope)
를 가지고 있습니다.
(ES6 이후로는 자바스크립트도 블록 유효범위
도 사용가능합니다.)
함수 내에서 정의된 매개변수와 변수는 함수 외부에서는 유효하지 않지만, 내부에서 정의된 변수는 함수 내부 어느 곳에서도 접근할 수 있습니다.
자바스크립트는 Lexical scope
특성을 지닙니다.
Lexical scope란 Scope가 함수 실행시점이 아닌 함수 정의 시점에 정해진다는 의미입니다.
아래의 예를 보도록 하겠습니다.
같은 함수 형태를 보이지만, 좌측은 ‘nero’를 우측은 ‘zero’를 출력합니다.
좌측은 함수를 처음 선언하는 순간부터 log()의 name 변수는 자신의 상위 스코프의 변수 name을 참조합니다.
그래서 Wrapper()에서 log()를 실행하기전에 참조변수인 name을 변경했기에 ‘nero’를 출력하고 있습니다.
그러나 우측에서는 Wrapper()안에서 새로운 name 변수를 정의 하였습니다.
하지만 log()는 이미 상위 스코의 변수인 name을 참조하고 있기때문에 ‘zero’를 출력하게 되는 것입니다.
다시 한번 말하자면 함수가 실행될 때가 아니라 정의될 때에 변수를 참조하게 됩니다.
Execution Context
는 실행 가능한 코드를 형상화하고 구분하는 추상적인 개념으로, 코드가 실행되는 환경
입니다. 변수나 함수의 실행 컨텍스트는 다른 데이터에 접근할 수 있는지, 어떻게 행동하는지를 규정합니다.
자바스크립트 인터프리터가 어떤 함수를 실행시킬때마다 그 함수에 대한 새로운 실행 컨텍스트가 생성됩니다.
실행 컨텍스트가 생성되면 자바스크립트 엔진은 실행에 필요한 여러 정보들을 담을 객체를 생성합니다.
이를 변수 객체(Variable Object)
라고 합니다. 해당 컨텍스트에서 정의된 모든 변수와 함수는 이 객체에 존재합니다.
실행 컨텍스트는 포함된 코드가 모두 실행된 후에 파괴되는데, 이때 해당 컨텍스트 내부에서 정의된 변수와 함수도 함께 파괴됩니다.
Scope VS Context
Scope는 변수의 가시성과 관련이 있으며, Context는 함수가 실행되는 객체를 나타냅니다.
Scope Chain
은 일종의 리스트로서 중첩된 함수의 Scope의 참조를 차례로 저장하고 있는 개념입니다. Scope Chain의 목적은 실행 컨텍스트가 접근할 수 있는 모든 변수와 함수에 순서를 정의하는 것입니다.
컨텍스트가 함수인 경우 ‘활성화 객체(activation object)’를 변수 객체(Variable Object)로 사용합니다.
중첩된 함수에서 변수 객체가 값을 찾지 못햇을 경우 해당 부모 컨텍스트에서 찾고 다시 그 다음 부모의 컨텍스에서 찾으며, 전역 컨텍스트에 도달할 때 까지 계속합니다. 전역 컨텍스트의 변수 객체는 항상 스코프 체인의 마지막에 존재합니다.
내부 컨텍스트는 스코프 체인을 통해 외부 컨텍스트 전체에 접근할 수 있지만 외부 컨텍스트는 내부 컨텍스트에 대해 전혀 알 수 없습니다. 컨텍스트 사이의 연결은 선형이며 순서가 중요합니다. 각 건텍스트는 스코프 체인을 따라 상위 컨텍스트에서 변수나 함수를 검색할 수 있지만 스코프 체인을 따라 내려가며 검색할 수는 없습니다.
클로저는 가장 맨 앞에서 언급했듯 한마디로 '접근하려고 하는 함수의 생명주기가 종료됬지만, 내부함수가 참조 하고 있어서 그 함수에 접근할 수 있는 함수'라고 말할 수 있습니다.
1 | function funA() { |
funA()가 실행종료 되었으니 x또한 접근할수가 없어야 하는데 return 된 funB()를 받은 funcC()를 실행하니 x를 참조 할 수 있게 되었습니다.
클로저는 외부 함수의 변수를 참조 합니다. 하지만 값의 복사값은 아닙니다.
1 | var add_the_handlers = function(nodes) { |
이 함수는 노드를 클릭하면 해당 노드가 몇 번째 노드인지를 경고창으로 알려주는것이 함수의 목적이지만, 함수 전체 노드의 수만 보여주게 됩니다. 이유는 함수의 i가 함수가 만들어지는 시점의 i가 아니라 그냥 상위 스코프의 변수 i를 참조하기 때문입니다.
1 | var add_the_handlers = function(nodes) { |
이제 onclick에 함수를 연결되어 있는 내부 함수에서 새로 함수를 정의하고 여기에 i를 넘기면서 곧바로 실행시켰습니다. 실행된 함수는 add_the_handlers에 정의된 i가 아니라 넘겨받은 i의 값을 이벤트 핸들러 함수에 연결하여 반환합니다.
이 반환되는 이벤트 핸들러 함수는 onclick에 할당합니다. 함수가 원한는 목적대로 구현이 되었습니다.
보통 코드를 잘때면 클로저를 생각하지 않고 자연스러운 흐름에 의해 클로저를 사용하는 경우가 많습니다. 앞으로는 그러한 부분에 있어서 좀더 의식적으로 이해하고 코드를 작성하도록 해야 하겠습니다.
]]>참고 :
요즘 흔히 서버와의 통신시 ajax를 사용하고 있습니다.
하지만 <form>
을 통해서도 서버와의 통신이 가능한데 어떤 차이점이 있을까요?
form 방식과 ajax 방식의 대표적인 차이점이라고 하면 역시 ajax는 페이지의 전환 없이 비동기
로 서버와 통신을 할 수 있게 되었다는 점입니다. 이러한 비동기
를 통해 전체 페이지가 아닌 일부분만을 업데이트 할 수 있게 해줍니다.
사용자 인터렉션에 의헤 데이터를 서버로 전송하는경우 반드시 이벤트에 의해 발생되게 됩니다.
이때, ajax를 사용해 통신을 할 경우 이벤트를 처리하는 이벤트 리스너를 별도로 생성해야합니다.
하지만 form을 이용한다면 별도의 코드 없이 HTML에서 만으로 submit 이벤트를 발생시켜서 데이터를 서버에 전송할 수 있습니다.
form에 form submit이 가능한 버튼이 있고, form에 포커스가 있는 상태라면 enter
를 누르면 form submit이 됩니다.
1 | <!-- 폼 전송이 가능한 버튼 --> |
form에는 이처럼 편의 기능이 있어서, 두가지 경우를 합쳐서 사용한는 경우가 많습니다.
form을 이용해 submit 이벤트를 발생시키고, 이후에 form 전송이 막고 ajax를 통해 비동기
로 통신을 하는 방식이 많이 사용 되고 있습니다.
content-type은 request로 보내는 데이터가 무엇인지 알려줍니다.
기본적으로 form과 ajax의 content-type은 application/x-www-form-urlencoded
으로 key=value&key=value 형태로 전송됩니다.
form에서는 기본 전송인 application/x-www-form-urlencoded
방식과 파일전송 방식인 multipart/form-data
을 사용하고 있습니다.
하지만 ajax는 기본을 제외하고 다른 라이브러리들은 이를 application/json
과 같은 다른 방식으로 사용하는 경우가 많으니 인지하고 사용해야 합니다.
변경된 이유는 예전과 다르게 다계층
의 데이터를 주고 받아야 할 일이 많아졌기 때문입니다.
]]>참고 :
Bootstrap은 plugin을 생성할때 코드를 어떻게 구조화하는지 tab plugin을 통해 분석해보며 이해해 봅니다.
버전은 v3.3.2을 사용합니다.
현재 export, import 혹은 require와 같은 방법을 통해 module이나 component를 효율적으로 나눠서 사용하고 있습니다.
그렇다면 위의 기능들이 없었을때는 어떻게 효율적으로 코드를 분할할 수 있었을까요?
만약 코드 구조가 현재 지원하고 있는 방식이 아니라 다른방식일 경우 어떻게 사용할 수 있을지 알기 위함입니다.
기본적으로 Bootstrap은 plugin을 구현시 생성자 패턴
과 프로토타입 패턴
을 적용해서 구현하고 있습니다.
패턴 적용시 자신만의 인스턴스를 가지면서도 참조 방식을 통해 메서드를 공유해 메모리를 절약할 수 있습니다.
Bootstrap은 다음과 같이 익명함수로 모든 기능을 정의해서 스코프가 섞이지 않도록 합니다.
1 | +function ($) { |
여기서 +
다음에 나오는 부분을 파서는 표현식으로 처리하며, 위와 같이 즉시 실행함수에서 사용됩니다.
만약, +
가 없는 상태라면 파서가 function
을 함수 표현식이 아닌 선언식으로 인식하여 뒤에 나오는 ()
가 오류로 처리됩니다.
참고 : https://stackoverflow.com/questions/13341698/javascript-plus-sign-in-front-of-function-name/13341710
jQuery의 ‘on’을 API 통해 이벤트를 등록하고 있습니다.
여기서 ‘이벤트 위임(Event Delegation)’ 방식으로 이벤트를 등록하고 있습니다.
이벤트 위임(Event Delegation)
이벤트 위임은 이벤트 버블링의 장점을 활용하여, 이벤트 헨들러를 하나만 할당해서 해당 타입의 이벤트를 모두 처리하는 테크닉입니다. DOM요소 하나에만 접근해 거기에만 이벤트 핸들러를 할당하므로 비용이 훨씬 적으며, 메모리를 훨씬 조금 사용합니다.
이벤트 위임의 또 하나의 장점은 동적으로 생성되는 요소에 일일이 이벤트를 할당할 필요가 없다는 점입니다.
1 | $(document) |
이벤트 대상은 ‘[data-toggle=”tab”]’와 ‘[data-toggle=”pill”]’이며, 이벤트 처리는 ‘clickHandler’에서 진행됩니다.
이때 걸려 있는 이벤트는 ‘click’ 이벤트로, 이 이벤트는 ‘bs, tab, data-api’라는 namespace를 갖습니다.
이벤트에 대한 이벤트 처리가 이루어집니다.
1 | var clickHandler = function (e) { |
엘리먼트의 e.preventDefault()를 통해 기본기능을 막고, 선언된 Tab Plugin을 .call()로 ‘show’를 호출하며 현재 ‘this’를 교체합니다.
1 | function Plugin(option) { |
우선, 대상되는 요소의 data정보에 ‘bs.tab’이 있는지 확인하는 작업을 하는데 이는 이전에 이벤트가 바인딩 되어 있는지 체크하는 작업입니다.
만약 이벤트가 바인딩이 되어 있지 않다면 선언된 Tab을 new 통해 인스턴스를 생성하여 해당 요소의 data에 ‘bs.tab’의 값으로서 할당합니다.
이 작업은 이벤트가 바인딩 되어있는지를 확인하는 flag로써 활용할 수 있으며, 실제로 기능을 하는 함수로 사용됩니다.
마지막으로 option으로 넘어온 ‘show’를 실행시킵니다. (data.show())
1 | var old = $.fn.tab |
jQuery의 Plugin을 설정시 $.fn에 연결합니다. jQuery에서는 이부분에 연결된 기능을 prototype으로 연결시킵니다.
다음으로는 Constructor에 연결합니다. 그 이유는 일부 동일한 다른 함수의 경우 (popover의 경우에는 tooptip을 상속받아서 사용)에는 다른 기능을 상속받아 사용하기 때문에 어떤 유형인지 확인이 어렵습니다. 그래서 Constructor를 사용해 어떤 유형인지 확인하는데 사용될 수 있습니다.
참고 : https://stackoverflow.com/questions/19680895/bootstrap-constructor
$.fn.tab.noConflict를 사용해 이전에 $.fn.tab에 사용되고 있었던 플러그인을 사용할 수 있습니다. (예전에 동일한 이름으로 플러그인을 사용하고 있다는 가정하에)
1 | var Tab = function (element) { |
부트스랩은 프로토타입 상속에 의해 기능들이 정의 되어 있습니다.
인스턴스를 생성하기위한 Tab 클래스를 생성하고, 필요한 상수를 선언하며, 기능들을 prototype에 추가합니다.
앞서 설명된 플러그인에서 탭 플러그인 설정에서 사용되는 클래스 입니다.
Tab.prototype.show에서는 요소를 노출하기 위한 action을 trigger하는 기능들을 모아놓았으며,
Tab.prototype.activate에서는 실제 요소를 활성화 시키는 .active 클래스를 컨트롤 합니다.
“높은 응집도 낮은 결합도”에 대한 이해가 조금이나마 와닿은것 같습니다.
예전에는 머리로 이했었던 느낌이지만 요즘은 이런게 필요하구나 라는 것을 직접 느끼고 있는것 같습니다.
사실 디자인 패턴을 공부하면서 직접 적용해서 작업하는건 쉽지 않는다고 판단이 들어서,
유명한 라이브러리들을 조금씩 열어보면서 이들은 어떤식으로 구조화를 했었고, 어떻게 진화하고 있는지 알아보려고 했습니다.
우선 제가 가장 많이 쓰고 있는 것중에 하나인 bootstrap중에서 가장 짧은 tab을 살펴 보았습니다.
더 공부해야겠지만, 이러한 방식을 더 잘 이해하면 현재 사용되는 모듈 방식을 좀 더 잘 사용할 수 있지 않을까 생각됩니다.
인트로에서도 언급되었듯 업데이트의 시작은 this.setState에서 부터 다뤄집니다.
ReactComponent
에서 컴포넌트를 상속받았고, updater
속성을 받습니다. (파트3)shouldComponentUpdate
가 지정되지 않은 경우에도 컴포넌트가 기본적으로 업데이트되는 이유shouldComponentUpdate
메소드가 호출되어 shouldUpdate
의 결과 값으로 재할당됩니다.componentWillUpdate
가 명시되어 있다면 그것을 호출합니다.
컴포넌트를 리랜더링합니다.
componentDidUpdate
호출을 대기열에 추가합니다.
shouldUpdateReactComponent -> ReactReconciler.receiveComponent -> ReactDOMComponent.receiveComponent
DOM 컴포넌트 인스턴스에 전달받은 엘리먼트를 할당하고 update 메소드를 호출합니다.
updateComponent 메소드는 실제로 prev와 next props를 기반으로 2가지 동작을 수행합니다.
‘ExampleApplication’는 button, ChildCmp, text string를 자식으로 가지고 있습니다.
해당 컴포넌트의 자식이 ‘content’형태인지 ‘complex’형태 인지 판단합니다.
complex인 경우
content인 경우
이전에 대기열에 넣어두었던 componentDidUpdate
를 호출합니다. (파트 12)
componentDidUpdate
를 호출할 수 있는 이유componentDidUpdate
를 호출되는 것입니다. (파트 10)저는 이 글들을 번역하고 공부하면서도 실제로 100% 이해했다고는 말할 수 없었습니다. 다만 제가 리엑트 이해도를 100%로 정했을때 스스로의 이해도가 10%정도의 수준이라면 15%정도 되지 않았을까라는 생각정도만 가지고 있습니다.
그 이유로는, 저는 무언가를 읽고 학습할때 글보다는 코드로 이해하는게 훨씬 이해도가 높습니다. 그래서 제가 처음보는 말들보다는 익숙한 예약어들이 많이 등장할때 훨씬 이해가 잘되는 느낌이었습니다.
그 예약어로는 라이프사이클 메소드를 들 수가 있었고, 제가 가지고 있는 라이프사이클 순서 사이사이에 위에서 알아본 일련의 과정들을 집어넣어보면서 이해했던것 같습니다.
제법 오랜시간동안 이거 한다고 다른걸 안하고 있었으니, 이제는 실제로 써먹을 수 있을지 간단히 리엑트 프로젝트를 한번 해봐야겠습니다!
]]>처음 뭔가를 배울때는 아무것도 모르는 상태에서 책이나 다른 문서의 예제를 약간씩 따라 치면서 익히곤 합니다.
그러다 스스로 뭔가 대충 “아, 정확히는 모르겠지만 이건 요런 방식이구나…” 라고 생각되는 시점이 오면 “그런데 이게 어떻게 되는거지?”라고 생각하게 되는 것 같은데 이때 Under-the-hood-ReactJS를 알게되었습니다.
영어실력이 좋지 않아서 번역기의 도움을 받으면서 몇 번씩 읽어보았고 제가 이해한 방식으로 해석하는 작업을 몇 차례 반복했습니다. 한달정도의 시간동안 틈틈히 공부 했지만, 그럼에도 이해 안된 부분들이 조금씩은 있어서, 번역된 내용중에 ‘한글인데 뭔말인지 모르겠다…’ 란 부분은 바로 그러한 부분이니 참고 부탁드립니다. :)
현재 저는 올초부터 리엑트를 공부하기 시작했지만, 실제 총 시간으로 따지만 약 두달 정도만 리엑트에 집중했던것 같습니다. 이것을 말하는 이유는 저와 비슷한 초보자들도 읽어볼 수 있을 만한 글이라는것을 말씀드리고 싶어서 입니다.
다음은 저자가 14개의 파트로 나눠서 작업한 부분들을 번역하면서 간단히 요약해 놓은 부분입니다.
전체적으로 두가지 프로세스를 다루고 있습니다 : ‘마운트’와 ‘업데이트’
코드에 상응하는 전체 스키마가 있는 것은 아닙니다.
마지막에 보이는 샘플 코드는 앞으로 나오는 파트들에 꾸준히 예제로 쓰이고 있습니다.
각 파트별로 해당되는 스키마를 잘라서 보여주기는 합니다만 전체 스키마와 샘플코드를 항상 띄워 놓고 다음 파트들을 살펴보면 보면 이해가 좀 더 수월한 것 같습니다.
ReactMount
를 사용하기 위한 인터페이스일 뿐입니다. 따라서 ReactDOM.render
를 호출하면 기술적으로 ReactMount.render
를 호출합니다.마운팅이란?
DOM 엘리먼트를 생성하고 제공된 container에 삽입하여 리엑트 컴포넌트를 초기화하는 작업입니다.
JSX를 작성해보셨다면, JSX 사용시 하나의 root 엘리먼트로 감싸주어야 한다는 사실을 알고있으실 텐데요. 그게 바로 TopLevelWrapper라고 생각하시면 될 것 같습니다.
JSX는 내부 컴포넌트로 변환됨
내부 컴포넌트의 종류
Virtual DOM
마운트라는 개념을 잘 생각해보고, 내부 컴포넌트의 종류 잘 기억하는게 좋은것 같습니다.
ReactUpdates
모듈을 통해 컴포넌트 인스턴스가 리엑트 생태계에 연결 되도록 합니다.
리엑트는 chunks 형태로 업데이트를 수행하기 때문에 사전/사후 처리를 한번만 적용할 수 있도록 트렌젝션을 활용합니다.
리엑트에서 트렌잭션이 어떻게 사용되고 있는지 말하고 있습니다.
Transaction
모듈에서 확장하여 사용되며, 어떤 트렌잭션 래퍼로 감싸는지에 따라 목적이 달라집니다.ReactDefaultBatchingStrategyTransaction
레퍼에는 ‘FLUSH_BATCHED_UPDATES’, ‘RESET_BATCHED_UPDATES’래퍼가 존재합니다.
initialize
메소드가 비어있지만, close
에는 ReactUpdates.flushBatchedUpdates
를 호출합니다.ReactUpdates.flushBatchedUpdates
는 dirty 컴포넌트에 대한 검증을 시작합니다. (파트 8 이후)해당 컴포넌트가 마운팅 되기전에 일련 작업들을 한번에 처리하기위해 트렌젝션을 사용해서 진행되고 있습니다.
파트 1의 트렌젝션에서의 메소드 실행부분에서 발생되는 또 다른 트렌잭션.
여기에서는 트랜젝션이 다음을 제어하는데 사용됩니다.
initialize
할 때 제한하고, close
할때 사용하도록 설정합니다.파트 2의 메소드 실행시 ReactReconciler.mountComponent
로 DOM에 넣을 준비가 된 마크업을 리턴합니다.
랜더링을 위한 마크업을 만들기 전에 다른 영역에 영향을 미칠 수 있는 부분을 제한하는데 이를 트랜젝션을 활용하고 있다는 것을 알 수 있습니다.
컴포넌트의 트리에 처음 삽입되는 컴포넌트는 TopLevelWrapper 입니다.
ExampleApplication 컴포넌트에 대한 ReactCompositeComponent 인스턴스 생성(new ExampleApplication())
초기 마운트 수행
render 메소드에서 얻은 엘리먼트를 기반으로 그 자식에 대한 가상 컴포넌트(ReactDOMComponent 인스턴스)를 생성합니다.
constructor
, 라이프 사이클 메소드인 componentWillMount
와 componentDidMount
를 연결시켜 볼 수 있습니다.HTML 태그에서 video, form, textarea의 경우에는 추가 래핑을 진행합니다.
내부 props가 올바르게 설정되었는지 확인하기 위해 유효성 검사 메서드를 호출합니다.
실제 HTML 엘리먼트는 document.createElement에 의해 생성됩니다.
자식 요소들을 관리하는 ReactMultiChild
모듈의 mountChildren
메소드를 통해 자식요소들을 인스턴스화하고 마운팅합니다.
전반적인 마운팅 프로세스에 대한 전체적인 확인
constructor
호출)React.createElement
로 엘리먼트 생성ReactDOMComponent
로 인스턴스화 함마운팅 메소드 실행의 결과로 document에 세팅할 준비가 된 HTML 엘리먼트를 가질 수 있었지만, 실제로는 HTML 마크업이 아닌, children, node(실제 DOM 노드)등의 필드를 가진 데이터 구조입니다.
parentNode.insertBefore로 인해 드디어 document에 엘리먼트가 추가됩니다.
아직 트렌젝션의 중간 단계이기 때문에 상위 두 단계의 트렌젝션을 모두 닫아줍니다(파트2).
여기까지가 마운팅 프로세스에 대한 일련의 과정입니다.
]]>리엑트를 공부하다 보면 자연스럽게 리덕스를 접하게 됩니다.
리덕스를 이해하기 위해 많은 문서를 접하지만, 저 같은 경우에는 코드랑 연결이 되어 있을 때 가장 잘 이해하는 것 같습니다. 그래서 코드로 리덕스를 접근해보려고 합니다.
“리덕스는 뭐다.”라는 부분은 많은 문서에서 잘 정리되어 있고, 저 또한 그런 곳을 많이 참조하여 읽습니다.
그래서 저는 제가 이해한 만큼에서 직접 사용되는 리덕스의 단어들을 코드로써 이야기해 보려고 합니다.
가장 먼저 스토어 입니다. 스토어는 리덕스를 처음 접하자마자 들을 수 있는 단어로 state(상태 값)총괄하는 부분입니다. state는 각 컴포넌트가 갖는 특정한 값일 수도 있으며, UI의 상태를 나타낼 수 있습니다.
1 | const initState = { |
컴포넌트들은 이 스토어에 액션을 dispatch하고, subscribe 하며 상태를 반영할 수 있습니다.
액션, dispatch는 천천히 알아보도록 하죠.
스토어에 대한 값을 변경하기 위해서 어떤 신호를 보내주어야 하는데 바로 그게 바로 액션입니다.
예를 들어 컴포넌트에서 어떤 이벤트가 발생한 경우 컴포넌트가 “이벤트가 발생했어! 값을 변화시켜줘!”라고 말하는 상태에 대한 변경 값이 들어 있습니다.
액션은 무엇이냐? 그냥 객체입니다.
다만, 한가지 알아두어야 하는 건 모든 액션객체는 ‘type’이라는 값을 지닙니다.1
2
3{
type: "INCREMENT"
}
추가로 이벤트 발생 후 변경할 값을 같이 전달해 주려면 아래와 같이 단순히 값을 추가하면 됩니다.
1 | { |
리듀서는 액션을 받아서 스토어의 값을 처리하는 작업을 해줍니다.
사실, 스토어는 그냥 저장소일 뿐 아무것도 하지 않습니다. 이 리듀서가 바리바리 움직여서 스토어의 값을 변화시켜줍니다.
그럼 코드로서는 어떨까요? 리듀서는 스토어의 값을 변형시켜준다고 했습니다.
위에서 언급된 것처럼 스토어는 값을 담은 객체일 뿐이고 리듀서는 그 값을 변경한다고 했으니 스토어라는 객체의 값을 변경시켜주는 함수일 뿐이죠.
대신 액션을 받아서 어떤 값인지 판단해서 어떻게 변경해줘야 할지 분기해야 햐죠.
그럼 그 부분을 코드로 작성해보면 아래와 같이 됩니다.
1 | // 스토어 |
사실 액션 설명할 때 액션 생성자를 같이 설명하는데, 리듀서 다음에 나온 이유는 제가 이해한 흐름대로 나열하기 때문이죠.
처음 예제들을 참고하며 공부했을 때 혼동이 있었던 부분이었습니다.
액션을 리듀서에 전달할 때 “액션 타입을 지정하는 파일(ActionTypes.js)을 만들고, 액션 생성자를 통해서, 리듀서로 전달한다.”라는 말이 직관적으로 머리에 들어오지 않았습니다.
그래서 저는 저 나름대로 이해를 했습니다.
Q. 액션 생성자를 만드는 이유.
A. 매번 객체(액션 객체)를 만들기 귀찮아서.
Q. ActionTypes.js란 파일을 만드는 이유.
A. 매번 문자열로 전달하면 귀찮아서.
액션 생성자와 디스패치의 설명 순서는 좀 애매했습니다.
액션 생성자를 설명할 때 “귀찮다”가 언제 인지를 알아야 사용의 편리함을 아니까요.
바로 디스패치를 통해 액션을 전달할 때 귀찮습니다.
이 단락에 액션 생성자 코드를 넣는 건 이상하지만 흐름으로는 이게 맞아 보여서 여기에 쓰겠습니다.
정말 이름 그대로 액션이란 객체를 만들어주는 객체(액션) 생성자일 뿐입니다.
1 | // () => ({}) 은, function() { return { } } 와 동일한 의미 |
디스패치는 컴포넌트에서 특정한 이벤트 발동 시 실행이 됩니다.
디스패치는 액션(객체)을 스토어로 넘겨주는 이벤트 트리거라고 생각하면 될 것 같습니다.
1 | import * as actions from '../actions'; |
onIncrement()가 실행되면 actions.increment()란 액션 생성자를 통해 액션객체를 생성하고 그 액션객체를 dispatch로 리듀서에 전달되는 겁니다.
그렇다면 이 모든 과정에서 어떻게 액션객체를 컴포넌트에서 스토어로 스토어에서 컴포넌트로 공유가 될 수 있을까요? 바로 ‘react-redux’ 가 해줍니다.
redux 에서 store는 단 하나입니다. 그렇다면 이게 어디에 위치하는 게 좋을까요?
바로 최상단에서 생성되고 하위로 흐르는 방식입니다.
이는 Provider로 전달할 수 있습니다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// Redux 관련 불러오기
import { createStore } from 'redux'
import reducers from './reducers';
import { Provider } from 'react-redux';
// 스토어 생성
const store = createStore(reducers);
// Provider를 사용해서 스토어를 전달합니다.
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
이렇게 하면 스토어가 컴포넌트로 흐르죠.
그럼 컴포넌트에서는 스토어의 state를 어떻게 받으며, action은 어떻게 스토어로 전달이 될까요?
connect()를 통해 state를 가지며 dispatch를 사용할 수 있습니다.
1 | import exampleComponent from '../components/exampleComponent'; |
미들웨어는 이름답게 중간에서 뭔가를 처리해줍니다. 그 중간은 바로 액션이 리듀서로 전달되는 사이입니다.
액션 생성자를 통해 액션이 생성되고 dispatch를 통해 리듀서로 전달되기 전에 어떠한 동작을 추가할 수 있습니다.
예를 들어 콘솔을 기록할 수도 있고 액션을 취소시킬 수도 있죠. 다른 액션을 디스패치 할 수도 있습니다.
미들웨어는 redux 의 applyMiddleware()를 사용해 적용할 수 있습니다.
1 | import { createStore, applyMiddleware } from 'redux'; |
아래의 그림은 이해한 부분들을 그림으로 그려놓은 Flow입니다.
]]>React를 접했을 때 immutable.js도 같이 알 되었는데, 당시에는 정확히 왜 필요한지 몰랐습니다.
하지만 몇 번 보다 보니 그 이유를 알게 되었고, 제가 이해한 부분과 사용법을 정리해보려고 합니다.
처음 ‘immutable:불변성’이란 단어는 낯설고 머리에 잘 와닿지 않았습니다.
불변성…불변성… 발음도 힘듭니다. 😧
저는 머리가 좋지 않은 관계로 한번은 더 풀어서 이해해야 했습니다.
내가 만드는 객체는 절대 변하지 않아, 그러니까 수정하려면 반드시 다른 걸 하나 새로 만들어야 해!
React에서는 state 혹은 props에 변화가 감지되면 컴포넌트를 리랜더링합니다.
그런데 레퍼런스 값으로 데이터를 가지고 있는 정보들은 값이 달라져도, 레퍼런스가 같기 때문에 React는 값의 변화를 인지하지 못합니다.그래서 객체를 새로 만들어주어야 하는데 이때 immutable.js을 통해 더욱 손쉽게 데이터를 변경할 수 있습니다.
우선 immutable.js 없이 값을 수정하는 예제를 한번 확인해보시죠.
React에서는 위에도 언급했듯 레퍼런스를 활용하는 데이터의 경우에는 새로운 값을 만들어 연결해주어야 하는데, 다음 예제와 같이 데이터가 배열인 경우 특정 idx 내부의 값을 변경할 때는 아래와 같이 적용해 볼 수 있습니다.
1 | Items: [ |
단순히 값을 변경해주는 부분인데 매우 번거로울 뿐만 아니라, 데이터가 구조가 복잡하거나 많을 때는 더욱 많은 리소스 사용하게 될 것입니다.
1 | Items[idx].tempValue = !Items[idx].tempValue; |
이렇게 사용해도 정상작동되면 참 좋을 텐데요? 😏
immutable 코드를 쓰기 전엔 8줄의 코드가 3줄로 줄었습니다.
1 | Items = Itmes.update(idx, (item) => { |
간단히 설명하면, immutable.js에서 제공되는 update를 활용하여 변경하고자 하는 idx를 받아 해당 위치의 값을 set로 수정하는데get를 사용해 현재 값을 받아 변경합니다.
immutable을 사용하면 객체는 MAP으로 감싸주어야 합니다.
배열의 경우에는 List로 감싸줍니다.
1 | // basic object |
1 | /* 자바스크립트 객체로 변환하기 */ |
1 | /* 값 설정하기 */ |
1 | /* 아이템 제거 */ |
리엑트는 컴포넌트들을 작성할때 어떤식으로 스타일을 입힐 수 있을까요?
React도 다른 웹구현 방식과 동일하게 Head에서 link 태그를 통해 css를 불러와 적용할 수 있습니다.
물론 style 태그를 통해 직접 스타일을 작성하는 것도 가능하죠.
편리하긴 하지만 이 방식은 Class name의 중복성을 피할수가 없습니다.
게다가 이렇게 사용하면 컴포넌트 중심으로 구현되는 React와는 조금 동떨어진 느낌입니다.
글보다는 우선 예제를 보시죠
1 | import React from 'react'; |
위의 코드처럼 .css를 import로 불러와서 컴포넌트에서 .으로 접근하여 사용하는 방식입니다.
보다 컴포넌트별로 구분할 수 있고 접근하기도 좀 더 명확해 보입니다.
CSS Module는 webpack을 이용하여 사용할 수 있습니다.
저도 webpack을 잘하면 떡주무르듯 위의 링크에서 적용하라는 대로 해서 사용할 수 있겠는데,
아직 webpack을 잘 모르는 상태라서 생각보다 적용이 쉽지 않네요.
그래서 create-react-app에서 eject를 해서 설정을 변경해서 적용해보았는데 저도 1번 방식이으로 하다가 컴포넌트 단위로 작업해보니 조금 새로웠습니다.
여기에 더불어 여러 classname을 적용할때 es6 template literals을 사용하지 않고도 편리하게 적용할 수 있는 classnames을 사용하거나 sass나 less 같은 preprocessor를 적용하여 사용할 수 있습니다.
styled-components는 컴포넌트 자체가 스타일을 입고 태어난다고 말할 수 있을것 같습니다.
아래와 같이 사용됩니다.
1 | const Button = styled.button` |
해당 스타일 방식은 컴포넌트 자체에 스타일을 입히기 때문에 props를 받아서 바로 스타일에 적용할 수도 있다는게 장점입니다.
]]>v1부터 현재까지 코드가 어떻게 변해왔는지 확인해봅니다.
reduceRight는 reduce를 내림차순으로 수행하는 함수입니다.
[~v0.3.3]
1 | _.reduceRight = function(obj, memo, iterator, context) { |
[~v0.5.1]
1 | _.reduceRight = function(obj, memo, iterator, context) { |
[~v0.6.0]1
2
3
4
5_.reduceRight = function(obj, memo, iterator, context) {
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) return obj.reduceRight(_.bind(iterator, context), memo);
var reversed = _.clone(_.toArray(obj)).reverse();
return _.reduce(reversed, memo, iterator, context);
};
[~v1.1.0]1
2
3
4
5
6
7
8_.reduceRight = function(obj, iterator, memo, context) {
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
return obj.reduceRight(iterator, memo);
}
var reversed = _.clone(_.toArray(obj)).reverse();
return _.reduce(reversed, iterator, memo, context);
};
[~v1.1.2]1
2
3
4
5
6
7
8_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
return obj.reduceRight(iterator, memo);
}
var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse();
return _.reduce(reversed, iterator, memo, context);
};
[~v1.1.3]1
2
3
4
5
6
7
8_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
}
var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse();
return _.reduce(reversed, iterator, memo, context);
};
[~v.1.1.4]1
2
3
4
5
6
7
8
9_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
if (obj == null) obj = [];
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
}
var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse();
return _.reduce(reversed, iterator, memo, context);
};
[~v1.2.3]1
2
3
4
5
6
7
8
9
10
11_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
}
var reversed = _.toArray(obj).reverse();
if (context && !initial) iterator = _.bind(iterator, context);
return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator);
};
[~v1.4.0]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
return arguments.length > 2 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
}
var length = obj.length;
if (length !== +length) {
var keys = _.keys(obj);
length = keys.length;
}
each(obj, function(value, index, list) {
index = keys ? keys[--length] : --length;
if (!initial) {
memo = obj[index];
initial = true;
} else {
memo = iterator.call(context, memo, obj[index], index, list);
}
});
if (!initial) throw new TypeError('Reduce of empty array with no initial value');
return memo;
};
[~v1.7.0]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16_.reduceRight = _.foldr = function(obj, iteratee, memo, context) {
if (obj == null) obj = [];
iteratee = createCallback(iteratee, context, 4);
var keys = obj.length !== + obj.length && _.keys(obj),
index = (keys || obj).length,
currentKey;
if (arguments.length < 3) {
if (!index) throw new TypeError(reduceError);
memo = obj[keys ? keys[--index] : --index];
}
while (index--) {
currentKey = keys ? keys[index] : index;
memo = iteratee(memo, obj[currentKey], currentKey, obj);
}
return memo;
};
[~v1.8.0]1
_.reduceRight = _.foldr = createReduce(-1);
v1부터 현재까지 코드가 어떻게 변해왔는지 확인해봅니다.
[~v0.1.0]1
2
3
4
5
6inject : function(obj, memo, iterator, context) {
_.each(obj, function(value, index) {
memo = iterator.call(context, memo, value, index);
});
return memo;
}
[~v0.2.0]1
2
3
4
5
6_.reduce = function(obj, memo, iterator, context) {
_.each(obj, function(value, index) {
memo = iterator.call(context, memo, value, index);
});
return memo;
};
[~v0.3.3]1
2
3
4
5
6
7_.reduce = function(obj, memo, iterator, context) {
if (obj && obj.reduce) return obj.reduce(_.bind(iterator, context), memo);
_.each(obj, function(value, index, list) {
memo = iterator.call(context, memo, value, index, list);
});
return memo;
};
[~v0.5.1]1
2
3
4
5
6
7_.reduce = function(obj, memo, iterator, context) {
if (obj && _.isFunction(obj.reduce)) return obj.reduce(_.bind(iterator, context), memo);
_.each(obj, function(value, index, list) {
memo = iterator.call(context, memo, value, index, list);
});
return memo;
};
[~v0.6.0]1
2
3
4
5
6
7_.reduce = function(obj, memo, iterator, context) {
if (nativeReduce && obj.reduce === nativeReduce) return obj.reduce(_.bind(iterator, context), memo);
each(obj, function(value, index, list) {
memo = iterator.call(context, memo, value, index, list);
});
return memo;
};
[~v1.1.3]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
var initial = memo !== void 0;
if (nativeReduce && obj.reduce === nativeReduce) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
}
each(obj, function(value, index, list) {
if (!initial && index === 0) {
memo = value;
} else {
memo = iterator.call(context, memo, value, index, list);
}
});
return memo;
};
[~v1.2.3]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduce && obj.reduce === nativeReduce) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
}
each(obj, function(value, index, list) {
if (!initial) {
memo = value;
initial = true;
} else {
memo = iterator.call(context, memo, value, index, list);
}
});
if (!initial) throw new TypeError('Reduce of empty array with no initial value');
return memo;
};
[~v1.7.0]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var reduceError = 'Reduce of empty array with no initial value';
_.reduce = _.foldl = _.inject = function(obj, iteratee, memo, context) {
if (obj == null) obj = [];
iteratee = createCallback(iteratee, context, 4);
var keys = obj.length !== +obj.length && _.keys(obj),
length = (keys || obj).length,
index = 0, currentKey;
if (arguments.length < 3) {
if (!length) throw new TypeError(reduceError);
memo = obj[keys ? keys[index++] : index++];
}
for (; index < length; index++) {
currentKey = keys ? keys[index] : index;
memo = iteratee(memo, obj[currentKey], currentKey, obj);
}
return memo;
};
[~v1.8.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
25var createReduce = function(dir) {
// Wrap code that reassigns argument variables in a separate function than
// the one that accesses `arguments.length` to avoid a perf hit. (#1991)
var reducer = function(obj, iteratee, memo, initial) {
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length,
index = dir > 0 ? 0 : length - 1;
if (!initial) {
memo = obj[keys ? keys[index] : index];
index += dir;
}
for (; index >= 0 && index < length; index += dir) {
var currentKey = keys ? keys[index] : index;
memo = iteratee(memo, obj[currentKey], currentKey, obj);
}
return memo;
};
return function(obj, iteratee, memo, context) {
var initial = arguments.length >= 3;
return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial);
};
};
_.reduce = _.foldl = _.inject = createReduce(1);
v1부터 현재까지 코드가 어떻게 변해왔는지 확인해봅니다.
[~v0.1.0]1
2
3
4
5
6
7
8map : function(obj, iterator, context) {
if (obj && obj.map) return obj.map(iterator, context);
var results = [];
_.each(obj, function(value, index) {
results.push(iterator.call(context, value, index));
});
return results;
}
[~v0.5.0]1
2
3
4
5
6
7
8_.map = function(obj, iterator, context) {
if (obj && _.isFunction(obj.map)) return obj.map(iterator, context);
var results = [];
_.each(obj, function(value, index, list) {
results.push(iterator.call(context, value, index, list));
});
return results;
};
1 | _.isFunction = function(obj) { |
function이 가지고 있는 property를 확인하는 방식으로 만들어져 있습니다.
[~v0.6.0]1
2
3
4
5
6
7
8_.map = function(obj, iterator, context) {
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
var results = [];
each(obj, function(value, index, list) {
results.push(iterator.call(context, value, index, list));
});
return results;
};
[~v1.1.1]1
2
3
4
5
6
7
8_.map = function(obj, iterator, context) {
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
var results = [];
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
});
return results;
};
[~v1.1.4]1
2
3
4
5
6
7
8
9_.map = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
});
return results;
};
[~v1.2.4]1
2
3
4
5
6
7
8
9
10_.map = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
});
if (obj.length === +obj.length) results.length = obj.length;
return results;
};
[~v1.4.2]1
2
3
4
5
6
7
8
9_.map = _.collect = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
});
return results;
};
[~v1.8.0]1
2
3
4
5
6
7
8
9
10
11_.map = _.collect = function(obj, iteratee, context) {
iteratee = cb(iteratee, context);
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length,
results = Array(length);
for (var index = 0; index < length; index++) {
var currentKey = keys ? keys[index] : index;
results[index] = iteratee(obj[currentKey], currentKey, obj);
}
return results;
};
v1부터 현재까지 코드가 어떻게 변해왔는지 확인해봅니다.
[~v0.1.0]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25window._ = {
each : function(obj, iterator, context) {
var index = 0;
try {
if (obj.forEach) {
obj.forEach(iterator, context);
} else if (obj.length) {
for (var i=0; i<obj.length; i++) iterator.call(context, obj[i], i);
} else if (obj.each) {
obj.each(function(value) { iterator.call(context, value, index++); });
} else {
var i = 0;
for (var key in obj) {
var value = obj[key], pair = [key, value];
pair.key = key;
pair.value = value;
iterator.call(context, pair, i++);
}
}
} catch(e) {
if (e != '__break__') throw e;
}
return obj;
}
}
[~v0.3.1]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19_.each = function(obj, iterator, context) {
var index = 0;
try {
if (obj.forEach) {
obj.forEach(iterator, context);
} else if (obj.length) {
for (var i=0, l = obj.length; i<l; i++) iterator.call(context, obj[i], i, obj);
} else if (obj.each) {
obj.each(function(value) { iterator.call(context, value, index++, obj); });
} else {
for (var key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) {
iterator.call(context, obj[key], key, obj);
}
}
} catch(e) {
if (e != '__break__') throw e;
}
return obj;
};
[~v0.4]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19var breaker = typeof StopIteration !== 'undefined' ? StopIteration : '__break__';
_.each = function(obj, iterator, context) {
var index = 0;
try {
if (obj.forEach) {
obj.forEach(iterator, context);
} else if (obj.length) {
for (var i=0, l=obj.length; i<l; i++) iterator.call(context, obj[i], i, obj);
} else {
var keys = _.keys(obj)
, l = keys.length;
for (var i=0; i<l; i++) iterator.call(context, obj[keys[i]], keys[i], obj);
}
} catch(e) {
if (e != breaker) throw e;
}
return obj;
};
v0.4.1
v0.4.3
v0.4.7
[~v0.5]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16_.each = function(obj, iterator, context) {
var index = 0;
try {
if (obj.forEach) {
obj.forEach(iterator, context);
} else if (_.isNumber(obj.length)) {
for (var i=0, l=obj.length; i<l; i++) iterator.call(context, obj[i], i, obj);
} else {
var keys = _.keys(obj), l = keys.length;
for (var i=0; i<l; i++) iterator.call(context, obj[keys[i]], keys[i], obj);
}
} catch(e) {
if (e != breaker) throw e;
}
return obj;
};
v0.5.1
v0.5.8
[~v0.6.0]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17var each = _.forEach = function(obj, iterator, context) {
var index = 0;
try {
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (_.isNumber(obj.length)) {
for (var i = 0, l = obj.length; i < l; i++) iterator.call(context, obj[i], i, obj);
} else {
for (var key in obj) {
if (hasOwnProperty.call(obj, key)) iterator.call(context, obj[key], key, obj);
}
}
} catch(e) {
if (e != breaker) throw e;
}
return obj;
};
[~v1.1.4]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var each = _.each = _.forEach = function(obj, iterator, context) {
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (_.isNumber(obj.length)) {
for (var i = 0, l = obj.length; i < l; i++) {
if (iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (hasOwnProperty.call(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
};
[~v1.1.7]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var each = _.each = _.forEach = function(obj, iterator, context) {
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length) {
for (var i = 0, l = obj.length; i < l; i++) {
if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (hasOwnProperty.call(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
};
[~v1.3.1]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var each = _.each = _.forEach = function(obj, iterator, context) {
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length) {
for (var i = 0, l = obj.length; i < l; i++) {
if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (_.has(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
};
[~v1.4.2]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var each = _.each = _.forEach = function(obj, iterator, context) {
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length) {
for (var i = 0, l = obj.length; i < l; i++) {
if (iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (_.has(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
};
[~v1.5.2]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var each = _.each = _.forEach = function(obj, iterator, context) {
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length) {
for (var i = 0, length = obj.length; i < length; i++) {
if (iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
var keys = _.keys(obj);
for (var i = 0, length = keys.length; i < length; i++) {
if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
}
}
};
[~v1.7.0]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16_.each = _.forEach = function(obj, iteratee, context) {
if (obj == null) return obj;
iteratee = createCallback(iteratee, context);
var i, length = obj.length;
if (length === +length) {
for (i = 0; i < length; i++) {
iteratee(obj[i], i, obj);
}
} else {
var keys = _.keys(obj);
for (i = 0, length = keys.length; i < length; i++) {
iteratee(obj[keys[i]], keys[i], obj);
}
}
return obj;
};
[~v1.8.0]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15_.each = _.forEach = function(obj, iteratee, context) {
iteratee = optimizeCb(iteratee, context);
var i, length;
if (isArrayLike(obj)) {
for (i = 0, length = obj.length; i < length; i++) {
iteratee(obj[i], i, obj);
}
} else {
var keys = _.keys(obj);
for (i = 0, length = keys.length; i < length; i++) {
iteratee(obj[keys[i]], keys[i], obj);
}
}
return obj;
};
]]>
- v0.3.1에서 “(Object.prototype.hasOwnProperty.call(obj, key)”를 for문과 같이 쓰이는 구조가 처음보는 익혀둬야 할것 같다.
- 점진적으로 여러 예외경우를 추가하면서 try catch 을 제거하는 방식도 괜찮은 방식이인것 같다.
- for문과 for in 문에 반복적으로 교체되고 있다. 장단점을 확실히 아는게 좋을것 같다.
- 확실히 최종보전부터 보면 이해가 안되지만 처음버전부터 보면 히스토리를 알게되서 이해가 더 알기 좋다.