소스 코드를 읽어 JavaScript 지식 향상
게시 됨: 2022-03-10자주 사용하는 라이브러리나 프레임워크의 소스 코드를 처음으로 깊숙이 파고 들었을 때를 기억합니까? 저에게 그 순간은 3년 전 프론트엔드 개발자로 첫 직장을 다닐 때 찾아왔습니다.
우리는 e-러닝 과정을 만드는 데 사용한 내부 레거시 프레임워크 재작성을 막 마쳤습니다. 재작성을 시작할 때 우리는 Mithril, Inferno, Angular, React, Aurelia, Vue 및 Polymer를 비롯한 다양한 솔루션을 조사하는 데 시간을 보냈습니다. 나는 매우 초심자였기 때문에(저는 저널리즘에서 웹 개발로 막 전환했습니다), 각 프레임워크의 복잡성에 겁을 먹고 각 프레임워크가 어떻게 작동하는지 이해하지 못했던 것을 기억합니다.
우리가 선택한 프레임워크인 Mithril을 더 깊이 조사하기 시작했을 때 이해가 커졌습니다. 그 이후로 자바스크립트와 일반적인 프로그래밍에 대한 나의 지식은 직장이나 내 프로젝트에서 매일 사용하는 라이브러리를 깊이 파고드는 데 많은 시간을 할애했습니다. 이 게시물에서는 좋아하는 라이브러리나 프레임워크를 교육 도구로 사용할 수 있는 몇 가지 방법을 공유합니다.
소스 코드 읽기의 이점
소스 코드를 읽는 것의 주요 이점 중 하나는 배울 수 있는 것이 많다는 것입니다. 처음 Mithril의 코드베이스를 살펴보았을 때 가상 DOM이 무엇인지 막연했습니다. 완료했을 때 가상 DOM은 사용자 인터페이스가 어떻게 생겼는지 설명하는 개체 트리를 만드는 것과 관련된 기술이라는 사실을 알게 되었습니다. 그런 다음 해당 트리는 document.createElement
와 같은 DOM API를 사용하여 DOM 요소로 바뀝니다. 업데이트는 사용자 인터페이스의 미래 상태를 설명하는 새 트리를 만든 다음 이전 트리의 개체와 비교하여 수행됩니다.
나는 다양한 기사와 튜토리얼에서 이 모든 것에 대해 읽었고, 그것이 도움이 되었지만, 우리가 제공한 애플리케이션의 맥락에서 직장에서 그것을 관찰할 수 있다는 것은 저에게 매우 큰 도움이 되었습니다. 또한 서로 다른 프레임워크를 비교할 때 어떤 질문을 해야 하는지도 배웠습니다. 예를 들어 GitHub 별을 보는 대신 "각 프레임워크가 업데이트를 수행하는 방식이 성능과 사용자 경험에 어떤 영향을 미치는가?"와 같은 질문을 할 수 있게 되었습니다.
또 다른 이점은 좋은 응용 프로그램 아키텍처에 대한 이해와 감사의 증가입니다. 대부분의 오픈 소스 프로젝트는 일반적으로 리포지토리와 동일한 구조를 따르지만 각 프로젝트에는 차이점이 있습니다. Mithril의 구조는 매우 단순하며 해당 API에 익숙하다면 render
, router
및 request
와 같은 폴더의 코드에 대해 정보에 입각한 추측을 할 수 있습니다. 반면 React의 구조는 새로운 아키텍처를 반영합니다. 메인테이너는 UI 업데이트를 담당하는 모듈( react-reconciler
)과 DOM 요소를 렌더링하는 모듈( react-dom
)을 분리했습니다.
이것의 이점 중 하나는 이제 개발자가 react-reconciler
패키지에 연결하여 자체 사용자 지정 렌더러를 작성하는 것이 더 쉽다는 것입니다. 최근에 연구하고 있는 모듈 번들러인 Parcel에도 React와 같은 packages
폴더가 있습니다. 주요 모듈의 이름은 parcel-bundler
이며 여기에는 번들 생성, 핫 모듈 서버 및 명령줄 도구 회전을 담당하는 코드가 포함되어 있습니다.
또 다른 이점은 저에게 반가운 일이었습니다. 언어 작동 방식을 정의하는 공식 JavaScript 사양을 읽는 것이 더 편해졌습니다. 내가 처음 사양을 읽은 것은 throw Error
와 throw new Error
(스포일러 경고 - 없음)의 차이점을 조사할 때였습니다. Mithril이 m
함수의 구현에서 throw Error
를 사용하고 throw new Error
보다 이를 사용하는 것이 이점이 있는지 궁금했기 때문에 이것을 조사했습니다. 그 이후로 나는 논리 연산자 &&
와 ||
반드시 부울을 반환할 필요는 없으며 ==
등호 연산자가 값을 강제하는 방법과 Object.prototype.toString.call({})
이 '[object Object]'
를 반환하는 이유를 제어하는 규칙을 찾았습니다.
소스 코드를 읽는 기술
소스 코드에 접근하는 방법에는 여러 가지가 있습니다. 시작하는 가장 쉬운 방법은 선택한 라이브러리에서 메서드를 선택하고 호출할 때 어떤 일이 발생하는지 문서화하는 것입니다. 모든 단일 단계를 문서화하지 말고 전체 흐름과 구조를 식별하도록 노력하십시오.
나는 최근에 ReactDOM.render
로 이 작업을 수행했고 결과적으로 React Fiber와 그 구현의 이유에 대해 많은 것을 배웠습니다. 고맙게도 React는 인기 있는 프레임워크이기 때문에 같은 문제에 대해 다른 개발자가 작성한 기사를 많이 접했고 이로 인해 프로세스가 빨라졌습니다.
이 심층 분석은 또한 협력 스케줄링의 개념, window.requestIdleCallback
메서드 및 연결 목록의 실제 예를 소개했습니다(React는 업데이트를 우선 순위 업데이트의 연결 목록인 대기열에 넣어 업데이트를 처리합니다). 이 작업을 수행할 때 라이브러리를 사용하여 매우 기본적인 응용 프로그램을 만드는 것이 좋습니다. 이렇게 하면 다른 라이브러리로 인한 스택 추적을 처리할 필요가 없기 때문에 디버깅할 때 더 쉽습니다.
심도 있는 검토를 하지 않는 경우 작업 중인 프로젝트의 /node_modules
폴더를 열거나 GitHub 저장소로 이동합니다. 이것은 일반적으로 버그나 흥미로운 기능을 발견했을 때 발생합니다. GitHub에서 코드를 읽을 때 최신 버전에서 읽고 있는지 확인하십시오. 분기를 변경하는 데 사용되는 버튼을 클릭하고 "태그"를 선택하여 최신 버전 태그가 있는 커밋의 코드를 볼 수 있습니다. 라이브러리와 프레임워크는 영원히 변경되므로 다음 버전에서 삭제될 수 있는 것에 대해 배우고 싶지 않습니다.
소스 코드를 읽는 또 다른 덜 관련된 방법은 'cursorylance' 방법이라고 부르는 것입니다. 코드 읽기를 시작했을 때 나는 express.js를 설치하고 /node_modules
폴더 를 열고 종속성을 살펴보았습니다. README
가 만족스러운 설명을 제공하지 않으면 출처를 읽었습니다. 이를 통해 다음과 같은 흥미로운 발견을 할 수 있었습니다.
- Express는 객체를 병합하지만 매우 다른 방식으로 병합하는 두 개의 모듈에 의존합니다.
merge-descriptors
는 소스 개체에서 직접 발견한 속성만 추가하고 열거할 수 없는 속성도 병합하는 반면utils-merge
는 개체의 열거 가능한 속성과 프로토타입 체인에서 찾은 속성만 반복합니다.merge-descriptors
Object.getOwnPropertyNames()
및Object.getOwnPropertyDescriptor()
를 사용하는 반면utils-merge
는for..in
을 사용합니다. -
setprototypeof
모듈은 인스턴스화된 객체의 프로토타입을 설정하는 크로스 플랫폼 방식을 제공합니다. -
escape-html
은 HTML 콘텐츠에 삽입될 수 있도록 콘텐츠 문자열을 이스케이프하기 위한 78줄 모듈입니다.
결과가 즉시 유용하지 않을 수 있지만 라이브러리 또는 프레임워크에서 사용하는 종속성에 대한 일반적인 이해가 있으면 유용합니다.
프론트 엔드 코드를 디버깅할 때 브라우저의 디버깅 도구는 가장 좋은 친구입니다. 무엇보다도, 그것들을 사용하면 언제든지 프로그램을 중지하고 상태를 검사하고, 함수 실행을 건너뛰거나, 프로그램에 들어가거나 나갈 수 있습니다. 때로는 코드가 축소되었기 때문에 즉시 가능하지 않을 수 있습니다. 저는 압축을 풀고 압축되지 않은 코드를 /node_modules
폴더의 해당 파일에 복사하는 경향이 있습니다.
사례 연구: Redux의 연결 기능
React-Redux는 React 애플리케이션의 상태를 관리하는 데 사용되는 라이브러리입니다. 이와 같은 인기 있는 라이브러리를 다룰 때 먼저 구현에 대해 작성된 기사를 검색합니다. 이 사례 연구를 수행하면서 이 기사를 접하게 되었습니다. 이것은 소스 코드를 읽는 또 다른 좋은 점입니다. 연구 단계에서는 일반적으로 자신의 생각과 이해를 향상시키는 이와 같은 유익한 기사로 이어집니다.
connect
는 React 컴포넌트를 애플리케이션의 Redux 저장소에 연결하는 React-Redux 기능입니다. 어떻게? 글쎄, 문서에 따르면 다음을 수행합니다.
"...전달한 구성 요소를 래핑하는 연결된 새 구성 요소 클래스를 반환합니다."
이 글을 읽은 후 다음과 같은 질문을 던질 것입니다.
- 함수가 입력을 받은 다음 추가 기능으로 래핑된 동일한 입력을 반환하는 패턴이나 개념을 알고 있습니까?
- 그러한 패턴을 알고 있다면 문서에 제공된 설명을 기반으로 이를 어떻게 구현합니까?
일반적으로 다음 단계는 connect
를 사용하는 매우 기본적인 예제 앱을 만드는 것입니다. 그러나 이번 기회에 나는 우리가 Limejump에서 구축하고 있는 새로운 React 앱을 사용하기로 결정했습니다. 왜냐하면 궁극적으로 프로덕션 환경으로 들어갈 애플리케이션의 컨텍스트 내에서 connect
을 이해하고 싶었기 때문입니다.
내가 집중하고 있는 구성 요소는 다음과 같습니다.
class MarketContainer extends Component { // code omitted for brevity } const mapDispatchToProps = dispatch => { return { updateSummary: (summary, start, today) => dispatch(updateSummary(summary, start, today)) } } export default connect(null, mapDispatchToProps)(MarketContainer);
4개의 더 작은 연결된 구성 요소를 감싸는 컨테이너 구성 요소입니다. connect
메소드를 내보내는 파일에서 가장 먼저 보게 되는 것 중 하나는 다음 주석입니다. connect는 connectAdvanced에 대한 파사드 입니다. 멀리 가지 않고 첫 번째 학습 순간이 있습니다. 즉, 작동 중인 파사드 디자인 패턴을 관찰할 수 있는 기회 입니다. 파일의 끝에서 우리는 connect
가 createConnect
라는 함수의 호출을 내보내는 것을 볼 수 있습니다. 매개변수는 다음과 같이 구조화되지 않은 기본값의 무리입니다.
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {})
다시 한 번, 우리는 또 다른 학습 순간을 발견합니다. 호출된 함수 내보내기 및 기본 함수 인수 분해 . 코드가 다음과 같이 작성되었으므로 구조화 부분은 학습 순간입니다.
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory })
이 오류가 발생했을 것입니다 Uncaught TypeError: Cannot destructure property 'connectHOC' of 'undefined' or 'null'.
이는 함수에 대체할 기본 인수가 없기 때문입니다.
참고 : 이에 대한 자세한 내용은 David Walsh의 기사를 참조하세요. 일부 학습 순간은 언어 지식에 따라 사소해 보일 수 있으므로 이전에 본 적이 없거나 더 배워야 할 것에 집중하는 것이 더 나을 수 있습니다.
createConnect
자체는 함수 본문에서 아무 작업도 수행하지 않습니다. 여기에서 사용한 connect
라는 함수를 반환합니다.
export default connect(null, mapDispatchToProps)(MarketContainer)
4개의 인수(모두 선택 사항)가 필요하며 처음 3개의 인수는 각각 인수의 존재 여부와 값 유형에 따라 동작을 정의하는 데 도움이 되는 match
함수를 거칩니다. 이제 match
에 제공된 두 번째 인수는 connect
로 가져온 세 가지 함수 중 하나이기 때문에 어떤 스레드를 따를지 결정해야 합니다.
첫 번째 인수가 함수인 경우 connect
첫 번째 인수를 래핑하는 데 사용되는 프록시 함수, 일반 개체를 확인하는 데 사용되는 isPlainObject
유틸리티 또는 모든 예외에서 중단되도록 디버거를 설정할 수 있는 방법을 보여주는 warning
모듈에 대해 배우는 순간이 있습니다. 매치 함수 후에 우리는 React 컴포넌트를 가져와 Redux에 연결하는 함수인 connectHOC
에 도달합니다. 구성 요소를 저장소에 연결하는 작업을 실제로 처리하는 함수인 wrapWithConnect
를 반환하는 또 다른 함수 호출입니다.
connectHOC
의 구현을 보면 구현 세부 정보를 숨기기 위해 connect
이 필요한 이유를 알 수 있습니다. React-Redux의 핵심이며 connect
를 통해 노출될 필요가 없는 로직을 포함합니다. 여기서 심층 분석을 끝내겠지만 계속 진행했다면 코드베이스에 대한 믿을 수 없을 정도로 자세한 설명이 포함되어 있기 때문에 이전에 찾은 참조 자료를 참조하기에 완벽한 시간이었을 것입니다.
요약
소스 코드를 읽는 것은 처음에는 어렵지만 모든 것이 그렇듯이 시간이 지날수록 쉬워집니다. 모든 것을 이해하는 것이 아니라 다른 관점과 새로운 지식으로 다가가는 것이 목표입니다. 핵심은 전체 과정에 대해 숙고하고 모든 것에 대해 강렬하게 호기심을 갖는 것입니다.
예를 들어 isPlainObject
함수는 if (typeof obj !== 'object' || obj === null) return false
를 사용하여 주어진 인수가 일반 객체인지 확인하기 때문에 흥미롭습니다. 처음 구현을 읽었을 때 Object.prototype.toString.call(opts) !== '[object Object]'
를 사용하지 않은 이유가 궁금했습니다. 이는 코드가 적고 Date와 같은 객체와 객체 하위 유형을 구별하는 것입니다. 물체. 그러나 다음 줄을 읽으면 매우 드물게 connect
을 사용하는 개발자가 Date 개체를 반환하는 경우 Object.getPrototypeOf(obj) === null
검사에 의해 처리됨을 알 수 있습니다.
isPlainObject
의 또 다른 흥미로운 점은 다음 코드입니다.
while (Object.getPrototypeOf(baseProto) !== null) { baseProto = Object.getPrototypeOf(baseProto) }
일부 Google 검색은 이 StackOverflow 스레드와 iFrame에서 시작된 개체에 대한 검사와 같은 경우를 코드에서 처리하는 방법을 설명하는 Redux 문제로 이끌었습니다.
소스 코드 읽기에 대한 유용한 링크
- "프레임워크를 리버스 엔지니어링하는 방법", Max Koretskyi, Medium
- "코드를 읽는 방법", Aria Stewart, GitHub