<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ZeroCho Blog]]></title><description><![CDATA[ZeroCho의 Javascript와 Node.js 그리고 Web 이야기]]></description><link>https://www.zerocho.com</link><image><url>https://ko.gravatar.com/userimage/106434188/feadce30f4d4d94d86a1e752497bd8bc.png?size=200</url><title>ZeroCho Blog</title><link>https://www.zerocho.com</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 20 Apr 2026 18:39:04 GMT</lastBuildDate><atom:link href="https://www.zerocho.com/api/rss.xml" rel="self" type="application/rss+xml"/><copyright><![CDATA[2016- ZeroCho]]></copyright><language><![CDATA[ko]]></language><managingEditor><![CDATA[ZeroCho]]></managingEditor><webMaster><![CDATA[ZeroCho]]></webMaster><category><![CDATA[HTML]]></category><category><![CDATA[CSS]]></category><category><![CDATA[Javascript]]></category><category><![CDATA[jQuery]]></category><category><![CDATA[EcmaScript]]></category><category><![CDATA[React]]></category><category><![CDATA[NodeJS]]></category><category><![CDATA[MongoDB]]></category><category><![CDATA[Algorithm]]></category><category><![CDATA[Git]]></category><category><![CDATA[etc]]></category><category><![CDATA[Webpack]]></category><item><title><![CDATA[제일 처음 커밋 앞에 커밋 추가하기]]></title><description><![CDATA[이 게시글은 첫 커밋 앞에 다른 커밋을 추가하는 방법을 다룹니다.
기존 커밋 히스토리가 A - B - C - D라면(A가 제일 오래된 첫 커밋) Z - A - B - C - D처럼 제일 앞에 Z를 끼워넣는 것이죠.
왜 이런 짓을 하는 걸까요? 다양한 이유가 있겠으나 저는 중간 커밋 수정하기 를 할 때 첫 커밋은 수정이 안 되는 것을 발견했습니다. 그래서 첫 커밋 앞에 빈 커밋(Z) 하나를 추가한 뒤에 두 번째 커밋이 된 A를 수정하는 것이죠. 다른 이유를 찾으셨다면 댓글로 남겨주세요!
방법은 다음과 같습니다.
git checkout --orphan tempgit rm -rf .git commit --allow-empty -m 'Z'git rebase --onto temp --root maingit branch -d temp 
Z(첫 커밋 메시지)나 main(현재 브랜치 이름) 부분은 여러분의 상황에 맞게 바꾸시면 됩니다. 다른 것들은 그대로 입력하세요.
간단한 원리를 설명하자면 다음과 같습니다. 처음에 기존 브랜치와 완벽하게 분리된 빈 브랜치(orphan branch) temp를 생성하고, 현재 파일을 전부 지웁니다(git rm -rf .)
현재 아무 파일/폴더가 없는데 --allow-empty 옵션을 사용해서 새로운 커밋 Z를 하나 만듭니다. 그러고나서 temp 브랜치 커밋 위에 main 브랜치의 커밋을 쌓는 겁니다. 그래서 Z 위에 A - B - C - D 가 올라갈 수 있게 됩니다. 이후에 temp 브랜치를 지우고 main 브랜치로 돌아가는 것입니다.
]]></description><link>https://www.zerocho.com/category/Git/post/673cc471d44b1e6d6a984f49</link><guid isPermaLink="true">https://www.zerocho.com/category/Git/post/673cc471d44b1e6d6a984f49</guid><category><![CDATA[Git]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Tue, 19 Nov 2024 17:01:37 GMT</pubDate></item><item><title><![CDATA[여러 커밋 합치기 - git squash]]></title><description><![CDATA[정신없이 깃 커밋을 하다보면 하나의 기능이 너무 여러 개의 커밋으로 쪼개져 있는 상황이 발생합니다. 다음과 같이 README.md를 수정하는 6개의 커밋이 있다고 칩시다.
Update README.mdUpdate README.md Update README.md Update README.md Update README.md Create README.md
커밋 기록에 이렇게 나와있으면 지저분하기 때문에 커밋을 하나로 합치면 좋을 것 같습니다.
마지막으로부터 6개의 커밋을 합칠 것이므로 git reset --soft HEAD~개수 명령어를 사용합니다. 그러면 커밋들이 합쳐지는데 최종적으로 git commit으로 커밋하면 됩니다.
git reset --soft HEAD~6git commit -m "커밋메시지"
git rebase를 사용하는 방법도 있습니다. 저희가 마지막 6개 커밋을 합치길 원하므로 다음과 같이 입력합니다.
git rebase -i HEAD~6
그러면 vim 에디터로 연결되는데 합칠 커밋들 앞에 붙은 pick을 두 번째 것부터 전부 squash로 바꾸면 됩니다. 예를 들어 다음과 같이 6개 의 커밋이 있다면
pick ccfbab3 Create README.mdpick ccfbab3 Update README.mdpick ccfbab3 Update README.mdpick ccfbab3 Update README.mdpick ccfbab3 Update README.mdpick ccfbab3 Update README.md
두 번째 것부터 전부 squash로 바꿉니다. 첫 번째 것은 pick이어야 합니다(합칠 대상 커밋입니다)
pick ccfbab3 Create README.mdsquash ccfbab3 Update README.mdsquash ccfbab3 Update README.mdsquash ccfbab3 Update README.mdsquash ccfbab3 Update README.mdsquash ccfbab3 Update README.md 
그러고나서 :wq를 눌러 저장하고, 다시 커밋메시지를 수정한 후 :wq로 저장을 하면 됩니다.
]]></description><link>https://www.zerocho.com/category/Git/post/6735635ff3923a320c714a92</link><guid isPermaLink="true">https://www.zerocho.com/category/Git/post/6735635ff3923a320c714a92</guid><category><![CDATA[Git]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Thu, 14 Nov 2024 02:41:35 GMT</pubDate></item><item><title><![CDATA[중간 커밋 수정하기 - git rebase -i]]></title><description><![CDATA[가끔씩 git commit 중에 중간에 낀 커밋 내용을 수정해야 하는 경우가 있습니다. 다만 위 아래로 커밋들이 쌓여있어 어떻게 해야 할지 난감한 경우가 많았죠. 다행히도 중간 커밋을 바꿀 수 있는 방법이 있습니다. 해당 git 커밋의 아이디를 안다면 git rebase -i 명령어를 사용하면 됩니다. 제가 바꾸고 싶은 커밋 아이디가 b08c300이라면 git rebase -i b08c300^을 입력합니다.
git rebase -i 아이디^  // 예시 git rebase -i b08c300^
아이디 뒤에 ^가 포인트입니다. ^를 붙이지 않으면 해당 아이디의 커밋이 포함되지 않습니다.
그러면 터미널이 에디터 모드로 전환되는데 내용 중에 제일 위에 pick 아이디라는 글자가 보일겁니다. 여기서 pick을 edit으로 바꾸고 저장하면 됩니다. 저장하는 방법이 처음 하는 사람에게는 조금 어려울 수 있습니다.
pick b08c300 add: builder  &lt;--------------- 내가 바꾸고 싶은 커밋이 이거라고 칩시다pick 7f6287f add: prototypepick cc8382d add: commandpick 6d486a8 add: command2pick 4b153de add: statepick d418140 add: strategypick d7ea826 add: template methodpick 573aff2 add: chain of responsibilitypick b6633fd add: observer pattern.git/rebase-merge/git-rebase-todo [unix] (11:29 14/11/2024)                                             1,1 꼭대기"~/WebstormProjects/grimpan/.git/rebase-merge/git-rebase-todo" [유닉스] 54L, 2149B
터미널에 vim 에디터가 보통 뜰텐데 a를 눌러서 입력모드(제일 하단에 -- 끼워넣기 --나 -- insert --가 보입니다)로 간 뒤 pick을 edit으로 변경합니다.
edit b08c300 add: builder  &lt;--------------- pick을 edit으로 변경pick 7f6287f add: prototypepick cc8382d add: commandpick 6d486a8 add: command2pick 4b153de add: statepick d418140 add: strategypick d7ea826 add: template methodpick 573aff2 add: chain of responsibilitypick b6633fd add: observer pattern.git/rebase-merge/git-rebase-todo [unix] (11:29 14/11/2024)-- 끼워넣기 --
esc 키를 눌러 명령어모드로 되돌아가 :wq 입력 후 엔터 키를 눌러 저장할 수 있습니다.
edit b08c300 add: builderpick 7f6287f add: prototypepick cc8382d add: commandpick 6d486a8 add: command2pick 4b153de add: statepick d418140 add: strategypick d7ea826 add: template methodpick 573aff2 add: chain of responsibilitypick b6633fd add: observer pattern.git/rebase-merge/git-rebase-todo [unix] (11:29 14/11/2024):wq   &lt;------------------ esc를 누른 후 :wq입력하면 글자가 여기에 입력됩니다.
그러면 소스 코드가 해당 커밋으로 되돌아갑니다. 여기서 여러분이 원하는 수정을 한 뒤 다음 명령어를 입력합니다
git add .git rebase --continue 
그러면 다시 에디터 모드가 되면서 커밋 메시지를 수정하는 부분이 나오는데, 커밋 메시지를 변경할 게 없다면 :wq를 입력하고 엔터를 눌러 저장하면 중간 커밋이 변경된 모습을 확인할 수 있습니다.
그런데 이 방법으로는 첫 번째 커밋은 수정할 수 없습니다. invalid upstream 에러가 나게되는데요. 이 때는 첫 번째 커밋(A라고 합시다) 앞에 하나의 커밋을 추가하고 두 번째 커밋이 된 A 커밋을 위 방법으로 수정하면 됩니다. 첫 번째 커밋을 추가하는 방법은 여기에 있습니다.
]]></description><link>https://www.zerocho.com/category/Git/post/67356085f3923a320c7145f9</link><guid isPermaLink="true">https://www.zerocho.com/category/Git/post/67356085f3923a320c7145f9</guid><category><![CDATA[Git]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Thu, 14 Nov 2024 02:29:25 GMT</pubDate></item><item><title><![CDATA[fatal: Need to specify how to reconcile divergent branches 해결하기]]></title><description><![CDATA[얼마 전에 다음과 같은 에러가 발생해서 git pull이 안 되었던 적이 있습니다. git reset 같은 명령어를 잘못 수행하면 head가 꼬여서 저런 문제가 발생할 수 있습니다.



그러면 이미지의 hint에 나오는대로
git config pull.rebase falsegit config pull.rebase truegit config pull.ff only
세 명령어 중 하나를 입력하면 됩니다. 설명 들어갑니다.
git config pull.rebase false 
기본 방식입니다. git pull을 받아올 때 받아온 원격 브랜치를 merge합니다. merge 시 새로운 커밋 하나가 생기며 합쳐지는데 보통 이름이 "Merge remote tracking branch 'origin/main'으로 되어 있습니다.
원격 A -&gt; B -&gt; C기존 D -&gt; E 
에서 D -&gt; E -&gt; F(Merge remote tracking branch 'origin/main') 이 되는 겁니다. 원격 브랜치는 나중에 F 브랜치를 fast forward 할 수 있습니다. fast forward 설명은 밑에 있습니다.
git config pull.rebase true
git pull을 받아올 때 받아온 원격 브랜치를 rebase합니다. 이렇게 하면 새로운 커밋은 생기지 않지만 기존 브랜치 위에 원격 브랜치의 커밋이 모두 올라가 버립니다.
원격 A -&gt; B -&gt; C기존 D -&gt; E
에서 D -&gt; E -&gt; A -&gt; B -&gt; C가 되어 버립니다. merge가 좋은지 rebase가 좋은지는 호불호의 영역이라 고르시면 됩니다.
git config pull.ff only
가장 간단한 방법인데, ff는 fast forward를 의미합니다. ff only는 fast forward만 가능하게 한다는 뜻입니다. 이것을 하려면 원격과 기존 브랜치가 분리되어 있으면 안 됩니다.
원격 A -&gt; B -&gt; C기존 A -&gt; B 인 경우는
A -&gt; B -&gt; C로 빠르게 pull 하는 게 fast forward 방식입니다.
원격 A -&gt; B -&gt; C기존 A -&gt; B -&gt; D 인 경우에는 사용할 수 없습니다.
제한적인 방법이지만 가장 쉬운 방법입니다.
해결 방법
그래서 해결 방법이 뭐냐고요? 기본 값인 merge 모드에서 에러가 나는 것이므로 git config pull.rebase true를 하고 git pull을 받으시면 됩니다. 끝나고 다시 git config pull.rebase false로 되돌려 놓는 것도 잊지 마세요.
]]></description><link>https://www.zerocho.com/category/Git/post/65f7e734900038f60f2adbf1</link><guid isPermaLink="true">https://www.zerocho.com/category/Git/post/65f7e734900038f60f2adbf1</guid><category><![CDATA[Git]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Mon, 18 Mar 2024 07:03:16 GMT</pubDate></item><item><title><![CDATA[클래스 static 메서드의 this는 무엇인가 - 그리고 싱글턴 만들기]]></title><description><![CDATA[자바스크립트 클래스에 static 메서드가 있다는 것은 다들 아실 겁니다. 그렇다면 static 메서드에서 this를 출력하면 무엇이 될까요?
class Test {  static getThis() {    return this;  }}console.log(Test.getThis()); // class Testconsole.log(Test.getThis() === Test); // true
window나 undefined 같은 게 아니라 Test 클래스 그 자체가 나옵니다. 이 this는 Test 그 자체이므로 인스턴스의 this와 구분이 됩니다.
이번엔 다음과 같은 코드를 봅시다.
class Test {  static value = 'initial';  static getValue() {    return this.value;  }  static setValue(value) {    this.value = value;  }  static getThis() {    return this;  }}console.log(Test.value); // initialTest.setValue('changed');console.log(Test.getValue()); // changedconsole.log(Test.value); // changedconsole.log(Test.getThis()); // class Test { value: 'changed' }Test.value = 'updated';console.log(Test.getValue()); // updatedconst t = new Test(); // 인스턴스 생성t.value = 'instance'; // 인스턴스의 value 추가console.log(Test.value); // 여전히 updatedconsole.log(t.value); // instance
static value는 Test.value로 접근 가능하고, static 메서드에서는 this가 Test이므로 this.value로 접근 가능합니다. 반대로 인스턴스 메서드에서는 this가 다르므로 Test.value로 접근해야 합니다.
싱글톤 클래스를 만들고 싶다면 그냥 클래스를 선언한 뒤 클래스에 static 속성과 static 메서드를 선언해도 될 것 같네요. 조금 응용하자면...
class Singleton {  static #instance;  constructor() {    if (!Singleton.#instance) {      Singleton.#instance = this;    }     return Singleton.#instance;  }  get() {    return Singleton.#instance;  }  static get() {    return this.#instance;  }}const a = new Singleton();const b = new Singleton();console.log(a.get() === b.get()); // trueconsole.log(a.get() === Singleton.get()); // trueconsole.log(Singleton.get() instanceof Singleton); // true
이렇게 하면 new로 호출하든 그냥 쓰든 모두 다 되는 싱글턴이 생성됩니다. static 메서드에서는 this를 쓰지만 인스턴스 메서드에서는 클래스명을 사용해야 서로 같은 걸 가리키게 됩니다.
]]></description><link>https://www.zerocho.com/category/JavaScript/post/65bc7d7a8d81167404d8ba6c</link><guid isPermaLink="true">https://www.zerocho.com/category/JavaScript/post/65bc7d7a8d81167404d8ba6c</guid><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Fri, 02 Feb 2024 05:28:26 GMT</pubDate></item><item><title><![CDATA[async 함수 내부에서 에러가 throw된 경우 catch 방법 - unhandled promise의 무서움]]></title><description><![CDATA[오늘은 unhandled promise error를 내면 안 되는 이슈를 알아보며 async/await과 error의 특징에 대해 살펴보겠습니다.
async function sub() {  throw 'hello';}async function main() {  await sub();}main();
브라우저: Uncaught (in promise) hello
노드: [UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "hello".] {
  code: 'ERR_UNHANDLED_REJECTION'
}
노드의 에러 메시지가 더 길지만 둘 다 결국은 Promise에 catch를 달지 않았다는 에러(unhandled promise)입니다.
다음과 같이 try/catch문을 쓰면 콘솔에 caught hello라고 뜨며 에러를 잡을 수 있습니다.
async function sub() {  try {    throw 'hello';  } catch (err) {    console.log('caught', err);  }}async function main() {  await sub();}main();
try/catch문을 main에 써도 될까요?
async function sub() {  throw 'hello';}async function main() {  try {    await sub();  } catch (err) {    console.log('caught', err);  }}main();
네 이번에도 제대로 잡히는 것을 볼 수 있습니다.
Promise에서 throw된 것도 상위 함수로 전파되며 상위에서 try/catch해도 됩니다.
그러면 다음과 같이 최상위 스코프에서 잡을 수 있을까요?
async function sub() {  throw 'hello';}async function main() {  await sub();}try {  main();} catch (err) {  console.log('caught', err);}
다시 uncaught (in promise) hello 에러가 뜨게 됩니다. 이 에러는 사실 다음 코드에서도 발생합니다.
async function sub() {  throw 'hello';}async function main() {  try {    sub();  } catch (err) {    console.log('caught', err);  }}main();
아까는 에러가 잡혔던 것 같은데 무엇이 달라진 걸까요? 바로 sub() 앞에 await이 빠졌다는 것입니다. await이 빠진 경우 try/catch문으로는 잡을 수 없게 됩니다.
이렇게 main 앞에도 await을 붙이면 에러가 잡힙니다. async 함수 없이 main 앞에 await을 붙이는 건 ESM에서 가능한 top level await 문법입니다(노드 CJS에서는 안 됩니다)
async function sub() { throw 'hello';}async function main() { await sub();}try { await main();} catch (err) { console.log('caught', err);}
Unhandled promise 에러가 무서운 이유는 한 번 터지면 상위에서도 잡히지 않는다는 점 때문입니다.
async function sub() {  throw 'hello';}async function main() {  try {    sub();  } catch (err) {    console.log('caught', err);  }}try {  await main();  console.log('done');} catch (err) {  console.log('caught', err);}
실행해보면 done이 콘솔에 찍히고 그 다음에 unhandled promise 에러가 발생합니다.
main 실행 시 sub();에서 unhandled promise 에러가 발생했지만 try/catch문에서 잡히지 않는 것입니다.
따라서 await을 반드시 붙여주어야 하고, await을 붙이지 않은 경우는 적어도 .catch() 메서드를 붙여주어야 합니다.
async function sub() {  throw 'hello';}async function main() {    sub().catch((err) =&gt; console.log('caught', err));}main();
이 방법은 지저분하니 await을 꼭 붙여줍시다.
return이 있을 때도 마찬가지입니다.
async function sub() {  throw 'hello';}function main() {  try {    return sub();  } catch (err) {    console.log('caught', err);  }}main();
위와 같이 하면 에러가 잡히지 않으니  다음과 같이 async/await을 도입해야 합니다. Nest.js를 쓸 때 많이 실수하던 부분이었습니다.
async function sub() {  throw 'hello';}async function main() {  try {    return await sub();  } catch (err) {    console.log('caught', err);  }}main();
return 뒤에 await을 붙이면 됩니다.
콜백 함수가 async인 경우도 살펴봅시다.
async function sub(cb) {  await cb();}async function main() {  await sub(async function cb() {    throw 'callback';  });}try {  main();  console.log('done');} catch (err) {  console.log('caught', err);}
실행하면 역시나 done이 출력된 후에 unhandled promise 에러가 발생합니다.
이제 아시겠지만 unhandled promise 에러는 try/catch문에서 잡히지 않습니다.
async function sub(cb) {  await cb();}async function main() {  await sub(async function cb() {  throw 'callback';  });}try {  await main();} catch (err) {  console.log('caught', err);}
해결책도 아시겠죠? main 앞에 await을 붙이면 됩니다. 또는 중간 어디에서든 await을 쓰는 곳에서 try / catch로 잡으면 됩니다.
async function sub(cb) {  try {    await cb();  } catch (err) {    console.log('caught', err);  }}async function main() {  await sub(async function cb() {    throw 'callback';  });}await main();
async function sub(cb) {  await cb();}async function main() {  try {    await sub(async function cb() {      throw 'callback';    });  } catch (err) {    console.log('caught', err);  }}await main();
이번 글을 요약하면 다음과 같습니다.

await의 에러는 상위 함수로 전파되며 try/catch문으로 잡을 수 있다
await을 붙이지 않고 프로미스에서 에러가 발생하면 unhandled promise 에러가 발생한다.
unhandled promise 에러는 try/catch문에서 잡히지 않으니 주의하자
await을 붙이지 않은 프로미스는 try/catch문으로 에러를 잡을 수 없다. await이나 catch()를 꼭 붙여야 한다.

]]></description><link>https://www.zerocho.com/category/JavaScript/post/65bc73518d81167404d8a0fd</link><guid isPermaLink="true">https://www.zerocho.com/category/JavaScript/post/65bc73518d81167404d8a0fd</guid><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Fri, 02 Feb 2024 04:45:05 GMT</pubDate></item><item><title><![CDATA[2023년 개인 회고 (및 블로그는 결산 없음 ㅠㅠ)]]></title><description><![CDATA[2022년 회고 링크 https://www.zerocho.com/category/etc/post/639fec47b7da21913e4bcd40 
2023년 회고입니다. 블로그 유기 선언 을 하면서 쓰지 않으려 했다가 그래도 회고는 일년에 한 번인데 써야하지 않나 싶어서 작성합니다.
가면 갈 수록 제 체력/정신력/의지력이 줄어들고 있다는 게 보입니다. 헬스를 꾸준히 해서 몸은 좋아지는데도 체력은 점점 줄어드네요. 정신력/의지력/집중력도 조금 씩 떨어지고 있습니다 ㅠㅠ 나이를 한 살씩 먹어서 그런 것일까요. 그래서 일할 때 최대한 효율적으로 일해야겠다는 생각을 계속 갖고 있습니다.
카카오모빌리티 퇴사
올해 가장 카카오모빌리티를 퇴사하였습니다. 3월에 퇴사하였지만 남은 휴가를 전부 다 소진하니 4월 말이 되더군요. 퇴사한 이유는 이 영상 에서 말을 다 했지만 영상이 기니 요약하자면

회사에 대한 기대 상실
개인 실력이 성장하지 않음(매너리즘)
스타트업 재창업 결심

이었던 것 같습니다. 저희 팀은 카카오모빌리티가 국정감사/과징금 이슈가 있기도 전에 식물 상태가 되어버려서 2022년 한 해를 거의 아무 것도 하지 않고 보냈습니다. 그 과정에서 휴식은 취했고 월급은 달달했지만 정체되어 있는 게 아닌가 하는 불안감에 휩싸였던 것 같습니다.
그리고 2024년에 돌아보았을 때 각종 규제로 아직도 사업을 이어나가지 못하고 있는 카카오모빌리티를 보면서 퇴사는 좋은 결정이었다고 생각합니다. 그래서 후회는 없습니다.
사실 퇴사만 놓고 보면 고민이 많았긴 한데 친구가 스타트업 같이 해보지 않을래라고 제의를 해서 화끈하게 퇴사할 수 있었던 것 같습니다. 경기가 어렵긴 하지만 저에게는 강의/유튜브 수익이 있으니 버틸 수 있을 것이라 생각했습니다.
싱가포르 여행
카카오모빌리티 퇴직금으로 싱가폴 여행을 떠났습니다. 동생과의 첫 여행이었는데 리프레시도 되고 너무 좋았습니다. 야경이 특히 너무나 멋있더군요. 근데 싱가포르는 다 좋은데 너무 덥습니다 ㅠㅠ



스모어톡 창업
카카오모빌리티를 퇴사하게 만든(?) 친구와 같이 창업을 했습니다. 그 친구도 고등학교 동창인데 다른 공동 창업자 한 명도 고등학교 후배라서 상산고 동문 스타트업이 되어버렸습니다.
저희가 하는 서비스는 플라멜 이라는 생성형 AI 서비스로 생성하길 원하는 장면만 입력하면 프롬프트 엔지니어링이나 후처리 같은 것은 쉽게 해서 이미지를 생성하는 그런 서비스입니다. 2023년 동안은 클로즈 베타로 진행했지만 2024년에는 오픈 베타로 대중에게 공개할 예정입니다.
AI쪽 서비스는 Myfit CTO 이후로는 처음인데 AI 기술이 엄청나게 발전했다는 것을 느낄 수 있었습니다. 개발쪽도 변화가 빠르다고 하는데 AI에 비하면 쨉도 안 됩니다. AI는 지난 주의 논문을 반박하거나 성능으로 압살하는 새로운 논문이 매 주 나오고 있습니다. 트렌드를 따라가려면 매 주 논문을 읽어야 하는...
일단 AI 서비스를 출시하는 것 자체도 많이 바뀌었습니다. replicate라는 감사한 서비스 덕분에 서버리스처럼 AI를 호스팅할 수 있게 되었더군요. 호스팅 비용보다 학습 비용이 더 많이 들고 있는데 나중에 규모가 커지면 호스팅 비용을 어떻게 감당해야 하나 고민이 됩니다.
AI 기술은 넘쳐나지만 AI로 서비스를 잘 하기는 어렵고, 수익화까지 하는 건 더더욱 어려운 것 같습니다. 경쟁자들도 너무 많고 기술은 계속 나오며 비용은 비싸고 소비자의 지불 의사는 적습니다 ㅠㅠ 일단 열심히 도전해봐야겠습니다.



타입스크립트 교과서
8월에는 타입스크립트 교과서 를 출간하였습니다. 인프런의 타입스크립트 강좌 파트1 , 파트2를 합 쳐놓은 것이라 집필에 4개월밖에 걸리지 않았습니다. 단순히 타입만 설명하는 게 아니라 직접 타입을 작성하고, 분석하는 것을 연습시켜주는 책입니다. 한 번 보시면 절대 후회하지 않을 책이라고 자부합니다. 교과서라는 이름은 Node.js 교과서와 마찬가지로 조금 마케팅적인 측면이 있습니다만 교과서라는 이름에 부끄럽지 않게 노력을 기울였습니다.



베트남 여행
10월에는 가족들과 베트남 여행을 다녀왔습니다. 냐쨩(나트랑)과 달랏에 다녀왔는데요. 달랏이 주인 여행이었습니다. 달랏은 평균 온도가 18~20도인 고산 지대로 베트남 사람도 여기로 피서를 올 정도로 시원한 곳이었습니다. 처음에 호텔에 갔는데 4성 호텔인데도 에어컨이 없어서 놀랐지만 매우 꿀잠을 잘 수 있는 그런 기후였습니다. 베트남에도 덥지 않은 곳이 있다는 게 놀라웠던...! 음식도 입맛에 맞고 제일 좋아하는 두리안!도 먹어보고 좋은 여행이었습니다.



유튜브 활동
강의에 집중하느라 유튜브도 조금 쉬고 있다가 11월부터 갑자기 의욕이 생겨서 다시 찍고 있습니다. 2024에는 구독자 5만명 꼭 달성해보고 싶습니다.
강의 활동
올해의 컨셉은 Yes맨이었습니다. 일정의 충돌만 없으면 들어오는 모든 제의에 Yes를 한 번 해보자는 마인드로 살았습니다. 협업 제의, 광고 문의에 전부 Yes를 하면서 알차게 살았던 것 같습니다.
올해에는 인프런 강좌를 4개 내었습니다. 자꾸 버전이 바뀔 때마다 리뉴얼 강좌를 내야하는 것이 힘들어서 완전 CS쪽 강좌를 내봤는데요. 이미 각 분야 강자들이 있는지라 그 쪽을 뚫기는 쉽지 않았습니다.

비전공자의 전공자 따라잡기 - 네트워크, HTTP  
비전공자의 전공자 따라잡기 - 데이터베이스,SQL 
비전공자의 전공자 따라잡기 - 자료구조(with JavaScript) 

세 편입니다.
그리고 연말에는 드디어! Next 14 버전 강의 를 출시했습니다. 제일 많이 팔린 제 강의 React Nodebird 의 최신 버전(하지만 React Nodebird는 page router라서 없애지는 않았습니다) 강의입니다.
그 외에도 LS그룹, LG CNS 등의 기업 강의를 진행하였고(라이브라서 너무 힘들었네요 ㅎㅎ;;), GDSC 숭실 발표, 앨리스코딩 발표, 유데미 코딩 페스티벌 등에 참가했습니다.
블로그 No결산
블로그 결산은 올해는 없습니다... GA가 Universal Analytics에서 GA4로 바뀌면서 데이터들이 언제부터인가 수집이 안 되더라구요. 놓치고 있다가 GA4로 이전하기는 했지만 8~11월 데이터가 없어져서 결산을 낼 수가 없게 되었습니다... ㅠㅠ 이미지 호스팅도 중단되어서 더이상 지난 회고의 이미지도 보이지 않네요.
일단 내년에는 다시 이미지를 살려 블로그 결산 해보도록 하겠습니다.
]]></description><link>https://www.zerocho.com/category/etc/post/65bc47748d81167404d8500c</link><guid isPermaLink="true">https://www.zerocho.com/category/etc/post/65bc47748d81167404d8500c</guid><category><![CDATA[etc]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Fri, 02 Feb 2024 01:37:56 GMT</pubDate></item><item><title><![CDATA[ES2024의 변화]]></title><description><![CDATA[2024년이 되었고, JS 역시 새로운 스펙을 내놓았습니다. 그런데 새 스펙이 단 한 개입니다. 그것도 쓸 일이 많을까 의심이 되는 RegExp v flag라는 기능뿐이네요.
RegExp V Flag
일단 v 플래그는 i나 g처럼 정규표현식 뒤에 붙이는 한 글자 플래그입니다.
const regex = /^\p{Emoji}$/v;
v 플래그를 알려면 u 플래그를 먼저 알아야 합니다. u는 유니코드를 의미하는 플래그입니다. 다음과 같이 정규표현식의 패턴 매칭을 통해 유니코드 이모지를 검색할 수 있습니다.
/^\p{Emoji}$/u.test('🙂') // true
다만 유니코드 이모지가 조합인 경우에는 제대로 검색되지 않습니다.
/^\p{Emoji}$/u.test('😵‍💫') // false
이래서 나온 게 v 플래그입니다. v 플래그를 사용하면 조합된 이모지도 검색할 수 있습니다. 다만 패턴 매칭의 이름이 조금 달라졌다는 점은 유의해야 합니다.
/^\p{RGI_Emoji}$/v.test('😵‍💫') // true
아래 글에 훨씬 더 구체적으로 내용이 나와 있습니다! 번역해서 읽어보세요.
https://v8.dev/features/regexp-v-flag 
2025년에는 더 많은 기능들이 추가되길 바라면서 이번 글은 마치겠습니다.
]]></description><link>https://www.zerocho.com/category/ECMAScript/post/65a73e6d543ac48350e508c7</link><guid isPermaLink="true">https://www.zerocho.com/category/ECMAScript/post/65a73e6d543ac48350e508c7</guid><category><![CDATA[ECMAScript]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Wed, 17 Jan 2024 02:41:49 GMT</pubDate></item><item><title><![CDATA[LF will be replaced by CRLF the next time Git touches it 해결법]]></title><description><![CDATA[mac에서 진행한 프로젝트를 windows에서 수정하거나, windows에서 진행한 프로젝트를 mac에서 진행하는 경우(이 때는 에러메시지의 LF와 CRLF가 반대로 나옴)에 해당 경고 메시지가 발생합니다.
깃이 LF를 CRLF로 다음 번에 바꾸겠다고 하는 메시지인데요. 먼저 LF, CRLF가 뭔지 알아보겠습니다.

LF(Line Feed): Mac과 Linux에서 쓰는 줄바꿈 문자인 \n을 의미합니다.
CRLF(Carriage Return+Line Feed): Windows에서 쓰는 줄바꿈 문자열인 \r\n을 의미합니다.

우리가 줄바꿈(엔터 클릭)을 할 때 운영체제에서는 \n나 \r\n을 몰래 붙이고 있던 것입니다!
안녕하세요.제로초입니다.
위와 같은 글이 있다고 하면 실제로는 다음과 같이 저장되고 있는 것이죠.
안녕하세요.\n제로초입니다.
즉, 현재 메시지는 mac과 windows가 사용하는 줄바꿈 문자열이 달라서 Git이 어떤 것을 선택해야할지 몰라 발생하는 문제인 것이죠. 이 메시지는 에러가 아닌 경고라서 무시해도 되긴 하지만 문제는 LF가 자동으로 CRLF로 수정돼서 나중에 mac에서 다시 작업할 때 불편하게 됩니다.
그렇다면 해결법은 Git에게 어떤 것을 쓸 지 알려주면 됩니다. Git에는 core.eol과 core.autocrlf 속성이 있습니다. eol은 줄바꿈 문자를 어떤 걸 쓸지에 대한 설정이고, autocrlf는 파일을 Git에 불러올 때(check in) 줄바꿈 문자를 바꿀지 여부에 대한 설정입니다.
eol은 crlf, native, lf가 있고, autocrlf에는 true, false, input이 있습니다.
eol이 crlf면 줄바꿈 문자로 CRLF를 쓰는 것이고, lf면 LF를 쓰는 것입니다. native는 운영체제에 따르는 것이므로 windows에서는 CRLF를 사용하게 되고, mac에서는 LF를 사용하게 됩니다.
autocrlf가 true면 CRLF를 사용하게 되고, input이면 LF를 사용하게 됩니다. false인 경우는 기존 파일의 줄바꿈 표시를 그대로 사용합니다. false인 경우에 다른 운영체제의 줄바꿈이 그대로 사용될 수 있으므로 문제가 발생할 수 있습니다.
저는 mac의 LF를 사용할 것이므로 다음과 같이 바꿔줍니다.
git config core.eol lfgit config core.autocrlf input
따라서 위와 같이 해결했습니다.
이 설정은 이 프로젝트에만 적용되지만 혹시나 현재 컴퓨터의 모든 프로젝트에 적용하고 싶다면 --global을 붙이면 됩니다.
git config --global core.eol lfgit config --global core.autocrlf input
]]></description><link>https://www.zerocho.com/category/Git/post/65110ada92e25c938ac41b53</link><guid isPermaLink="true">https://www.zerocho.com/category/Git/post/65110ada92e25c938ac41b53</guid><category><![CDATA[Git]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Mon, 25 Sep 2023 04:21:46 GMT</pubDate></item><item><title><![CDATA[413 Request Entity Too Large 오류 해결법 - nginx]]></title><description><![CDATA[413 Request Entity Too Large 오류는 주로 서버의 허용량보다 더 큰 파일을 올렸을 때 발생합니다. 따라서 서버의 허용량을 늘려주면 됩니다.
http 블록 안에 다음과 같이 용량을 기입하면 됩니다. k, m, g(각각 키로바이트, 메가바이트, 기가바이트) 같은 단위를 쓸 수 있고, 0으로 두면 무제한입니다.
http {    ...    client_max_body_size 20m;    ...} 
http 외에 location이나 server 블록 안에 개별적으로 기입도 가능합니다.
변경 후에 nginx 재시작하는 것 잊지 마세요!
sudo service nginx reload또는sudo systemctl reload nginx
]]></description><link>https://www.zerocho.com/category/HTTP/post/6507ad4a457fe081afb2a769</link><guid isPermaLink="true">https://www.zerocho.com/category/HTTP/post/6507ad4a457fe081afb2a769</guid><category><![CDATA[HTTP]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Mon, 18 Sep 2023 01:52:10 GMT</pubDate></item><item><title><![CDATA[ECMAScript 모듈(ESM)에서 __filename, __dirname 쓰는 법]]></title><description><![CDATA[ESM에서는 __filename, __dirname이 사라졌습니다. 현재 파일을 가리키는 import.meta.url만 존재하죠. __filename, __dirname을 임의로 추가하고 싶다면
import path from "path";import { fileURLToPath } from "url";const __filename = fileURLToPath(import.meta.url);const __dirname = path.dirname(__filename);
하면 됩니다.
]]></description><link>https://www.zerocho.com/category/NodeJS/post/65014ffc0804e3cb665ca7eb</link><guid isPermaLink="true">https://www.zerocho.com/category/NodeJS/post/65014ffc0804e3cb665ca7eb</guid><category><![CDATA[NodeJS]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Wed, 13 Sep 2023 06:00:28 GMT</pubDate></item><item><title><![CDATA[ESM 모듈 불러올 때 TS2307, TS1479 해결법]]></title><description><![CDATA[일단 대부분의 경우 Node.js와 타입스크립트를 같이 쓸 때 , 그리고 ESM 모듈을 불러올 때 해당 에러가 발생할 겁니다. 
TS2307: Cannot find module 패키지명  or its corresponding type declarations. 
설마 타입 정의가 없는 패키지를 받으신 건 아니죠..? 그러면 @types를 설치하거나 직접 declare module로 타이핑을 하면 됩니다.
이 글은 CommonJS에서 ESM을 import할 때 발생하는 TS2307 에러에 대해 다룹니다.
먼저 모듈을 Node16으로 맞춰줍니다. Node16이면 현재 프로젝트가 CommonJS면 CommonJS로 결과를 내놓고, ESM이면 ESM으로 결과를 내놓습니다.
"module": "Node16",
그리고 import 대신에 dynamic import문을 사용해야 합니다. import('모듈명')을 해서 Promise로 모듈 결괏값을 받으면 됩니다. 이 게시글 참조  이러면 모든 에러가 사라집니다.
여기서 문제가 type만 import하고 싶을 때입니다. import type을 하는 경우는 ESM이든 CommonJS이든 상관 없지 않느냐 할 수 있는데(실제로 저도 왜 에러가 나는지 이해가 안 됩니다) 여전히 다음과 같은 에러가 남아 있습니다.
import type { ShowPropertyProps } from 'adminjs'; // adminjs에서 에러
TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("adminjs")' call instead.
typescript 5.2까지는 다음과 같이 할 수밖에 없습니다.
// @ts-expect-errorimport type { ShowPropertyProps } from 'adminjs';
@ts-expect-error를 붙여주면 해결됩니다. 그런데 @ts-expect-error를 쓰는 것에서 에러가 난다면 .eslintrc에서 rules로 ban-ts-comment를 off 해줍시다.
rules: {  ...  '@typescript-eslint/ban-ts-comment': 'off',  ...}
typescript 5.3부터는 다음과 같이 할 수 있게 되었습니다. import attributes 문법입니다.
import type { ShowPropertyProps } from 'adminjs' with { 'resolution-mode': 'import' };

]]></description><link>https://www.zerocho.com/category/TypeScript/post/6500178bc8629d716bf282c6</link><guid isPermaLink="true">https://www.zerocho.com/category/TypeScript/post/6500178bc8629d716bf282c6</guid><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Tue, 12 Sep 2023 07:47:23 GMT</pubDate></item><item><title><![CDATA[CommonJS에서 ESM 모듈(ECMAScript module) require하기 - Error [ERR_REQUIRE_ESM]]]></title><description><![CDATA[지난 글에서  ESM 모듈에서 CommonJS 모듈을 import하는 방법을 알아봤다면(아무 문제 없이 할 수 있었습니다) 이번에는 CommonJS 모듈에서 ESM 모듈을 require해보겠습니다. 과연 지난 번처럼 잘 될까요? 다음 세 파일을 준비합니다.
package.json
...  "type": "commonjs",...
esm.mjs
export const c = 'd';export default {  a: 'b',  c,};
cjs.js
const a = require('./esm.mjs');const { c } = require('./esm.mjs');console.log(a);console.log(c);
node cjs로 코드를 실행해보면  다음과 같은 에러가 발생합니다.
node:internal/modules/cjs/loader:1117    throw new ERR_REQUIRE_ESM(filename, true);    ^Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/zerocho/esm.mjs not supported.Instead change the require of /Users/zerocho/esm.mjs to a dynamic import() which is available in all CommonJS modules.    at Object.&lt;anonymous&gt; (/Users/zerocho/cjs.js:1:11) {  code: 'ERR_REQUIRE_ESM'}
CommonJS에서 ESM 모듈을 require하려고 하니 에러가 발생하는 것입니다. 해결 방법은 에러 메시지에 나와 있습니다. dynamic import()로 바꾸라고 하네요.
cjs.js를 다음과 같이 수정합니다.
import('./esm.mjs').then((module) =&gt; {  console.log(module);});
import()는 프로미스인지라 then의 결괏값으로 모듈을 가져옵니다. 여러 ESM 모듈을 동시에 불러오려면 Promise.all을 해야할 수도 있습니다.
node cjs로 파일을 실행해보면 정상적으로 불러옵니다.
[Module: null prototype] { c: 'd', default: { a: 'b', c: 'd' } }
한 가지 특이한 점은, export default 했던 것은 default라는 키 아래에 데이터가 들어있다는 점입니다. 한 뎁스 들어가야 해서 번거롭긴 하지만 이렇게 하는 것이 최선입니다.
그런데 이렇게 해도 타입스크립트에서는 ESM 모듈 import 시 TS2307: Cannot find module 패키지명  or its corresponding type declarations. 에러가 발생하는 경우가 있습니다. 그럴 때는 이 게시글 을 참고하세요!
]]></description><link>https://www.zerocho.com/category/NodeJS/post/650007d7c8629d716bf254d3</link><guid isPermaLink="true">https://www.zerocho.com/category/NodeJS/post/650007d7c8629d716bf254d3</guid><category><![CDATA[NodeJS]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Tue, 12 Sep 2023 06:40:23 GMT</pubDate></item><item><title><![CDATA[ESM에서 CommonJS 모듈 import 하기]]></title><description><![CDATA[ESM vs CommonJS 시리즈로 계속 블로깅을 하고 있습니다.
이번 게시글에서는 ESM 모듈에서 CommonJS 모듈 파일을 import하는 방법에 대해 알아보겠습니다. 다음 게시글은 반대로 CommonJS에서 ESM 모듈을 require하는 방법에 대해 알아봅니다. 준비할 파일은 3가지입니다.
package.json
...  "type": "module",...
esm.js
import a, { c } from './cjs.cjs'; // 확장자 명시 필요console.log(a); // { a: 'b', c: 'd' }console.log(c); // d
cjs.cjs
exports.a = 'b';exports.c = 'd';
package.json에는 현재 프로젝트가 ESM임을 알리고, cjs.cjs에 CommonJS 스타일로 모듈을 export 한 뒤 esm.js에서는 ESM 스타일로 모듈을 import합니다.
node esm으로 파일을 실행해보면 문제 없이 실행됩니다. 즉, ESM에서 CommonJS 파일을 불러올 때는 큰 문제가 없습니다. 반대로 CommonJS에서 ESM을 불러올 때는 문제가 발생합니다. 다음 게시글을 보시죠.
]]></description><link>https://www.zerocho.com/category/NodeJS/post/65000648c8629d716bf24f73</link><guid isPermaLink="true">https://www.zerocho.com/category/NodeJS/post/65000648c8629d716bf24f73</guid><category><![CDATA[NodeJS]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Tue, 12 Sep 2023 06:33:44 GMT</pubDate></item><item><title><![CDATA[package.json에 추가된 exports 필드(속성)]]></title><description><![CDATA[요즘 노드 생태계에서 ESM과 CommonJS의 싸움이 한창입니다. 슬슬 표준인 ESM으로 넘어가고 있기는 하지만 아직 저마다의 사정으로 CommonJS를 고수하고 있기도 합니다. 대표적으로 서버 프레임워크로 많이 쓰는 Nest.js가 아직 CommonJS를 고수하고 있고 ESM으로 이전할 계획은 아직 없다고 합니다.
다행히 요즘 많은 라이브러리들이 CommonJS와 ESM을 동시에 지원하고 있습니다. 내 프로젝트가 ESM이라면 라이브러리의 ESM 모듈(ESM의 M이 모듈이라서 동어 반복이긴 하지만 그냥 ESM 모듈이라고 하겠습니다)을 불러오고, 내 프로젝트가 CommonJS라면 라이브러리의 CommonJS 모듈을 불러오는 셈이죠. 그런데 내 프로젝트가 어떻게 해당 라이브러리의 올바른 모듈을 불러올 수 있는 것일까요? 
바로 package.json에 추가한 exports 필드(속성)를 보고 불러오는 것입니다. 참고로 내 프로젝트를 ESM으로 만들고 싶다면 이 게시글 을 읽어보세요.
기존에는 package.json의 main 필드를 주로 참고했습니다. main 필드에 적힌 경로의 파일이 제일 중요한 파일이었기 때문이죠. 하지만 ESM 모듈과 CommonJS 모듈 두 개를 동시에 지원하는 경우에는 main 필드에 두 파일을 모두 적을 수가 없습니다. 그래서 나온 게 exports 필드라고 보시면 됩니다. exports 필드가 있으면 main, module, types 등보다 exports 필드를 더 우선시합니다.
유명한 axios 라이브러리의 package.json을 한 번 보시죠.
  "exports": {    ".": {      "types": {        "require": "./index.d.cts",        "default": "./index.d.ts"      },      "browser": {        "require": "./dist/browser/axios.cjs",        "default": "./index.js"      },      "default": {        "require": "./dist/node/axios.cjs",        "default": "./index.js"      }    },    "./lib/adapters/http.js": "./lib/adapters/http.js",    "./lib/adapters/xhr.js": "./lib/adapters/xhr.js",    "./unsafe/*": "./lib/*",    "./unsafe/core/settle.js": "./lib/core/settle.js",    "./unsafe/core/buildFullPath.js": "./lib/core/buildFullPath.js",    "./unsafe/helpers/isAbsoluteURL.js": "./lib/helpers/isAbsoluteURL.js",    "./unsafe/helpers/buildURL.js": "./lib/helpers/buildURL.js",    "./unsafe/helpers/combineURLs.js": "./lib/helpers/combineURLs.js",    "./unsafe/adapters/http.js": "./lib/adapters/http.js",    "./unsafe/adapters/xhr.js": "./lib/adapters/xhr.js",    "./unsafe/utils.js": "./lib/utils.js",    "./package.json": "./package.json"  },
exports 필드가 위와 같이 적혀져 있습니다. 일단 .(점)은 axios를 의미합니다. 즉 import 'axios'나 require('axios')를 할 때 어떤 파일을 불러올 것인지를 적어둔 것이라 보시면 됩니다. 이 부분은 복잡하니까 잠깐 넘어가고 아래 './lib/adapters/http.js'의 경우에는 import 'axios/lib/adapters/http.js'를 했을 때 './lib/adapters/http.js' 파일을 불러온다는 것을 알 수 있습니다. ESM이든 CommonJS든 불러오는 파일이 동일한 경우에는 이렇게 간단하게 적을 수 있습니다.
이제 다시 .(점)을 봅시다. 점 아래에는 types, browser, default가 키가 있습니다. 이들은 조건입니다. 조건에 대한 리스트는 여기 에 있습니다. types는 타입스크립트에서 타입을 불러올 때, browser는 브라우저에서 사용하는 경우, default는 그 외 기본적인 경우를 의미합니다. default 속성은 반드시 객체에서 제일 마지막에 있어야 합니다. 각각의 조건에서 다시 아래 require과 default가 있습니다. 이들도 조건이라고 보면 됩니다. require는 CommonJS에서 불러올 때 참조할 파일이고, default는 그 외의 경우 불러올 파일입니다.
      "types": {        "require": "./index.d.cts",        "default": "./index.d.ts"      },
즉, types+require는 CommonJS 타입스크립트 환경에서 타입을 찾으려고 할 때 참조할 파일을 적은 것이고 index.d.cts를 참조하라고 되어 있습니다. cts 확장자가 낯설다면 CommonJS인 ts 파일을 명시적으로 가리키는 것이라고 보면 됩니다(ESM 확장자인 mts와 상응) types+default는 CommonJS가 아닌 타입스크립트 환경(ESM이 되겠죠)에서 타입을 찾으려고 할 때 참조할 파일을 적은 것입니다. index.d.ts를 보라고 되어 있습니다.
index.d.cts와 index.d.ts를 각각 살펴보면 확실히 각자 모듈에 맞게 다르게 타이핑되어 있습니다.
index.d.cts
...declare const axios: axios.AxiosStatic;export = axios;
index.d.ts
...declare const axios: AxiosStatic;export default axios;
axios 깃헙에서  browser와 default의 파일도 한 번 확인해보세요. browser+require는 CommonJS 브라우저 코드, browser+default는 ESM 브라우저 코드, default+require는 node의 CommonJS 코드, default+default는 node의 ESM 코드입니다. 첫 번째 default는 types도, browser도 아닌 상황을 의미하는 것이고, 두 번째 default는 require이 아닌 상황(CommonJS가 아닌 상황)이므로 서로 다른 default인 것입니다. browser 코드에는 노드 모듈들을 불러오는 부분이 제외되어 있습니다.
참고로 다음과 같이 development, production처럼 process.env.NODE_ENV 별로 다른 파일을 가져오게 할 수도 있습니다.(웹팩 설명에서 발췌)
{  "type": "module",  "exports": {    "node": {      "development": {        "module": "./index-with-devtools.js",        "import": "./wrapper-with-devtools.js",        "require": "./index-with-devtools.cjs"      },      "production": {        "module": "./index-optimized.js",        "import": "./wrapper-optimized.js",        "require": "./index-optimized.cjs"      },      "default": "./wrapper-process-env.cjs"    },    "development": "./index-with-devtools.js",    "production": "./index-optimized.js",    "default": "./index-optimized.js"  }}
이렇게 명시를 해놓으면 import나 require할 때 올바르게 가져올 수 있습니다. 하지만 가장 좋은 것은 빨리 모든 생태계가 ESM으로 통일돼서 이런 상황이 없어지는 것이겠죠 ㅠㅠ 그런데 여전히 production vs development, browser vs node 같은 상황은 존재하므로 exports 필드는 모듈 생태계 통합 이후에도 존재할 겁니다.
다음 글에서는 CommonJS 환경에서 ESM 모듈을 불러오는 방법에 대해 알아보겠습니다.
]]></description><link>https://www.zerocho.com/category/NodeJS/post/64fff6adc8629d716bf2172c</link><guid isPermaLink="true">https://www.zerocho.com/category/NodeJS/post/64fff6adc8629d716bf2172c</guid><category><![CDATA[NodeJS]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Tue, 12 Sep 2023 05:27:09 GMT</pubDate></item><item><title><![CDATA[자료구조(해시테이블, hash table)]]></title><description><![CDATA[이번 시간에는 해시테이블에 대해 한 번 알아보도록 하겠습니다. 해시테이블은 객체같은 키-값 구조라고 보면 됩니다.
자바스크립트에서는 굳이 해시테이블을 쓸 필요가 없습니다. 이미 객체나 배열이라는 훌륭한 자료구조가 있기 때문이죠. 속성을 원없이 넣을 수 있습니다. 하지만 자료구조를 배울 때는 항상 데이터의 양(또는 길이)을 사전에 정의해야한다는 것을 기억해야 합니다. 배열이나 객체 모두 넣을 수 있는 양이 처음부터 정해져 있는 것이죠. C같은 언어를 할 때는 처음에 자료구조를 만들 때부터 데이터 길이를 명시해줘야하는 경우가 있어서 그렇습니다.
예를 들어 해시테이블에 30개의 칸만 존재한다고 해봅시다. 그런데 데이터는 31개면 어떻게 될까요? 마지막 데이터는 연결 리스트의 형태로 저장됩니다. 즉, 29개의 칸에는 1개의 데이터가 저장되고, 1개의 칸에는 2개의 데이터가 저장될 수 있는 것이죠. 그렇다면 그 1개의 칸은 어떻게 정하게 될까요? 이 때 해시(hash)라는 개념이 나옵니다. 해시테이블을 해시들을 활용한 테이블이라서 해시테이블인 것입니다.
넣을 데이터를 어떤 알고리즘을 통해 1~30까지의 숫자로 변경하는 것이 해시입니다. 최대한 데이터가 고르게 나오는 알고리즘이어야 한 칸에 31개의 데이터가 저장되고 나머지 29칸은 텅텅 비어있는 것과 같은 불상사를 막을 수 있습니다. 예를 들어 dog는 14으로, cat은 12로 horse는 5로 변경되게 만드는 알고리즘이면 됩니다. 이 때, dog는 알고리즘을 수행할 때마다 매 번 같은 14으로 변경되어야 합니다. 그래야 데이터를 항상 해시테이블에서 찾을 수 있습니다. 해시테이블의 구조가 키-값이라고 했는데 정확히는 키-&gt;해시-값 구조입니다. 키를 해시로 바꿔서 저장하거든요.
해시 함수를 만들어보겠습니다.
function stringToInteger(str, mod) {  return str.split('').reduce((a, c) =&gt; a + c.charCodeAt(), 0) % mod}stringToInteger('dog', 30); // 14
이런 해시 함수를 만들어보았습니다. 각 단어의 charCode를 찾아 모두 더한 후 30으로 나눈 나머지를 구하는 함수입니다. d의 코드는 100, o의 코드는 111, g의 코드는 103이니 다 더해서 314이고, 30으로 나눈 나머지는 14가 됩니다. 참고로 효율적인 해시 함수는 아닙니다. 효율적이지 않다는 것은 데이터가 고르게 나온다는 게 보장되지 않았다는 뜻입니다. 해시테이블의 어떤 칸에 데이터가 몰려있을 수 있는 것입니다. 그냥 간단하게 사용하는 해시 함수라는 것!
coh라는 단어도 stringToInteger를 거치면 14가 나옵니다. dog와 결괏값이 같은데 이렇게 해시가 겹치는 현상을 해시 충돌(hash collision)이라고 부릅니다. 해시 충돌이 최소한으로 일어나는 알고리즘이 좋은 알고리즘입니다.
이제 해시테이블을 구현해봅시다. 연결 리스트 의 코드를 기억해야 합니다.
class Node {  next = null;  constructor(key, data) {    this.key = key;    this.data = data;  }}
class Hashtable {  arr = [];  constructor(mod) {    this.mod = mod;  }    get(key) {    const index = stringToInteger(key, this.mod);    let target = this.arr[index];    let found = null;    while (target) {      if (target.key === key) {        found = target.data;        break;      }      target = target.next;    }    return found;  }  set(key, data) {    const index = stringToInteger(key, this.mod);    if (this.arr[index]) {      let target = this.arr[index];      while (target.next) {        target = target.next;      }      target.next = new Node(key, data);    } else {      this.arr[index] = new Node(key, data);    }    return index;  }  remove(key) {    const index = stringToInteger(key, this.mod);    let prev = null;    let target = this.arr[index];    let found = null;    while (target) {      if (target.key === key) {        found = target.data;        if (prev) {          prev.next = target.next;        } else {          this.arr[index] = null;        }        target.next = null;        break;      }      prev = target;      target = target.next;    }    return found;  }}
const hashtable = new Hashtable(30);hashtable.set('dog', 'bowwow');hashtable.set('cat', 'meow');hashtable.set('coh', 'bowwow2');hashtable.set('boi', 'bowwow3');hashtable.get('coh'); // bowwow2hashtable.remove('coh'); // bowwow2hashtable.get('coh'); // nullhashtable.get('boi'); // bowwow3
hashtable을 조회해보면 12, 14 인덱스에 데이터가 들어있는 것을 확인할 수 있습니다. dog, coh, boi는 index가 모두 14라서 연결리스트로 구성되어 있습니다.
해시 테이블의 시간 복잡도는 삽입, 조회, 제거 모두 O(1)입니다. 정말 대단한 자료구조이죠! 하지만 해시 충돌이 발생하는 경우에는 최악의 경우 O(n)이 됩니다. 한 칸에 모든 데이터가 몰려있는데 찾고자 하는 데이터가 연결리스트의 마지막에 위치한 경우를 생각해보면 됩니다. 그래서 해시테이블에서는 충돌이 발생하지 않게 만드는 것이 매우 중요합니다.
공간 복잡도는 O(n)입니다.
]]></description><link>https://www.zerocho.com/category/Algorithm/post/647ed7d39c8c1de546b6233a</link><guid isPermaLink="true">https://www.zerocho.com/category/Algorithm/post/647ed7d39c8c1de546b6233a</guid><category><![CDATA[Algorithm]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Tue, 06 Jun 2023 06:53:08 GMT</pubDate></item><item><title><![CDATA[자료구조(우선순위 큐, priority queue)]]></title><description><![CDATA[오랜만에 자료구조 포스팅으로 돌아왔습니다. 이번 시간에는 우선순위 큐를 구현해보도록 하겠습니다.
우선순위 큐는 큐 에서 진화된 형태로, 맨 뒤에만 추가되고 맨 앞에서만 나가는 기본적인 FIFO 큐와는 다르게, 우선순위가 높은 노드가 먼저 나갈 수 있습니다. 따라서 일반적인 대기열이 아니라 중요도가 나눠져 있는 대기열을 구현할 때 좋습니다. 줄 서는 시스템에서 VIP를 우대한다든가(자본주의 세상!!!)... 태스크들이 있을 때 긴급 태스크를 먼저 처리하게 한다든가...
우선순위 큐는 힙 (이진 힙)을 사용해서 구현할 수 있습니다. 힙에다 새로운 값을 넣는 게 우선순위 큐에 값을 넣는 것이고, 힙에서 값을 빼는게 우선순위 큐에 값을 제거하는 것입니다. 힙 소스코드에 데이터를 추가로 받게끔 수정하면 됩니다.
class PriorityQueue {  arr = [];  #reheapUp(self, idx) {    if (idx) {      const parent = parseInt((idx - 1) / 2);      if (self.arr[idx].priority &gt; self.arr[parent].priority) {        const temp = self.arr[idx];        self.arr[idx] = self.arr[parent];        self.arr[parent] = temp;        this.#reheapUp(self, parent);      }    }  }  #reheapDown(self, idx) {    let left = 0;    let right = 0;    let large;    if (idx * 2 + 1 &lt; self.arr.length) {      left = self.arr[idx * 2 + 1].priority;      if (idx * 2 + 2 &lt; self.arr.length - 1) {        right = self.arr[idx * 2 + 2].priority;      }      if (left &gt; right) {        large = idx * 2 + 1;      } else {        large = idx * 2 + 2;      }      if (self.arr[idx].priority &lt; self.arr[large].priority) {        const temp = self.arr[idx];        self.arr[idx] = self.arr[large];        self.arr[large] = temp;        this.#reheapDown(self, large);      }    }  }   insert(priority, data) {    const last = this.arr.length;    this.arr[last] = {}; // 객체로 변경    this.arr[last].priority = priority;    this.arr[last].data = data;    this.#reheapUp(this, last);    return true;  };   delete() {    if (this.arr.length === 0) {      return false;    }    const del = this.arr[0];    this.arr[0] = this.arr.pop();    this.#reheapDown(this, 0);    return del.data;  };   peek() {    return this.arr[0]?.data;  };}
insert 시 priority와 data를 받도록 수정했습니다. #reheapUp, #reheapDown에서도 priority를 기반으로 데이터를 교체합니다.
const pq = new PriorityQueue();pq.insert(5, 'one');pq.insert(3, 'two');pq.insert(4, 'three');pq.insert(2, 'four');pq.insert(6, 'five');pq.insert(1, 'six');pq.insert(7, 'king'); // 새치기pq.delete(); // 'king'pq.peek(); // 'five'
FIFO 큐와 비교할 때 큐를 넣는 것, 빼는 것이 모두 O(lg(n))으로 시간 복잡도가 증가했지만 이 정도면 충분히 실전에서 사용할 수 있습니다.
우선순위 큐는 다른 자료구조로도 구현할 수는 있습니다. 정렬된 배열이나, 피보나치 힙, 이항 힙 등으로도 구현 가능합니다. 하지만 성능이 이진 힙보다 낮거나, 좋더라도 구현이 복잡하기에 이진 힙으로 만족하는 편입니다.
]]></description><link>https://www.zerocho.com/category/Algorithm/post/647ecdc99c8c1de546b6227b</link><guid isPermaLink="true">https://www.zerocho.com/category/Algorithm/post/647ecdc99c8c1de546b6227b</guid><category><![CDATA[Algorithm]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Tue, 06 Jun 2023 06:10:18 GMT</pubDate></item><item><title><![CDATA[로그인/댓글 작성 시 느려지는 문제 해결했습니다.]]></title><description><![CDATA[메일 전송이 고장나서 서버가 죽는 문제였네요.
이제 로그인/댓글 자유롭게 다셔도 됩니다. 
]]></description><link>https://www.zerocho.com/category/etc/post/647d830da8acc82f1b034821</link><guid isPermaLink="true">https://www.zerocho.com/category/etc/post/647d830da8acc82f1b034821</guid><category><![CDATA[etc]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Mon, 05 Jun 2023 06:39:09 GMT</pubDate></item><item><title><![CDATA[Flex, Grid]]></title><description><![CDATA[이번 시간에는 Flex와 Grid에 대해 포스팅해보겠습니다.
IE가 사망한 2023년에는 이제 flex와 grid를 좀 더 눈치보지 않고 사용할 수 있게 되었습니다. 예전에 float과 inline-block(심지어 table로 그리드 작업하던 때도 있었죠...)으로 작업하던 때와 비교해서 생산성이 많이 올라갔습니다. flex와 grid는 너무나 훌륭한 교육 자료가 있어 굳이 제가 설명을 쓸 필요가 없을 것 같습니다.
Flexbox Froggy
https://flexboxfroggy.com/#ko
 Grid Garden
https://cssgridgarden.com/#ko 
이 두 사이트만 반복적으로 해보면 됩니다. 저도 까먹을 때마다 한 번씩 다시 해봅니다.
두 사이트 모두 codepip 에서 만들었습니다.  다른 유용한 게임들도 많으니 한 번 해보면 좋습니다.
]]></description><link>https://www.zerocho.com/category/CSS/post/64458a6dcf84a6c93f436da8</link><guid isPermaLink="true">https://www.zerocho.com/category/CSS/post/64458a6dcf84a6c93f436da8</guid><category><![CDATA[CSS]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Sun, 23 Apr 2023 19:43:42 GMT</pubDate></item><item><title><![CDATA[제로초스쿨 슬랙 커뮤니티 링크입니다.]]></title><description><![CDATA[https://www.zerocho.com/slack   
슬랙 커뮤니티 링크입니다.
타임어택, 공모전, 질의응답, 고민상담 등의 강좌 관련 컨텐츠를 여기서 진행합니다! 이미 수강생 천 분 넘게 들어 계시니 많이 들어와주세요~
]]></description><link>https://www.zerocho.com/category/etc/post/64035f98d8a7e6a9fdb57ad8</link><guid isPermaLink="true">https://www.zerocho.com/category/etc/post/64035f98d8a7e6a9fdb57ad8</guid><category><![CDATA[etc]]></category><dc:creator><![CDATA[ZeroCho]]></dc:creator><pubDate>Sat, 04 Mar 2023 15:11:20 GMT</pubDate></item></channel></rss>