소스 코드를 공부하는 것은 의심할 여지 없이 당신의 개발 경력의 궤적을 바꿀 수 있습니다. 표면 아래를 한 단계만 들여다보는 것만으로도 대부분의 평범한 개발자와 차별화될 수 있습니다.
이것은 완벽을 향한 첫 걸음이에요!
개인적인 이야기를 하나 드리겠습니다. 제가 현재 AI/ML 스타트업에서 일하고 있는데, 팀은 Neo4j 데이터를 서버에서 프런트엔드로 가져와 시각화하는 방법을 알아내지 못했고, 12시간 만에 프레젠테이션을 했습니다. 저는 프리랜서로 영입되었고, 당황한 모습을 분명히 볼 수 있었습니다. 문제는 Neo4j에서 반환된 데이터가 시각화 도구인 neo4jd3 에서 예상한 올바른 형식이 아니었다는 것입니다.
상상해보세요: Neo4jd3는 삼각형을 기대하고 Neo4j는 정사각형을 반환합니다. 바로 호환되지 않는 불일치입니다!
곧 Mastered 에서 JavaScript와 Neo4j를 이용한 그래프 데이터 과학을 할 수도 있겠네요! 이 이미지는 향수를 불러일으킵니다.
선택지는 두 가지뿐이었습니다. Neo4j 백엔드 전체를 다시 만드는 것, 아니면 Neo4jd3의 소스 코드를 연구하여 예상되는 형식을 파악한 다음, 정사각형을 삼각형으로 변환하는 어댑터를 만드는 것.
neo4jd3 <- adapter <- Neo4j
저는 기본적으로 소스 코드를 읽으며 어댑터인 neo4jd3-ts를 만들었습니다.
import createNeoChart, { NeoDatatoChartData } from "neo4jd3-ts";
어댑터는 NeoDatatoChartData
이고, 그 외의 모든 것은 과거입니다. 저는 이 교훈을 마음에 새겼고, 기회가 있을 때마다 사용하는 모든 도구에서 한 단계 더 낮춥니다. 너무 널리 퍼져서 때로는 설명서도 읽지 않습니다.
이 접근 방식은 내 경력을 엄청나게 바꿔 놓았습니다. 내가 하는 모든 일이 마법처럼 보입니다. 몇 달 만에 나는 중요한 서버 마이그레이션과 프로젝트를 이끌게 되었는데, 그 이유는 내가 소스로 한 걸음 내딛었기 때문입니다.
이것이 이 시리즈의 주제입니다. API에 안주하지 않고, 그 이상을 향해 나아가 이러한 도구를 재창조하는 법을 배우는 것입니다. AI 과대광고의 세계에서 평범함을 벗어나는 것이 개발자를 평범함을 넘어 가치 있게 만드는 것입니다!
이 시리즈를 통해 저는 인기 있는 JavaScript 라이브러리와 도구를 연구하고, 그것들이 어떻게 작동하는지, 그리고 그것들로부터 어떤 패턴을 배울 수 있는지 한 번에 하나씩 알아내는 것을 계획하고 있습니다.
저는 주로 백엔드 엔지니어입니다(풀 스택이긴 하지만 90%는 백엔드를 담당합니다). 그래서 Express.js보다 시작하기에 더 나은 도구는 없습니다.
저는 당신이 프로그래밍 경험이 있고 프로그래밍의 기본을 잘 이해하고 있다고 가정합니다! 당신은 고급 초보자로 분류될 수 있습니다.
기본을 가르치는 동안 소스 코드를 배우고 가르치는 것은 정말 어렵고 지루할 것입니다. 시리즈에 참여할 수는 있지만 어려울 것으로 예상하세요. 모든 것을 다룰 수는 없지만 최대한 노력해 보겠습니다.
이 글은 어떤 이유에서인지 Express 이전 버전입니다. Express가 사용하는 아주 작은 라이브러리인 merge-descriptors 에 대해 다루기로 했습니다. 이 글을 쓰는 현재 이 라이브러리의 다운로드 횟수는 27,181,495회이고 코드는 단 26줄에 불과합니다.
이를 통해 구조를 확립하고 JavaScript 모듈을 구축하는 데 중요한 객체의 기본 사항을 소개할 기회가 주어집니다.
진행하기 전에 시스템에 Express 소스 코드 와 병합 설명자가 있는지 확인하세요. 이렇게 하면 IDE에서 열 수 있고, 저는 우리가 찾고 있는 곳에 줄 번호를 안내해 드릴 수 있습니다.
Express는 육중한 라이브러리입니다. 다른 도구로 넘어가기 전에 몇 개의 기사에 걸쳐 가능한 한 많은 내용을 다루겠습니다.
IDE에서 Express 소스를 열고(줄 번호가 있는 것이 좋음) lib
폴더로 이동한 후, 진입 파일인 express.js
파일을 엽니다.
17번째 줄에 첫 번째 라이브러리가 있습니다.
var mixin = require('merge-descriptors');
사용법은 42행과 43행에 있습니다.
mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
여기서 무슨 일이 일어나고 있는지 알아보기 전에, 한 걸음 물러나서 데이터 구조를 넘어 JavaScript의 객체에 대해 이야기해야 합니다. 구성, 상속, 프로토타입, 믹스인(이 글의 제목)에 대해 논의하겠습니다.
Express 소스 코드를 닫고, 중요한 객체 기본 사항을 학습하는 동안 따라할 수 있도록 새 폴더를 만드세요.
객체는 데이터와 행동의 캡슐화이며, 객체 지향 프로그래밍(OOP) 의 핵심입니다. 재미있는 사실: JavaScript의 거의 모든 것이 객체입니다.
const person = { // data name: "Jane", age: 0, // behavior grow(){ this.age += 1; } };
person
객체의 여는 중괄호와 닫는 중괄호 사이의 모든 것을 객체 자체 속성 이라고 합니다. 이것은 중요합니다.
자신의 속성은 객체에 직접적으로 있는 속성입니다. name
, age
, grow
person
자신의 속성입니다.
이것은 모든 JavaScript 객체에 prototype
속성이 있기 때문에 중요합니다. 위의 객체를 함수 청사진으로 인코딩하여 person
객체를 동적으로 생성할 수 있도록 합시다.
function createNewPerson(name, age){ this.name = name; this.age = age; } createNewPerson.prototype.print = function(){ console.log(`${this.name} is ${this.age}`); }; const john = new createNewPerson("John", 32);
프로토타입은 JavaScript 객체가 다른 객체로부터 속성과 메서드를 상속하는 방식입니다. Own Properties
와 Prototype
의 차이점은 객체의 속성에 액세스할 때입니다.
john.name; // access
JavaScript는 높은 우선순위를 가지므로 먼저 Own Properties
를 찾습니다. 속성을 찾지 못하면 null을 찾고 오류를 throw할 때까지 객체의 자체 prototype
객체를 재귀적으로 찾습니다.
프로토타입 객체는 자신의 프로토타입을 통해 다른 객체로부터 상속받을 수 있습니다. 이를 프로토타입 체인이라고 합니다.
console.log(john.hasOwnProperty('name')); // true console.log(john.hasOwnProperty('print')); // false, it's in the prototype
그러나 john
에서는 print
작동합니다.
john.print(); // "John is 32"
이것이 JavaScript가 프로토타입 기반 언어로 정의되는 이유입니다. 우리는 프로토타입으로 속성과 메서드를 추가하는 것 외에도 상속과 같은 더 많은 것을 할 수 있습니다.
상속의 "hello world"는 mammal 객체입니다. JavaScript로 다시 만들어 보겠습니다.
// our Mammal blueprint function Mammal(name) { this.name = name; } Mammal.prototype.breathe = function() { console.log(`${this.name} is breathing.`); };
JavaScript에서는 Object
객체 내부에 정적 함수가 있습니다.
Object.create();
{}
와 new functionBlueprint
와 비슷하게 객체를 생성하지만, 차이점은 create
상속받을 매개변수로 프로토타입을 받을 수 있다는 것입니다.
// we use a cat blueprint function here (implemented below) Cat.prototype = Object.create(Mammal.prototype); // correction after we inherited all the properties Cat.prototype.constructor = Cat;
이제 Cat
Mammal
에서 발견되는 breathe
방법을 갖게 되지만, 중요한 것은 Cat
Mammal
프로토타입으로 지정하고 있다는 것입니다.
포유류 블루프린트 : 먼저 Mammal
함수를 정의하고 그 프로토타입에 breathe
메서드를 추가합니다.
Cat 상속 : Cat
함수를 만들고 Cat.prototype
Object.create(Mammal.prototype)
으로 설정합니다. 이렇게 하면 Cat
프로토타입이 Mammal
에서 상속되지만 constructor
포인터가 Mammal
로 변경됩니다.
생성자 수정 : Cat.prototype.constructor
를 수정하여 Cat
을 다시 가리키도록 하여 Cat
객체가 Mammal
에서 메서드를 상속하는 동안 정체성을 유지하도록 합니다. 마지막으로 Cat
에 meow
메서드를 추가합니다.
이 접근 방식을 사용하면 Cat
객체가 Mammal
( breathe
등)과 자체 프로토타입( meow
등) 모두의 메서드에 액세스할 수 있습니다.
우리는 그것을 수정해야 합니다. 전체 예를 만들어 보겠습니다.
function Cat(name, breed) { this.name = name; this.breed = breed; } Cat.prototype = Object.create(Mammal.prototype); // cat prototype pointing to mammal // correction after we inherited all the properties Cat.prototype.constructor = Cat; // we are re-pointing a pointer, the inherited properties are still there Cat.prototype.meow = function() { console.log(`${this.name} is meowing.`); };
Cat.prototype.constructor = Cat
이해하려면 포인터에 대해 알아야 합니다. Mammal
에서 Object.create
로 상속하면 Cat
프로토타입의 포인터가 Mammal
로 변경되는데, 이는 잘못된 것입니다. 우리는 부모 Mammal
있음에도 불구하고 Cat
자체로 개별적이기를 원합니다.
그래서 우리는 그것을 바로잡아야 합니다.
이 예에서 Cat
프로토타입 체인을 사용하여 Mammal
에서 상속합니다. Cat
객체는 breathe
와 meow
메서드에 모두 액세스할 수 있습니다.
const myCat = new Cat("Misty", "Ragdoll"); myCat.breathe(); // Misty is breathing. myCat.meow(); // Misty is meowing.
우리는 포유류로부터도 유전을 물려받은 개를 만들 수 있습니다.
function Dog(name, breed) { this.name = name; this.breed = breed; } Dog.prototype = Object.create(Mammal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.bark = function() { console.log(`${this.name} is barking.`); }; const myDog = new Dog('Buddy', 'Golden Retriever'); myDog.breathe(); // Buddy is breathing. myDog.bark(); // Buddy is barking.
우리는 기본적인 클래식 상속을 만들었지만, 왜 이게 중요한 걸까요? 우리가 소스 코드를 다루고 있다고 생각했어요!
네, 사실이지만 프로토타입은 상속을 넘어 효율적이고 유연한 모듈을 구축하는 핵심입니다. 간단하고 잘 작성된 모듈조차도 프로토타입 객체로 가득 차 있습니다. 우리는 단지 기본을 깔고 있습니다.
상속에 대한 대안은 객체 구성으로, 두 개 이상의 객체를 가져와 병합하여 "슈퍼" 객체를 형성합니다.
믹스인은 상속을 사용하지 않고 객체가 다른 객체에서 메서드를 빌릴 수 있도록 합니다. 관련 없는 객체 간에 동작을 공유하는 데 편리합니다.
이것이 바로 우리의 첫 번째 탐색이 하는 일입니다. 즉, 우리가 먼저 다루기로 약속했던 merge-descriptors
라이브러리입니다.
우리는 이미 Express에서 그것이 어디서 어떻게 사용되는지 보았습니다. 이제 우리는 그것이 객체 구성에 사용되는 것을 압니다.
17번째 줄에 첫 번째 라이브러리가 있습니다.
var mixin = require('merge-descriptors');
사용법은 42행과 43행에 있습니다.
mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
우리가 아는 바에 따르면 mixin
은 EventEmitter.prototype
과 proto
app
이라는 객체로 구성한다는 것을 이미 추론할 수 있습니다.
Express에 대해 이야기할 때 app
에 대해서도 이야기하겠습니다.
merge-descriptors
의 전체 소스 코드는 다음과 같습니다.
'use strict'; function mergeDescriptors(destination, source, overwrite = true) { if (!destination) { throw new TypeError('The `destination` argument is required.'); } if (!source) { throw new TypeError('The `source` argument is required.'); } for (const name of Object.getOwnPropertyNames(source)) { if (!overwrite && Object.hasOwn(destination, name)) { // Skip descriptor continue; } // Copy descriptor const descriptor = Object.getOwnPropertyDescriptor(source, name); Object.defineProperty(destination, name, descriptor); } return destination; } module.exports = mergeDescriptors;
처음부터 항상 함수가 어떻게 사용되는지, 그리고 함수가 취하는 매개변수가 무엇인지 살펴보세요.
// definition mergeDescriptors(destination, source, overwrite = true) // usage var mixin = require('merge-descriptors'); mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
App
은 우리의 목적지입니다. 우리는 mixin
객체 구성을 의미한다는 것을 알고 있습니다. 대략적으로, 이 패키지가 하는 일은 소스 객체를 대상 객체로 구성하는 것이고, 덮어쓰기 옵션이 있습니다.
덮어쓰기는 가정에 따라 app
(대상)이 소스와 정확히 동일한 속성을 가지고 있을 경우 true
덮어쓰기이고, 그렇지 않으면 해당 속성을 그대로 두고 건너뜁니다.
우리는 객체가 동일한 속성을 두 번 가질 수 없다는 것을 알고 있습니다. 키-값 쌍(객체)에서 키는 고유해야 합니다.
Express에서는 덮어쓰기가 false
입니다.
기본적인 정리 작업은 다음과 같습니다. 항상 예상되는 오류를 처리하세요.
if (!destination) { throw new TypeError('The `destination` argument is required.'); } if (!source) { throw new TypeError('The `source` argument is required.'); }
흥미로운 부분은 12번째 줄입니다.
for (const name of Object.getOwnPropertyNames(source)) {
위에서 알 수 있듯이 OwnProperty
무엇을 의미하는지, 따라서 getOwnPropertyNames
분명히 자체 속성의 키를 가져온다는 것을 의미합니다.
const person = { // data name: "Jane", age: 0, // behavior grow() { this.age += 1; } }; Object.getOwnPropertyNames(person); // [ 'name', 'age', 'grow' ]
키를 배열로 반환하고, 다음 인스턴스에서 해당 키를 반복합니다.
for (const name of Object.getOwnPropertyNames(source)) {
다음은 대상과 소스에 현재 루핑하고 있는 동일한 키가 있는지 확인하는 것입니다.
if (!overwrite && Object.hasOwn(destination, name)) { // Skip descriptor continue; }
overwrite가 false이면 해당 속성을 건너뜁니다. 덮어쓰지 않습니다. continue
하는 일은 바로 루프를 다음 반복으로 옮기고 그 아래에 있는 코드, 즉 다음 코드를 실행하지 않는 것입니다.
// Copy descriptor const descriptor = Object.getOwnPropertyDescriptor(source, name); Object.defineProperty(destination, name, descriptor);
우리는 이미 getOwnProperty
무슨 뜻인지 알고 있습니다. 새로운 단어는 descriptor
입니다. 우리 자신의 person
객체에서 이 함수를 테스트해 보겠습니다.
const person = { // data name: "Jane", age: 0, // behavior grow() { this.age += 1; } }; Object.getOwnPropertyDescriptor(person, "grow"); // { // value: [Function: grow], // writable: true, // enumerable: true, // configurable: true // }
이것은 값으로 grow
함수를 반환하며 다음 줄은 그 자체로 설명이 가능합니다.
Object.defineProperty(destination, name, descriptor);
소스에서 설명자를 가져와서 목적지에 쓰는 것입니다. 소스 자체 속성을 목적지 객체에 자체 속성으로 복사하는 것입니다.
person
객체에서 예를 들어 보겠습니다.
const val = { value: function isAlien() { return false; }, enumerable: true, writable: true, configurable: true, }; Object.defineProperty(person, "isAlien", val);
이제 person
isAlien
속성이 정의되어야 합니다.
요약하자면, 이 다운로드 수가 많은 모듈은 소스 객체의 자체 속성을 대상 객체로 복사하며 덮어쓰기 옵션도 제공합니다.
우리는 이 소스 코드 계층 에서 첫 번째 모듈을 성공적으로 다루었으며, 앞으로 더욱 흥미로운 내용이 나올 예정입니다.
이것은 소개였습니다. 우리는 모듈을 이해하는 데 필요한 기본 사항을 다루고, 부산물로 대부분 모듈의 패턴인 객체 구성과 상속을 이해하는 것으로 시작했습니다. 마지막으로, 우리는 merge-descriptors
모듈을 탐색했습니다.
이 패턴은 대부분 기사에서 널리 퍼질 것입니다. 다루어야 할 필수적인 기본 사항이 있다고 생각되면 첫 번째 섹션에서 살펴보고 소스 코드를 다룰 것입니다.
다행히도 merge-descriptors
Express에서 사용되는데, 이는 우리의 시작 소스 코드 연구의 초점입니다. 따라서 Express를 충분히 잘 실행했다고 느낄 때까지 더 많은 Express.js 소스 코드 기사를 기대하고, 그런 다음 Node.js와 같은 다른 모듈이나 도구로 전환하세요.
도전과제로 그동안 할 수 있는 일은 병합 설명자에서 테스트 파일로 이동하고 전체 파일을 읽는 것입니다. 직접 소스를 읽는 것이 중요합니다. 무엇을 하는지, 무엇을 테스트하는지 알아내고, 그것을 깨뜨리고, 다시 고치거나 더 많은 테스트를 추가하세요!
프로그래밍 기술을 향상시키기 위해 더욱 독점적이고 실용적이며 더 긴 콘텐츠에 관심이 있으시다면 Ko-fi 에서 더 많은 정보를 찾으실 수 있습니다.