<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>suleesulee</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 05 Mar 2026 13:28:40 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>suleesulee</title>
            <url>https://velog.velcdn.com/images/in__32/profile/e219cb75-2603-476b-a7e4-a6b3a3f7d127/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. suleesulee. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/in__32" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Pm2 cluster 모드에서 env 설정하기 (with multi service)]]></title>
            <link>https://velog.io/@in__32/Pm2-cluster-%EB%AA%A8%EB%93%9C%EC%97%90%EC%84%9C-env-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-with-multi-service</link>
            <guid>https://velog.io/@in__32/Pm2-cluster-%EB%AA%A8%EB%93%9C%EC%97%90%EC%84%9C-env-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-with-multi-service</guid>
            <pubDate>Thu, 05 Mar 2026 13:28:40 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/in__32/post/78cb840d-58ed-4297-9b66-a2b3bb94d58f/image.png" alt="">
안녕하세요 백엔드 개발자 이수인입니다! 오늘은 pm2 cluster 모드에서 env 설정하는 방법에 대해서 정리해보려 합니다. 오랜 구글링과 pm2 issue를 여러 개 읽어봐도 자료가 부족해서 해결이 되지 않았던 나름 어려운 문제였습니다. 오픈소스 코드를 읽어본 끝에 해결을 했는데, 다른 분들은 저처럼 고생하지 않았으면 하는 마음에 이 글을 적어봅니다. 저처럼 cluster 모드를 사용하고 multi service에, 각 service 마다 다른 환경변수 파일을 설정하시려는 분들께 도움이 될 것이라 생각합니다.</p>
<p>글은 아래 순서로 적어보려합니다.</p>
<ul>
<li>pm2 cluster를 사용하게 된 계기</li>
<li>문제 상황</li>
<li>간략한 코드 구성</li>
<li>문제 해결 과정</li>
<li>원인</li>
<li>해결 방법</li>
<li>결론</li>
</ul>
<h1 id="pm2-cluster를-사용하게-된-계기">pm2 cluster를 사용하게 된 계기</h1>
<p>먼저 pm2 를 사용한 이유는 다들 같겠지만, 저희 Node.js 프로세스를 백그라운드 환경에서 실행하고 싶었기 때문입니다. 그런데 문제가 하나 있었습니다. 여지껏 프로세스를 띄울 때 아래 명령어로 띄웠습니다.</p>
<pre><code class="language-shell">pm2 start npx dotenv -e .env.admin -- node dist/apps/admin-server/main.js&quot; --name inhu-backend-admin-dev</code></pre>
<p><code>fork 모드</code>이며, 별다른 옵션이 없죠. 이런 방식의 문제가 무중단 배포가 되지 않습니다. 그러면 배포를 할 때마다 한 번쯤 봤을 <code>502 error</code> 가 뜨게되죠.
<img src="https://velog.velcdn.com/images/in__32/post/5b84776e-1a8d-422d-894f-8660db89fb66/image.png" alt="">
왜 502 error 가 나는지는 아래 블로그에서 정말 잘 설명해주기 때문에 꼭 한 번 읽어보시는 걸 추천드립니다.</p>
<blockquote>
<p><a href="https://engineering.linecorp.com/ko/blog/pm2-nodejs">https://engineering.linecorp.com/ko/blog/pm2-nodejs</a></p>
</blockquote>
<p>결과적으로 위 글에서 <code>pm2 무중단 배포</code>를 위해서 <code>cluser</code> 모드를 사용해야 함을 설명해줍니다. 저 또한 무중단 배포를 위해 <code>cluster</code> 모드를 사용했습니다. 하지만 위 글은 ExpressJS 기준이라서 main 파일에서 NestJS는 조금 다르게 해야 됩니다. 아래처럼 <code>Graceful shutdown</code> 을 위한 설정과 <code>pm2</code> 를 위한 설정을 담아주면 됩니다.</p>
<pre><code class="language-ts">// main.ts

/**
import
**/

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableShutdownHooks(); // Graceful shutdown

  /**
  기타 코드
  **/

  await app.listen(process.env.PORT ?? 3000);

  // pm2 setting
  if (process.send) {
    process.send(&#39;ready&#39;);
  }
}
bootstrap();
</code></pre>
<h1 id="문제-상황">문제 상황</h1>
<p>그런데 정말 큰 문제가 발생했습니다!!! fork 모드일 때는 <code>.env</code> 안의 환경변수들이 잘 읽어지는데 cluster 모드일 때는 읽어지지 않았습니다. 환경변수에 <code>PORT</code>, <code>DB URL</code> 등이 있었는데 이게 없으니 <code>404</code>, <code>500</code>, <code>502</code> 에러가 나고 난리도 아니었습니다...</p>
<h1 id="간략한-코드-구성">간략한 코드 구성</h1>
<p>원활한 설명을 위해서 간략한 코드 구성을 소개하려 합니다.</p>
<h3 id="1-pm2-process-구성">1. pm2 process 구성</h3>
<p><img src="https://velog.velcdn.com/images/in__32/post/58258a10-b0a8-44d4-b995-6ec9e13f483e/image.png" alt="">
저희 서비스는 총 3개가 있습니다. 그래서 일단 프로세스는 3개 띄운 상태이고요. 또한 각각의 환경변수 파일도 달라서 <code>.env.user</code>, <code>.env.admin</code>, <code>.env.batch</code> 총 3개 존재합니다.</p>
<h3 id="2-pm2는-ecosystemconfigjs-로-관리">2. pm2는 ecosystem.config.js 로 관리</h3>
<p>원래는 서비스마다 명령어를 치면서 관리했는데 옵션도 늘어나고 매번 명령어 어디있나 찾기 귀찮아서 <code>ecosystem.config.js</code> 로 관리하기로 했습니다. 문제가 됐던 설정 파일은 아래와 같습니다.</p>
<pre><code class="language-ts">module.exports = {
  apps: [
    {
      name: &#39;inhu-backend-user-dev&#39;,
      script: &#39;dist/apps/user-server/main.js&#39;,
      node_args: &#39;-r dotenv/config&#39;,
      env: {
          DOTENV_CONFIG_PATH: &quot;.env.user&quot;
      },
      instances: 1,
      exec_mode: &#39;cluster&#39;,
      wait_ready: true,
      listen_timeout: 50000,
      kill_timeout: 5000,
    },
    {
      name: &#39;inhu-backend-admin-dev&#39;,
      script: &#39;dist/apps/admin-server/main.js&#39;,
      node_args: &#39;-r dotenv/config&#39;,
      env: {
              DOTENV_CONFIG_PATH: &quot;.env.admin&quot;
      },
      instances: 1,
      exec_mode: &#39;cluster&#39;,
      wait_ready: true,
      listen_timeout: 50000,
      kill_timeout: 5000,
    },
    {
      name: &#39;inhu-backend-batch-dev&#39;,
      script: &#39;dist/apps/batch-server/main.js&#39;,
      node_args: &#39;-r dotenv/config&#39;,
      env: {
              DOTENV_CONFIG_PATH: &quot;.env.batch&quot;
      },
    },
  ],
};</code></pre>
<h1 id="문제-해결-과정">문제 해결 과정</h1>
<h3 id="1-환경변수-파일-경로가-안-들어간-것인가">1. 환경변수 파일 경로가 안 들어간 것인가?</h3>
<p>먼저 이 생각이 들었습니다. 그래서 잘 설정이 된 <code>inhh-backend-batch-dev</code> 와 잘 설정이 안 된 <code>inhu-backend-user-dev</code>를 비교해보기로 했습니다. env 설정을 보는 방법은 <code>pm2 env &lt;process-name&gt;</code>을 하면 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/in__32/post/03202f8d-20e8-4c20-9b7b-a5c4aec50af1/image.png" alt="">
<img src="https://velog.velcdn.com/images/in__32/post/c405fcbb-2169-4f0f-a008-3a3add571ff0/image.png" alt=""></p>
<p>두 개 모두 잘 설정이 됐습니다. 그래서 경로 문제는 아니라고 생각을 했습니다.</p>
<h3 id="2-cluster-모드는-환경변수-설정이-안-되는-건가">2. cluster 모드는 환경변수 설정이 안 되는 건가?</h3>
<p>조금 극단적이고 상식적으로 생각해보면 당연히 돼야 하는 거지만 확인해보고 싶었습니다. 그래서 <code>ecosystem.config.js</code> 의 <code>env</code> 필드에 직접 환경변수를 넣어봤습니다. 아래처럼 말이죠.</p>
<pre><code class="language-ts">module.exports = {
  apps: [
    /**
    user
    **/
    {
      name: &#39;inhu-backend-admin-dev&#39;,
      script: &#39;dist/apps/admin-server/main.js&#39;,
      node_args: &#39;-r dotenv/config&#39;,
      env: {
        ADMIN_SERVER_PORT: 3001, // 포드 직접 설정
        DOTENV_CONFIG_PATH: &quot;.env.admin&quot;
      },
      instances: 1,
      exec_mode: &#39;cluster&#39;,
      wait_ready: true,
      listen_timeout: 50000,
      kill_timeout: 5000,
    },
    /**
    batch
    **/
  ],
};</code></pre>
<p>이렇게 하고 환경변수를 확인해보니, 아래 사진처럼 정상적으로 환경변수가 들어갔습니다.
<img src="https://velog.velcdn.com/images/in__32/post/3f8c0d7c-1974-4bac-8d37-c45e865ca224/image.png" alt="">
그래서 이렇게 환경변수들을 모두 <code>ecosystem.config.js</code>에 옮길까 했지만, 그러면 <code>ecosystem.config.js</code> 에 <code>pm2 설정 + 환경변수</code> 가 있는 것이 너무 더럽다고 생각했습니다. 그래서 환경변수 파일은 별도로 분리하고 fork 모드처럼 파일을 부르는 방식으로 설정해보려 했습니다.</p>
<h3 id="3-pm2-demon이-깨끗하지-않아서">3. pm2 demon이 깨끗하지 않아서?</h3>
<p>pm2 issue 를 읽어보면서 깨끗한 pm2 demon에서는 된다는 의견을 봤습니다. 그래서 pm2 kill 을 한 후에 start를 했지만 여전히 환경변수가 잘 안 읽혔습니다.</p>
<blockquote>
<p><a href="https://github.com/Unitech/pm2/issues/5766">https://github.com/Unitech/pm2/issues/5766</a></p>
</blockquote>
<h3 id="4-환경변수를-읽는-과정에서-문제">4. 환경변수를 읽는 과정에서 문제?</h3>
<p>누구는 잘 된다, 누구는 잘 안 된다. 정말 다양한 의견들이 나왔습니다. 애초에 환경변수를 읽는 과정에서 cluster 모드는 조금 다르다고 생각을 했습니다. 그런데 구글링을 해도 좋은 글이 나오지 않아서 pm2 오픈소스를 읽는 게 차라리 빠르다고 생각했습니다.</p>
<h1 id="원인">원인</h1>
<p>결론부터 말하면 <code>cluster</code> 모드와 <code>fork</code> 모드의 <code>env</code> 전달 타이밍이 다르기때문에 발생하는 문제입니다. 여기부터 조금 복잡해지니 용어부터 정리하고 가겠습니다.</p>
<h3 id="용어-정리">용어 정리</h3>
<ul>
<li><code>app.env</code>: ecosystem의 <code>env: {...}</code></li>
<li><code>pm2_env</code>: pm2 내부 실행 설정 객체</li>
<li><code>process.env</code>: 런타임에서 앱이 실제로 읽는 환경변수
  ※ 여기서 <code>process.env</code>는 문맥에 따라 두 가지입니다.<ul>
<li><code>daemon process.env</code>: pm2 daemon(부모) 프로세스의 환경변수</li>
<li><code>worker process.env</code>: 실제 앱 worker(자식) 프로세스의 환경변수</li>
</ul>
</li>
</ul>
<p>우리가 실제로 runtime에 읽는 환경변수는 <code>worker process.env</code>에 담기게 됩니다. 그래서 <code>worker process.env</code>에 원하는 값이 없게 되거나(혹은 늦게 들어오면) 환경변수를 의도대로 못 읽게 되죠. 그럼 이번엔 <code>fork</code> 모드와 <code>cluster</code> 모드의 동작 과정에 대해서 알아보겠습니다. 그 전에 먼저 두 모드의 공통 로직부터 알아보겠습니다.</p>
<h3 id="공통-로직">공통 로직</h3>
<ol>
<li>pm2가 <code>daemon process.env</code> + <code>app.env</code>를 합쳐 앱 실행 env를 준비</li>
<li>실행 직전 <code>pm2_env</code> 형태로 확정</li>
<li><code>fork</code> / <code>cluster</code> 분기</li>
</ol>
<h3 id="fork-모드">fork 모드</h3>
<ol>
<li>pm2 daemon이 자식 프로세스를 만들 때 <code>pm2_env</code>를 자식의 <code>worker process.env</code>로 직접 전달</li>
<li>그래서 자식 시작 시점부터 <code>worker process.env</code>에 앱별 <code>app.env</code> 값이 들어있음</li>
</ol>
<h3 id="cluster-모드">cluster 모드</h3>
<ol>
<li>pm2 daemon이 worker를 만들 때 <code>pm2_env</code>를 JSON 문자열 형태로 전달</li>
<li>이 JSON 문자열은 먼저 <code>worker process.env.pm2_env</code>에 들어감</li>
<li>Node preload(<code>-r dotenv/config</code>)가 먼저 실행될 수 있음 (이 시점에는 앱별 <code>app.env</code> 값이 <code>worker process.env</code>에 아직 완전히 복원되기 전일 수 있음)</li>
<li>그 다음 worker에서 <code>pm2_env</code> JSON을 파싱해 <code>worker process.env</code>로 복원</li>
</ol>
<p><code>fork</code> 모드와 <code>cluster</code> 모드의 차이점이 보이시나요? <code>fork</code>는 값으로 환경변수를 전달하지만 <code>cluster</code>는 JSON으로 넘기고 뒤늦게 파싱해서 <code>worker process.env</code>에 값을 복사하죠. 그러면 여기서 의문인 점은 <code>값으로 넘기는 거랑 JSON으로 넘기는 게 왜 중요하냐?</code> 입니다. 저희가 <code>ecosystem.config.js</code> 옵션으로</p>
<pre><code class="language-ts">node_args: &#39;-r dotenv/config&#39;
env: {
  DOTENV_CONFIG_PATH: &quot;.env.batch&quot;,
},</code></pre>
<p>를 적었죠. 이 명령어는 preload 단계에서 실행되고, 이때 <code>process.env.DOTENV_CONFIG_PATH</code>를 읽어 해당 파일을 로드한 뒤 <code>worker process.env</code>에 값을 주입합니다.</p>
<p>그런데 중요한 점은 이 명령어는 preload, 즉 초기 1회 실행이라는 점입니다. Node.js 공식 문서에서도 가장 먼저 실행된다고 나와있죠.
<img src="https://velog.velcdn.com/images/in__32/post/04200c99-4415-4cf8-8d79-e5910e9510d3/image.png" alt="">
그래서 <code>fork</code> 모드일 때는 자식 생성 시점부터 <code>worker process.env</code>에 값이 존재하니 문제가 없었고, <code>cluster</code> 모드일 때는 JSON 파싱/복원 전에 preload가 먼저 실행되면서, daemon 기준 값으로 파일을 선택해 worker에 의도한 환경변수가 반영되지 않는 상황이 생겼던 것입니다.</p>
<h1 id="해결방법">해결방법</h1>
<p>순서가 꼬여서 발생한 것을 알았으니 순서만 고쳐주면 해결이 됩니다. <code>-r dotenv/config</code>에 <code>.env</code> 파일 선택을 맡기지 않고 <code>ecosystem.config.js</code>에서 <code>.env.user/.env.admin</code>을 미리 파싱해 각 앱의 <code>app.env</code>에 주입을 합니다.
즉 런타임 preload 타이밍 문제를 피해, pm2가 프로세스를 띄우기 전에 앱별 <code>app.env</code>를 확정하는 방식으로 해결했습니다.</p>
<pre><code class="language-ts">const fs = require(&#39;fs&#39;);
const path = require(&#39;path&#39;);
const dotenv = require(&#39;dotenv&#39;);

function loadEnvFile(fileName) {
  const filePath = path.join(__dirname, fileName);
  try {
    return dotenv.parse(fs.readFileSync(filePath));
  } catch (err) {
    console.error(`Error loading ${fileName}:`, err);
    return {};
  }
}

module.exports = {
  apps: [
    {
      name: &#39;inhu-backend&#39;,
      script: &#39;dist/apps/user-server/main.js&#39;,
      env: {
        ...loadEnvFile(&#39;.env.user&#39;),
      },
      instances: 1,
      exec_mode: &#39;cluster&#39;,
      wait_ready: true,
      listen_timeout: 50000,
      kill_timeout: 5000,
    },
    {
      name: &#39;inhu-backend-admin&#39;,
      script: &#39;dist/apps/admin-server/main.js&#39;,
      env: {
              ...loadEnvFile(&#39;.env.admin&#39;),
      },
      instances: 1,
      exec_mode: &#39;cluster&#39;,
      wait_ready: true,
      listen_timeout: 50000,
      kill_timeout: 5000,
    },
    {
      name: &#39;inhu-backend-batch&#39;,
      script: &#39;dist/apps/batch-server/main.js&#39;,
      node_args: [&#39;-r&#39;, &#39;dotenv/config&#39;],
      env: {
              DOTENV_CONFIG_PATH: &quot;.env.batch&quot;,
      },
    },
  ]
}</code></pre>
<h1 id="결론">결론</h1>
<p>이번 기회에 오픈소스 코드를 보면서 cluster 환경에서 env 설정이 안 되는 문제를 해결해봤습니다. 코드가 장황해서 동작 과정을 이해하는 데 꽤 고생을 했습니다. 그걸 다시 글로 적으려고 하니 괜히 헷갈려서 힘들었지만 가치있던 경험이었습니다.
이 글이 도움이 되었으면 좋겠네요. 그럼 저는 다른 주제로 다시 찾아오겠습니다!
<img src="https://velog.velcdn.com/images/in__32/post/333790c5-4260-4d1c-9bc8-77a185e84800/image.png" alt=""></p>
<p>참고자료
<a href="https://engineering.linecorp.com/ko/blog/pm2-nodejs">https://engineering.linecorp.com/ko/blog/pm2-nodejs</a>
<a href="https://github.com/Unitech/pm2/issues/5766">https://github.com/Unitech/pm2/issues/5766</a>
<a href="https://github.com/Unitech/pm2/issues/4718">https://github.com/Unitech/pm2/issues/4718</a>
<a href="https://github.com/Unitech/pm2">https://github.com/Unitech/pm2</a>
<a href="https://nodejs.org/api/cli.html#r-require-module">https://nodejs.org/api/cli.html#r-require-module</a>
<a href="https://docs.nestjs.com/fundamentals/lifecycle-events">https://docs.nestjs.com/fundamentals/lifecycle-events</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Discord 로 서버 error 메시지를 받아보자! (with. pm2-discord) (1)]]></title>
            <link>https://velog.io/@in__32/Discord-%EB%A1%9C-%EC%84%9C%EB%B2%84-error-%EB%A9%94%EC%8B%9C%EC%A7%80%EB%A5%BC-%EB%B0%9B%EC%95%84%EB%B3%B4%EC%9E%90-with.-pm2-discord-1</link>
            <guid>https://velog.io/@in__32/Discord-%EB%A1%9C-%EC%84%9C%EB%B2%84-error-%EB%A9%94%EC%8B%9C%EC%A7%80%EB%A5%BC-%EB%B0%9B%EC%95%84%EB%B3%B4%EC%9E%90-with.-pm2-discord-1</guid>
            <pubDate>Sun, 07 Sep 2025 17:21:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/in__32/post/78cb840d-58ed-4297-9b66-a2b3bb94d58f/image.png" alt=""></p>
<p>안녕하세요 백엔드 개발자 이수인입니다! 오늘은 discord 로 서버 error 알림을 받아보는 방법에 대해 소개해보려 합니다. 먼저 어떻게 메시지가 오는지 궁금하신 분들을 위해 사진부터 첨부하고 포스팅 시작하겠습니다.
<img src="https://velog.velcdn.com/images/in__32/post/fea66098-6fad-40b0-9ba9-99201d421c8a/image.png" alt=""></p>
<blockquote>
<p>저희 서비스는 현재 pm2로 서버를 실행하고 있습니다. 그래서 docker로 배포하신 분들에겐 큰 도움이 안 될 수 있습니다. 하지만 docker로 배포하는 것도 고려하고 있기 때문에 docker로도 알림을 보낼 수 있게 구현할 예정입니다.</p>
</blockquote>
<h2 id="구현-계기">구현 계기</h2>
<p>에러가 발생하게 되면 서버에 접속을 해서 에러 메시지를 확인해야 하죠. 하지만 이 방식은 <strong>서버에 접속해야 하는</strong> 불편함도 있지만, <strong>에러가 발생했을 때 바로 알 수 없다</strong>는 단점이 있습니다. 이 문제를 해결하기 위해 고민을 하던 중, 저희 팀원 중 한 명이 디스코드로 에러 메시지를 받을 수 있다고 했습니다. 그러면 알림도 오고, 메시지도 확인할 수 있어서 대응할 수 있게 되죠. 저희 팀원은 &#39;이런 방법이 있다~&#39; 라고만 했기 때문에 어떻게 구현해야 할지 고민할 필요가 있었죠.</p>
<h2 id="discord-로-어떻게-에러-메시지가-가지">discord 로 어떻게 에러 메시지가 가지?</h2>
<p>discord로 에러 메시지를 보내는 방법은 생각보다 간단합니다.</p>
<ol>
<li>에러 메시지를 받을 채널 생성하기</li>
<li>서버 설정 들어가기</li>
<li>앱 &gt; 연동 클릭</li>
<li>&#39;새 웹후크&#39; 클릭하여 생성하기</li>
<li>사진, 이름, 채널 원하는 대로 수정하기</li>
<li>&#39;웹후크 URL 복사&#39; 를 클릭해서 웹후크 url 복사하기 (민감한 정보이니 꼭 환경변수로 관리해야 합니다!)</li>
<li>해당 url로 메시지와 함께 post 요청 보내기</li>
</ol>
<p>생각보다 원리는 간단하죠? discord 는 그냥 메시지 받고 출력하는 용도입니다.</p>
<h2 id="방법1-service-함수로-구현하기">방법1) service 함수로 구현하기?</h2>
<p>가장 쉽게 생각해볼 수 있는 방법입니다. 위에서 설명했던 것처럼 discord 에 에러 메시지를 보내는 방법은 정말 간단합니다. 그래서 service 함수로 처리해버릴 수 있죠. 코드로 봐볼까요?</p>
<pre><code class="language-ts">public async sendErrorMessage(
    title: string,
    message: string,
    error: Error,
    context: DiscordWebhookContext,
  ) {
    await this.httpService.axiosRef.post(
      this.ERROR_WEBHOOK_URL,
      {
        content: `# **🚨 에러 로그 알림**`,
        embeds: [
          {
            title,
            description: `${message}

  Error Object
  \`\`\`
  ${this.getErrorStr(error)}
  \`\`\`
                `, // Description 추가
            color: 16711680, // 빨간색 (16진수)
            author: {
              name: context,
            },
            footer: {
              text: `시간: ${new Date().toLocaleString()}`,
            },
            timestamp: new Date().toISOString(),
          },
        ],
      },
      {
        headers: {
          &#39;Content-Type&#39;: &#39;application/json&#39;,
        },
      },
    );
  }</code></pre>
<p>이제 에러가 발생하면 catch 해서 <code>sendErrorMessage</code> 함수를 호출하면 끝입니다. 어려운 방법이 아니죠.</p>
<h3 id="문제">문제</h3>
<p>그런데 문제가 있습니다. 이 함수 호출은 <strong>NestFactory 가 create 되어야만 호출</strong>할 수 있습니다. 만약에 create 되기 전에 호출되면 에러 메시지를 보낼 수 없습니다. ioredis 설정 실패나 env read 실패 같은 것들 말이죠. 그래서 nestjs 의 create 와 상관없이 동작할 수 있는 다른 방법을 고민해볼 필요가 있었습니다.</p>
<h2 id="방법2-pm2-discord-라이브러리">방법2) pm2-discord 라이브러리</h2>
<p>저희가 pm2로 서버를 돌리고 있으니 pm2 에서 해결할 수 없을까? 고민을 하고 구글링 결과 <code>pm2-discord</code> 라는 라이브러리를 찾을 수 있었습니다. 이 라이브러리는 nestjs 의 생성과 create와 상관없이, pm2 에서 발생하는 event 를 감지하기 때문에 제가 원했던 요구사항을 만족시켜줬습니다.
<a href="https://github.com/FranciscoG/pm2-discord">https://github.com/FranciscoG/pm2-discord</a></p>
<p>사용 방법은 정말 간단합니다. readme 를 읽어보면 알겠지만,</p>
<pre><code>pm2 install pm2-discord
pm2 set pm2-discord:discord_url https://discord_url</code></pre><p>이전에 복사해둔 웹후크 url을 설정을 하고,</p>
<ul>
<li>log</li>
<li>error</li>
<li>kill</li>
<li>exception</li>
<li>restart</li>
<li>delete</li>
<li>stop</li>
<li>restart overlimit</li>
<li>exit</li>
<li>start</li>
<li>online</li>
</ul>
<p>위 옵션 중 필요한 것들을 true/false 로 설정하면 됩니다. 간단하죠? 에러 메시지 관련을 다루고 있으니 <code>error</code> 옵션을 true 로 설정하면 되겠네요.</p>
<p>잘 되는지 테스트가 필요합니다. 저는 대충 에러 발생시키는 용도 api를 만들어봤습니다.</p>
<pre><code class="language-ts">@Get(&#39;test-error-log&#39;)
  testErrorLog() {
    console.error(
      &#39;서버 중단 없는 에러 로그 테스트입니다. 이 메시지가 디스코드에 보여야 합니다.&#39;,
    );

    return {
      status: &#39;OK&#39;,
      message:
        &#39;Error log has been sent, but the server is still running perfectly!&#39;,
    };
  }</code></pre>
<p>내용은 어떻게 작성해도 상관 없지만, <code>console.error</code> 는 필요합니다. 이제 이 api 를 호출하면,
<img src="https://velog.velcdn.com/images/in__32/post/5e188950-2f87-4ee2-87dd-6520fb3b89e3/image.png" alt="">
이렇게 알림이 오게 됩니다.</p>
<h3 id="문제-가독성-떨어짐">문제) 가독성 떨어짐</h3>
<p>그런데 이런 메시지 형태 괜찮으신가요? 지금 예시는 한 줄이라서 크게 신경 쓰이지 않을 수 있습니다. 그러면 다른 예시를 보여드릴게요.
<img src="https://velog.velcdn.com/images/in__32/post/fb118de1-fa5c-4a95-9d18-735c336b201e/image.png" alt="">
어떤가요? 잘 읽히나요? 일단 저는 안 읽힙니다. <strong>심지어 위 사진은 메시지가 2개</strong> 온 겁니다. 따로 감싸져서 오는 게 아니라 그냥 텍스트가 오니까 잘 읽히지 않습니다. 그리고 <strong>언제 발생한 에러인지</strong>, <strong>어떤 process에서 발생한 에러인지</strong> 등의 필요한 정보들을 판별하기 쉽지 않습니다.</p>
<p>맨 처음에 보여준 사진 기억하시나요? 기억 못 하시는 분들을 위해 다시 첨부해보면,
<img src="https://velog.velcdn.com/images/in__32/post/aa6ea6ce-e499-4af1-a78f-61e83ca5947a/image.png" alt=""></p>
<p>이렇습니다. 어떤가요? 잘 읽히지 않나요? 
<img src="https://velog.velcdn.com/images/in__32/post/43d26791-b6b0-4c2a-983e-a3b88571e5d9/image.png" alt=""></p>
<p>이렇게 여러 개가 와도 구분이 잘 됩니다. 이런 형식으로 보내려면 <code>embed</code> 로 감싸서 보내고, 필요한 정보들을 담아서 보내야 합니다. 이렇게 보내는 게 저희의 목표입니다.</p>
<h2 id="그래서-어떻게-하는-건데">그래서 어떻게 하는 건데?</h2>
<p>경험이 부족한 저한테 제일 쉽지 않은 문제였습니다. 위에 옵션을 보면 알겠지만, 에러 메시지를 커스텀하는 옵션은 없습니다. 그래서 처음에는 방법이 없다고 생각하고 에러 메시지를 커스텀할 수 있는 다른 라이브러리를 찾았습니다. 그런데 아무리 찾아도 없고, 된다고 하는 것들은 안 되더라고요... 결국에 고민을 하다가 <strong>pm2-discord 코드를 내가 수정하면 안 되나?</strong> 라는 생각이 들더라고요. 그래서 어떻게 수정을 했는지 공유해보려 합니다.</p>
<h2 id="원본-코드-커스텀하기">원본 코드 커스텀하기</h2>
<p>원본 코드를 읽어보면 알겠지만 생각했던 것보다 코드가 간단합니다. 총 5개의 함수 <strong>discord에 메시지 보내기, buffer 관리, message queue 관리, 메시지 생성하기, 이벤트 등록하기</strong> 가 있습니다. 코드를 설명하는 글이 아니기 때문에 설명은 생략하고, 결론적으로 수정해야 할 함수는 당연하게도 <strong>discord 에 메시지 보내기, 메시지 생성하기</strong> 입니다. 원본 코드 보겠습니다.</p>
<pre><code class="language-js">function sendToDiscord(message) {

  var description = message.description;

  if (!conf.discord_url) {
    return console.error(&quot;There is no Discord URL set, please set the Discord URL: &#39;pm2 set pm2-discord:discord_url https://[discord_url]&#39;&quot;);
  }

  var payload = {
    &quot;content&quot; : description
  };

  var options = {
    method: &#39;post&#39;,
    body: payload,
    json: true,
    url: conf.discord_url
  };

  request(options, function(err, res, body) {
    if (err) {
      return console.error(err);
    }

    if (res.statusCode !== 204) {
      console.error(&quot;Error occured during the request to the Discord webhook&quot;);
    }
  });
}

function createMessage(data, eventName, altDescription) {
  if (data.process.name === &#39;pm2-discord&#39;) {
    return;
  }

  if (conf.process_name !== null &amp;&amp; data.process.name !== conf.process_name) {
    return;
  }

  var msg = altDescription || data.data;
  if (typeof msg === &quot;object&quot;) {
    msg = JSON.stringify(msg);
  } 

  messages.push({
    name: data.process.name,
    event: eventName,
    description: stripAnsi(msg),
    timestamp: Math.floor(Date.now() / 1000),
  });
}
</code></pre>
<p>여기서 각각 수정할 내용은 아래와 같습니다.</p>
<p><strong>sendToDiscord</strong></p>
<ul>
<li>embed 로 감싸서 보내기</li>
</ul>
<p><strong>createMessage</strong></p>
<ul>
<li>원하는 메시지 형태로 커스텀하기</li>
</ul>
<p>그렇게 어렵지 않으니 바로 코드로 보겠습니다. 수정한 부분은 주석으로 명시할게요.</p>
<pre><code class="language-ts">function sendToDiscord(message) {
  if (!discordUrl) {
    return console.error(
      &quot;There is no Discord URL set, please set the Discord URL: &#39;pm2 set pm2-discord:discord_url https://[discord_url]&#39;&quot;
    );
  }

  var payload = {
    content: message.content,
    embeds: [message.embed], // embed 로 감싸서 보내기
  };

  var options = {
    method: &quot;post&quot;,
    body: payload,
    json: true,
    url: discordUrl,
  };

  request(options, function (err, res, body) {
    if (err) {
      return console.error(err);
    }

    if (res.statusCode !== 204) {
      console.error(&quot;Error occured during the request to the Discord webhook&quot;);
    }
  });
}

// 반복되는 메시지 포멧팅으로 함수 생성
function makeEmbedFormat(content, color, msgName, msg, processName) {
  return {
    content: content,
    embed: {
      color: color,
      fields: [
        {
          // 메시지 소제목
          name: msgName,
          value: `\`\`\`\n${msg}\n\`\`\``,
          inline: false,
        },
        {
          // 언제 발생한 메시지인지
          name: &quot;Time&quot;, 
          value: new Date().toISOString().replace(&quot;T&quot;, &quot; &quot;).slice(0, 19),
          inline: true,
        },
        {
          // 어떤 process 에서 발생한 건지
          name: &quot;Process&quot;,
          value: processName,
          inline: true,
        },
      ],
    },
  };
}

function createMessage(data, eventName, altDescription) {
  const processName = data.process.name;

  if (processName === &quot;pm2-discord&quot;) {
    return;
  }

  if (conf.process_name !== null &amp;&amp; processName !== conf.process_name) {
    return;
  }

  var msg = altDescription || data.data;
  if (typeof msg === &quot;object&quot;) {
    msg = JSON.stringify(msg);
  }

  // 이벤트마다 원하는 메시지 형태로 만들기
  const embedFormatters = {
    log: () =&gt;
      makeEmbedFormat(
        &quot;# **📜 LOG **&quot;,
        5763719,
        &quot;Log Message&quot;,
        msg,
        processName
      ),
    error: () =&gt;
      makeEmbedFormat(
        &quot;# **🚨 ERROR **&quot;,
        15158332,
        &quot;Error Message&quot;,
        msg,
        processName
      ),
    exception: () =&gt;
      makeEmbedFormat(
        &quot;# **🚨 EXCEPTION **&quot;,
        15158332,
        &quot;Exception Message&quot;,
        msg,
        processName
      ),
    restart: () =&gt;
      makeEmbedFormat(
        &quot;# **🔄 PROCESS RESTARTED **&quot;,
        16776960,
        &quot;Restart Message&quot;,
        msg,
        processName
      ),
    delete: () =&gt;
      makeEmbedFormat(
        &quot;# **❌ PROCESS DELETED **&quot;,
        15158332,
        &quot;Delete Message&quot;,
        msg,
        processName
      ),
    stop: () =&gt;
      makeEmbedFormat(
        &quot;# **🔴 PROCESS STOPPED **&quot;,
        15158332,
        &quot;Stop Message&quot;,
        msg,
        processName
      ),
    exit: () =&gt;
      makeEmbedFormat(
        &quot;# **🔴 PROCESS EXITED **&quot;,
        15158332,
        &quot;Exit Message&quot;,
        msg,
        processName
      ),
    start: () =&gt;
      makeEmbedFormat(
        &quot;# **🟢 PROCESS STARTED **&quot;,
        5763719,
        &quot;Start Message&quot;,
        msg,
        processName
      ),
    online: () =&gt;
      makeEmbedFormat(
        &quot;# **🟢 PROCESS ONLINE **&quot;,
        5763719,
        &quot;Online Message&quot;,
        msg,
        processName
      ),
  };

  const formatter = embedFormatters[eventName];
  const result = formatter();

  messages.push({
    name: processName,
    event: eventName,
    content: result.content,
    embed: result.embed,
    timestamp: Math.floor(Date.now() / 1000),
  });
}</code></pre>
<p>코드가 길 뿐이지 어렵지 않을 거라고 생각합니다.</p>
<h3 id="환경변수-추가하기">환경변수 추가하기</h3>
<p>현재 설정 값에는 <code>discord_url</code> 이거 하나밖에 없습니다. 그런데 채널 여러 개에서 알림을 받고 싶을 경우에는 한 개만으론 부족하죠. 그래서 <code>package.json</code> 에서 <code>config</code> field 에 원하는 값을 추가하면 됩니다. 아래처럼 말이죠</p>
<pre><code class="language-js">&quot;config&quot;: {
    &quot;discord_url&quot;: null,
    &quot;discord_error_url&quot;: null, // 추가한 설정 값
    &quot;process_name&quot;: null,
    &quot;log&quot;: true,
    &quot;error&quot;: false,
    &quot;kill&quot;: true,
    &quot;exception&quot;: true,
    &quot;restart&quot;: false,
    &quot;delete&quot;: false,
    &quot;stop&quot;: true,
    &quot;restart overlimit&quot;: true,
    &quot;exit&quot;: false,
    &quot;start&quot;: false,
    &quot;online&quot;: false,
    &quot;buffer&quot;: true,
    &quot;buffer_seconds&quot;: 1,
    &quot;queue_max&quot;: 100
  }</code></pre>
<h2 id="적용하기">적용하기</h2>
<p>그럼 이 코드를 실제로 적용해봐야겠죠? 원본 코드에서는 <code>pm2-install pm2-discord</code> 이렇게 했지만, 제가 fork 따서 개인 레포에서 수정했기 때문에 좀 다르게 해야 됩니다. <code>pm2 install https://github.com/사용자이름/레포지토리이름</code> 이렇게 하면 됩니다. 저는 <code>pm2 install https://github.com/suleeesulee/pm2-discord</code> 이렇게 했습니다. 설치 후 <code>pm2 list</code> 를 하면
<img src="https://velog.velcdn.com/images/in__32/post/9e7a8703-f89f-4515-8866-80cb0c551be8/image.png" alt="">
이렇게 설치가 잘 된 것을 확인 할 수 있습니다.</p>
<h2 id="설정-값-확인하기">설정 값 확인하기</h2>
<p>현재 설정된 값들을 확인하고 싶으면 <code>pm2 conf pm2-discord</code> 혹은 더 자세히 보고 싶으면 <code>pm2 describe pm2-discord</code> 로 확인할 수 있습니다.</p>
<h2 id="개선해야-할-부분">개선해야 할 부분</h2>
<p>현재 저희 프로젝트에 적용을 하고 잘 에러를 반환하지만 개선해야 할 부분들이 많습니다.</p>
<ol>
<li>현재 js 로 작성했지만, 안정적인 타입 지원을 위해 ts 로 수정해야 함</li>
<li>여러 discord 웹후크 url을 설정했을 때 조건문 남발하지 않도록 객체지향적으로 잘 설계해야 함</li>
<li>pm2 만 적용할 수 있지만 docker로 배포하는 경우에도 가능하게 해야 함</li>
</ol>
<p>1, 2번은 어느정도 쉽게 해결이 될 거 같은데 솔직히 3번 문제는 어떻게 해야 할지 고민을 계속하고 있습니다. 3번 문제가 해결되면 여러 프로젝트에서 활용할 수 있을 것 같고 좋은 글 소재가 될 것 같습니다.</p>
<h2 id="마무리">마무리</h2>
<p>아직 수정 중인 코드이기 때문에 조금 부족해 보일 수 있습니다. 피드백 적극 환영합니다. 꼭 좋은 코드로 완성할 테니, 이용해주시면 감사하겠습니다. 그럼 글 마치겠습니다!
<img src="https://velog.velcdn.com/images/in__32/post/333790c5-4260-4d1c-9bc8-77a185e84800/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[NestJS] - Passport 없이 객체지향적으로 소셜 로그인 구현하기 (with 전략패턴)]]></title>
            <link>https://velog.io/@in__32/NestJS-Passport-%EC%97%86%EC%9D%B4-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-%EC%A0%84%EB%9E%B5%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@in__32/NestJS-Passport-%EC%97%86%EC%9D%B4-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-%EC%A0%84%EB%9E%B5%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Sat, 30 Aug 2025 19:19:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/in__32/post/9cb7b338-767d-4ea5-93cd-67adae91a434/image.png" alt="">
안녕하세요! 백엔드 개발자 이수인입니다. 오늘은 passport 없이 객체지향적으로 소셜 로그인을 구현해 보려 합니다.</p>
<p>사실 소셜 로그인은 오래 전에 구현했었습니다. 그땐 정말 괜찮게 설계했다고 생각했거든요. 그런데 시간이 지나고 코드 좀 수정하려 하는데 계속 뭐가 어긋나고, 이상하다는 생각이 자꾸 들더라고요. 그래서 &#39;이 코드 설계는 근본적으로 문제다!&#39; 라고 생각해서 그냥 전부 다 갈아엎어 버렸습니다... 지금부터 기존에는 어떤 방식으로 했고, 어떤 문제에 직면했고, 그 문제를 어떻게 풀어갔는지를 공유해보려 합니다.</p>
<h3 id="왜-passport-없이">왜 passport 없이?</h3>
<p>passport 라이브러리를 쓰면 쉽게 구현을 할 수는 있어요. 하지만 저는 <strong>OAuth 를 자세히 이해</strong>하고 <strong>자유롭게 커스터마이징</strong>할 수 있고 <strong>객체지향적 설계를 연습</strong>하고자 굳이 passport 없이 제가 구현을 했습니다.</p>
<h2 id="기존-코드의-문제점">기존 코드의 문제점</h2>
<p>저희 서비스는 <strong>카카오 로그인</strong>과 <strong>애플 로그인</strong>을 사용합니다. 먼저 카카오 로그인을 구현하고 나중에 애플 로그인을 구현했습니다. <strong>전략 패턴</strong>을 사용하기 때문에 추상화하는 작업이 정말 중요했죠. 근데 문제는 제가 추상화를 한 경험이 없었기 때문에 잘 할 줄 몰랐다는 것입니다. 추상화를 할 때 너무 카카오 로그인에만 초점을 둬서 애플 로그인을 구현할 때 여러 문제들을 직면했습니다. 그럼 문제의 코드 설계를 보시죠.</p>
<h3 id="폴더-구조">폴더 구조</h3>
<blockquote>
<p>이번 포스팅을 이해하는 용도로 일부 생략하여 가볍게 표현하겠습니다.</p>
</blockquote>
<pre><code>src/auth
├── ...
├── auth.controller.ts
├── services
│   ├── auth.service.ts
│   └── ...
└── strategies // 전략 패턴
    └── social-login
        ├── apple
        │   ├── apple.strategy.ts
        │   └── dto
        ├── base // 추상 클래스
        │   └── social-auth-base.strategy.ts
        └── kakao
            ├── kakao.strategy.ts
            └── dto</code></pre><p>전략 패턴을 적용하기 위해 <code>strategies</code> 라는 폴더를 만들고, 소셜 로그인 폴더로 <code>social-login</code>을 만들었습니다. 그리고 추상화 폴더를 명시적으로 구분하고 싶어서 <code>base</code>라는 폴더를 만들었습니다. 그럼 다음으로 대망의 문제의 코드들을 보죠.</p>
<h3 id="문제-코드">문제 코드</h3>
<blockquote>
<p>아래 코드는 수정의 지옥에 빠지다가 망해서 버린 코드입니다. 결론적으로 필요한 기능이 일부 빠진 완성하지 못한 코드입니다. 코드가 길지만 요약 정리가 있으니 가볍게 더러운 코드구나~ 정도만 봐주시면 감사하겠습니다.</p>
</blockquote>
<pre><code class="language-ts">// src/auth/strategies/social-login/base/social-auth-base.strategy.ts

/**
 * @template TToken 소셜 로그인 제공자가 반환하는 토큰 데이터 타입 (ex: KakaoTokenDto)
 * @template TUserInfo 소셜 로그인 제공자가 반환하는 사용자 정보 데이터 타입 (ex: KakaoUserInfo)
 */
export abstract class SocialAuthBaseStrategy&lt;TToken = any, TUserInfo = any&gt; {
  /**
   * 소셜 로그인 인증을 요청하는 URL
   */
  protected abstract authLoginUrl: string;

  /**
   * social 토큰을 요청하는 URL
   * - 사용자가 로그인 후 인가 코드(code)를 이용해 토큰을 발급받는 엔드포인트
   */
  protected abstract socialTokenUrl: string;

  /**
   * social 토큰 요청에 필요한 파라미터를 반환
   *
   * @param code 소셜 로그인에서 제공하는 인가 코드
   */
  protected abstract getSocialTokenParams(code: string): Record&lt;string, string&gt;;

  /**
   * 소셜 로그인 인증 페이지 URL을 반환
   */
  public abstract getAuthLoginUrl(): string;

  /**
   * 소셜 로그인 응답에서 식별자 토큰을 추출
   *
   * @param socialToken 소셜 로그인에서 반환된 토큰 객체
   */
  public abstract getToken(socialToken: TToken): string;

  /**
   * 토큰을 이용해 소셜 사용자 정보를 조회
   *
   * @param token 소셜 로그인에서 발급받은 식별자 토큰
   */
  public abstract getUserInfo(token: string): Promise&lt;TUserInfo&gt;;

  /**
   * 각 소셜 서비스의 사용자 정보를 공통 DTO(SocialUserInfoDto)로 변환
   *
   * @param userInfo 소셜 로그인에서 제공한 원본 사용자 정보
   */
  public abstract extractUserInfo(userInfo: TUserInfo): SocialUserInfoDto;

  /**
   * 소셜 로그인 제공자로부터 토큰을 요청하는 메서드
   *
   * @param code 소셜 로그인 제공자가 발급한 인가 코드 (authorization code)
   */
  public async getSocialToken(code: string): Promise&lt;TToken&gt; {
    const params = this.getSocialTokenParams(code);

    const response = await axios.post&lt;TToken&gt;(
      this.socialTokenUrl,
      new URLSearchParams(params),
      { headers: { &#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded&#39; } },
    );
    if (!response?.data) {
      throw new UnauthorizedException(&#39;Failed to issue token&#39;);
    }

    return response.data;
  }
}
</code></pre>
<p>뭔 <strong>추상화가 이렇게 복잡하고 더러워?</strong> 라고 생각할 수 있습니다. 저도 이때는 몰랐습니다... 아직 끝이 아닙니다.</p>
<pre><code class="language-ts">// src/auth/auth.controller.ts

  /**
   * 소셜 로그인 요청을 처리하는 엔드포인트
   * - 해당 소셜 로그인 페이지로 리다이렉트
   */
  @Get(&#39;:provider/login&#39;)
  public socialLogin(
    @Provider() provider: AuthProvider | null,
    @Res() res: Response,
  ): void {
    if (!provider) {
      return res.redirect(
        this.configService.get&lt;string&gt;(&#39;MAIN_PAGE_URL&#39;) || &#39;/&#39;,
      );
    }

    const socialAuthService = this.authService.getSocialAuthStrategy(provider);
    return res.redirect(socialAuthService.getAuthLoginUrl());
  }

  /**
   * 소셜 로그인 콜백 처리 엔드포인트
   */
  @Get(&#39;:provider/callback&#39;)
  public async handleGetCallBack(
    @Provider() provider: AuthProvider | null,
    @Query(&#39;code&#39;) code: string,
    @Res({ passthrough: true }) res: Response,
  ) {
    return this.login(res, provider, code);
  }</code></pre>
<pre><code class="language-ts">// src/auth/services/auth.service.ts

  public async generateTokenPairWithSocialAuth(
    socialAuthService: SocialAuthBaseStrategy,
    token: string,
  ): Promise&lt;TokenPair&gt; {
    const userInfo = await socialAuthService.getUserInfo(token);
    const extractedUserInfo = socialAuthService.extractUserInfo(userInfo);

    const user = await this.userService.createUser(extractedUserInfo);

    const payload = { idx: user.idx };
    const jwtAccessToken =
      await this.loginTokenService.signAccessToken(payload);
    const jwtRefreshToken =
      await this.loginTokenService.signRefreshToken(payload);

    this.tokenStorage.saveRefreshToken(user.idx, jwtRefreshToken);

    return { accessToken: jwtAccessToken, refreshToken: jwtRefreshToken };
  }

  /**
   * 소셜 로그인 인증 코드(code)를 사용하여 사용자 인증 후 토큰 발급
   */
  public async login(provider: AuthProvider, code: string): Promise&lt;TokenPair&gt; {
    const socialAuthService = this.getSocialAuthStrategy(provider);

    const socialToken = await socialAuthService.getSocialToken(code);
    const token = socialAuthService.getToken(socialToken);
    return this.generateTokenPairWithSocialAuth(socialAuthService, token);
  }</code></pre>
<p><strong>진짜 코드 더럽네!!</strong> 라고 생각할 수 있습니다. 저도 글 쓰면서 왜 이렇게 코드를 짰나 화가날 정도입니다. 위 코드들을 UML로 보면 아래와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/in__32/post/98a8c00c-e907-49ca-b64e-89e90881de68/image.png" alt=""></p>
<h3 id="아니-그래서-문제가-뭔데">아니 그래서 문제가 뭔데?</h3>
<p>서론이 너무 길었습니다. 바로 무엇이 문제인지 말해보죠. 제 기존 코드들은 객체지향 원칙들을 많이 위반하고 있습니다. 그래서 제가 답이 없다고 생각한 거고요. 좀 더 자세히 보기 전에 이 포스팅은 객체지향 원칙을 설명하는 글이 아니기 때문에 이 점은 생략하겠습니다.</p>
<h4 id="1-단일-책임-원칙srp-위반">1. 단일 책임 원칙(SRP) 위반</h4>
<p><code>AuthService</code>가 <strong>사용자 인증</strong> 이라는 자신의 핵심 책임을 넘어, 각 소셜 로그인 전략의 내부 동작 절차를 직접 조율하는 부가적인 책임까지 지게 되었습니다. <strong>getToken</strong>, <strong>getuserInfo</strong>, <strong>extractUserInfo</strong>, <strong>getSocialToken</strong> 이렇게 각 전략의 메서드를 순서대로 호출하는 것은 <code>AuthService</code>가 하위 모듈의 상세 구현에 지나치게 관여하는 것으로, 이는 명백한 SRP 위반입니다.</p>
<h4 id="2-개방-폐쇠-원칙ocp-위반">2. 개방 폐쇠 원칙(OCP) 위반</h4>
<p>추상 클래스에 함수들을 쪼개다 보니, 굉장히 유지보수에 안 좋아졌습니다. 확장에 열려있어야 되는데, 그렇지 못하게 됐습니다. 만약에 애플 로그인에선 조금 다르게 처리하고 싶다고 했을 때 조건문으로 처리해야 하죠.</p>
<h4 id="3-인터페이스-분리-원칙isp-위반">3. 인터페이스 분리 원칙(ISP) 위반</h4>
<p><code>AppleStrategy</code> 에서 어떤 함수를 추가하고 싶었습니다. 그러면 <strong>추상 클래스에도 추가</strong>해야 하죠. 그런데 이러면 어떤 문제가 발생하나요? 바로 <strong><code>KakaoStrategy</code> 에서도 구현</strong>을 해야 한다는 것입니다. 카카오에서는 필요없지만 구현을 해야 하는 어이없는 상황이 생기죠. 그래서 중간 합의점을 찾아야 되나? 라는 생각을 진짜 많이 했습니다. 그래서 함수 이름들도 이상해지고 난리도 아니였습니다. 제가 정말 답 없다고 느낀 부분입니다.</p>
<h4 id="4-의존-역전-원칙dip-위반">4. 의존 역전 원칙(DIP) 위반</h4>
<p>겉보기에는 <code>AuthService</code>가 <code>SocialAuthBaseStrategy</code>라는 추상 클래스에 의존하여 DIP를 지키는 것처럼 보입니다. 하지만 실제로는 그 추상 클래스가 노출하는 여러 메서드들의 호출 순서, 즉 내부 구현 방식에 강하게 의존하고 있었습니다. 진정한 DIP는 <strong>무엇</strong>을 할지만 정의된 순수 추상화에 의존해야 하는데, 제 코드는 <strong>어떻게</strong>가 담긴 불완전한 추상화에 의존하여 유연성을 잃어버렸고, 결과적으로 수정의 지옥의 빠져버렸습니다.</p>
<h3 id="그래서-어떻게-수정했는데">그래서 어떻게 수정했는데?</h3>
<p>최대한 간략하게 작성하면서도 원칙을 위반하지 않게 코드를 수정해봤습니다.</p>
<pre><code class="language-ts">export interface ISocialLoginStrategy {
  getSocialLoginRedirect(): string;

  socialLogin(provider: AuthProvider, req: Request): Promise&lt;OAuthInfo&gt;;
}</code></pre>
<p>일단 class -&gt; interface 로 수정을 해봤습니다. 이거는 어차피 <strong>무엇을 해야 하는지</strong> 만을 원하는 것이지 <strong>어떻게 무엇을</strong> 해야 하는지까지 원하지 않기 때문에 interface 로 수정을 했습니다.</p>
<p>기존에 <code>AuthService</code> 에서 <code>login</code> 함수에 정말 많은 책임을 가지고 구현체에 의존을 하고 있었다면 <code>socialLogin</code> 이라는 함수로 합쳐서 책임을 분리하고 추상 인터페이스에 의존하게 했습니다.</p>
<pre><code class="language-ts">// auth.service.ts

public async login(
    req: Request,
    provider: AuthProvider,
    issuedBy: TokenIssuedBy,
  ) {
    const strategy = this.socialAuthProviderMap[provider];
    if (!strategy) {
      throw new InternalServerErrorException();
    }

    const oauthInfo = await strategy.socialLogin(provider, req);

    const userModel = await this.upsertUserByOauthInfo(oauthInfo);

    return await this.loginTokenService.issueTokenSet(
      { idx: userModel.idx },
      issuedBy,
    );
  }

  private async upsertUserByOauthInfo(
    oauthInfo: OAuthInfo,
  ): Promise&lt;UserModel&gt; {
    const user = await this.userCoreService.getUserBySocialId(
      oauthInfo.snsId,
      oauthInfo.provider,
    );

    if (user) {
      return user;
    }

    return await this.userCoreService.createUser({
      nickname: &#39;새로운 인후러&#39;,
      profileImagePath: null,
      social: {
        provider: oauthInfo.provider,
        snsId: oauthInfo.snsId,
      },
    });
  }</code></pre>
<p>그래서 이렇게 <code>auth.service.ts</code> 가 굉장히 깔끔해졌습니다.</p>
<p>그럼 이번엔 카카오와 애플 구현체를 봐볼까요? 그냥 어떻게 작성했는지 정도로만 봐주시면 감사하겠습니다.</p>
<pre><code class="language-ts">// kakao.strategy.ts

@Injectable()
export class KakaoLoginStrategy implements ISocialLoginStrategy {
  private readonly KAKAO_CLIENT_ID: string;
  private readonly KAKAO_REDIRECT_URI: string;

  constructor(
    private readonly configService: ConfigService,
    private readonly httpService: HttpService,
  ) {
    this.KAKAO_CLIENT_ID =
      this.configService.get&lt;string&gt;(&#39;KAKAO_CLIENT_ID&#39;) ?? &#39;&#39;;
    this.KAKAO_REDIRECT_URI =
      this.configService.get&lt;string&gt;(&#39;KAKAO_REDIRECT_URI&#39;) ?? &#39;&#39;;
  }

  public getSocialLoginRedirect(): string {
    return (
      `https://kauth.kakao.com/oauth/authorize` +
      &#39;?response_type=code&amp;&#39; +
      `redirect_uri=${this.KAKAO_REDIRECT_URI}&amp;` +
      `client_id=${this.KAKAO_CLIENT_ID}`
    );
  }

  public async socialLogin(
    provider: AuthProvider,
    req: Request,
  ): Promise&lt;OAuthInfo&gt; {
    const kakaoAccessToken = await this.getKakaoAccessToken(req);

    const { data } =
      await this.httpService.axiosRef.get&lt;GetKakaoUserInfoResponseDto&gt;(
        &#39;https://kapi.kakao.com/v2/user/me&#39;,
        {
          headers: {
            Authorization: `Bearer ${kakaoAccessToken}`,
            &#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded;charset=utf-8&#39;,
          },
        },
      );

    return {
      snsId: data.id.toString(),
      provider,
    };
  }

  private async getKakaoAccessToken(request: Request): Promise&lt;string&gt; {
    if (request.query.token) {
      return request.query.token as string;
    }

    const code = request.query.code as string;
    const result =
      await this.httpService.axiosRef.post&lt;GetKakaoTokenResponseDto&gt;(
        &#39;https://kauth.kakao.com/oauth/token&#39;,
        new URLSearchParams({
          grant_type: &#39;authorization_code&#39;,
          client_id: this.KAKAO_CLIENT_ID,
          redirect_uri: this.KAKAO_REDIRECT_URI,
          code: encodeURIComponent(code),
        }),
        { headers: { &#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded&#39; } },
      );
    return result.data.access_token;
  }
}</code></pre>
<pre><code class="language-ts">// apple.strategy.ts

@Injectable()
export class AppleLoginStrategy implements ISocialLoginStrategy {
  private readonly APPLE_CLIENT_ID: string;
  private readonly APPLE_CLIENT_SECRET: string;
  private readonly APPLE_REDIRECT_URI: string;
  private readonly jwksClient: JwksClient;

  constructor(
    private readonly configService: ConfigService,
    private readonly httpService: HttpService,
    private readonly jwtService: JwtService,
  ) {
    this.APPLE_CLIENT_ID =
      this.configService.get&lt;string&gt;(&#39;APPLE_CLIENT_ID&#39;) ?? &#39;&#39;;
    this.APPLE_CLIENT_SECRET =
      this.configService.get&lt;string&gt;(&#39;APPLE_CLIENT_SECRET&#39;) ?? &#39;&#39;;
    this.APPLE_REDIRECT_URI =
      this.configService.get&lt;string&gt;(&#39;APPLE_REDIRECT_URI&#39;) ?? &#39;&#39;;
    this.jwksClient = new JwksClient({
      jwksUri: &#39;https://appleid.apple.com/auth/keys&#39;,
    });
  }

  public getSocialLoginRedirect(): string {
    return (
      &#39;https://kauth.kakao.com/oauth/authorize&#39; +
      `?client_id=${this.APPLE_CLIENT_ID}&amp;` +
      `redirect_uri=${encodeURIComponent(this.APPLE_REDIRECT_URI)}&amp;` +
      &#39;response_type=code&amp;&#39; +
      &#39;scope=name%20email&amp;&#39; +
      &#39;response_mode=form_post&amp;&#39; +
      &#39;state=a_random_csrf_token_string_12345&amp;&#39; +
      &#39;nonce=another_random_string_for_nonce_67890&#39;
    );
  }

  public async socialLogin(
    provider: AuthProvider,
    req: Request,
  ): Promise&lt;OAuthInfo&gt; {
    const appleIdToken = await this.getAppleIdToken(req);
    const decodedToken = await this.decodeIdToken(appleIdToken);

    return {
      snsId: decodedToken.sub,
      provider,
    };
  }

  private async getAppleIdToken(req: Request): Promise&lt;string&gt; {
    const code = req.body.code;

    const result =
      await this.httpService.axiosRef.post&lt;GetAppleTokenResponseDto&gt;(
        &#39;https://appleid.apple.com/auth/token&#39;,
        new URLSearchParams({
          client_id: this.APPLE_CLIENT_ID,
          client_secret: this.APPLE_CLIENT_SECRET,
          code: code,
          grant_type: &#39;authorization_code&#39;,
          redirect_uri: this.APPLE_REDIRECT_URI,
        }),
      );
    return result.data.id_token;
  }

  private async decodeIdToken(idToken: string): Promise&lt;GetAppleDecodedDto&gt; {
    const decodedToken = this.jwtService.decode(idToken, { complete: true });

    const kid = decodedToken.header.kid;
    const signingKey = await this.jwksClient.getSigningKey(kid);
    const publicKey = signingKey.getPublicKey();

    return verify(idToken, publicKey, {
      algorithms: [&#39;RS256&#39;],
      issuer: &#39;https://appleid.apple.com&#39;,
      audience: this.APPLE_CLIENT_ID,
    }) as GetAppleDecodedDto;
  }
}</code></pre>
<p>이제 카카오와 애플은 <code>socialLogin</code> 만 구현하면 됩니다. 그래서 쓸 데 없는 함수를 구현해야 하는 문제 없이 각각 자유롭게 구현을 할 수 있게 됐습니다.</p>
<p>최종 UML 을 보면 아래와 같습니다.
<img src="https://velog.velcdn.com/images/in__32/post/76c619df-eab7-4371-b3a3-7aafb227d781/image.png" alt=""></p>
<h2 id="마무리">마무리</h2>
<p>사실 그냥 passport 쓸 걸... 이라는 생각도 많이 했었는데 문제가 발생하고 해결을 하는 과정에서 많은 것들을 배워갈 수 있었습니다. 특히 <strong>객체지향 5원칙</strong>!!! 전공 수업 배울 때 &#39;이런 거 왜 쓰는 거야?&#39; 라고 궁시렁대며 공부했었는데... 정말 중요한 개념이었단 걸 뒤늦게 알았습니다.</p>
<p>제 코드가 완벽한 것은 아닙니다. 피드백 적극 환영입니다!</p>
<p>그럼 이상으로 포스팅 마치겠습니다.
<img src="https://velog.velcdn.com/images/in__32/post/9a3c15d7-cb4d-4e36-a5e7-f595e586cd44/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[VSCode] 이거 다 알면 VSC 단축키 마스터 (with. mac)]]></title>
            <link>https://velog.io/@in__32/vscode-mac-shortcuts</link>
            <guid>https://velog.io/@in__32/vscode-mac-shortcuts</guid>
            <pubDate>Wed, 06 Aug 2025 09:46:11 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요, 백엔드 개발자 이수인입니다!</p>
<p>velog에 개발 글 꾸준히 올려야지! 하고 마음만 먹은 지 어언... 다시 키보드를 잡기가 쉽지 않더라고요. 그래서 이번엔 정말 제가 좋아하는 주제인 단축키로 시작해서 다시 마음 좀 잡아보려고합니다.</p>
<h3 id="시작하기-전에-단축키-꼭-써야-할까요">시작하기 전에... 단축키, 꼭 써야 할까요?</h3>
<p>저는 단축키 쓰는 걸 정말 좋아합니다. 궁금한 건 못 참고 단축키 커스텀도 해보는 편인데, 단축키만 잘 써도 업무 효율이랑 속도가 정말 말도 안 되게 올라가거든요.</p>
<p>그래서 주변에 &quot;이거 진짜 편해!&quot; 하고 알려주면... 다들 귀 막고 마우스를 쓰더라고요... (그럴 때마다 솔직히 답답합니다 ㅎㅎ..;;)</p>
<p>이 글이 단축키에 관심 없던 분들에게도 &#39;오, 꽤 유용한데?&#39; 라는 생각이 드는 계기가 되었으면 좋겠습니다.</p>
<h3 id="이-글은-이렇게-구성했어요">이 글은 이렇게 구성했어요</h3>
<ul>
<li><p><strong>텍스트모든 단축키에 움짤(gif)을 넣었어요.</strong>
글로만 보면 감이 안 오니까요! 직접 보면서 따라 해보실 수 있도록 준비했습니다.</p>
</li>
<li><p><strong>Mac 사용자 기준입니다.</strong>
저는 맥북을 쓰기 때문에 mac 단축키들로 구성돼 있습니다 (윈도우 차별 (-,-;;) )</p>
</li>
<li><p><strong>macOS 시스템 단축키도 포함되어 있어요.</strong>
몇 개는 vsc 전용 단축키가 아니라 mac 시스템 단축키인 것들도 있습니다. 그런 것들은 따로 명시할 테니 이 점 참고하세요!</p>
</li>
<li><p><strong>카테고리 별 내용을 묶어봤습니다.</strong>
&#39;<em>카테고리가 분류가 왜 이래?</em>&#39; 라고 생각이 들 수 있는데... 제가 국어를 못 하고 글을 잘 못 써서... 분류를 잘 못 하고 단어 선택을 못 한 것일 수 있습니다. 이해해주시면 감사하겠습니다...</p>
</li>
<li><p><strong>스압 주의!</strong>
내용이 진짜 엄청 길어요... &#39;<em>단축키가 뭐이리 많아!</em>&#39; 라고 생각하실 수 있는데 제 기준 유용하다고 생각하는 것들 다 적어봤습니다. 관심의 영역인 만큼 필요한 것들만 얻어가시면 좋겠습니다 ^^ (저는 실제로 다 외우고 다 쓴다는 사실)</p>
</li>
</ul>
<blockquote>
<p>tmi) 양이 굉장히 많고 다른 작업과 병행해서 며칠 동안 작성하느라 말투가 오락가락함</p>
</blockquote>
<hr>
<h1 id="텍스트--코드-편집">텍스트 &amp; 코드 편집</h1>
<blockquote>
<p>코드를 작성하고 수정하는 모든 순간의 효율을 올려주는 단축키들입니다.</p>
</blockquote>
<h2 id="수정">수정</h2>
<h3 id="커서-기준-특정-단어-선택하고-싶을-때---cmd--d">커서 기준 특정 단어 선택하고 싶을 때 -&gt; cmd + d</h3>
<p>저는 텍스트 편집기로 메모장을 안 쓰고 vscode 를 씁니다. 그 이유 중 하나가 <code>cmd + d</code> 때문일 정도로 정말정말정말 자주 쓰는 단축키입니다.</p>
<p>가끔 특정 단어를 선택하고 싶을 때가 있죠? 그럴 때 마우스 왼쪽 더블 클릭하면 단어가 선택되는데, <code>cmd + d</code> 가 이 기능을 대체해 줍니다.
<img src="https://velog.velcdn.com/images/in__32/post/f36e70cd-695d-4628-9d75-1bc9e4c99b46/image.gif" alt=""></p>
<h3 id="한-파일에서-모든-변수함수-이름-수정---cmd--d">한 파일에서 모든 변수/함수 이름 수정 -&gt; cmd + d!!</h3>
<p>코딩을 하다보면 변수나 함수 이름이 마음에 안 들 때가 있습니다. 근데 보통 변수나 함수가 이곳저곳에 흩어져 있죠. 일일이 수정하면 정말 답답하고 놓칠 때도 있습니다. 그럴 때 <code>cmd + d</code> 를 쭉~ 눌러보세요! 그러면 일치하는 모든 단어가 선택됩니다! 이것만 알아도 효율 많이 올라갑니다!
추가로 변수/함수라고 하긴 했는데 단어들 조합이든, 문장이든 일치하는 게 있으면 선택이 됩니다.
<img src="https://velog.velcdn.com/images/in__32/post/d3440c7a-63b3-4a50-9d94-feb268e08084/image.gif" alt=""></p>
<h3 id="커서를-여러-개-올리고-싶을-때---cmd--마우스-왼쪽-클릭">커서를 여러 개 올리고 싶을 때 -&gt; cmd + 마우스 왼쪽 클릭</h3>
<p>정말 가끔 특정 부분을 모두 수정하고 싶을 때가 있습니다. 커서를 여러 개 올리면 해결이 되죠.
<img src="https://velog.velcdn.com/images/in__32/post/b10d433c-6c80-4450-a124-06e20d437a6b/image.png" alt="">
위 사진 처럼 특정 부분에 <code>홍길동씨,</code> 를 넣으려고 하는 경우! 이럴 때 원하는 삽입 위치에 <code>cmd + 마우스 왼쪽 클릭</code> 을 하면 커서가 새로 생깁니다.
<img src="https://velog.velcdn.com/images/in__32/post/dab7486a-2e96-4e9d-af58-d9d4d8ca3d66/image.gif" alt=""></p>
<p>지금은 예시가 짧고 단순해서 별로일 거 같지만, 좀 긴 상황에서는 꽤 유용합니다.</p>
<h3 id="마우스-안-쓰고-커서-여러-개-올리고-싶을-때---cmd--opt--방향키↑↓">마우스 안 쓰고 커서 여러 개 올리고 싶을 때 -&gt; cmd + opt + 방향키↑/↓</h3>
<p>이것도 커서를 여러 개 올리는 방법 중 하나입니다. <code>cmd + 마우스 왼쪽 클릭</code> 처럼 원하는 부분을 선택하지는 못하지만, 같은 열에 커서를 여러 개 올리고 싶을 때 쓰기 좋습니다. 근데 그냥 이 단축키만 쓰는 상황이 많지 않아서 저는 보통 <code>cmd + d</code> 랑 같이 쓰는 거 같아요. 말로 하기 어려워서 영상으로 보시죠.
<img src="https://velog.velcdn.com/images/in__32/post/ac424d2a-edc0-44c7-bb6b-68b867e23cd3/image.gif" alt=""></p>
<h3 id="다음-줄을-생성하고-싶은데-커서가-중간에-있을-때---cmd--enter">다음 줄을 생성하고 싶은데 커서가 중간에 있을 때 -&gt; cmd + enter</h3>
<p>이것도 정말정말정말 진짜 자주 쓰는 단축키입니다. 코드 작성하는데 다음 줄을 생성해야 할 때가 있죠? 그런데 커서는 중간에 있는 경우... 그럴 때 보통 <code>cmd + -&gt;</code> 로 맨 끝으로 이동한 후 enter 를 하시는데, 솔직히 저는 불편합니다 😂 굳이 방향키로 손이 가야 되잖아요!! 그래서 <code>cmd + enter</code> 이거만 하면 굳이 방향키로 이동 안 해도 다음 줄이 생성됩니다. 이거 진짜 편해요.
<img src="https://velog.velcdn.com/images/in__32/post/887bfbf7-24c7-49bc-9f02-857072a77289/image.gif" alt=""></p>
<h3 id="현재-커서-라인을-이동하고-싶을-때---opt--방향키↑↓">현재 커서 라인을 이동하고 싶을 때 -&gt; opt + 방향키↑/↓</h3>
<p>27번째 라인에 있는 코드를 25번째나 29번째 라인으로 옮기고 싶을 때가 있죠. 그럴 때 <code>cmd + x</code> 로 잘라서 원하는 위치에 넣으셔도 됩니다. 위/아래로 몇십, 혹은 몇백 줄 이동시키는 거는 잘라 넣는 게 좋지만, 위/아래로 몇 줄 이동 안 시키는 경우 저는 보통 <code>opt + 방향키↑/↓</code> 를 쓰는 편입니다. ^^ 이거는 취향 많이 갈리더라고요.
<img src="https://velog.velcdn.com/images/in__32/post/54b404dd-b537-496d-a91d-0fabab57bae1/image.gif" alt=""></p>
<h3 id="현재-커서-라인을-아래로-복붙하고-싶을-때---opt--shift--방향키↑↓">현재 커서 라인을 아래로 복붙하고 싶을 때 -&gt; opt + shift + 방향키↑/↓</h3>
<p>경우가 많지 않을 수 있는데, 특정 라인을 위/아래로 복사하고 싶을 때가 있습니다.
<img src="https://velog.velcdn.com/images/in__32/post/5b8cbbbb-5f3d-439d-9f85-4809c0c90d89/image.png" alt=""></p>
<p>저는 위 사진처럼 console.log 안에 문장이 있는데, 아래로 복붙해서 특정 단어만 바꾸고 싶을 때, 그때 사용합니다. 아무튼 그럴 때 <code>opt + shift + 방향키↑/↓</code> 를 누르면 아래로 라인이 모두 복붙됩니다. 굳이 이렇게 안 쓰고 <code>cmd + c</code> 한 다음에 엔터 누르면서 붙여 넣으면 안 되냐? 라고 할 수 있지만... 저는 바로 아래 붙여 넣는 거면 이게 좋더라고요 ^^ 참고로 커서 위치는 상관없습니다.
<img src="https://velog.velcdn.com/images/in__32/post/9607d643-9d97-42bf-a54d-4633ace835ba/image.gif" alt=""></p>
<h3 id="현재-커서-라인을-선택하고-싶을-때---cmd--l">현재 커서 라인을 선택하고 싶을 때 -&gt; cmd + L</h3>
<p>이것도 제가 정말정말정말 자주 쓰는 단축키입니다. 이 단축키는 단순히 현재 커서 라인을 선택해 줍니다. 그리고 계속 누르면 이어지는 아래 라인들을 계속 선택해 줍니다. 아래처럼 말이죠.
<img src="https://velog.velcdn.com/images/in__32/post/540ebec0-dbf0-4e44-be95-a2654ca0a260/image.gif" alt=""></p>
<p>이것만 놓고 보면 별거 아닌 거 같은데요, 저는 보통 다른 단축키랑 응용해서 쓰는 편입니다. 바로 이어서 설명하겠습니다.</p>
<h3 id="현재-커서-라인을-선택-후-삭제하고-싶을-때---cmd--l--backspace">현재 커서 라인을 선택 후 삭제하고 싶을 때 -&gt; cmd + L + backspace</h3>
<p>현재 커서 라인을 삭제하고 싶을 때 저는 보통 <code>cmd + L + backspace</code> 를 사용합니다. 근데 보통 다들 <code>cmd + x</code> 로 삭제하더라고요. 그래도 제가 이 단축키를 쓰는 이유는 제가 <em>&#39;어떤 라인을 선택했는지 시각적으로 확인하고 삭제할 수 있기 때문&#39;</em> 입니다. <code>cmd + L</code> 을 쭉~ 누르면 연속되게 라인을 선택할 수 있기 때문에, 많은 라인을 삭제할 때 더 유용하더라고요 ^^. 굳이라고 생각이 들면 <code>cmd + x</code> 로만 하셔도 됩니다.
<img src="https://velog.velcdn.com/images/in__32/post/3b11a1d2-bbae-47ab-ae11-56456648562b/image.gif" alt=""></p>
<h3 id="여러-라인-선택-후-이동하고-싶을-때---cmd--l--opt--방향키↑↓">여러 라인 선택 후 이동하고 싶을 때 -&gt; cmd + L + opt + 방향키↑/↓</h3>
<p><code>cmd + L</code> 을 쭉~ 누르면 여러 라인을 선택할 수 있다고 했죠. 그리고 <code>opt + 방향키↑/↓</code> 을 누르면 라인을 이동할 수도 있다고 했죠. 이거를 합치면 아래처럼 여러 라인을 선택하고 이동할 수 있답니다. 저는 보통 짧은 함수를 이동할 때 쓰는 편입니다.
<img src="https://velog.velcdn.com/images/in__32/post/ba68bba0-5f59-4357-aea8-150cf66dd52a/image.gif" alt=""></p>
<h3 id="여러-라인-선택-후-아래로-복붙하고-싶을-때---cmd--l--opt--shift--방향키↑↓">여러 라인 선택 후 아래로 복붙하고 싶을 때 -&gt; cmd + L + opt + shift + 방향키↑/↓</h3>
<p>이것도 <code>cmd + L</code> 을 쭉~ 누르고 <code>opt + shift + 방향키↑/↓</code> 를 누르면 복붙이 된답니다. 저는 보통 비슷한 함수를 1개 더 만들어야 할 때, 원본 함수를 선택해서 복붙하는 용으로 씁니다.
<img src="https://velog.velcdn.com/images/in__32/post/1f6e729e-9923-417f-9d3f-5403c0dcf2d8/image.gif" alt=""></p>
<p>지금 당장 생각이 안 나는데 <code>cmd + L</code> 로 응용할 수 있는 게 많아요~ 제가 정리한 단축키들 보면서 기호에 맞게 응용하면 됩니다!</p>
<h3 id="import-를-해야-돼요---cmd--">import 를 해야 돼요! -&gt; cmd + .</h3>
<p>다른 언어랑 프레임워크에서는 잘 모르겠지만, nodejs 에서 import 를 많이 하죠. 자동완성으로 나오면 enter 나 tab 을 누르면 자동으로 import 가 됩니다. 그런데 실수로 enter 나 tab 을 안 누르면 다시 지웠다가 입력해야 하는 번거로움이 있습니다. 그럴 때 <code>cmd + .</code> 을 써보세요! 그러면 &#39;Add ~ 뭐시기&#39; 가 나와서 enter 를 누르면 됩니다~ 그 외에 다른 것도 나오는데 그건 솔직히 안 쓰고 뭔지 잘 모르겠습니다 ㅎ
<img src="https://velog.velcdn.com/images/in__32/post/6a1ea545-4771-4d0b-8b57-b5495978eb11/image.gif" alt=""></p>
<h3 id="커서를-되돌릴래요---cmd--u">커서를 되돌릴래요! -&gt; cmd + u</h3>
<p>저만 그런지 모르겠는데 가끔씩 <code>cmd + 방향키↑/↓</code> 를 잘못 눌러서 맨 위/아래로 가는 경우가 있어요. 이게 상관없을 때도 있는데 코드가 100줄이 넘어가는데 중간 라인에서 작업하다가 실수로 맨 위/아래로 가면 좀 짜증 나더라고요. 그럴 때 <code>cmd + u</code> 를 누르면 다시 중간으로 돌아온답니다 ^^ <code>cmd + z</code> 랑 비슷해요.
<img src="https://velog.velcdn.com/images/in__32/post/048c5213-eb71-45ae-b987-ca7c13f7d326/image.gif" alt=""></p>
<hr>
<h2 id="에러">에러</h2>
<h3 id="코드가-너무-많은데-에러는-많고-에러-라인으로-바로-이동---f8">코드가 너무 많은데 에러는 많고... 에러 라인으로 바로 이동! -&gt; F8</h3>
<p>코드가 몇백 줄을 넘어가는데 에러가 정말 많이 뜰 때가 있죠. 상상만 해도 싫지만, 그 에러 라인으로 바로 이동하고 싶을 때 <code>F8</code> 을 누르면 에러 라인으로 차근차근 이동합니다. 굳이 스크롤 내리지 않아도 되고, 정확하게 에러를 찾을 수 있으니 많이 유용합니다.
<img src="https://velog.velcdn.com/images/in__32/post/ec03e339-778f-4c54-a751-402b057d7a61/image.gif" alt=""></p>
<h3 id="빨간-줄이-생겨서-무슨-에러인지-봐야-되는데-마우스는-쓰기-싫어---cmd--k--i">빨간 줄이 생겨서 무슨 에러인지 봐야 되는데... 마우스는 쓰기 싫어! -&gt; cmd + k + i</h3>
<p>빨간 줄이 생기면 무슨 에러인지 봐야 하죠. 그럴 때 마우스를 갖다 대면 무슨 에러인지 표시해 주긴 합니다. 그런데 저는 마우스 없이 코딩하는 걸 좋아하기 때문에 이것조차 단축키를 씁니다. 오지랖같지만, 막상 쓰면 편해요. 아무튼 에러가 나는 곳으로 이동해서 <code>cmd + k + i</code> 를 누르면 에러가 표시됩니다.
<img src="https://velog.velcdn.com/images/in__32/post/d77aa5b7-11fe-4342-a15c-64fb67e82991/image.gif" alt=""></p>
<hr>
<h2 id="시각적으로-깔끔하게">시각적으로 깔끔하게</h2>
<p>코드가 몇백 줄 넘어가면서 함수가 정말 많으면 무슨 함수가 있는지도 모르겠고 하나도 안 읽힙니다. 그럴 때 함수만 표시되면 좋겠죠? 그리고 원하는 함수들만 보고 싶을 겁니다. 그럴 때 필요한 단축키들이 있는데, 이게 여러 단축키를 조합하는 거라 일단 다 적고 최종적으로 어떻게 쓰는지 보여드리겠습니다.</p>
<h3 id="현재-파일에-코드가-너무-많아-다-접어버릴래---cmd--k--0-숫자-0입니다">현재 파일에 코드가 너무 많아... 다 접어버릴래! -&gt; cmd + k + 0 (숫자 0입니다)</h3>
<p>일단 괄호가 안에 들어가는 것들이 많아, 들여 써질 때 왼쪽에 보시면 그 라인을 접을 수 있습니다. 일단 이것들을 다 접어버려야 좀 보기 편해집니다. 그럴 때 <code>cmd + k + 0</code> 을 누르면 현재 파일에서 접을 수 있는 거 다 접어버립니다. 아래처럼 말이죠. (찍고 보니까 접었다 펴는 거 같은데, 그냥 접기만 하는 단축키입니다.)
<img src="https://velog.velcdn.com/images/in__32/post/3ad5a5c4-2bc3-4a1a-ba58-11f721eed77b/image.gif" alt=""></p>
<h3 id="접혀-있는-거-하나씩-열어-볼래---cmd--opt---혹은-cmd--k--l">접혀 있는 거 하나씩 열어 볼래~ -&gt; cmd + opt + ] 혹은 cmd + k + L</h3>
<p>다 접혀있으면 이제 하나씩 열어봐야겠죠? 그럴 때 열고자 하는 위치로 가서 <code>cmd + opt + ]</code> 혹은 <code>cmd + k + L</code> 을 누르면 접혀있는 거를 열어볼 수 있습니다.
<img src="https://velog.velcdn.com/images/in__32/post/54dea5a9-fdeb-4a5c-99f9-5321ab6f8ce4/image.gif" alt=""></p>
<h3 id="연-거-다시-닫을래---cmd--opt---혹은-cmd--k--l">연 거 다시 닫을래 -&gt; cmd + opt + [ 혹은 cmd + k + L</h3>
<p>연 거를 다시 닫고 싶을 수도 있죠? 그럴 때 <code>cmd + opt + [</code> 혹은 <code>cmd + k + L</code> 로 닫을 수 있습니다. 위랑 이어져서 눈치채셨을 수 있는데, <code>cmd + k + L</code> 은 toggle 로 작동합니다. 그리고 개인적으로 저는 <code>cmd + opt + [</code> 이랑 <code>cmd + opt + ]</code> 이거는 뭔가 손이 잘 안 가더라고요. 그리고 toggle 이 아니라서 저는 보통 여닫는 거를 <code>cmd + k + L</code> 을 사용합니다. 
추가로 이 접히는 단축키가 현재 라인의 상위 블록이 접히는 겁니다. 이게 표현이 어려운데 아래 코드로 예를 들면,
<img src="https://velog.velcdn.com/images/in__32/post/fca9dbc9-d56d-4a22-aee5-0108eac00060/image.png" alt="">
<code>obj1</code> 을 접고 싶으면 2, 3, 4, 5 번째 라인에서 적용하면 되고, <code>test</code> 함수를 접고 싶으면 1, 7, 13 번째 라인에서 적용하면 됩니다. 영상으로 보죠.
<img src="https://velog.velcdn.com/images/in__32/post/9e82e2a6-867e-4506-89a8-8a430d16726a/image.gif" alt=""></p>
<h3 id="접혀-있는-거-안에까지-모두-열고-닫을-때-모두-닫을래---cmd--k--n">접혀 있는 거 안에까지 모두 열고, 닫을 때 모두 닫을래! -&gt; cmd + k + n</h3>
<p>위 영상들 보시면 알겠지만, <code>cmd + k + L</code> 이게 안에 접혀 있는 게 있으면 그것도 열지는 못 해요. 그리고 닫아줄 때 모두 닫아주지 않고요. 이게 vsc 내장 단축키에 아무리 찾아도 없길래 제가 직접 커스텀해서 만들어봤습니다. 
F1 -&gt; Open Keyboard Shortcuts 을 입력 후 [ ] 안에 아래 내용을 넣으면 됩니다.
<img src="https://velog.velcdn.com/images/in__32/post/6a83fc3c-e7f0-4a25-9739-bf9aea28507c/image.gif" alt=""></p>
<blockquote>
</blockquote>
<p>{
    &quot;key&quot;: &quot;cmd+k cmd+n&quot;,
    &quot;command&quot;: &quot;editor.toggleFoldRecursively&quot;,
    &quot;when&quot;: &quot;editorTextFocus &amp;&amp; foldingEnabled&quot;
  }</p>
<p>그러면 <code>cmd + k + L</code> 처럼 toggle 로 작동됩니다~ 아래 영상 보시면 모두 열리는 것을 확인할 수 있습니다. 평상시 <code>cmd + k + 0</code> 과 <code>cmd + k + L</code> 을 즐겨 쓰시던 분들에겐 정말정말 유용할 거예요~
<img src="https://velog.velcdn.com/images/in__32/post/ce551340-bdfb-4809-b9f7-d4518cecee43/image.gif" alt=""></p>
<h3 id="그냥-다-열래---cmd--k--j">그냥 다 열래!! -&gt; cmd + k + j</h3>
<p>현재 파일에 접혀 있는 거 그냥 다 열고 싶을 때 cmd + k + j 를 작성하면 모두 열립니다. (이것도 찍고 보니까 접었다 펴는 거 같은데, 그냥 열기만 합니다.)
<img src="https://velog.velcdn.com/images/in__32/post/4022c887-ad18-44ff-8e04-efd9af94d2bb/image.gif" alt=""></p>
<h4 id="정리">정리</h4>
<p>그래서 저는 파일에 함수들이 너무 많을 때 먼저 <code>cmd + k + 0</code> 으로 모두 닫은 다음에 열고자 하는 함수들을 상황에 따라 <code>cmd + k + L</code> 로 열거나, <code>cmd + k + n</code> 으로 여닫으면서 읽습니다!</p>
<hr>
<h2 id="기타">기타</h2>
<p>카테고리에 담기는 애매하지만, 아무튼 코드 작업인 단축키를 적어보겠습니다.</p>
<h3 id="속성들을-보고-싶을-때---cmd--i">속성들을 보고 싶을 때 -&gt; cmd + i</h3>
<p>필드 안에 들어가야 할 속성들이 있는데 그 안에 뭐가 필요한지 모를 때가 있습니다. 그래서 뭐인지 찾아야 하는 데 여간 불편한 게 아니죠. 그럴 때 필요한 게 <code>cmd + i</code> 입니다. 이거는 영상 보는 게 이해가 잘 됩니다.
<img src="https://velog.velcdn.com/images/in__32/post/985bc34b-a3fa-4572-9355-763dde80249c/image.gif" alt=""></p>
<p>근데 copilot 을 쓰는 사람이라면 이 단축키가 안 먹히고 copilot 을 호출하게 돼요. 그럴 때 위에서 했던 것처럼 &#39;F1 -&gt; Open Keyboard Shortcuts&#39; 을 입력해서 json 파일을 열고 [] 안에 아래 내용을 붙여넣으면 됩니다.</p>
<blockquote>
<p>{
    &quot;key&quot;: &quot;cmd+i&quot;,
    &quot;command&quot;: &quot;-inlineChat.startWithCurrentLine&quot;,
    &quot;when&quot;: &quot;editorFocus &amp;&amp; github.copilot.chat.editor.enableLineTrigger &amp;&amp; inlineChatHasProvider &amp;&amp; !editorReadonly &amp;&amp; !inlineChatVisible&quot;
  },
  {
    &quot;key&quot;: &quot;cmd+i&quot;,
    &quot;command&quot;: &quot;editor.action.triggerSuggest&quot;,
    &quot;when&quot;: &quot;editorHasCompletionItemProvider &amp;&amp; textInputFocus &amp;&amp; !editorReadonly &amp;&amp; !suggestWidgetVisible&quot;
  },
  {
    &quot;key&quot;: &quot;cmd+i&quot;,
    &quot;command&quot;: &quot;-editor.action.triggerSuggest&quot;,
    &quot;when&quot;: &quot;editorHasCompletionItemProvider &amp;&amp; textInputFocus &amp;&amp; !editorReadonly &amp;&amp; !suggestWidgetVisible&quot;
  },</p>
</blockquote>
<h3 id="특정-변수함수가-정의-혹은-사용된-곳으로-이동-혹은-확인하고-싶을-때---f12-혹은-opt--마우스-왼쪽-클릭">특정 변수/함수가 정의 혹은 사용된 곳으로 이동 혹은 확인하고 싶을 때 -&gt; F12 혹은 opt + 마우스 왼쪽 클릭</h3>
<p>어떤 파일에서 작성한 함수를 다른 파일에서 쓸 때가 있습니다. 다른 파일에서 읽다가 &#39;이 함수가 뭐지?&#39; 싶을 때가 있죠. 혹은 라이브러리에서 쓴 함수가 어떻게 구성되어 있는지 확인하고 싶을 때도 있죠. 그럴 때 그 단어에서 <code>F12</code> 를 누르면 사용된 곳으로 이동합니다.
<img src="https://velog.velcdn.com/images/in__32/post/255c116f-caa4-4cc4-ab38-76d4526c1af8/image.gif" alt=""></p>
<p>만약에 특정 함수가 이곳저곳에서 많이 쓰이면 영상처럼 나옵니다. 즉, 어디서 이 함수가 쓰였는지도 확인할 수 있습니다. 이동하고 싶으면 원하는 거에서 enter 를 누르면 됩니다.
<img src="https://velog.velcdn.com/images/in__32/post/0db328f8-b6af-4437-b0d8-063748b14e3c/image.gif" alt=""></p>
<p>추가로 저는 <code>opt + 마우스 왼쪽 클릭</code> 으로 설정 돼있는데 <code>cmd + 마우스 왼쪽 클릭</code> 일 수도 있습니다.</p>
<hr>
<h1 id="파일관련-작업">파일관련 작업</h1>
<blockquote>
<p>파일 관련 작업할 때 쓰는 단축키입니다~</p>
</blockquote>
<h2 id="파일-탐색기">파일 탐색기</h2>
<h3 id="파일-탐색기로-가고-싶을-때---cmd--shift--e">파일 탐색기로 가고 싶을 때 -&gt; cmd + shift + e</h3>
<p>파일 탐색기라고 하면 해당 프로젝트의 폴더, 파일들이 있는 공간입니다. 보통 파일 탐색기에서 무엇을 하시나요? 이름 변경, 삭제, 열기, 폴더 접기 등등이 있겠죠? 이런 작업을 할 때 마우스를 많이 쓰는데, 이거 단축키로 하면 진짜 많이 편해요. 그래서 앞으로 이런 상황에 맞는 단축키들을 쓰려고 할 때 파일 탐색기로 이동할 필요가 있습니다. <code>cmd + shift + e</code> 를 누르면 파일 탐색기로 focus 가 이동합니다! 그리고 <code>방향키↑/↓</code> 로 이동할 수 있습니다.
<img src="https://velog.velcdn.com/images/in__32/post/45847b6a-afdb-421a-81b0-58f785eaa03f/image.gif" alt=""></p>
<h3 id="이름을-변경하고-싶어---enter">이름을 변경하고 싶어! -&gt; enter</h3>
<p>이제 파일 탐색기로 이동했습니다. 원하는 파일 혹은 폴더로 이동해서 <code>enter</code> 를 누르면 파일 이름을 변경할 수 있습니다. 사실 이거는 mac 시스템 단축키라 finder 같은 곳에서도 사용할 수 있습니다.
<img src="https://velog.velcdn.com/images/in__32/post/6bf6d652-1bd6-475c-bac8-c2514c832506/image.gif" alt=""></p>
<h3 id="파일폴더를-열고-싶어---spacebar">파일/폴더를 열고 싶어! -&gt; spacebar</h3>
<p>원하는 파일 혹은 폴더로 이동했을 때 파일은 작업화면에 열어주고, 폴더는 접거나 열어줍니다. (솔직히 왜 여는 게 enter 가 아니라 spacebar 인지 아직도 모르겠음;;) 이것도 Mac 시스템 단축키입니다.
<img src="https://velog.velcdn.com/images/in__32/post/a219da95-6191-4b52-a2b5-4b9d8cd17577/image.gif" alt=""></p>
<h3 id="삭제할-거야---cmd--backspace">삭제할 거야! -&gt; cmd + backspace</h3>
<p>원하는 파일 혹은 폴더에서 <code>cmd + backspace</code> 를 누르면 삭제됩니다. &#39;아 근데 이거 삭제하면 안 되는데!!!!&#39; ... 걱정하지마세요 <code>cmd + z</code> 하면 돌아오더라고요. 이것도 mac 시스템 단축키입니다.
<img src="https://velog.velcdn.com/images/in__32/post/50eca216-5e64-4713-acbf-02b29e8bf75a/image.gif" alt=""></p>
<h3 id="여러-개-열래---cmd--shift--enter여러-개-선택--spacebar">여러 개 열래! -&gt; (cmd + shift + enter)(여러 개 선택) + spacebar</h3>
<p>파일을 한 개만 열지 않고 많이 열 때도 있죠? 그럴 때 원하는 파일에서 <code>cmd + shift + enter</code> 를 누르면 파일이 선택됩니다. 다 선택했으면 <code>spacebar</code> 를 눌러서 모두 열 수 있습니다! 사실 이게 여러 개 열려고 찾아낸 단축키가 아니었습니다. 파일 여는 단축키를 자주 써보시면 알겠지만, 이게 새 창에서 안 열리고 자꾸 이미 열은 파일에서 변경이 되더라고요. 저는 이게 너무 불편해서 새 창으로 열려고 할 때도 씁니다.
<img src="https://velog.velcdn.com/images/in__32/post/6b10e76a-6394-4143-bb85-a6f6e29518cf/image.gif" alt=""></p>
<h3 id="root-폴더로-이동할래---esc">root 폴더로 이동할래! -&gt; esc</h3>
<p>보통 root 폴더 하위에 폴더를 만들고 그 폴더에서 작업하는 경우가 많습니다. 그런데 진짜 가끔 root 폴더에서 새로운 파일/폴더를 만드는 경우도 있는데, 그럴 때 <code>esc</code> 를 누르면 root 폴더로 focusing 이 돼서 root 하위에 파일/폴더를 만들 수 있습니다.
<img src="https://velog.velcdn.com/images/in__32/post/0c1eb1eb-7701-4768-a907-89825df253fd/image.gif" alt=""></p>
<h3 id="파일-생성---cmd--n">파일 생성 -&gt; cmd + n</h3>
<p>특정 폴더에서 파일을 만들고 싶을 때가 있죠? 그럴 때 <code>cmd + n</code> 을 누르면 됩니다. 사실 이 단축키는 커스텀 단축키입니다. 위에서 했던 것처럼 &#39;F1 -&gt; Open Keyboard Shortcuts&#39; 을 입력해서 json 파일을 열고 [] 안에 아래 내용을 붙여 넣으면 됩니다.</p>
<blockquote>
<p>{
    &quot;key&quot;: &quot;cmd+n&quot;,
    &quot;command&quot;: &quot;explorer.newFile&quot;,
    &quot;when&quot;: &quot;!editorFocus&quot;
  }</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/in__32/post/b2b5fef6-aa79-4b0e-b538-3b9102d18fce/image.gif" alt=""></p>
<h3 id="폴더-생성---opt--n">폴더 생성 -&gt; opt + n</h3>
<p>폴더를 만들고 싶을 때도 있습니다. 이럴 때 <code>opt + n</code> 을 누르면 되는데, 이것도 커스텀 단축키입니다.마찬가지로 아래 내용을 [] 안에 복붙하면 됩니다.</p>
<blockquote>
<p>{
    &quot;key&quot;: &quot;alt+n&quot;,
    &quot;command&quot;: &quot;explorer.newFolder&quot;,
    &quot;when&quot;: &quot;!editorFocus&quot;
  }</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/in__32/post/afb19d5a-cc6e-4c11-a9db-83504f11ad47/image.gif" alt=""></p>
<h3 id="파일-탐색기에서-할-거-끝났다-이제-코드로-돌아가야지---cmd--1">파일 탐색기에서 할 거 끝났다~ 이제 코드로 돌아가야지! -&gt; cmd + 1</h3>
<p>파일 탐색기에서 작업을 다 끝내고 다시 코드로 돌아와서 작업을 해야 하겠죠? 그럴 때 <code>cmd + 1</code> 을 눌러보세요! 그러면 다시 코드로 돌아옵니다. (여기서 숫자도 어떤 숫자인지에 따라서 의미가 달라지는데, 이거는 아래 어딘가에서 다시 적겠습니다)
<img src="https://velog.velcdn.com/images/in__32/post/f4debb8d-1447-4c73-b747-b7cd3b15b2cb/image.gif" alt=""></p>
<h2 id="파일-문서-작업">파일 문서 작업</h2>
<h3 id="현재-띄워진-파일-간-좌우로-이동---cmd--방향키--">현재 띄워진 파일 간 좌/우로 이동 -&gt; cmd + 방향키&lt;-/-&gt;</h3>
<p>파일들 여러 개 있는 상태에서 좌우로 이동하고 싶을 때 있죠? 그럴 때 <code>cmd + 방향키&lt;-/-&gt;</code> 를 하면 이동이 됩니다. 이거 크롬에서도 되더라고요!
<img src="https://velog.velcdn.com/images/in__32/post/751d222e-e5d1-448f-95a1-6bb717695e4f/image.gif" alt=""></p>
<h3 id="파일-분할하기---ctrl--cmd--방향키--">파일 분할하기 -&gt; ctrl + cmd + 방향키&lt;-/-&gt;</h3>
<p>파일 2개를 분할해서 작업하고 싶을 때가 있습니다. 그럴 때 <code>ctrl + cmd + 방향키&lt;-/-&gt;</code> 를 누르면 파일이 분할돼요. 여기서 방향키는 &#39;어디로 이동시키겠다&#39; 입니다. 처음에 분할시킬 때는 <code>ctrl + cmd + 방향키-&gt;</code> 로 분할을 시킬 수 있습니다. 그러다가 왼쪽 파일을 오른쪽으로 이동시키고 싶으면 <code>ctrl + cmd + 방향키-&gt;</code> 반대로 오른쪽을 왼쪽으로 이동시키고 싶으면 <code>ctrl + cmd + 방향키&lt;-</code> 을 사용하면 됩니다. 오른쪽 화면에 파일이 1개밖에 없을 때 <code>crtl + cmd + 방향키 &lt;-</code> 를 하면 다시 한 화면으로 돌아옵니다. 말을 잘못해서 그렇지 영상으로 보시면 이해가 될 거라 생각합니다..
<img src="https://velog.velcdn.com/images/in__32/post/7c3875b2-2b5c-44e8-b009-981038bddc47/image.gif" alt=""></p>
<h3 id="분할된-파일-간-이동---cmd--숫자">분할된 파일 간 이동 -&gt; cmd + 숫자</h3>
<p>아까 코드로 돌아갈 때 <code>cmd + 1</code> 을 누른다고 했습니다. 여기서 숫자가 무엇이냐에 따라 좀 달라지는데요. 왼쪽부터 1이고 오른쪽으로 갈수록 1씩 증가합니다. 그러니까 파일이 1개만 있을 때는 <code>cmd + 1</code> 로 해야 하고, 파일이 2개일 때 왼쪽이 <code>cmd + 1</code> 오른쪽이 <code>cmd + 2</code> 입니다.
<img src="https://velog.velcdn.com/images/in__32/post/e3f24a7a-723d-44f0-bce1-5ffe94ee92c2/image.gif" alt="">
그래서 만약에 파일탐색기에서 작업을 하다가 코드로 돌아올 때 단순히 <code>cmd + 1</code> 만 하는 게 아니라 어디로 이동할지에 따라서 <code>cmd + 2</code> 가 될 수도 있습니다.</p>
<h3 id="분할된-화면이-너무-작아-ㅠㅠ-좀-크게-할래---cmd--k--m">분할된 화면이 너무 작아 ㅠㅠ 좀 크게 할래! -&gt; cmd + k + m</h3>
<p>화면을 분할하면 장점이 여러 개를 볼 수 있다지만, 단점은 좀 작아진다는 거죠. 분할된 파일을 다 이동시켜서 크게 보는 방법도 있지만, 가끔! 분할된 화면 각각에 파일이 정말 많으면 그러기 쉽지 않죠. 그럴 때 크게 하고자 하는 화면에 <code>cmd + k + m</code> 을 누르면 화면이 커집니다! 그리고 다시 누르면 돌아오고요.
<img src="https://velog.velcdn.com/images/in__32/post/cab3582b-a27b-4665-a572-6bc116c7f090/image.gif" alt=""></p>
<h3 id="현재-열어둔-파일-모두-닫을래---cmd--k--w">현재 열어둔 파일 모두 닫을래! -&gt; cmd + k + w</h3>
<p>파일 생성도 하고 이것저것 참고하다 보니 파일이 잔뜩 쌓이신 경험이 있을 겁니다. 일일이 다 클릭해도 되지만 ㅋㅋ;; <code>cmd + k + w</code> 로 다 지울 수 있어요!!
<img src="https://velog.velcdn.com/images/in__32/post/a57cfb34-10d7-4a16-b203-13989d6409ce/image.gif" alt=""></p>
<h3 id="현재-파일-빼고-모두-닫을래---cmd--opt--t">현재 파일 빼고 모두 닫을래! -&gt; cmd + opt + t</h3>
<p>방금 상황처럼 파일이 많은 상태인데, 현재 작업 중인 파일을 제외하고 모두 닫고 싶은 경우도 있을 거라고 생각합니다 (저만 그런가요? ㅎㅎ;;) 아무튼! 이럴 때 <code>cmd + opt + t</code> 를 누르면 현재 파일을 제외하고 모두 닫힙니다!
<img src="https://velog.velcdn.com/images/in__32/post/c79283cf-acb2-400a-a142-663eeac017df/image.gif" alt=""></p>
<h3 id="삭제한-파일-다시-열래---cmd--shift--t">삭제한 파일 다시 열래! -&gt; cmd + shift + t</h3>
<p>이거는 적을까 말까 고민했는데 모르는 사람들이 좀 있는 거 같더라고요. 가끔 파일 실수로 닫을 때 있죠? 그럴 때 <code>cmd + shift + t</code> 를 누르면 다시 살아납니다. 이거는 크롬에서도 동작합니다.
<img src="https://velog.velcdn.com/images/in__32/post/2ffd6f6e-bc51-404c-a7bb-2185ca645c32/image.gif" alt=""></p>
<h3 id="vsc-새-윈도우로-열래---cmd--shift--n">vsc 새 윈도우로 열래! -&gt; cmd + shift + n</h3>
<p>여기서 새 윈도우라고 하면 새 파일이 아니라 완전 새로운 vsc를 말하는 겁니다. 그러니까 이렇게요
<img src="https://velog.velcdn.com/images/in__32/post/619fc237-35a2-4e2e-9fb0-fbb0c22ec0d4/image.png" alt=""></p>
<p>현재 작업 중인 폴더 유지한 채 새 윈도우 열고 싶을 때도 있잖아요. 그럴 때 <code>cmd + shift + n</code> 을 누르면 됩니다.
<img src="https://velog.velcdn.com/images/in__32/post/80b06bd6-1da7-4c46-9ffe-692b1d6a5ff8/image.gif" alt=""></p>
<h3 id="새-윈도우간-이동---cmd--">새 윈도우간 이동 -&gt; cmd + ~</h3>
<p>새 윈도우 열었는데 윈도우간 이동도 필요하겠죠? <code>cmd + ~</code> 을 누르면 이동이 됩니다~ 이거는 vsc 만이 아니라 mac 단축키입니다.
<img src="https://velog.velcdn.com/images/in__32/post/26369ad0-13c2-4549-b0dd-399eccb5efaf/image.gif" alt=""></p>
<h3 id="이전에-열었던-폴더-열을래---ctrl--r">이전에 열었던 폴더 열을래! -&gt; ctrl + r</h3>
<p>이것도 꽤 유용한 단축키인데요. 현재 A 라는 폴더에서 작업하고 있는데 B 라는 폴더를 열어서 잠깐 뭐 확인하고 싶을 때가 있습니다. 그럴 때 <code>ctrl + r</code> 을 누르면 과거 열었던 폴더들을 보여주고 여기서 선택하면 열 수 있습니다. 이때! 새 윈도우에서 열어주는 게 아니라 현재 윈도우에서 여는 겁니다. 그러니까 위에 단축키로 새 윈도우를 열고 <code>ctrl + r</code> 을 누르면 현재 작업 중인 폴더가 방해되지 않겠죠?
<img src="https://velog.velcdn.com/images/in__32/post/4ff6f2c7-6ada-4ce0-9173-7c1889253816/image.gif" alt=""></p>
<h3 id="특정-윈도우-닫을래---cmd--shift--w">특정 윈도우 닫을래! -&gt; cmd + shift + w</h3>
<p>이 단축키는 특정 윈도우를 닫는 단축키입니다. &#39;<code>cmd + w</code> 로도 닫히는데요?&#39; 라고 할 수 있는데요. 그렇게 해도 닫히지만, 만약에 특정 윈도우에 파일이 잔뜩 쌓여있으면 <code>cmd + w</code> 계속 눌러야 닫힙니다. 그런데 <code>cmd + shift + w</code> 를 누르면 그런 거 상관없이 다 닫아버립니다. 그렇게 자주 쓰이지는 않은데, 알아두면 생각보다 유용해요. 이거는 mac 단축키이더라고요.
<img src="https://velog.velcdn.com/images/in__32/post/b6ddeb39-0f52-48ea-83a6-fe9c6997783e/image.gif" alt=""></p>
<h3 id="파일-검색해서-열고-이동할래---cmd--p">파일 검색해서 열고 이동할래~ -&gt; cmd + p</h3>
<p>저는 뭐 <code>cmd + 방향키&lt;-/-&gt;</code> 로 파일 이동하며 찾고, <code>cmd + 숫자</code> 로 화면 분활된 거 전환하면서 찾고 <code>cmd + shift + e</code> 로 파일탐색기 들어가서 열고 그러는데, 제 친구는 그냥 <code>cmd + p</code> 로 웬만한 거 거의 하더라고요. 이 단축키는 파일 이름으로 검색하거나 히스토리 기반으로 여는 단축키입니다. 취향 차이인데, 저는 이름이 바로바로 생각이 안 나서 귀찮게 이것저것 섞어서 쓰는 편입니다 ㅋㅋ
<img src="https://velog.velcdn.com/images/in__32/post/9c25e26b-3f5b-421b-8469-03f3ff07a761/image.gif" alt=""></p>
<h2 id="기타-1">기타</h2>
<h3 id="터미널-열기-및-전환---ctrl--">터미널 열기 및 전환 -&gt; ctrl + ~</h3>
<p>말 그대로 터미널 열기 및 전환하는 단축키입니다. 근데 전 솔직히 잘 안 쓰고 정말 특수한 경우에만 써요. (ctrl 이 들어간 순간부터 뭔가 손에 안 감깁니다 ㅋㅋ) 그리고 저만 그런지 모르겠는데 가끔 이 단축키가 안 먹어요;; 그래도 쓰는 사람이 있을까봐 적어봤습니다. 이건 따로 영상 안 넣겠습니다.(갑자기 또 단축키가 안 먹어서;;)</p>
<h3 id="오른쪽-사이드바-열기닫기-터미널-열기로도-가능---cmd--j">오른쪽 사이드바 열기/닫기 (터미널 열기로도 가능) -&gt; cmd + j</h3>
<p>저는 터미널 있는 그 사이드바를 오른쪽에 두기 때문에 편의상 &#39;오른쪽 사이드바&#39; 라고 하겠습니다. 저는 사이드바에 보통 터미널만 띄웁니다. 그래서 터미널 여는 단축키로 쓰고 있어요. 일단 <code>cmd + j</code> 를 누르면 사이드바가 열립니다. 열린 상태면 닫히고요. 근데 이 단축키가 닫혀 있는 상태에서 누르면 사이드바가 열리고 터미널로 커서가 이동하는데, 열려 있는 상태면 닫히다 보니;; 열려 있을 땐 두 번 눌러서 터미널로 이동합니다. 이게 <code>ctrl + ~</code> 보다 손에 더 감겨서 저는 그냥 두 번 눌러요. 파일 탐색기처럼 코드로 돌아가고 싶으면 <code>cmd + 숫자</code> 를 누르면 됩니다 ^^
<img src="https://velog.velcdn.com/images/in__32/post/9fb18d68-4a61-44d7-bebe-d3d3dea55433/image.gif" alt=""></p>
<h3 id="왼쪽-사이드바-열기닫기---cmd--b">왼쪽 사이드바 열기/닫기 -&gt; cmd + b</h3>
<p>그냥 말 그대로 왼쪽 사이드바 열기/닫기입니다.
<img src="https://velog.velcdn.com/images/in__32/post/3efa4389-e838-4257-9db6-b57017265a4c/image.gif" alt="">
제발 왼쪽 오른쪽 사이드바 닫을 때 마우스로 닫지 마세요!! <code>cmd + b + j</code> 로 정리합시다.</p>
<h3 id="추가-사이드바-열기닫기---cmd--opt--b">추가 사이드바 열기/닫기 -&gt; cmd + opt + b</h3>
<p>이게 정확한 명칭이 뭔지 몰라서 그냥 추가 사이드바라고 하겠습니다. 이거를 누르면 추가로 사이드바가 열리고 닫히는데 저는 보통 여기에 copilot 을 놓습니다. 굳이 이렇게 하는 이유는 터미널을 연 상태에서 copilot 을 열면 터미널이 닫히고, copilot 을 연 상태에서 터미널을 열면 copilot 이 닫혀서 굳이 이렇게 합니다. 그러면 같이 열 수 있거든요.<img src="https://velog.velcdn.com/images/in__32/post/be40789c-90c5-4a27-8cf6-bdc66d3c1c64/image.gif" alt=""></p>
<hr>
<h1 id="마무리">마무리</h1>
<p>아는 거 다 적다 보니 엄청 긴 글이됐네요... 저한테 떨어지는 게 없긴 하지만, 저는 많은 사람들이 단축키를 쓰고 희열을 느꼈으면 좋겠습니다. 그런 마음에 제가 아는 거 최대한 많이 적어봤습니다. ( <em><strong>저 진짜 열심히 만들었는데 좋아요라도 눌러주시면 정말 감사하겠습니다</strong></em> )</p>
<p>간혹 어떤 단축키가 안 먹히는 경우가 있습니다. 설정이 다르게 되어있거나, 다른 거랑 충돌 나면 안 먹힐 수 있습니다. 그런 경우는 댓글 남겨주시면 도와드리겠습니다.</p>
<p>솔직히 더 적을 수 있는데 너무 기초적인 것 같은 것들, 혹은 잘 제가 안 쓰는 것들은 없을 수도 있습니다. 만약에 여기에 없지만 좋은 단축키가 있다면 공유 부탁드립니다 ㅎㅎ</p>
<p>마지막으로 궁금한 단축키가 있다면 편하게 댓글 달아주세요. 제가 아는 것이면 적고, 저도 모르는 것이면 열심히 json 파일 읽으면서 찾거나 커스텀해서라도 알려드리겠습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Wsl 설치 에러...
Wsl/CallMsi/REGDB_E_CLASSNOTREG]]></title>
            <link>https://velog.io/@in__32/Wsl-%EC%84%A4%EC%B9%98-%EC%97%90%EB%9F%AC...WslCallMsiREGDBECLASSNOTREG</link>
            <guid>https://velog.io/@in__32/Wsl-%EC%84%A4%EC%B9%98-%EC%97%90%EB%9F%AC...WslCallMsiREGDBECLASSNOTREG</guid>
            <pubDate>Mon, 20 Jan 2025 17:38:59 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요 백엔드 개발자 이수인이라고 합니다.
오늘은 wsl을 설치하면서 제가 삽질했던 것들을 적어보려고 합니다.</p>
<p>사실 이전에 wsl을 설치했었는데, 포멧을 했어서 다시 깔려고 터미널에서 <strong>wsl --install</strong> 을 적었습니다. 그런데...
<img src="https://velog.velcdn.com/images/in__32/post/9a0d0ac7-5811-44e3-b8a0-c0e40c7ddafa/image.png" alt="">
이랬다가 20초는 훨씬 뒤에
<img src="https://velog.velcdn.com/images/in__32/post/2903c452-971a-41a8-8dc5-59e997d08c4b/image.png" alt="">
이렇게 되더라고요. 그래서 다시 <strong>wsl --install</strong> 을 적어도 똑같은 결과만 나왔습니다. 도대체 뭐가 문제일까 여러 가지를 고민했습니다.</p>
<hr>
<h3 id="가정1---터미널-문제">가정1 - 터미널 문제</h3>
<p>당연히 아닐 텐데 의심스러워서 해봤습니다.
<img src="https://velog.velcdn.com/images/in__32/post/d7d1a687-1898-49ab-9ed3-5c889c277107/image.png" alt="">
너무 당연히 잘 나와서 터미널 문제는 아니였습니다.</p>
<h3 id="가정2---wsl-명령어-문제">가정2 - wsl 명령어 문제</h3>
<p><strong>wsl --install</strong> 이 아닌 다른 것도 문제이지 않을까? 해서 <strong>wsl -l -v</strong> 를 입력해봤습니다. 그랬더니...
<img src="https://velog.velcdn.com/images/in__32/post/a72c9e70-b222-440f-b750-6f82d3fa6237/image.png" alt="">
이것도 문제가 발생하더라고. 그래서 wsl 명령어에 무슨 문제가 있겠다고 생각했습니다.</p>
<hr>
<h3 id="삽질1---설정-문제">삽질1 - 설정 문제</h3>
<p>wsl을 설치하기 위해서 설정해줘야 할 것들이 있습니다. <strong>win + s</strong> 로 <strong>windows 기능 켜기/끄기</strong> 를 검색하면 <strong>Linux용 Windows 하위 시스템</strong> 과 <strong>가상 머신 플랫폼</strong> 을 활성화 해줘야 됩니다. 혹시 이게 비활성화 돼있는지 확인해봤는데 활성화 돼있더라고요.</p>
<h3 id="삽질2---설정-초기화-후-재부팅">삽질2 - 설정 초기화 후 재부팅</h3>
<p>위에서 말했던 <strong>windows 기능 켜기/끄기</strong> 에서</p>
<ol>
<li>활성화 해야 되는 설정들을 모두 비활성화 한 후에 컴퓨터 재부팅</li>
<li>다시 활성화 한 후에 컴퓨터 재부팅</li>
<li>wsl --install</li>
</ol>
<p>이렇게 했는데 결과는 똑같이 오류...</p>
<h3 id="삽질3---powershell에서-실행-관리자-권한으로-열기">삽질3 - powershell에서 실행, 관리자 권한으로 열기</h3>
<p>cmd가 아닌 powershell로 해야 되는 건가? 해서 했더니 결과는 똑같이 오류...
그럼 관리자 권한으로 열어서 해야 되는 건가? 해서 했더니 결과는 똑같이 오류...</p>
<h3 id="삽질4---수동-설치">삽질4 - 수동 설치</h3>
<p><a href="https://learn.microsoft.com/ko-kr/windows/wsl/install-manual">https://learn.microsoft.com/ko-kr/windows/wsl/install-manual</a></p>
<p>이거는 ms에서 제공하는 wsl 수동 설치 메뉴얼입니다. 과정을 적어보자면</p>
<pre><code>dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart</code></pre><p>이 코드는 위에서 말했던 설정들을 활성화하는 코드입니다. 이후 </p>
<p><a href="https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi">https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi</a></p>
<p>이 링크 들어가면 Linux 커널 업데이트 패키지를 다운받는데, 다운받고 실행 한 후</p>
<pre><code>wsl --set-default-version 2</code></pre><p>이거를 입력해서 wsl2를 기본 버전으로 설정하면 될 줄 알았는데,wsl 명령어가 또 먹히지 않아서 실패...</p>
<h3 id="삽질5---microsoft-store에서-ubuntu-설치">삽질5 - microsoft store에서 ubuntu 설치</h3>
<p>그냥 ubuntu를 설치하면 되지 않을까? 해서 microsoft store에서 ubuntu를 설치해봤습니다.</p>
<p><img src="https://velog.velcdn.com/images/in__32/post/1d89971e-4570-435c-96e0-4e72b5b984e2/image.png" alt="">
이거를 깔고 실행해봤는데...</p>
<pre><code>Installing, this may take a few minutes...
WslRegisterDistribution failed with error: 0x8007019e
Error: 0x8007019e Linux? Windows ?? ???? ????? ???? ?? ????.</code></pre><p>이렇게 뜨더라고요. 그래서 이것도 실패...</p>
<h3 id="삽질6---wsl-관련-파일-삭제">삽질6 - wsl 관련 파일 삭제</h3>
<p>파일 탐색기에서 wsl 관련 파일이 문제인 건가? 해서 관련 파일들을 삭제해봤습니다. 그런데 또 실패...</p>
<hr>
<h2 id="해결">해결!!</h2>
<p>도통 무슨 문제인지 모르겠고 스트레스 받아서 일단 접고 자기 전에 핸드폰으로 구글링 좀 해봤었습니다. </p>
<p><a href="https://github.com/microsoft/WSL/releases">https://github.com/microsoft/WSL/releases</a></p>
<p>이 링크 들어가면 버전 마다 나오는데, 여기서 자기 환경에 맞는 최신 msi 파일을 설치하면 된다고 했습니다.
<img src="https://velog.velcdn.com/images/in__32/post/4218de6a-8929-4164-aa98-ef7ba8fedba3/image.png" alt=""></p>
<p>자고 일어나자마자 가장 최근 버전인 2.4.8 버전에서 <strong>wsl.2.4.8.0.x64.msi</strong> 을 설치후 <strong>wsl --install</strong> 을 해봤는데...
<img src="https://velog.velcdn.com/images/in__32/post/f292b18c-76ec-4ae2-abbb-6c97b6a9fb7b/image.png" alt="">
성공!! 기뻤는데 또 무슨 문제가 있을지도 모르니까 일단 기다려봤습니다.
<img src="https://velog.velcdn.com/images/in__32/post/3af38950-a3fd-4c2c-9c15-059bdac325f5/image.png" alt="">
이렇게 잘 되는 걸 보니까 너무 행복하더라고요.</p>
<h2 id="오늘의-결론">오늘의 결론</h2>
<p>구글링을 잘하자. 삽질할 때 시간을 많이 썼는데 구글링 좀만 잘하니까 너무 잘 나오더라고요... 그래도 해결했으니까 기분은 good!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RDB 설계를 더 잘해보자]]></title>
            <link>https://velog.io/@in__32/RDB-%EC%84%A4%EA%B3%84%EB%A5%BC-%EB%8D%94-%EC%9E%98%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@in__32/RDB-%EC%84%A4%EA%B3%84%EB%A5%BC-%EB%8D%94-%EC%9E%98%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sat, 18 Jan 2025 15:41:29 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>db 설계 기획에서 배운 내용에 대한 정리 글입니다. 아직 초보 개발자라 정답이라기보다, 이런 방식으로 했구나 정도로만 읽어주시면 감사하겠습니다.</p>
</blockquote>
<p>안녕하세요. Inhu 프로젝트의 백엔드 개발자 이수인이라고 합니다. 오늘은 RDB 설계를 하면서 여러 가지 고민했던 부분들을 얘기해보려고 합니다.
얘기하기 전에 이번에 하는 프로젝트에 대한 첫 글이라 가볍게 프로젝트 소개부터 해보겠습니다.</p>
<hr>
<h1 id="서론">서론</h1>
<p><img src="https://velog.velcdn.com/images/in__32/post/5ee876b8-9f87-41d8-b4bf-845bc34374b4/image.png" alt=""></p>
<h2 id="프로젝트-소개">프로젝트 소개</h2>
<p><strong>Inhu</strong>는 인하대학교 후문에 카페, 술집, 식당들을 소개해주는 어플입니다. 각 장소마다 후기를 볼 수 있기도 하고 자기가 발견한 새로운 카페나 술집, 식당이 있다면 제보도 할 수 있습니다.</p>
<h2 id="테이블-소개">테이블 소개</h2>
<p><img src="https://velog.velcdn.com/images/in__32/post/c73d3187-ca42-4969-8fc3-79568e793e2a/image.png" alt=""></p>
<h3 id="주요-테이블">주요 테이블</h3>
<ul>
<li><strong>place</strong>: 장소 정보를 저장하는 테이블</li>
<li><strong>user</strong>: 사용자 정보를 관리하는 테이블</li>
<li><strong>review</strong>: 사용자가 남긴 리뷰를 저장하는 테이블</li>
<li><strong>place_type</strong>: 장소의 유형을 저장하는 테이블 (ex 식당, 카페, 술집 ...)</li>
<li><strong>place_type_mapping</strong>: 장소와 타입 간의 관계를 관리하는 매핑 테이블</li>
<li><strong>bookmark</strong>: 사용자가 설정한 북마크를 저장하는 테이블</li>
</ul>
<p>테이블이 생각보다 많아서 사진에 담기 어렵더라고요. 그래서 주요 테이블 몇 개를 적어보았습니다. 이후 자세히 얘기할 것들은 확대해서 붙일 겁니다. 그럼 저희의 고민의 흔적들을 살펴보시죠.</p>
<hr>
<h1 id="본론">본론</h1>
<h2 id="1-hard-delete-vs-soft-delete-어떤-것이-더-좋을까">1. hard delete vs soft delete 어떤 것이 더 좋을까?</h2>
<p>저는 soft delete는 사용자 정보처럼 혹시 모르는 것들이나, 댓글처럼 나중에 문제됐을 때 확인하기 위한 정보들만 soft delete 방식을 채택하는 줄 알았습니다. 그런데 생각보다 soft delete를 생각해봐야 할 상황들이 많더라고요?</p>
<h3 id="history로써-가치가-있나">history로써 가치가 있나?</h3>
<p>먼저 고민을 했던 부분입니다. 테이블의 정보가 나중에 유의미한 데이터로 남을 수 있는가를 계속 고민했었습니다.
<img src="https://velog.velcdn.com/images/in__32/post/2ceee7b9-ad02-4e18-9de5-119fda1058b9/image.png" alt=""></p>
<p>예를 들어 <strong>bookmark</strong> 테이블이 그렇습니다. 처음에 bookmark를 저장하고 삭제한 데이터는 특별한 가치가 없다고 생각했습니다. 그래서 hard delete를 하려고 했죠. 하지만, <strong>&#39;나중에 사용자가 저장하고 삭제한 bookmark로 선호도의 흐름을 파악하고 싶으면 어떻게 할 것인가?&#39;</strong> 라는 관점으로 바라봤을 때는 달랐습니다. 이때는 bookmark의 데이터가 history로써 가치가 있기 때문에 soft delete 방식을 사용했습니다.</p>
<h3 id="hard-delete를-해서-얻는-이점이-뭔데">hard delete를 해서 얻는 이점이 뭔데?</h3>
<p>사실 bookmark가 history로써 가치가 있기 때문에 soft delete를 사용하기로 결정이 났는데도 마음 한구석에서는 찝찝한 게 있었습니다. &#39;사용자가 실수로 저장하거나 삭제할 수도 있고, 정말 관심없어서 삭제한 건데 굳이 데이터로 남겨야 되나?&#39; 이런 제 의문에 얻은 답변이 &#39;hard delete를 해서 얻는 이점이 무엇인가?&#39; 였습니다. </p>
<p><strong>어차피 timestamp wit time zone 으로 저장하면 8byte이고 1억개 정도 모여야 1GB인데, 굳이 이거를 포기하면서까지 hard delete를 사용하는 이유가 무엇인가?</strong> 나중에 정말 유의미한 데이터로 남을 수 있고 끽해야 8byte인데 왜 hard delete를 쓰는가? 를 고민할 필요가 있었습니다.</p>
<h3 id="그럼-혹시-매핑-테이블도-soft-delete">그럼 혹시 매핑 테이블도 soft delete?</h3>
<p><img src="https://velog.velcdn.com/images/in__32/post/1707e5ff-ed9e-4206-b00a-e6e9502c075d/image.png" alt=""></p>
<p>왼쪽에 보이는 <strong>place_type_mapping</strong> 테이블에서는 soft delete가 아닌 hard delete를 사용한 것을 볼 수 있습니다. 앞에서 얘기했던 두 가지 논리대로면 soft delete를 사용하는 게 맞는데 저희는 hard delete를 사용했습니다. 왜냐하면 이 테이블의 역할은 단순히 매핑만 하는 것입니다. 그 외에 어떤 역할도 없기 때문에 hard delete를 사용했습니다. 특별한 역할이 있었더라면 soft delete를 사용했을 것입니다. 대신에 <strong>place_type_mapping</strong> 테이블이 매핑하는 <strong>place_type</strong> 테이블과 과 <strong>place</strong> 테이블은 soft delete를 사용했습니다.</p>
<hr>
<h2 id="2-varchar-vs-varchar10">2. varchar vs varchar(10)</h2>
<p><img src="https://velog.velcdn.com/images/in__32/post/c6a04b81-6734-4d2b-96af-224ca105801f/image.png" alt="">
<strong>user</strong> 테이블의 <strong>nickname</strong> 컬럼으로 예를 들어보겠습니다. 저희는 닉네임 최대 글자를 10글자로 제한하기로 했습니다. 그런데 <strong>varchar(10)</strong> 으로 안 하고 그냥 <strong>varchar</strong> 로 설계했습니다. 왜일까요?</p>
<h3 id="유연성-good">유연성 good!</h3>
<p>가장 큰 이유는 유연성때문입니다. 나중에 닉네임 최대 글자가 10글자가 아닌 12글자로 바뀌었다고 가정해보겠습니다. 이때 <strong>varchar(10)</strong> 으로 설정했다면 db에서 제약이 걸리게 되고, db 수정을 다 해줘야 됩니다. 벌써 피곤하고 곤란하기도 하고 여러 문제가 생길 수 있겠죠. 하지만 그냥 <strong>varchar</strong> 로 했다면? db에서 제약이 안 걸리기 때문에 쉽게 변경이 가능합니다.</p>
<p>이런 이유때문에 그냥 <strong>varchar</strong> 로 설계했습니다. 대신에 코드적으로 제한을 걸면 유연하게 대처가 가능하겠죠?</p>
<hr>
<h2 id="3-table-네이밍">3. table 네이밍</h2>
<p><img src="https://velog.velcdn.com/images/in__32/post/105294e4-2c9b-4107-81eb-7e3dd41f6c70/image.png" alt=""></p>
<p>개발에서 무언가의 이름을 짓는다는 것은 참 어려운 것 같습니다. 어떤 역할이고 무슨 의미인지는 알지만 박수치는 이름을 짓기란 참 어렵죠. 특히 관련 컨벤션이 있다면 그것도 고려해야 돼서 어려운 것 같습니다. 저희도 이름을 짓는 부분에서 고민을 많이 했습니다. 제일 마음에 안 들었던 부분은 <strong>mapping</strong> 테이블입니다.
<img src="https://velog.velcdn.com/images/in__32/post/1152a733-5d3f-483f-b74d-82944276f6ec/image.png" alt="">
지금 사진에 보이는 테이블에 대한 설명이 없어서 가볍게 설명하겠습니다.
<img src="https://velog.velcdn.com/images/in__32/post/2c2a4130-adb9-4375-a1cb-cf98648dbb8e/image.png" alt="">
위 사진처럼 사용자한테 서비스 만족 조사를 받습니다. &#39;매우 만족했어요&#39;, &#39;만족스러웠어요&#39; 와 같은 서비스 항목들이 <strong>service1</strong> 테이블에 들어갑니다. 다음으로 어떤 사용자가 어떤 서비스 항목을 선택했는가는 <strong>service1_result</strong> 테이블에 들어갑니다. 즉 <strong>service1_result</strong> 는 mapping 테이블입니다. 이전에 erd 사진에 보이는 다른 테이블도 같은 맥락입니다.</p>
<p>저희가 처음에 mapping 테이블의 이름을 정할 때 <strong>table1_table2_mapping</strong> 과 같은 방식으로 정했습니다. 그래서 처음에 정한 이름은 <strong>user_service1_mapping</strong> 이었죠. 그런데 이름만 길고 그렇게 확 와닿지도 않더라고요. </p>
<h3 id="의미적으로-해결">의미적으로 해결</h3>
<p>mapping 이라는 단어가 괜히 헷갈리기만 한 것 같아서 의미적으로 해결할 수 있으면 굳이 mapping 이라는 단어를 안 쓰기로 했습니다. 그래서 나온 결과가 <strong>service1_result</strong> 입니다. <strong>service1</strong> 과 <strong>user</strong> 테이블을 mapping 해주는 것은 맞지만 결국에는 <strong>service1</strong> 에 대한 결과를 담은 테이블인 거잖아요? 그래서 <strong>service1_result</strong> 처럼 의미가 와닿게 작성해봤습니다.</p>
<h3 id="예외">예외</h3>
<p>의미적으로 해결할 수 있으면 그렇게 했겠지만 그게 잘 적용되지 않았던 것도 있었습니다.
<img src="https://velog.velcdn.com/images/in__32/post/1707e5ff-ed9e-4206-b00a-e6e9502c075d/image.png" alt="">
위에서 보여줬던 <strong>place_type_mapping</strong> 은 의미적으로 해결되지 않았기 때문에 mapping 이라는 단어를 썼습니다. 게다가 <strong>table1_table2_mapping</strong> 형식으로 작성해야 되는데 <strong>place_type</strong> 과 <strong>place</strong> 는 place 라는 단어가 겹칩니다. 그래서 mapping 하는 주체는 <strong>place_type</strong> 테이블이기 때문에 <strong>place_type_mapping</strong> 이라고 적어봤습니다.</p>
<h1 id="결론">결론</h1>
<p>RDB 설계를 하면서 많은 부분을 고민해봤고 그 과정에서 많은 것들을 배우면서 완성했습니다. RDB 설계에서 가장 중요한 것은 확장성과 유연성을 고려하며 설계하는 것이라고 생각합니다. 이전 프로젝트 때 DB 설계 잘못 했다가 프로젝트 끝날 때까지 발목을 잡혔던 경험이 있어서 이번 설계에 집중할 수 있었고 많이 배울 수 있었던 것 같습니다. 긴 글 읽어주셔서 감사합니다!
<img src="https://velog.velcdn.com/images/in__32/post/72948419-2f42-4519-810d-5aded5de1de7/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>