<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>준영의 지식 블로그</title>
    <link>https://til-dev.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 23 May 2026 16:23:27 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>준영(Junzero)</managingEditor>
    <item>
      <title>i18n, google-sheets 로 모두를 위한 다국어 지원하기</title>
      <link>https://til-dev.tistory.com/13</link>
      <description>&lt;h2&gt;들어가며&lt;/h2&gt;
&lt;p&gt;위키백과에 따르면, 전세계에는 약 7,139 개의 언어가 있다고 합니다.&lt;/p&gt;
&lt;p&gt;다양한 언어를 사용하는 유저들을 타겟으로 하는 서비스라면, 국제화는 빼놓을 수 없는 요소입니다.&lt;/p&gt;
&lt;p&gt;그리고 그런 서비스가 성장할수록, 서비스 내에서 제공해야 하는 다국어 텍스트도 많아질 수밖에 없는데요.&lt;/p&gt;
&lt;p&gt;점점 많아지는 다국어 텍스트가 사용자 경험과 개발자 경험에 어떤 영향을 미칠 수 있는지를 알아보고,&lt;/p&gt;
&lt;p&gt;다국어 텍스트를 효율적으로 관리할 수 있는 방법을 소개합니다.&lt;/p&gt;
&lt;br /&gt;

&lt;h2&gt;TL:DR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;자주 추가, 수정될 여지가 있는 다국어 텍스트는 소스 코드와 별개의 장소에서 관리하자. (for 개발자 경험)&lt;/li&gt;
&lt;li&gt;그러나 웹앱이 실행될 때에는 실행 환경과 가까운 곳에 위치해야 한다. (for 사용자 경험)&lt;/li&gt;
&lt;li&gt;따라서 웹앱의 빌드 타임에 다국어 텍스트를 불러와 소스 코드에 위치하게 하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;

&lt;h2&gt;대상 독자&lt;/h2&gt;
&lt;p&gt;이 글은 다음과 같은 사전지식이 있는 독자를 대상으로 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JavaScript 문법에 대한 기본적인 이해&lt;/li&gt;
&lt;li&gt;Git 에 대한 기본적인 이해&lt;/li&gt;
&lt;li&gt;액셀의 행과 열, 셀에 대한 용어 이해&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;

&lt;h2&gt;소스 코드에서 다국어 텍스트를 관리하는 것의 불편함&lt;/h2&gt;
&lt;p&gt;웹앱에 다국어 텍스트를 적용하려면 우선 다국어 텍스트를 어디에 저장할지를 정해야 합니다.&lt;/p&gt;
&lt;p&gt;가장 먼저 떠올릴 수 있는 건 소스 코드에 다국어 텍스트를 저장하는 것인데요. &lt;/p&gt;
&lt;p&gt;예를 들면 다음과 같이 언어별 파일을 두고 텍스트를 관리할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// ko-KR.json

{
  &amp;quot;hello&amp;quot;: &amp;quot;안녕하세요&amp;quot;,
     &amp;quot;contact&amp;quot;: &amp;quot;연락처&amp;quot;,
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// en-US.json

{
  &amp;quot;hello&amp;quot;: &amp;quot;Hello&amp;quot;,
     &amp;quot;contact&amp;quot;: &amp;quot;contact&amp;quot;,
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// ja-JP.json

{
  &amp;quot;hello&amp;quot;: &amp;quot;こんにちは&amp;quot;,
     &amp;quot;contact&amp;quot;: &amp;quot;連絡先情報&amp;quot;,
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 소스 코드에 저장된 다국어 텍스트는 추가, 수정이 필요할 때마다 다음과 같은 과정을 거치게 됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;기획 단계에서의 텍스트 변경 요청&lt;/li&gt;
&lt;li&gt;소스 코드의 다국어 텍스트 파일 변경&lt;/li&gt;
&lt;li&gt;변경사항 커밋&lt;/li&gt;
&lt;li&gt;변경사항 리뷰 및 메인 브랜치 병합&lt;/li&gt;
&lt;li&gt;배포&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;간단한 텍스트 하나를 수정하여 반영하는 경우에도 위와 같은 과정을 반복적으로 거쳐야 한다는 불편함이 있습니다.&lt;/p&gt;
&lt;p&gt;만약 다국어 텍스트를 소스 코드 외부에 두고 관리하면 어떨까요? &lt;/p&gt;
&lt;p&gt;개발자는 다국어 텍스트 파일을 직접 수정하지 않고도 웹앱에 변경 사항을 반영할 수 있습니다.&lt;/p&gt;
&lt;p&gt;그렇다면 어떻게 다국어 텍스트를 소스 코드 외부에 두고 관리하면서도, 웹앱에 이를 적용할 수 있을지 알아보겠습니다.&lt;/p&gt;
&lt;br /&gt;

&lt;h2&gt;다국어 텍스트 관리 툴 선택&lt;/h2&gt;
&lt;p&gt;다국어 텍스트를 관리할 수 있는 툴의 선택지는 매우 다양합니다.&lt;/p&gt;
&lt;p&gt;선택지가 많을수록 요구사항을 분명히 해야 시간 낭비를 줄일 수 있습니다.&lt;/p&gt;
&lt;p&gt;제가 다국어 텍스트 관리 툴에 원하는 바는 다음과 같았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;텍스트 수정 과정이 간편한가?&lt;/li&gt;
&lt;li&gt;변경내역을 조회할 수 있는가? (텍스트 수정시 발생할 수 있는 실수를 돌리기 위해)&lt;/li&gt;
&lt;li&gt;외부에서 텍스트를 조회할 수 있는 API 를 제공하는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다국어 텍스트 관리에만 집중한 솔루션은 많았지만, (&lt;a href=&quot;https://tolgee.io/&quot;&gt;Tolgee&lt;/a&gt;, &lt;a href=&quot;https://simplelocalize.io/&quot;&gt;SimpleLocalize&lt;/a&gt; 등)&lt;/p&gt;
&lt;p&gt;해당 솔루션들은 무료가 아님에도 사용이 불편한 지점들이 하나씩 있었습니다.&lt;/p&gt;
&lt;p&gt;결국 저의 요구사항을 모두 만족하는 도구는 Google Sheets 가 유일했습니다.&lt;/p&gt;
&lt;p&gt;Google Sheets 는 액셀 시트를 클라우드 환경에서 사용할 수 있는 Google 의 제품으로,&lt;/p&gt;
&lt;p&gt;기존에 액셀을 사용해본 사람이라면 누구나 손쉽게 수정, 관리할 수 있다는 점이 특장점으로 다가왔습니다.&lt;/p&gt;
&lt;br /&gt;

&lt;h2&gt;Google Sheets 에 다국어 텍스트 작성하기&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.google.com/spreadsheets/u/0/&quot;&gt;Google Sheets&lt;/a&gt; 는 Google 계정만 있으면 손쉽게 시작할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Google Sheets 에서 새 스프레드 시트를 만든 뒤, 다국어 텍스트를 다음과 같이 작성해볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLwqNj/dJMcag5x38Y/nVvGnR1T0vwjTezKFaumKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLwqNj/dJMcag5x38Y/nVvGnR1T0vwjTezKFaumKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLwqNj/dJMcag5x38Y/nVvGnR1T0vwjTezKFaumKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLwqNj%2FdJMcag5x38Y%2FnVvGnR1T0vwjTezKFaumKk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;1열은 각 다국어 텍스트를 구분할 키를 작성해두고, 다른 열에는 언어별 텍스트를 작성합니다.&lt;/p&gt;
&lt;br /&gt;

&lt;h2&gt;다국어 텍스트 불러오기 &amp;amp; 저장하기 구현&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;google-spreadsheet&lt;/code&gt; 라이브러리를 사용하면 Google Sheets 의 각 시트 정보를 불러오고, 수정하는 것도 가능합니다.&lt;/p&gt;
&lt;p&gt;이를 위해서 아래와 같은 라이브러리들을 설치해줍니다. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const dotenv = require(&amp;#39;dotenv&amp;#39;);
const fs = require(&amp;#39;fs&amp;#39;);
const { JWT } = require(&amp;#39;google-auth-library&amp;#39;);
const { GoogleSpreadsheet } = require(&amp;#39;google-spreadsheet&amp;#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;

&lt;p&gt;Google Sheets 에 대한 EMAIL_ID 와 PRIVATE_KEY  등은 &lt;a href=&quot;https://theoephraim.github.io/node-google-spreadsheet/#/guides/authentication&quot;&gt;google-spreadsheet 공식문서의 인증 파트&lt;/a&gt; 를 참고하면 얻을 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;  const authorize = new JWT({
    email: process.env.GOOGLE_SHEET_API_EMAIL_ID,
    key: process.env.GOOGLE_SHEET_API_PRIAVTE_KEY,
    scopes: [
      &amp;#39;https://www.googleapis.com/auth/spreadsheets&amp;#39;
    ]
  });&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;



&lt;p&gt;Google Sheets 에 대한 액세스 권한 등을 얻었다면, 다국어 텍스트가 저장된 시트 정보를 불러오는 스크립트를 작성합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const GOOGLE_SPREAD_SHEET_ID = &amp;#39;your_google_sheets_id&amp;#39;;
const TRANSLATION_NAMESPACE_SHEET_GID = &amp;#39;yout_spread_sheet_tab_gid&amp;#39;

const doc = new GoogleSpreadsheet(GOOGLE_SPREAD_SHEET_ID, authorize);
await doc.loadInfo();
const sheet = doc.sheetsById[`${TRANSLATION_NAMESPACE_SHEET_GID}`];
// 불러올 셀의 범위는 각 행의 라벨을 제외하고, 다국어 텍스트 키와 값만 해당되는 범위로 설정합니다.
const cells = await sheet.getCellsInRange(&amp;#39;A2:D&amp;#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 &lt;code&gt;cells&lt;/code&gt; 에는 이제 Google Sheets 에서 불러온 다국어 텍스트가 배열의 형태로 저장됩니다.&lt;/p&gt;
&lt;p&gt;다국어 텍스트를 웹앱에서 불러오는 것 까지는 성공했는데, 이쯤에서 고민해봐야 할 것이 있습니다.&lt;/p&gt;
&lt;p&gt;바로 다국어 텍스트를 불러오는 시점, 즉 이 파일을 언제 실행하느냐에 대한 문제입니다.&lt;/p&gt;
&lt;p&gt;이 문제는 제가 이 글을 쓰게 된 가장 큰 이유이기도 합니다.&lt;/p&gt;
&lt;br /&gt;

&lt;h2&gt;다국어 텍스트를 불러오는 시점&lt;/h2&gt;
&lt;p&gt;웹앱의 라이프사이클을 아주 간단히 요약하면 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;웹앱 빌드&lt;/li&gt;
&lt;li&gt;빌드된 코드가 유저의 요청에 의해 브라우저에서 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;br /&gt;

&lt;p&gt;결론부터 이야기하면, 다국어 텍스트를 불러오는 시점은 웹앱 빌드 시점이어야 합니다. &lt;/p&gt;
&lt;p&gt;아니면 적어도, 유저의 요청에 의해 브라우저에서 웹앱 코드가 실행되기 전에는 소스 코드에 이미 다국어 텍스트가 포함되어 있어야 합니다.&lt;/p&gt;
&lt;p&gt;왜냐하면 다국어 텍스트를 Google Sheets 같은 외부 공간에서 불러오는 데에는 일정 시간이 소요되기 때문인데요.&lt;/p&gt;
&lt;p&gt;약 4000줄의 다국어 텍스트를 가지고 여러번 테스트해본 결과, 평균 2초에서 많게는 10초까지 걸렸습니다.&lt;/p&gt;
&lt;p&gt;만약 다국어 텍스트를 웹앱 빌드 시점이 아닌 브라우저에서 코드가 실행되는 런타임에 불러오려고 한다면,&lt;/p&gt;
&lt;p&gt;유저는 제대로 된 텍스트를 보기 위해 몇 초를 기다려야 한다는 의미이며, 이는 사용자 경험에 심각한 악영향을 끼칠 수 있습니다.&lt;/p&gt;
&lt;br /&gt;

&lt;p&gt;반면 웹앱이 빌드되는 시점에 다국어 텍스트를 불러와 저장하면, 앞에서 얘기한 2초 정도의 시간이 브라우저 런타임이 아닌 빌드타임으로 옮겨오게 됩니다.&lt;/p&gt;
&lt;p&gt;사용자 입장에서의 2초는 참아주기 어려운 딜레이지만, 웹앱 빌드 관점에서의 2초는 그 무게가 비교적 가벼운 편입니다.&lt;/p&gt;
&lt;p&gt;따라서, 다국어 텍스트를 Google Sheets 등의 외부로부터 불러오는 시점은 웹앱이 브라우저 등의 런타임에서 실행되기 이전이어야 합니다.&lt;/p&gt;
&lt;br /&gt;

&lt;p&gt;이렇게 빌드타임에 불러온 다국어 텍스트는 소스 코드에 포함되도록 하여, 런타임에는 딜레이 없이 사용자에게 다국어 텍스트가 보여지도록 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const koMap = {};
const enMap = {};
const jaMap = {};

cells.forEach(([key, ko, en, ja]) =&amp;gt; {
  koMap[key] = ko;
  enMap[key] = en;
  jaMap[key] = ja;
});

fs.writeFileSync(`yourFolderName/yourFileName.ts`, `
  export const koI18n = ${JSON.stringify(koMap)};
  export const enI18n = ${JSON.stringify(enMap)};
  export const jaI18n = ${JSON.stringify(jaMap)};
`);&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;

&lt;p&gt;package.json 의 scripts 에 &lt;code&gt;pre&lt;/code&gt; 키워드를 사용하여 해당 스크립트 파일이 빌드 타임 직전에 실행되도록 하면, 런타임에는 다국어 텍스트 파일이 존재하리라는 것을 확신할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// package.json
{
  ...,
  &amp;quot;scripts&amp;quot;: {
    &amp;quot;build&amp;quot;: &amp;quot;turbo build&amp;quot;,
    &amp;quot;dev&amp;quot;: &amp;quot;turbo dev&amp;quot;,
    &amp;quot;lint&amp;quot;: &amp;quot;turbo lint&amp;quot;,
    &amp;quot;format&amp;quot;: &amp;quot;prettier --write \&amp;quot;**/*.{ts,tsx,md}\&amp;quot;&amp;quot;,
        &amp;quot;predev&amp;quot;: &amp;quot;turbo translate&amp;quot;,
      &amp;quot;prebuild&amp;quot;: &amp;quot;turbo translate&amp;quot;,
      &amp;quot;translate&amp;quot;: &amp;quot;node 다국어텍스트불러오는스크립트파일.js&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;

&lt;h2&gt;다국어 텍스트 파일 서비스에 적용하기&lt;/h2&gt;
&lt;p&gt;생성된 다국어 텍스트 파일의 내용을 보면 아래와 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// youtFolderName/yourFileName.ts

export const koI18n = {
 &amp;quot;hello&amp;quot;: &amp;quot;안녕하세요&amp;quot;,
 &amp;quot;contact&amp;quot;: &amp;quot;연락처&amp;quot;
};

export const enI18n = {
 &amp;quot;hello&amp;quot;: &amp;quot;Hello&amp;quot;,
 &amp;quot;contact&amp;quot;: &amp;quot;contact&amp;quot;
};

export const jaI18n = {
    &amp;quot;hello&amp;quot;: &amp;quot;こんにちは&amp;quot;,
     &amp;quot;contact&amp;quot;: &amp;quot;連絡先情報&amp;quot;,
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 생성된 다국어 텍스트 파일은 &lt;a href=&quot;https://www.i18next.com/&quot;&gt;i18next&lt;/a&gt; 와 같은 국제화 라이브러리에 주입하여 실제 서비스 화면에 렌더링되게 됩니다.&lt;/p&gt;
&lt;p&gt;i18next 에 저희가 저장한 다국어 텍스트 파일을 주입해 보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import i18next from &amp;#39;i18next&amp;#39;;
import {koI18n, enI18n, jaI18n} from &amp;#39;.youtFolderName/yourFileName.ts&amp;#39;

i18next.init({
  lng: &amp;#39;ko&amp;#39;,
  debug: true,
  resources: {
    ko: {
      translation: koI18n
    },
    en: {
      translation: enI18n
    },
    ja: {
      translation: jaI18n
    }
  }
}, function(err, t) {
  // i18next 초기화 완료.
  // i18next.t(&amp;#39;키 이름&amp;#39;) 으로 다국어 텍스트를 렌더링할 수 있습니다.
  document.getElementById(&amp;#39;output&amp;#39;).innerHTML = i18next.t(&amp;#39;hello&amp;#39;); // 안녕하세요 or Hello or こんにちは
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;i18next 에서는 언어 초기화 이외에도 언어 변경 등 다양한 기능을 제공하지만, 이 글에서는 해당 라이브러리의 사용 방법 등에 대해 자세히 다루지는 않겠습니다. i18next 에 대해 더 알고 싶은 분은 &lt;a href=&quot;https://www.i18next.com/overview/api&quot;&gt;i18next API 문서&lt;/a&gt;에서 확인하실 수 있습니다.&lt;/p&gt;
&lt;br /&gt;

&lt;h2&gt;요약&lt;/h2&gt;
&lt;p&gt;지금까지의 내용을 요약하면,&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;다국어 텍스트는 웹앱에서 사용하는 소스 코드 외부에 두고 관리합니다.&lt;/li&gt;
&lt;li&gt;적어도 웹앱 런타임 이전에는 외부에 존재하는 다국어 텍스트가 소스 코드에 존재할 수 있도록 합니다.&lt;/li&gt;
&lt;li&gt;다국어 텍스트를 소스 코드 외부에 두고 관리하는 이유는 개발자 경험을 위해서입니다.&lt;/li&gt;
&lt;li&gt;소스 코드 외부에서 관리하는 다국어 텍스트를  다시 빌드 타임에 소스 코드로 불러오는 이유는, 유저 경험을 위해서입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;br /&gt;

&lt;h2&gt;전체 스크립트&lt;/h2&gt;
&lt;p&gt;글에서 예시로 든 다국어 텍스트 불러오기 &amp;amp; 저장 과정의 전체 스크립트 입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const dotenv = require(&amp;#39;dotenv&amp;#39;);
const fs = require(&amp;#39;fs&amp;#39;);
const { JWT } = require(&amp;#39;google-auth-library&amp;#39;);
const { GoogleSpreadsheet } = require(&amp;#39;google-spreadsheet&amp;#39;);

const GOOGLE_SPREAD_SHEET_ID = &amp;#39;your_google_sheets_id&amp;#39;;
const TRANSLATION_NAMESPACE_SHEET_GID = &amp;#39;yout_spread_sheet_tab_gid&amp;#39;

const fetchTranslationSheet = async () =&amp;gt; {
  const authorize = new JWT({
    email: process.env.YOUR_GOOGLE_SHEETS_API_EMAIL,
    key: process.env.YOUR_GOOGLE_SHEETS_API_PRIAVTE_KEY,
    scopes: [
      &amp;#39;https://www.googleapis.com/auth/spreadsheets&amp;#39;
    ]
  });

  const doc = new GoogleSpreadsheet(GOOGLE_SPREAD_SHEET_ID, authorize);
  await doc.loadInfo();
  const sheet = doc.sheetsById[`${TRANSLATION_NAMESPACE_SHEET_GID}`];
  // 불러올 셀의 범위는 각 행의 라벨을 제외하고, 다국어 텍스트 키와 값만 해당되는 범위로 설정합니다.
  const cells = await sheet.getCellsInRange(&amp;#39;A2:D&amp;#39;);

  const koMap = {};
  const enMap = {};
  const jaMap = {};

  cells.forEach(([key, ko, en, ja]) =&amp;gt; {
    koMap[key] = ko;
    enMap[key] = en;
    jaMap[key] = ja;
  });

  fs.writeFileSync(`yourFolderName/yourFileName.ts`, `
    export const koI18n = ${JSON.stringify(koMap)};
    export const enI18n = ${JSON.stringify(enMap)};
    export const jaI18n = ${JSON.stringify(jaMap)};
  `);
}

(async() =&amp;gt; {
  dotenv.config();
  await fetchTranslationSheet();
})();&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;

&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;다국어 텍스트를 서비스에 적용하면서 겪었던 비효율을 직접 해결해 본 경험이라서, 시간가는 줄 모르고 즐겁게 작업했습니다.&lt;/p&gt;
&lt;p&gt;특히 서비스가 커져감에 따라 Git 에 포함되는 다국어 텍스트의 라인 수가 4,000 줄이 넘어가고 있던 상황이었는데, &lt;/p&gt;
&lt;p&gt;이 작업이 머지되면서 해당 텍스트 파일을 모두 지우는 PR을 올릴 때 조금의 희열(?)을 느꼈습니다.&lt;/p&gt;
&lt;p&gt;물론 글 작성 시점에서는 아직 해당 코드를 작성한 지 얼마 되지 않았기 때문에, 시간이 흘러서 위 방법의 허점을 발견한다거나, 좀 더 효율적인 방법을 발견할 지도 모르겠습니다. 그때는 또 최선의 방법을 찾아야겠죠?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxK8fm/dJMcaajXnIi/Tuers2rjokhGRYWbdQbEtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxK8fm/dJMcaajXnIi/Tuers2rjokhGRYWbdQbEtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxK8fm/dJMcaajXnIi/Tuers2rjokhGRYWbdQbEtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxK8fm%2FdJMcaajXnIi%2FTuers2rjokhGRYWbdQbEtk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br /&gt;

&lt;h2&gt;레퍼런스&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://developers.google.com/sheets/api/guides/concepts?hl=ko&quot;&gt;https://developers.google.com/sheets/api/guides/concepts?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://simplelocalize.io/&quot;&gt;https://simplelocalize.io/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tolgee.io/&quot;&gt;https://tolgee.io/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.i18next.com/&quot;&gt;https://www.i18next.com/&lt;/a&gt;&lt;/p&gt;
&lt;br /&gt;

&lt;p&gt;이 글은 2024년에 작성한 글을 옮겨온 것입니다.&lt;/p&gt;</description>
      <category>google-sheets</category>
      <category>i18n</category>
      <category>react</category>
      <category>다국어</category>
      <category>프론트엔드</category>
      <author>준영(Junzero)</author>
      <guid isPermaLink="true">https://til-dev.tistory.com/13</guid>
      <comments>https://til-dev.tistory.com/13#entry13comment</comments>
      <pubDate>Mon, 16 Feb 2026 15:32:52 +0900</pubDate>
    </item>
    <item>
      <title>react-image-crop 으로 이미지 크롭하기</title>
      <link>https://til-dev.tistory.com/12</link>
      <description>&lt;h2&gt;들어가며&lt;/h2&gt;
&lt;p&gt;현재 개발중인 서비스에서, 이미지와 관련된 프론트 개발을 여럿 진행했습니다.&lt;/p&gt;
&lt;p&gt;그중 개발 과정에서 고생도 많이 하고, 배운 점도 많았던 기능이 바로 이미지 크롭 기능이었는데요.&lt;/p&gt;
&lt;p&gt;이번 포스트에서는 이미지 크롭 기능을 서비스에 도입하면서 배운 점들과 고민했던 것들, 고려해야 했던 것들, 마지막으로 개선할 점들을 적어보고자 합니다.&lt;/p&gt;
&lt;h2&gt;TL:DR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;라이브러리는 요구사항을 충족하면서 가능한 용량이 작은 것을 선택하자.&lt;/li&gt;
&lt;li&gt;이전 페이지와 독립적이라면 페이지, 아니라면 모달로 구현하자. 이미지 크롭의 경우에는 모달이 적절하다.&lt;/li&gt;
&lt;li&gt;이미지와 캔버스는 픽셀 단위의 래스터 이미지로, width 나 height 값으로 소수점을 갖지 않음에 주의하자.&lt;/li&gt;
&lt;li&gt;이미지 크롭의 역할은 로컬 이미지 선택과 이미지 적용의 중간에 있다. 사이드 이펙트 뿐만 아니라 이미지 크롭이 제거될 상황까지도 고려하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;대상 독자&lt;/h2&gt;
&lt;p&gt;이 글은 다음과 같은 사전지식이 있는 독자를 대상으로 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTML, CSS, JavaScript 에 대한 기본적인 이해&lt;/li&gt;
&lt;li&gt;SPA(Single Page Application) 에 대한 이해&lt;/li&gt;
&lt;li&gt;래스터이미지에 대한 이해&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 글에서는 다음과 같은 내용을 다룹니다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이미지 크롭에 대한 개념&lt;/li&gt;
&lt;li&gt;이미지 크롭을 구현할 때 고려해야 할 것들&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 글에서 다음과 같은 내용은 다루지 않습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이미지 크롭 기능을 구현하는 방법&lt;/li&gt;
&lt;li&gt;이미지 크롭 라이브러리 사용방법&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;고려할 것 0. 이미지 크롭, 무엇이며 왜 필요할까?&lt;/h2&gt;
&lt;p&gt;페이지에 40x40 사이즈의 유저 프로필 이미지를 표시하는 시나리오를 상상해 봅시다.&lt;/p&gt;
&lt;p&gt;개발자 입장에서는 유저가 어떤 크기와 비율의 이미지를 설정할 지 미리 알 수 없습니다.&lt;/p&gt;
&lt;p&gt;항상 유저가 주어진 비율의 이미지만 사용해준다면 좋겠지만, 보통 유저는 비율까지 신경써가면서 이미지를 보관해두지는 않습니다.&lt;/p&gt;
&lt;p&gt;40x40 사이즈와 다른 크기, 혹은 비율의 이미지를 가진 유저가 프로필 이미지를 설정하기 위해서는, 이미지가 해당 영역에 맞게 줄어드는 과정에서 일부 잘리는 것을 감수하거나, 포토샵 등에서 이미지를 자른 후 다시 돌아와야 한다는 불편함이 있습니다.&lt;/p&gt;
&lt;p&gt;해당 과정에서 귀찮음을 느낀 유저가 다시 애플리케이션으로 돌아와줄지는 알 수 없는 일입니다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에, 많은 서비스들은 유저 편의성을 높이기 위해 이미지 업로드 이전에 이미지를 자르는 기능을 제공하곤 합니다.&lt;/p&gt;
&lt;p&gt;이 때, 이미지를 자르는 기능을 크롭(crop) 이라고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccbIaU/dJMcaajXnpn/ZJK24HXcEj6ZMEehtawBl0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccbIaU/dJMcaajXnpn/ZJK24HXcEj6ZMEehtawBl0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccbIaU/dJMcaajXnpn/ZJK24HXcEj6ZMEehtawBl0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ccbIaU/dJMcaajXnpn/ZJK24HXcEj6ZMEehtawBl0/img.gif&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;프로필에 들어갈 이미지를 선택하고, 크롭하고, 업로드하는 기능이 들어간 웹 애플리케이션&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;고려할 것 1. 요구사항 정리&lt;/h2&gt;
&lt;p&gt;프로필 이미지에 크롭 기능을 넣는 것을 가정했을 때, 구현 이전에 그려본 이미지 크롭의 대략적인 사용 시나리오는 아래와 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;프로필 이미지 업로더를 클릭합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;파인더 (윈도우 사용자의 경우 파일 탐색기)가 열리고, 사용자가 이미지를 선택합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;이미지 크롭 창이 열리고, 사용자가 선택한 이미지가 크롭 가이드 영역과 함께 창 내에 그려집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;사용자가 크롭 가이드 영역을 늘리거나 줄이고, 이동함으로써 원하는 이미지 영역을 선택한 뒤, 완료 버튼을 누릅니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;크롭 가이드에 의해 선택된 영역이 프로필 이미지로 설정됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이전의 애플리케이션에서 이미 제공하고 있던 기능은 위 프로세스에서 1-&amp;gt; 2-&amp;gt; 5 에 해당했습니다. &lt;/p&gt;
&lt;p&gt;사용자가 로컬에서 이미지를 선택하면, 해당 이미지가 바로 사용자의 프로필 이미지로 설정되었던 것이죠. &lt;/p&gt;
&lt;p&gt;이제 기존의 프로필 이미지 설정 프로세스에 3, 4 번을 끼워넣는 것이 제가 풀어야 할 문제였습니다.&lt;/p&gt;
&lt;h2&gt;고려할 것 2. 라이브러리 선택&lt;/h2&gt;
&lt;p&gt;이미지 크롭 기능 개발을 손쉽게 할 수 있도록 도와주는 많은 라이브러리가 있는데, 저는 react-easy-crop 과 react-image-crop 중에서 고민을 했습니다. 두 라이브러리의 차이를 간단히 정리해 보면 다음 표와 같습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;react-easy-crop&lt;/th&gt;
&lt;th&gt;react-image-crop&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;용량&lt;/td&gt;
&lt;td&gt;494kb&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;110kb&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용경험&lt;/td&gt;
&lt;td&gt;무&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;유&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추가 기능&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;이미지 줌 인/아웃, 비디오 크롭, etc&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Github Star&lt;/td&gt;
&lt;td&gt;2.1k&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.6k&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;last updated at&lt;/td&gt;
&lt;td&gt;1 month&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;상대적으로 더 우위에 있다고 생각한 항목을 볼드처리했습니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;용량 : 라이브러리 용량이 작아야 전체 애플리케이션의 용량도 가벼워집니다. react-image-crop 의 압승입니다.&lt;/li&gt;
&lt;li&gt;사용경험 : 해당 라이브러리를 다시 선택할 만큼, 이전에 사용해본 경험이 나쁘지 않았습니다.&lt;/li&gt;
&lt;li&gt;추가 기능 : 라이브러리에서 추가적인 기능을 더 제공하는 건 분명 이점입니다. 나중에 요구사항이 추가되어 해당 추가기능이 필요할 수도 있기 때문입니다. 하지만 지금 당장의 요구사항은 단순히 인풋 이미지를 잘라서 업로드 할 수만 있으면 되기 때문에, 이미지 줌이나 비디오 크롭 등의 언제 추가될지도 모르는 요구사항을 위해 더 크고 복잡한 번들을 선택하고 싶지는 않았습니다.&lt;/li&gt;
&lt;li&gt;Github Star : &lt;code&gt;star 가 많을수록 좋은 라이브러리이다&lt;/code&gt; 라는 인과가 항상 성립하는 것은 아니지만, 해당 라이브러리의 신뢰도를 나타내는 중요한 지표임에는 틀림없습니다.&lt;/li&gt;
&lt;li&gt;last updated at : 해당 라이브러리가 아직 maintainer 에 의해 유지보수되고 있는지가 중요했습니다. 왜냐하면 라이브러리를 사용하다가 문제가 생기거나, 기여할 만한 요소를 찾았을 때 maintainer 가 해당 라이브러리에 관심을 끊은 상태라면, 혼자서 문제를 해결해나가야 할 수도 있기 때문입니다. 다행히도 두 라이브러리 모두 maintainer 가 최근 라이브러리를 업데이트했다는 사실을 확인했고, issue 탭과 댓글창에서도 활발하게 커뮤니케이션이 오가고 있는 것을 확인했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위의 요소들을 고려한 끝에, &lt;strong&gt;제공하는 기능이 요구사항을 충족하기에 부족함이 없고, 전체 애플리케이션에 부하를 최대한 덜 줄 수 있는 라이브러리인 react-image-crop을 선택&lt;/strong&gt;하게 되었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;특정 라이브러리를 선택했지만, 이 글에서는 최대한 라이브러리에 편중된 이야기는 빼고 이미지 크롭의 본질에 대한 이야기를 다룹니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;고려할 것 3. 이미지 크롭 화면은 페이지일까? 모달일까?&lt;/h2&gt;
&lt;p&gt;SPA 의 구조를 이용하면, 페이지가 바뀌어도 상태를 유지할 수 있습니다. 따라서 페이지든 모달이든 이미지 크롭 화면을 구현하는 데에 큰 문제는 없습니다. &lt;/p&gt;
&lt;p&gt;그러나 만약 개발자에게 선택권이 있는 상황이라면, 저는 다른 화면과 독립적으로 존재할 수 있는 경우에는 페이지, 그렇지 않은 화면이라면 모달을 선택합니다. 이러한 방법은 이미지 크롭 화면 외에도 미리보기 화면 등을 구현할 때에도 잘 들어맞고 있습니다. (회원가입 절차가 여러 페이지에 걸쳐 이뤄지는 절차 화면 같은 경우에는 페이지가 나을 수도 있습니다)&lt;/p&gt;
&lt;p&gt;크롭할 이미지가 뜨는 화면은 이전 화면에서 사용자가 프로필 이미지 설정 버튼을 누르고, 이미지 파인더의 이미지를 선택하지 않았다면 볼 수 없는 화면입니다. 이미지 크롭 화면은 페이지 내의 이미지 업로드 버튼에 의존하고 있는 것이죠. &lt;/p&gt;
&lt;p&gt;따라서 저는 이미지 크롭 화면을 모달로 구현했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u45lz/dJMcachLRNJ/MfA5ugFvFZvPAjnsa53ur1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u45lz/dJMcachLRNJ/MfA5ugFvFZvPAjnsa53ur1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u45lz/dJMcachLRNJ/MfA5ugFvFZvPAjnsa53ur1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu45lz%2FdJMcachLRNJ%2FMfA5ugFvFZvPAjnsa53ur1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;왼쪽이 이미지 크롭, 오른쪽이 메인 페이지 미리보기입니다. 두 화면 모두 페이지처럼 보이지만, 화면을 다 덮는 형태의 모달입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;고려할 것 4. 크롭된 이미지는 canvas 위에 그려진다&lt;/h2&gt;
&lt;p&gt;크롭이 작동하는 방식을 요약하면, 크롭 가이드 영역의 크기와 위치 값을 토대로, canvas 의 2D context 객체가 가지고 있는 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage&quot;&gt;drawImage()&lt;/a&gt;  메서드를 통해 원본 이미지를 canvas 위에 그리는 것입니다. 따라서 이미지 크롭 기능을 개발할 때에는 canvas 를 빼놓고는 이야기할 수 없습니다. 이번 장에서는 canvas 를 이용하는 과정에서 고려해야 할 점들을 다룹니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wt36M/dJMcaiPLrL0/IgCFiLOYEo3KDnxIVqErgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wt36M/dJMcaiPLrL0/IgCFiLOYEo3KDnxIVqErgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wt36M/dJMcaiPLrL0/IgCFiLOYEo3KDnxIVqErgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwt36M%2FdJMcaiPLrL0%2FIgCFiLOYEo3KDnxIVqErgk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;캔버스의 사이즈는 점선으로 표현된 크롭 가이드 영역의 크기와 원본 이미지가 축소, 혹은 확대된 정도를 곱하여 얻을 수 있습니다.&lt;/p&gt;
&lt;p&gt;이렇게 마련된 캔버스 위에 2DContext.drawImage() 을 실행하여 이미지의 일부 영역만을 그립니다. &lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;4-1. 이미지의 원본 크기와 화면에 그려진 크기가 다를 수 있다&lt;/h3&gt;
&lt;p&gt;이미지가 그려지는 화면의 크기에 따라 다르겠지만, 이미지를 크롭하는 화면이 이미지의 원본 크기보다 작은 경우가 많습니다. 예를 들면, 이미지의 원본 크기는  3000x2000 인데에 비해 이미지를 그릴 화면의 크기는 1440x660 밖에 안되는 것이죠. 우리가 캔버스 위에 그릴 크롭 이미지는 원본 이미지를 잘라 그려지는 것이므로, 이미지의 원본 크기와 화면에 그려진 크기를 고려하여 canvas 의 사이즈를 구해야 합니다.&lt;/p&gt;
&lt;p&gt;이때 크롭된 이미지를 그릴 canvas 의 width 와 height 를 구하는 식은 다음과 같습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const canvas = document.createElement(&amp;#39;canvas&amp;#39;);

const imageWidthScale = (image.naturalWidth / image.width);
const imageHeightScale = (image.naturalHeight / image.height);
canvas.width = crop.width * imageWidthScale;
canvas.height = crop.height * imageHeightScale;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;image 의 widthScale과 heightScale 을 canvas 의 크기 할당에 사용하는 이유는, 앞서 얘기했듯 크롭 가이드 영역이 캔버스에 옮겨야 할 이미지 영역의 사이즈 정보를 정확하게 알아야 하기 때문입니다. 이미지의 원본 크기만을 고려하거나, 반대로 이미지가 화면에 그려진 크기만을 고려하면 자칫 너무 작거나 큰 캔버스 사이즈를 얻을 수 있습니다.&lt;/p&gt;
&lt;h3&gt;4-2. 원본 이미지와 캔버스의 비율이 100% 같을 수는 없다&lt;/h3&gt;
&lt;p&gt;캔버스의 사이즈는 항상 정수 값만을 갖습니다. 만약 정수로 떨어지지 않는 값이 들어오면 내림으로 처리합니다. 이러한 내림 처리로 인해 캔버스가 원본 이미지의 비율을 유지하지 못하는 경우가 있습니다. 예시를 통해 보겠습니다.&lt;/p&gt;
&lt;p&gt;위의 식을 잠시 다시 언급하면, 캔버스의 사이즈에 들어가는 값 중에는 나눗셈으로 인해 생성되는 값인 &lt;code&gt;imageWidthScale&lt;/code&gt; 과 &lt;code&gt;imageHeightScale&lt;/code&gt; 이 있습니다. 이 두 값은 나눗셈의 결과값이 정수일 때에는 아무런 문제가 되지 않습니다.&lt;/p&gt;
&lt;p&gt;예를들어 이미지 원본의 크기가 3000x2000 이고, 좁은 화면 폭에 의해 이미지의 렌더링 크기가 1500x1000이 된 경우를 식에 대입해보면, 캔버스의 width 와 height 가 모두 80px 로, 우리가 원하는 1:1 비율을 얻을 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;imageWidthScale = 3000 / 1500 // 2
imageHeightScale = 2000 / 1000 // 2
canvas.width = 40 * 2 // 80
canvas.height = 40 * 2 // 80

// canvas 의 최종 크기 : 80x80&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그러나, 반응형 컨테이너에 의해 렌더링 크기가 소수점을 갖게 되는 경우가 있습니다. 이는 이미지가 원본의 비율을 유지하기 위해 줄어들면서 일어나는 현상입니다. 그럼에도 불구하고, javaScript 로 조회할 수 있는 width 또는 height 는 반올림 된 값을 반환합니다. &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mhg58/dJMcagdpMEm/0DfDJUMdZSp5vyNluBabkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mhg58/dJMcagdpMEm/0DfDJUMdZSp5vyNluBabkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mhg58/dJMcagdpMEm/0DfDJUMdZSp5vyNluBabkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMhg58%2FdJMcagdpMEm%2F0DfDJUMdZSp5vyNluBabkk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;브라우저에 표현된 height 값과 이미지의 Rendered size 가 서로 다른 걸 알 수 있습니다. javaScript 로 이미지의 width 나 height 값을 조회하면 Rendered size 를 반환합니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이로인해  &lt;code&gt;imageWidthScale&lt;/code&gt; 과 &lt;code&gt;imaegHeightScale&lt;/code&gt; 두 값이 서로 다른 값을 갖게 되면서, 최종적으로 캔버스에 할당되는 width 와 height 역시 서로 다른 값을 갖게 됩니다. &lt;/p&gt;
&lt;p&gt;예를들어 이미지 원본의 크기가 1125x2184 이고, 좁은 화면 폭에 의해 이미지의 렌더링 크기가 311x604 가 된 경우를 위의 식에 대입해보면, 캔버스의 width 와 height 에 서로 다른 값이 할당되는 것을 알 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;imageWidthScale = 1125 / 311 // 3.6173633441
imageHeightScale = 2184 / 604 // 3.6158940397
canvas.width = 40 * 3.6173633441 // 144.694533764
canvas.height = 40 * 3.6158940397 // 144.635761588

// canvas 의 최종 크기 : 144x144&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앞서 얘기했듯, 캔버스의 사이즈는 항상 정수 값만을 갖습니다. 만약 정수로 떨어지지 않는 값이 들어오면 내림으로 처리합니다. 따라서 위처럼 캔버스의 width 와 height 가 소수점이 다른 값을 갖더라도 크게 문제가 되지는 않습니다. 캔버스는 두 수를 모두 내림 처리하여 결국에는 우리가 원하는 비율인 1:1의 144x144 캔버스 사이즈를 갖게 될 테니까요.&lt;/p&gt;
&lt;p&gt;그러나, 서로 다른 두 수를 각각 내림 처리 한다는 것이 반드시 두 수를 같은 정수로 만들어주지는 않습니다. 이번에는 이미지 원본의 크기가 3024x4032 이고, 좁은 화면 폭에 의해 이미지의 렌더링 크기가 298x397이 된 경우를 식에 대입해 보겠습니다. (크롭 가이드 영역의 크기는 120x120 입니다)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;imageWidthScale = 3024 / 298 // 10.1476510067
imageHeightScale = 4032 / 397 // 10.1561712846
canvas.width = 120 * 10.1476510067 // 1217.718120804
canvas.height = 120 * 10.1561712846 // 1218.740554152

// canvas 의 최종 크기 : 1217 x 1218&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;내림으로 인해 canvas 의 최종 width 와 height 가 서로 다른 값을 갖게 되었습니다. 이처럼 canvas 는 자신에게 할당된 width 와 height 값을 내림 처리하므로, 크롭된 이미지의 비율이 원본 이미지와는 다른 비율을 갖게 될 수 있다는 것을 고려해야 합니다.&lt;/p&gt;
&lt;p&gt;만약 이를 고려하지 않고, 1:1 비율의 이미지를 요구하는 컨테이너에 다른 비율의 이미지를 넣으면 이미지가 자칫 깨진 것처럼 보일 수 있습니다. 이는  &lt;code&gt;object-fit: cover&lt;/code&gt; 속성을 적용해도 해결할 수 없는 문제로, 이와 같은 현상에 대해서는 다른 포스팅에서 좀 더 다뤄볼 예정입니다.&lt;/p&gt;
&lt;h2&gt;고려할 것 5. 크롭의 역할은 어디까지일까?&lt;/h2&gt;
&lt;p&gt;위의 요구사항 정리에서 살펴봤던, 이미지 크롭 기능을 사용하는 사용자 여정을 다시 한 번 나열해보면 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;프로필 이미지 업로더를 클릭합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;파인더 (윈도우 사용자의 경우 파일 탐색기)가 열리고, 사용자가 이미지를 선택합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;이미지 크롭 창이 열리고, 사용자가 선택한 이미지가 크롭 가이드 영역과 함께 창 내에 그려집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;사용자가 크롭 가이드 영역을 늘리거나 줄이고, 이동함으로써 원하는 이미지 영역을 선택한 뒤, 완료 버튼을 누릅니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;크롭 가이드에 의해 선택된 영역이 프로필 이미지로 설정됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;여기서 이미지 크롭의 역할은 어디까지일까요?&lt;/p&gt;
&lt;p&gt;저는 3번과 4번 까지가 이미지 크롭의 역할이라고 생각했습니다. 위의 1~5번에 해당하는 모든 기능을 이미지 크롭의 역할로 보지 않았던 이유는, 이미지를 선택하는 도메인이 어디냐에 따라 크롭을 사용하지 않는 곳도 있었기 때문입니다. 예를 들어, 프로필이 아닌 게시글 이미지 업로드를 하는 경우에는 2번에서 여러 이미지를 한 번에 선택할 수도 있고, 그렇게 선택한 이미지가 바로 게시글 에디터에 반영되어야 했습니다.&lt;/p&gt;
&lt;p&gt;따라서 이미지 크롭을 구현할 때는 원래 이미지 크롭이 없었을 때의 기존 모듈들의 인터페이스를 최대한 유지하려 노력했습니다. 모종의 이유로 이미지 크롭 기능을 다시 들어내야 한다고 했을 때, 기존 이미지 업로드 프로세스가 동작했던 방식인 1 -&amp;gt; 2- &amp;gt; 5 로 쉽게 돌아갈 수 있게끔 말이죠. 이처럼 프로세스의 중간에 기능을 끼워넣을 때에는 해당 기능이 불러올 사이드 이펙트 뿐만 아니라, 해당 기능을 들어내야 할 상황까지 고려해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdDZft/dJMcaaxuzzZ/HvD6uYKCBklsHWw9IKkkI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdDZft/dJMcaaxuzzZ/HvD6uYKCBklsHWw9IKkkI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdDZft/dJMcaaxuzzZ/HvD6uYKCBklsHWw9IKkkI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdDZft%2FdJMcaaxuzzZ%2FHvD6uYKCBklsHWw9IKkkI1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;File 타입의 image 인터페이스를 그대로 유지함으로써, 이미지 크롭 모듈이 기존 프로세스에 자연스럽게 녹아들도록 했습니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;개인적으로 시각적인 작업물을 좋아하기도 하고, 대부분의 이미지 업로드 기능에 크롭 기능이 들어갔기 때문에 많은 유저에게 노출될 수 있다는 점에서 재미있는 작업이었습니다.&lt;/p&gt;
&lt;p&gt;아쉬웠던 점은, 빠르게 문제를 해결하려다가 오히려 문제 해결이 더 늦어지는 등, 이슈가 되었던 문제들에 대해 근본적인 원인을 깊게 파헤치지 않았던 점이 아쉬웠습니다. 앞으로는 문제 해결에 필요한 지식 습득에 소모되는 시간을 너무 아까워하지는 말아야겠다는 생각이 듭니다.&lt;/p&gt;
&lt;p&gt;마지막으로, 시작은 단순 크롭이었지만 이미지와 직접적인 연관이 있다보니 이미지와 관련된 다양한 지식들을 학습할 수 있어서 좋았습니다.&lt;/p&gt;
&lt;br /&gt;


&lt;p&gt;이 글은 제가 2024년에 작성한 글을 옮겨온 것입니다.&lt;/p&gt;</description>
      <category>react</category>
      <category>react-image-crop</category>
      <category>이미지</category>
      <category>이미지크롭</category>
      <category>프론트엔드</category>
      <author>준영(Junzero)</author>
      <guid isPermaLink="true">https://til-dev.tistory.com/12</guid>
      <comments>https://til-dev.tistory.com/12#entry12comment</comments>
      <pubDate>Mon, 16 Feb 2026 15:28:12 +0900</pubDate>
    </item>
    <item>
      <title>기술블로그로 알아보는 테크니컬라이팅 강의 후기 (Udemy)</title>
      <link>https://til-dev.tistory.com/11</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.notion.so/ac5b18a482fb4df497d4e8257ad4d516&quot;&gt;글쓰기 모임 글또&lt;/a&gt;를 시작하면서 글을 자주 쓰게 되었다. 모임에서 요구하는 글에 정해진 형식은 없지만, 기술적으로 좋은 글을 쓰고 싶다는 개인적인 욕망이 있었다. 그런데 유데미에서 글또 회원들을 대상으로 무료로 강의를 제공해주셔서, 테크니컬 라이팅 관련 강의를 보게 되었다. 이 글은 해당 강의를 토대로, 나름대로 이해한 바를 덧대어 작성한 글이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테크니컬 라이팅의 정의와 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테크니컬 라이팅이란 우리말로 직역하면 기술적 글쓰기로, 독자에게 기술적인 정보를 전달하는 것이 목적인 글쓰기를 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 독자에 따라 다양한 해석이 가능한 문학과는 다르게, 독자가 글을 자의적으로 해석할 여지를 남기지 않아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외의 테크니컬 라이팅과 문학 글쓰기의 차이점은 아래와 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;분류&lt;/th&gt;
&lt;th&gt;문학 글쓰기&lt;/th&gt;
&lt;th&gt;테크니컬 라이팅&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;내용&lt;/td&gt;
&lt;td&gt;창의&lt;/td&gt;
&lt;td&gt;사실&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대상&lt;/td&gt;
&lt;td&gt;일반&lt;/td&gt;
&lt;td&gt;특정인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;표현&lt;/td&gt;
&lt;td&gt;상징&lt;/td&gt;
&lt;td&gt;직설&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;구성&lt;/td&gt;
&lt;td&gt;자유&lt;/td&gt;
&lt;td&gt;순차&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테크니컬 라이팅을 해야 하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신이 생각하는 바를 실시간으로 정확히 전달할 수 있으면서, 구두로 논의한 내용을 100% 기억할 수 있는 것이 아니라면 테크니컬 라이팅을 해야 한다고 생각한다. 왜냐하면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;효과적인 의사소통
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구두로 생각을 전달하게 되면, 언어적인 표현 외에도 반언어적, 비언어적 표현이 상대방에게 전달되기 때문에 상대방이 의미를 오해하는 경우가 있을 수 있다. 기술적인 내용을 전달할 때에는 팩트 외의 다른 요소들이 팩트 전달에 영향을 끼쳐서는 안된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정보 기록과 보존
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;말, 또는 기억은 쉽게 휘발되지만, 글로 적는 건 휘발되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;경쟁력 강화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;본인이 터득한 지식과 경험을 공유한다는 것은, 혼자서만 성장하려는 것이 아닌, 주변 사람들의 성장에도 신경을 쓴다는 것을 의미한다. 결과적으로 본인의 경쟁력을 강화하는 결과로 돌아오기도 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 문서 작성 프로세스 1. 계획&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 글감 선정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연한 소리지만, 글을 쓰려면 무슨 글을 쓸지에 대한 고민이 선행되어야 한다. 어떤 글을 쓸지는 작성자 개인의 니즈에 따라 다르겠지만, 글 주제를 좁히기에 어려운 이들을 위한 몇가지 포인트들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;글을 작성할 때 참고할 양질의 자료를 충분히 찾을 수 있는가
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기술 문서를 작성할 때, 가장 중요한 것은 사실을 기반으로 하는 것이다. 때문에 글의 내용을 뒷받침할 만한 자료의 양과 질은 글의 신뢰도를 높이는 데에 도움이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;주제가 충분히 좁은가
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 편의 글을 일정 내에 좋은 품질로 완성하기 위해서는 글의 주제를 좁히는 것이 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;수정 전&lt;/th&gt;
&lt;th&gt;수정 후&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript 활용하기&lt;/td&gt;
&lt;td&gt;JavaScript 에서 타임존 다루기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub 사용법&lt;/td&gt;
&lt;td&gt;GitHub 를 사용한 효율적인 문서 검토 방법&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. 독자 선정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 글은 독자가 어느 정도 배경 지식, 혹은 맥락(context)을 갖고 있다는 것을 전제로 작성된다. 따라서 기술 문서를 작성할 때에는 먼저 어느 배경 지식을 가지고 있는 독자를 대상으로 하는 글인지를 분명히 해야 한다. 그렇지 않는다면 글에서 사용되는 모든 용어에 대해 일일이 설명을 덧붙여야 할 것이다. 아래 예시를 보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn/JavaScript#prerequisites&quot;&gt;Prerequisites&lt;/a&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript is arguably more difficult to learn than related technologies such as &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn/HTML&quot;&gt;HTML&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn/CSS&quot;&gt;CSS&lt;/a&gt;. Before attempting to learn JavaScript, you are strongly advised to get familiar with at least these two technologies first, and perhaps others as well. Start by working through the following modules:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web&quot;&gt;Getting started with the Web&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML&quot;&gt;Introduction to HTML&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn/CSS/First_steps&quot;&gt;Introduction to CSS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 내용은 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn/JavaScript&quot;&gt;MDN 자바스크립트 가이드&lt;/a&gt;에 있는 내용이다. 자바스크립트는 관련있는 웹 기술인 HTML, CSS 보다 더 어려울 수 있으니, HTML, CSS 에 익숙하지 않은 사람이라면 HTML, CSS 관련 문서를 먼저 읽고 오라는 내용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이러한 배경지식이 없는 사람들도 글의 대상 독자로 선정했다고 가정해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에 관한 기술문서임에도 HTML, CSS 관련 용어가 나올 때마다 해당 용어를 설명하는데에 꽤 많은 지면을 할당해야 할 것이다. 이로인해 자바스크립트만을 학습하러 온 독자는 자신이 원하는 정보를 찾는 데에 애를 먹게 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 독자가 선정되지 않았기 때문에 작성자는 기술을 어느 깊이까지 설명해야 할지 감을 잡기가 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 기술 문서의 독자를 선정한 뒤 글쓰기에 들어가는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 문서 작성 프로세스 2. 초안 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글의 주제를 정했다면, 팩트를 전달하는 것이 중요하지, 글의 미관이나 전개 방식등은 사실 별로 중요하지 않다. 따라서 글의 초안을 작성할 때에는 자신있는 부분부터 일단 작성하는 것이 좋다. 글로 적는 것 조차 어렵다면, 우선 머리속에 떠오르는 키워드 부터 나열해보자. 키워드가 이어지면 문장이 되고, 문장이 모이면 단락이 되며, 이것이 곧 문서로까지 이어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 기술 문서의 초안을 작성할 때 지켜야 할 3C 원칙을 보면서, 좀 더 좋은 초안을 작성할 수 있는 방법에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 명확 (Clear) 하게&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모호함이 없고 확실하게 정확한 내용과 선명한 문장을 사용한다.&lt;/li&gt;
&lt;li&gt;수식어보다 명확하고 객관적인 데이터를 사용한다. (매우, 아주, 꽤, 상당히, 괄목할 만하게, 대단히, ...)&lt;/li&gt;
&lt;li&gt;링크를 걸 때에는 해당 링크에서 확인할 수 있는 내용을 링크 텍스트로 분명하게 적는다.&lt;/li&gt;
&lt;li&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;수정 전&lt;/th&gt;
&lt;th&gt;수정 후&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;불과 얼마 전까지만&lt;/b&gt; 해도 데이터 센터에 관심이 없었다.&lt;/td&gt;
&lt;td&gt;&lt;b&gt;LEED Platinum 인증을 받기 전까지&lt;/b&gt; 데이터 센터에 관심이 없었다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USB 전송 속도가 &lt;b&gt;매우&lt;/b&gt; 향상되었다.&lt;/td&gt;
&lt;td&gt;USB 2.0 의 최대 전송 속도는 &lt;b&gt;초당 35MB&lt;/b&gt; 이고, USB 3.0 의 최대 전송 속도는 &lt;b&gt;초당 400MB&lt;/b&gt; 이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;앱 시작 시간이 &lt;b&gt;너무 길면&lt;/b&gt; 알람을 보냅니다.&lt;/td&gt;
&lt;td&gt;앱 시작에 &lt;b&gt;2초 이상 걸리면&lt;/b&gt; 알람을 보냅니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;앞에서 설명한 방법이 유일한 방법이라고 &lt;b&gt;말할 수는 없지 않을까 싶다.&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;앞에서 설명한 방법이 유일한 방법은 &lt;b&gt;아닐 것이다.&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;향후 퀵커머스 산업은 계속 성장할 것이라 &lt;b&gt;판단되는 바입니다.&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;향후 퀵커머스 산업은 계속 성장할 &lt;b&gt;것입니다.&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;새로 나온 '함께 주문' 서비스를 알아보자. &lt;b&gt;이 서비스&lt;/b&gt;를 사용하면 다른 사람들과 장바구니를 공유할 수 있다.&lt;/td&gt;
&lt;td&gt;새로 나온 '함께 주문' 서비스를 알아보자. &lt;b&gt;''함께 주문''&lt;/b&gt;을 사용하면 다른 사람들과 장바구니를 공유할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;오류 코드를 확인하려면 &lt;b&gt;여기&lt;/b&gt;를 참고하세요.&lt;/td&gt;
&lt;td&gt;오류 코드를 확인하려면 &lt;b&gt;오류 코드 목록&lt;/b&gt;을 참고하세요.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 간결 (Concise) 하게&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단문으로 쓴다.&lt;/li&gt;
&lt;li&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;수정 전&lt;/th&gt;
&lt;th&gt;수정 후&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;나는 사방에서 매미들이 주변의 나무들이 진저리를 칠 정도로 목청을 다해서 발악적으로 시끄럽게 울어대는, 맞은편에서 사람이 오면 비켜설 자리가 없을 정도로 비좁은 오솔길을 혼자 쓸쓸히 걷고 있었다.&lt;/td&gt;
&lt;td&gt;나는 오솔길을 걷고 있었다. 혼자였다. 오솔길은 비좁아 보였다. 맞은편에서 오는 사람과 마주치면 비켜설 자리가 없을 정도였다. 매미들이 시끄럽게 울어대고 있었다. 발악적이었다. 주변의 나무들이 진저리를 치고 있었다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
쉬운 표현을 쓴다.
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;수정 전&lt;/th&gt;
&lt;th&gt;수정 후&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;예약 기능 편의를 제고하고자 익익월 말까지 시스템을 정비할 예정입니다.&lt;/td&gt;
&lt;td&gt;예약 기능을 편리하게 사용할 수 있게 다다음 달 말까지 시스템을 정비할 예정입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;말하듯 쓴다.
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;수정 전&lt;/th&gt;
&lt;th&gt;수정 후&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;클라우드 상품으로부터 서버 편의성을 제공받을 수 있다.&lt;/td&gt;
&lt;td&gt;클라우드 상품을 이용하면 쉽게 서버를 관리할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. 일관 (Consistent) 되게&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문서 주제의 일관성 유지 : 같은 주제를 나타내고 있는지 항상 확인할 것
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기존 주제를 잘 지킨 예 (출시 테스트 only)&lt;/th&gt;
&lt;th&gt;기존 주제에서 벗어난 예 (출시 테스트 -&amp;gt; 테스트 가이드)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5월 말 출시 예정인 서비스의 출시 테스트를 진행 중이며, 지금까지 큰 문제는 발견되지 않아 출시 일정에는 문제가 없을 것 같습니다. 자세한 진행 상황은 보고서 페이지를 참고해 주시기 바랍니다.&lt;/td&gt;
&lt;td&gt;출시 테스트를 진행하다 보니 테스트 가이드를 보완해야 할 것 같습니다. 테스트 가이드 보완 계획은 이전 메일을 참고해 주세요. 가이드는 다음달 말에 업데이트를 완료할 계획입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;형식의 일관성 유지 : 각 문장을 일관성 있게 끝맺음 맺을 것
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;수정 전&lt;/th&gt;
&lt;th&gt;수정 후&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;* 수행 업무 : 타사 사례 리서치&lt;br /&gt;* 기술 블로그 작성 및 검수 프로세스를 정립한다.&lt;/td&gt;
&lt;td&gt;수행하는 업무는 다음과 같습니다.&lt;br /&gt;* 타사 사례 리서치&lt;br /&gt;* 기술 블로그 글 작성&lt;br /&gt;* 글 검수 프로세스 정립&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;용어나 표현을 일관되게 사용한다 : 문서를 여러 명이 쓸 경우, 쓸 용어를 미리 정해둘 것&lt;/li&gt;
&lt;li data-ke-style=&quot;style1&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크에서 DNS 서버에 액세스해야 하는 &lt;b&gt;인스턴스&lt;/b&gt;의 보안 그룹 ID 를 지정합니다.&lt;br /&gt;로드 밸런서에 연결된 시큐리티 그룹에는 &lt;b&gt;instance&lt;/b&gt;와 통신할 수 있는 규칙이 있어야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-style=&quot;style1&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 문서 작성 프로세스 3. 검토&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발에서 QA 와 디버깅이 제품의 퀄리티를 결정하듯, 작성한 기술 문서가 올바르게 작성되었는지 검토하고 고치는 것이 기술 문서의 퀄리티를 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 기술 문서를 잘 검토하고, 고치는 방법에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 문서 구조 검토 (MECE)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서에 빠진 내용이 없는지, 내용 중에 겹치는 것이 없는지 검사할 때 MECE 라는 개념을 주로 사용한다. 이는 Mutually Exclusive, Collectively Exhaustive 의 준말이다. 우리말로 직역하면 ''상호배제와 전체포괄''로, 각 항목들이 상호 배타적이면서 모였을 때는 완전히 전체를 이루는 것을 의미한다. 아래의 예시를 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Yloqr/dJMcabwmOFf/0BJ3qQK7WKwMlhXkKFb7G1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Yloqr/dJMcabwmOFf/0BJ3qQK7WKwMlhXkKFb7G1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Yloqr/dJMcabwmOFf/0BJ3qQK7WKwMlhXkKFb7G1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYloqr%2FdJMcabwmOFf%2F0BJ3qQK7WKwMlhXkKFb7G1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1258&quot; height=&quot;928&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;928&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 소개 기술문서로 본 MECE 적용 예시 (from. 강의 영상)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1,2 번의 주제인 서비스 특징과 서비스 특장점은 서비스의 특징에 대해 소개한다는 점에서 중복되는 지점이므로, 1. 서비스 특징 으로 묶는다.&lt;/li&gt;
&lt;li&gt;3번의 소주제인 연동 서비스와 서비스 이용 방법은 하나의 주제에 묶일 만큼 연관이 있지 않으므로, 소주제를 2. 연동 서비스, 3. 이용 방법 으로 분리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 자주 보는 문장 오류 검토&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;문장 오류 종류&lt;/th&gt;
&lt;th&gt;수정 전&lt;/th&gt;
&lt;th&gt;수정 후&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;주어와 서술어 미스매치&lt;/td&gt;
&lt;td&gt;클라우드 서비스의 장점은 필요할 때 원하는 만큼만 리소스를 사용할 수 있다.&lt;/td&gt;
&lt;td&gt;클라우드 서비스의 장점은 필요할 때 원하는 만큼만 리소스를 사용할 수 있다는 점이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;목적어 생략&lt;/td&gt;
&lt;td&gt;툴팁을 보려면 물음표 아이콘 위에 살짝 올려 주세요.&lt;/td&gt;
&lt;td&gt;툴팁을 보려면 물음표 아이콘 위에 마우스 포인터를 올려 주세요.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;불필요한 말&lt;/td&gt;
&lt;td&gt;발송 중에 취소해도 일부 수신자에게는 발송이 진행될 수 있습니다.&lt;/td&gt;
&lt;td&gt;발송 중에 취소해도 일부 수신자에게는 발송될 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;불필요한 조사&lt;/td&gt;
&lt;td&gt;앱을 종료하려면 홈 메뉴를 클릭을 하고 종료 버튼을 클릭을 합니다.&lt;/td&gt;
&lt;td&gt;앱을 종료하려면 홈 메뉴를 클릭하고 종료 버튼을 클릭합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;늘어난 말&lt;/td&gt;
&lt;td&gt;계획되지 않은 장애로 인하여 서버가 서비스를 제공하지 못하게 되면, 자동으로 장애 조치를 수행합니다.&lt;/td&gt;
&lt;td&gt;서버에 장애가 발생하면 자동으로 장애 조치를 수행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;외국어 남용&lt;/td&gt;
&lt;td&gt;익월 배포 issue 없게 준비합시다. 새 feature를 추가하는 것보다 performance를 높이는 stance는 유지하고요. 관련 부서에도 wording 다듬어서 quick하게 comm. 해주세요.&lt;/td&gt;
&lt;td&gt;익월 배포 문제 없게 준비합시다. 새 기능을 추가하는 것보다 성능을 높이는 입장은 유지하고요. 관련 부서에도 내용 다듬어서 빠르게 협의 해주세요.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외국어보다 우리말을 사용한다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외국어 대신 사용할 수 있는 우리말을 찾아본다.&lt;/li&gt;
&lt;li&gt;적당한 우리말이 없다면 음차해서 사용한다. (server -&amp;gt; 서버)&lt;/li&gt;
&lt;li&gt;특정 분야 전문 용어라면 원문을 병기한다. -&amp;gt; 텐서(tensor)는 배열이나 행렬과 매우 유사한..&lt;/li&gt;
&lt;li&gt;타사 서비스나 제품 이름인 고유명사는 원문 그대로 쓴다. (Microsoft Azure || Adobe Acrobat)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. 용어 검토&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대상 독자를 선정했음에도 불구하고, 문서에서 사용하는 모든 용어를 대상 독자가 이해하기는 어려울 수 있다. 따라서 아래의 조건에 해당하는 용어에 대해서는 적절한 설명을 추가해주는 것이 바람직하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정한 개념으로 정의된 단어&lt;/li&gt;
&lt;li&gt;절대적인 의미를 갖는 단어 (분야에 따라서 용어의 의미가 다를 수 있으므로)&lt;/li&gt;
&lt;li&gt;단어를 줄여 쓴 약어&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-4. 단락 길이 검토&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단락이 너무 길면 글이 지루하게 보이기 마련이다. 따라서, 하나의 단락은 중심 생각 하나만 담는 것이 중요하다. 개발에서 함수 하나가 여러 역할을 하면 디버깅과 코드 리딩이 어려워지는 것과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-5. Semantic Symbol 사용 검토&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글에 사용되는 다양한 심볼들은 그 의미와 원래의 용도에 맞게 사용해야 글의 가독성을 높일 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;점 목록 : 순서에 관계없는 동등한 항목을 나열&lt;/li&gt;
&lt;li&gt;숫자 목록 : 순서가 있는 동등한 항목을 나열&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;시각 자료 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스크린샷 : 사물의 생김새를 보여줄 때&lt;/li&gt;
&lt;li&gt;그래프 : 수나 양을 표현할 때&lt;/li&gt;
&lt;li&gt;다이어그램 : 구조, 관계 등의 논리적인 흐름을 나타낼 때&lt;/li&gt;
&lt;li&gt;표 : 항목을 비교하거나 정리할 때&lt;/li&gt;
&lt;li&gt;시각 자료 앞에 설명을 놓을 것&lt;/li&gt;
&lt;li&gt;글에서 언급하기 쉽도록, 시각 자료에 제목을 달 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-6. 보안 검토&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글이 외부에 공개되어서는 안되는 내용을 담고 있지는 않은지 검토해봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 추가 팁&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 문서는 사실에 기반해 내용을 정확하게 전달하는 것이 가장 중요하다. 그러나 독자들이 내가 쓴 글을 계속 읽고 싶게 하는 소프트한 문서 작성 스킬 역시 중요하다. 이번에는 독자들이 조금 더 글에 흥미를 갖고, 원하는 정보를 얻어갈 수 있게끔 해주는 방법에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 도입부 작성 요령&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글의 도입부는 독자로 하여금 글을 계속 읽어야 할 동기를 부여하거나, 흥미를 돋구는 역할을 한다. 이 때 효과적인 방법들을 소개한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;질문으로 먼저 시작
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;독자에게 질문을 던짐으로써 글의 전개에 적극적인 참여자로 개입하게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;용어나 개념 정리로 시작
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다소 클래식한 방법으로, 글에서 사용할 용어나 개념에 대해 간단히 정리함으로써 독자의 배경 지식을 기술 문서를 이해하기에 적합한 수준으로 끌어올려 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;나만의 경험 서술로 시작
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저자만의 개인적인 경험은 독자로 하여금 저자가 좀 더 친근한 인물처럼 느껴지게끔 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;유명한 인물이 썼던 문장을 인용하면서 시작
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유명한 인물의 문장과 글의 주장이 서로 연관이 있음을 은연중에 드러냄으로써, 유명인이 가지고 있는 신뢰 자산을 빌려와 글의 신뢰도를 높일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. 개요 작성 요령&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개요는 주로 도입부 직후에 나오는 것으로, 본격적으로 글을 시작하기에 앞서 글에 대해 좀 더 포멀한 소개를 하는 부분이다. 독자는 개요를 읽고 어떤 글을 읽게 될 지 파악할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;글에서 다루는, 다루지 않는 주제
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;글의 제목과 도입부는 글이 담고 있는 내용을 다소 함축적으로 보여주거나 과장하는 부분이 있다. 따라서 독자들에게 이 글이 실제로 어떤 내용을 다루는지, 또는 다루지 않는지 분명히 해야 독자와 저자간의 오해를 줄일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;대상 독자
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 글이 어떤 배경지식, 혹은 맥락을 가진 독자를 대상으로 하는지 분명히 해야 한다. 이를 통해 저자는 글의 본문에서 언급하지 않은 맥락에 대해 설명하지 않아도 되므로 더 깊은 이야기를 지면에 할당할 수 있고, 독자들은 불필요한 시간 낭비를 줄일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;글을 읽고 독자가 알 수 있는 것
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;글을 읽으면 어떤 정보를 얻어갈 수 있는지 독자에게 미리 인지시켜 준다. 이를 통해 독자는 자신이 글에 대해 기대하는 바와 글의 내용이 실제로 일치하는지 미리 가늠해볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TL:DR
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Too Long: Did not Read 의 준말로, 대부분의 기술 문서는 내용이 복잡하고 긴 경우가 많으므로, 본문의 내용을 최대한 요약하여 제공하는 요약문이다. 바쁜 독자들은 본문을 훑어가며 요약하지 않고, 이 부분만 읽으면 되므로 시간을 절약할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 중 나왔던 내용 중 ,글쓰기가 어려운 이유에 대해 'write to express, not impress' 라는 표현이 나왔다. 인상을 남기기 위해 글을 쓰지 말고, 표현하기 위해 글을 쓰라는 문장이다. 이 문장을 보고 나니 그동안 나는 독자에게 어느정도 인상을 주어야 한다는 생각을 하고 있었던 것 같다. 그래서 글을 쓰는 속도가 느렸고, 아웃풋이 없으니 결과적으로 글을 쓰고 싶다는 생각도 줄어들었다. 앞으로는 글을 쓸 때 독자에게 인상을 주려고 하기보다는, 내가 알고 있는 것과 공부한 것, 느낀 점등을 온전히 표현하는 데에 집중해보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레퍼런스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.udemy.com/course/techwriting/&quot;&gt;https://www.udemy.com/course/techwriting/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 콘텐츠는 유데미로부터 강의 쿠폰을 제공받아 작성되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년에 작성한 글을 옮겨왔습니다&lt;/p&gt;</description>
      <category>Udemy</category>
      <category>글쓰기</category>
      <author>준영(Junzero)</author>
      <guid isPermaLink="true">https://til-dev.tistory.com/11</guid>
      <comments>https://til-dev.tistory.com/11#entry11comment</comments>
      <pubDate>Mon, 16 Feb 2026 00:50:17 +0900</pubDate>
    </item>
    <item>
      <title>코딩 자율학습 리눅스 입문 with 우분투 서평</title>
      <link>https://til-dev.tistory.com/10</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;언제부터인지는 기억나지 않지만, 저에게 리눅스는 개발자라면 꼭 한 번은 배워두어야 하는 이미지로 자리 잡혀 있었습니다. 또 비교적 최근에는 AWS 의 클라우드 서비스의 운영체제가 리눅스로 되어 있다는 것을 알게 되고, 컴퓨터에 있는 여러 파일들을 스크립트를 통해 관리할 필요성을 느끼게 되는등 언젠가는 리눅스를 배워야겠다는 생각을 갖고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 리눅스를 처음 접하거나, 리눅스를 다루고 있지만 개념을 잘 모르는 사람을 대상으로 하는 책입니다. 책은 크게 두 파트로 나뉘어 있는데, 파트1 은 리눅스라는 운영체제를 이해하는 데에 중점을 두고 있고, 파트2 는 리눅스를 활용하기 위한 기술적인 측면(Bash, 프로그래밍, &amp;hellip;)에 중점을 두고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파트 1에서 특히 좋았던 건, 프로세스에 대한 개념과 실습이 자연스럽게 이어지는 부분이었습니다. 이전에 CS 관련 서적에서 프로세스를 접할 때는 단순 개념 설명에 그쳐 방금 읽은 내용이 금방 머릿속에서 휘발되는 느낌이었기에 아쉬웠습니다. 하지만 이 책에서는 프로세스의 개념 소개와 프로세스 관리 방법에 대한 실습이 자연스럽게 이어져 있어 좀 더 오래 기억에 남을 것 같다는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파트 2에서는 Bash 사용 방법에 대한 소개와 실습이 나오는데, 가장 흥미로웠던 점은 Bash의 변수에는 데이터 타입 없이 모든 데이터가 문자열로 처리된다는 점이었습니다. 이는 데이터 타입이 비교적 자유로운 언어인 자바스크립트로 개발을 시작한 저에게도 처음에는 놀라운 사실이었지만, Bash가 주로 시스템 명령어나 파일 조작을 위해 쓰인다는 걸 알고는 합리적인 선택이다 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책 전체적으로는, 대부분의 지식에 대해 독자가 당연히 알고 있을 것이라고 가정하지 않아 입문자를 배려한다는 컨셉이 확실해서 좋았습니다. 예를 들어, 개발자라면 모르기 어려운 인덴트라는 용어가 처음 쓰이는 부분에서 &amp;lsquo;코드 들여쓰기를 지칭하는 인덴테이션(indentation), 흔히 줄여서 인덴트&amp;rsquo; 라고 설명해주는 부분이 그랬습니다. 덕분에 모르고 있었거나 잘못 알고 있던 지식들(리눅스에 한정되지 않은)을 습득할 수 있어서 좋았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나 아쉬웠던 점은, 실습 내용이 좀 더 실생활에서 쓰일 만한게 좀 더 많았으면 좋았을 것 같습니다. 각 단원의 마무리 부분에 있는 셀프체크를 통해 앞에서 배운 내용들을 응용하여 텍스트 내용을 출력해보거나 하는 건 다른 입문서와 크게 다르지 않게 느껴졌습니다. 개인적으로는 실생활에 사용하기 위해 리눅스를 배우게 된 만큼, 책에서 배운 내용이 어떻게 쓰일 수 있는지를 알고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총평을 남기자면, 저처럼 리눅스에 대해 거의 모르지만 리눅스를 배워보고 싶거나, 한 번쯤 셸 스크립트를 써보고 싶은 이들에게는 너무 좋은 입문서라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;길벗 출판사의 후원을 받아 서평을 작성했습니다&lt;/p&gt;</description>
      <category>런잇</category>
      <category>리눅스</category>
      <category>프로그래밍</category>
      <author>준영(Junzero)</author>
      <guid isPermaLink="true">https://til-dev.tistory.com/10</guid>
      <comments>https://til-dev.tistory.com/10#entry10comment</comments>
      <pubDate>Sat, 7 Dec 2024 21:13:25 +0900</pubDate>
    </item>
    <item>
      <title>Udemy-R3F-로 배우는-인터랙티브-웹-개발-정리-및-후기</title>
      <link>https://til-dev.tistory.com/9</link>
      <description>&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;HTML - 웹에서 표현하는 3가지 방법&lt;/span&gt;&lt;/h2&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1. DOM, 2. SVG&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;일반적인 HTML 문법으로 그릴 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;CSS 적용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;계층적 구조를 가지기 때문에, 내부 엘리먼트들에 이벤트들을 부착할 수 있다. (click, hover, ...)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;웹표준 / 웹접근성 (screenReadableText, alt, ...)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;SEO 검색엔진 최적화&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Cmd + F 로 텍스트를 찾을 수 있다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;SVG 는 벡터 기반이라서, 다양한 해상도에서 선명하고 깨끗한 상태 유지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;SVG 는 정확한 오브젝트 영역 탐지 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;3. Canvas&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;javaScript 로 그릴 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;픽셀 기반&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;성능이 좋다. 1000~10000개 넘는 오브젝트를 그릴 때 좋은 성능을 보임&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;2D, 3D(WebGL) 표현 가능&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;R3F 환경 설정&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;vite + react 프로젝트 생성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;coffeescript&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;npm create vite@latest&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Select a Framework : React&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Select a variant : TypeScript&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;react-three-fiber 관련 라이브러리 설치&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;coffeescript&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;npm install three @types/three @react-three/fiber @react-three/drei&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;three : 자바스크립트 3D 라이브러리. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;@react-three/fiber : React 렌더링 방식으로 three.js 를 사용할 수 있게 해주는 라이브러리.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;@react-three/drei : 개발 편의성을 위해 @react-three/fiber 를 사용할 때 필요할만한 컴포넌트들을 추상화하여 제공하는 라이브러리. 관련 컴포넌트들은 &lt;/span&gt;&lt;span&gt;&lt;a style=&quot;color: #4183c4;&quot; href=&quot;https://drei.pmnd.rs/?path=/docs/staging-accumulativeshadows--docs&quot;&gt;&lt;span&gt;@react-three/drei에서 제공하는 스토리북 페이지&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span&gt;에서 둘러볼 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Scene, Camera, Renderer 의 구분&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Scene :&amp;nbsp; 사람, 카메라, 사물 등이 존재하는 공간 자체를 의미한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Camera : Scene 에서 원하는 장면을 포착하기 위한 렌즈(눈)을 의미한다. 카메라에 담긴 시각적 정보들은 Renderer 에 전달된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Renderer : Camera 로부터 전달받은 시각적 정보들을 화면에 보여주는 객체를 의미한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;useFrame&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;매 프레임 마다 실행되는 훅&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;인자로 전달하는 콜백 함수 안에 코드를 삽입하면, 매 프레임마다 해당 코드가 실행된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pf&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt; &amp;nbsp;const boxRef = useRef&amp;lt;THREE.Mesh&amp;gt;(null);
 &amp;nbsp;
 &amp;nbsp;useFrame((state, delta, xrFrame) =&amp;gt; {
 &amp;nbsp; &amp;nbsp; &amp;nbsp;boxRef.current.rotation.x += 0.01;
 &amp;nbsp; &amp;nbsp; &amp;nbsp;boxRef.current.scale.z += -0.01;
  })&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote style=&quot;background-color: #ffffff; color: #777777; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;매 프레임마다 박스를 x 축으로 0.01px만큼 회전시키고, z 축으로 0.01px 만큼 크기를 키우는 코드.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Renderer&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;카메라가 포착한 화면을 어떻게 그릴지에 대한 책임을 지는 주체&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;export default function ThreeElement() {
 &amp;nbsp;useFrame((state, delta) =&amp;gt; {
 &amp;nbsp; &amp;nbsp; &amp;nbsp;boxRef.current?.rotateY(0.01);
  })
 &amp;nbsp;
 &amp;nbsp;return (
 &amp;nbsp; &amp;nbsp;&amp;lt;&amp;gt;
 &amp;nbsp; &amp;nbsp;&amp;lt;/&amp;gt;
  )
}
​&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;useFrame 을 통해 프레임마다 렌더링을 새로 할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Camera&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Scene 에 존재하는 객체를 포착하는 책임을 지는 주체&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Camera 의 종류에는 Orthographic, Perspective 가 있다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Orthographic: 원근법이 없는 2D 카메라&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Perspective(default) : 원근법이 적용되는 3D 카메라&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Canvas 컴포넌트에 camera 라는 prop 을 전달하여 카메라 속성을 변경할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;Canvas camera={{
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;near: 1,
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;far: 20,
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;fov: 75,
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;position: [5,5,0]
 &amp;nbsp; &amp;nbsp;  }}&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Scene&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;3D 공간 자체에 대한 책임을 지는 주체&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Mesh&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;하나의 Mesh 는 Geometery 와 Material 로 이루어져 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Geometery : 모양. 형태, 구조, 모델링&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Material : 모델링 겉에 씌우는 컬러, 스킨, 텍스쳐&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Scene 에 Mesh 를 넣는 코드 예시1&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;// 화면에 그릴 Mesh는 빨간색 박스 Geometry 이고, 빨간색 Material 을 갖고 있다.
&amp;lt;mesh ref={boxRef}&amp;gt; 
 &amp;nbsp;&amp;lt;boxGeometry /&amp;gt;
 &amp;nbsp;&amp;lt;meshStandardMaterial color={'red'} /&amp;gt;
&amp;lt;/mesh&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Scene 에 Mesh 를 넣는 코드 예시2&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;const { scene } = useThree();
const geometry = new Three.BoxGeometry(1, 1, 1);
const material = new Three.MeshBasicMaterial({color: 0x00ff00})
const cube = new Three.Mesh(geometry, material);
scene.add(cube);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Controls, Helper&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;three.js 계열의 라이브러리들에서는 3D 개발을 할 때 사용할 수 있는 추상화된 컴포넌트, 또는 옵션들을 제공한다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;OrbitControls&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt; : @react-three/drei 에서 제공. 화면 회전, 확대, 축소 등을 마우스나 터치 이벤트로 핸들링 할 수 있게 도와주는 컨트롤러. 자세한 prop 인터페이스는 &lt;/span&gt;&lt;span&gt;&lt;a style=&quot;color: #4183c4;&quot; href=&quot;https://drei.pmnd.rs/?path=/docs/controls-orbitcontrols--docs&quot;&gt;&lt;span&gt;스토리북 링크&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span&gt;에서 확인할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;axesHelper&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt; : @react-three/fiber 에서 제공. x, y, z 축을 각각 빨강, 초록, 파랑 색으로 그려서 각 축의 방향을 알기 쉽게 해준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;gridHelper&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt; : @react-three/fiber 에서 제공. 화면에 그리드를 그려준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;useControls&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt; : leva 라이브러리에서 제공. 3D 개체에 적용할 수 있는 숫자 값을 쉽게 조절할 수 있는 GUI 를 제공한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dust&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;const color = useControls({
 &amp;nbsp; &amp;nbsp;value: 'green'
  })
​
const grid = useControls({
 &amp;nbsp;segment: {value: 10, min: 2, max: 100, step: 1}
})
​
return (
&amp;lt;Canvas&amp;gt;
  &amp;lt;color attach={'background'} args={[color.value]} /&amp;gt;
 &amp;nbsp;&amp;lt;OrbitControls 
 &amp;nbsp; &amp;nbsp;minAzimuthAngle={-Math.PI / 4} // 최소 X 축 회전 각도
 &amp;nbsp; &amp;nbsp;maxAzimuthAngle={Math.PI / 4} &amp;nbsp;// 최대 X 축 회전 각도
 &amp;nbsp; &amp;nbsp;minPolarAngle={Math.PI / 6}    // 최소 Y 축 회전 각도
 &amp;nbsp; &amp;nbsp;maxPolarAngle={Math.PI - (Math.PI / 6)} // 최대 Y 축 회전 각도
 &amp;nbsp;/&amp;gt;
 &amp;nbsp;&amp;lt;axesHelper args={[6]} /&amp;gt; // x,y,z 축 가이드라인 길이 (단위: m)
  &amp;lt;gridHelper args={[10, grid.segment]} /&amp;gt; // 그리드 크기, 분할 정도 (10m 의 그리드를 grid.segment칸으로 분할)
&amp;lt;/Canvas&amp;gt;
)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Object3D&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Object 를 변환하는 방법에는 크게 3가지 방법이 있다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Position&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Rotation&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Scale&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Geometry, Material 은 위의 변환 방법을 사용하지 못한다. 왜냐하면 Geometry, Material 은 Object3D 를 상속받지 않았기 때문이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;mesh ref={boxRef} 
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;position={[0, 0, 0]}
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;scale={[1, 1, 1]}
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;rotation={[0, Three.MathUtils.degToRad(45), 0]}
 &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;gt;
 &amp;nbsp;&amp;lt;boxGeometry /&amp;gt;
 &amp;nbsp;&amp;lt;meshStandardMaterial color={'red'} /&amp;gt;
&amp;lt;/mesh&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;export declare type MeshProps = Object3DNode&amp;lt;THREE.Mesh, typeof THREE.Mesh&amp;gt;;
export declare type BoxGeometryProps = BufferGeometryNode&amp;lt;THREE.BoxGeometry, typeof THREE.BoxGeometry&amp;gt;; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;
export declare type MeshStandardMaterialProps = MaterialNode&amp;lt;THREE.MeshStandardMaterial[THREE.MeshStandardMaterialParameters]&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote style=&quot;background-color: #ffffff; color: #777777; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;@react-three/fiber 라이브러리 내부 코드를 보면, Mesh 는 Object3DNode 타입을 상속받았지만, Geometry 와 Material 은 각각 BufferGeometryNode, MaterialNode 타입을 상속받은 걸 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;group 컴포넌트로 묶은 컴포넌트 목록은 Object3D 방식으로 변환했을 때, 함께 변환된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Scene 은 World 좌표계, Object는 Local 좌표계를 갖고 있다. 위에서 살펴봤던 axesHelper를 mesh 안에 넣어보면 확인할 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dust&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt; &amp;nbsp;const { scene } = useThree();
​
​
 &amp;nbsp;scene.rotation.x = Three.MathUtils.degToRad(45); // World 좌표계에 대한 x 축 회전
 
​
 &amp;nbsp;return (
 &amp;nbsp; &amp;nbsp;&amp;lt;&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;directionalLight position={[5,5,5]} /&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;group position={[0, 0, 0]} rotation={[0, 2, 0]}&amp;gt; {/* group 내부의 Local 좌표계에 대한 y 축 회전 */}
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;mesh ref={boxRef}
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;scale={[1, 1, 1]}
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;boxGeometry /&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;meshStandardMaterial color={'red'} /&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;/mesh&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;mesh ref={boxRef} 
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;scale={[1, 1, 1]}
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;position={[2, 0, 0]}
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;boxGeometry /&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;meshStandardMaterial color={'blue'} /&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;/mesh&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;mesh ref={boxRef} 
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;scale={[1, 1, 1]}
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;position={[0, 2, 0]}                        {/* 초록색 박스의 Local 좌표계에 대한 y 축 회전 */}
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;boxGeometry /&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;axesHelper args={[3]} /&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;meshStandardMaterial color={'green'} /&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;/mesh&amp;gt;
 &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;lt;/group&amp;gt;
 &amp;nbsp; &amp;nbsp;&amp;lt;/&amp;gt;
  )&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Geometry&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;위에서 Object3D 인 Mesh는 Geometry 와 Material 로 이루어져 있다고 설명했다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Geometry 는 모양. 형태, 구조, 모델링&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;물체가 구성되는 순서는 점 -&amp;gt; 선 -&amp;gt; 면&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;따라서 Geometry 의 최소 단위는 점 3개( = 선 3개) 가 모인 삼각형 면이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;모든 3D 물체는 삼각형 면이 모여서 이루어진다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;작업자의 모델링 편의성을 위해서 사각형 면으로 모델링하는 경우가 많으나, 실제로 컴퓨터에는 삼각형 면으로 입력된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;모든 Geometry 는 BufferGeometry 를 상속받는다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;background-color: #f8f8f8; text-align: left;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #770088;&quot;&gt;export&lt;/span&gt; &lt;span style=&quot;color: #770088;&quot;&gt;declare&lt;/span&gt; &lt;span style=&quot;color: #770088;&quot;&gt;type&lt;/span&gt; &lt;span&gt;BoxGeometryProps&lt;/span&gt; &lt;span style=&quot;color: #981a1a;&quot;&gt;=&lt;/span&gt; &lt;span&gt;BufferGeometryNode&lt;/span&gt;&lt;span style=&quot;color: #981a1a;&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;THREE&lt;/span&gt;.&lt;span&gt;BoxGeometry&lt;/span&gt;, &lt;span style=&quot;color: #770088;&quot;&gt;typeof&lt;/span&gt; &lt;span style=&quot;color: #000000;&quot;&gt;THREE&lt;/span&gt;.&lt;span style=&quot;color: #000000;&quot;&gt;BoxGeometry&lt;/span&gt;&lt;span style=&quot;color: #981a1a;&quot;&gt;&amp;gt;&lt;/span&gt;;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;Event&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-mark=&quot;*&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Three.js 에서는 카메라에서 가까운 쪽 부터 이벤트가 발생된다. 따라서 화면에서 클릭이벤트가 걸린 두 물체가 겹쳐있으면 카메라에 가까운 물체부터 이벤트가 발생함. (일반적인 HTML 구조에서는 이벤트가 발생한 가장 안쪽 객체부터 이벤트가 발생하는 것과 반대임)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;raycaster 가 이런 이벤트전파의 주된 역할&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;후기&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이전에 MAYA 라는 3D 툴을 공부한적이 있었는데, 그 때 배웠던 개념들이 다시 떠올라서 반가운 기분이 들었다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;수업에서는 개념 설명 이후 작은 프로젝트를 만드는 실습이 이어졌는데, 따라해보면서 느낀 점은 기존에 접하던 프론트엔드 개발과는 느낌이 많이 다르다는 것이었다. 기존의 프론트엔드 개발은 부모 컴포넌트와 자식 컴포넌트간의 관계가 데이터 구조, 스타일 등에 영향을 주는 느낌이었다면 , 3D 웹개발은 모든 객체가 서로 독립적으로 존재한다는 느낌을 받았다. (물론 아직 찍먹만 해본 수준이라 뭘 모르고 하는 얘기일 수도..)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;아무튼 개발을 배우기 전부터 3D 에 관심이 있었어서, 블로그에 조금씩 3D 프로젝트를 적용해볼 생각이다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;레퍼런스&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://www.udemy.com/course/react-three-fiber-r3f/&quot;&gt;https://www.udemy.com/course/react-three-fiber-r3f/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;해당 콘텐츠는 유데미로부터 강의 쿠폰을 제공받아 작성되었습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <author>준영(Junzero)</author>
      <guid isPermaLink="true">https://til-dev.tistory.com/9</guid>
      <comments>https://til-dev.tistory.com/9#entry9comment</comments>
      <pubDate>Sun, 31 Mar 2024 09:36:39 +0900</pubDate>
    </item>
    <item>
      <title>SOLID 5대 원칙 - SRP</title>
      <link>https://til-dev.tistory.com/8</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SOLID 5대 원칙이란, 객체 지향 프로그래밍을 할 때 지켜야 할 원칙으로 자주 언급되곤 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 그 중 &lt;b&gt;Single Responsibility Principle, 단일 책임 원칙&lt;/b&gt;에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하나의 모듈은 하나의 책임을 가져야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 단언에 가까운 문장을 볼 때, 반례를 떠올려 보는 것이 해당 주장에 힘을 실어준다고 생각하는 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 위의 문장과 반대되는 예시를 떠올려보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 게시글을 저장하는 경우를 예시로, 최대한 간소화해서 표현하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 입력된 데이터가 게시글 저장에 필요한 데이터 형식에 맞는지 검사한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 저장소에 게시글을 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 위 두 로직을 작성한 코드가 하나의 함수 안에 들어가 있다면, 아래와 같은 모습일 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1699914533414&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ArticleService {

	ArticleRepository articleRepository = ArticleRepository.getInstance();
	
	public void saveArticle(String title, String contents) {
        	if(title.length &amp;gt; 30) { 
            // 제목 최대 길이 에러!
            return;
        }
	        if(contents.length &amp;gt; 200) {
        	// 콘텐츠 최대 길이 에러!
            return;
        }
        
        article = new Article(title, contents);
        
        
        articleRepository.save(article);
        
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다시피 현재 게시글 제목의 최대 길이 제한은 30자 이하이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 만약 게시글 제목의 최대 길이 제한을 50자 이하로 늘리고, 경고 메시지도 바꿔달라는 요청이 들어오면 어떨까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼핏 보기에는 title.length &amp;gt; 30 부분의 조건문을 약간 수정하고, 조건문 바디의 에러 메시지도 살짝만 바꾸면 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이러한 해결 방법의 문제점은, 수정한 부분이 정상작동하는지 테스트를 시행할 때 드러나게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;게시글 제목의 최대 길이 제한을 변경했으나, 게시글 작성까지 테스트를 해봐야 하는 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문장에서 무언가 위화감을 느꼈으면 좋겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 위의 게시글 저장 로직에서 게시글 제목 길이 제한과 약간의 에러 메시지 외에는 아무것도 변경하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 게시글을 저장하는 기존의 다른 로직은 아무런 문제없이 실행될 것이라는 걸 99% 확신할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 실제로 해당 변경사항이 잘 동작하는지 확인해보려면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목 길이 검사부터 콘텐츠 길이 검사, 저장소에 게시글을 저장하는 것 까지의 모든 과정을 실행해봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이런 문제가 발생했을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;saveArticle -&amp;gt; 게시글 저장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'게시글 저장' 이라는 우리의 목표를 다시 한번 떠올려보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 고려했던 모든 사소한 조건이나 제한 등은 잠시 잊고, 우리가 원하는 것은 '게시글' 을 '저장'하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 메서드 이름도 saveArticle 로 지었지 않았는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, saveArticle 은 게시글을 저장하는 단 하나의 책임에만 집중해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하나의 모듈을 변경하는 이유는 단 하나여야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드대로라면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시글 제목 최대 길이 제한을 변경하는 작업도 게시글 저장 메서드 안에서 코딩 해야하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소에 게시글을 저장하는 작업에 대해 변경사항이 있을 때에도 게시글 저장 메서드 안에서 코딩해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 하나의 메서드를 변경하는 이유가 점점 많아질수록, 어느 순간에는 두 코드가 섞이게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 문제가 발생했을 때 디버깅을 어렵게 하는 주된 이유가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개발자마다 '단일 책임' 이라고 느끼는 범위가 다를 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 내용을 정리해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 모듈은 하나의 책임을 가져야 한다는 주장으로 시작해서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 모듈을 변경하는 이유는 단 하나여야 디버깅과 수정에 용이한 코드가 된다는 것까지 이어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어찌보면 매우 심플한 원칙인 것 같으나, 실무에 적용할 때에는 분명히 어려움이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 게시글 저장 코드를 보고 누군가는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'게시글을 저장할 때 입력값 검사는 무조건 해야 하니까, saveArticle의 책임이라고 볼 수도 있지 않나?' 라고 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 간단한 예시에도 개발자마다 이견이 있을 수 있는데, 더욱 크고 복잡한 실무 프로젝트에서는 이러한 의견 충돌이 더욱 빈번할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의견 충돌이 일어나게 되는 이유를 한 번 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;반대의 케이스 : 낮은 응집도&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 모듈은 하나의 변경 사유만 가져야 하므로, 모든 모듈을 최대한 잘게 나누면 원칙도 지키고 동료와 논쟁할 필요도 없지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이는 관심사가 같음에도 불구하고 서로 모여있지 않아서, 코드를 수정할 때 어느 곳을 고쳐야 하는지 알 수 없게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 봤던 게시글 저장에서 처리해야 하는 부가 기능은 입력값 검사 하나 뿐이라서 이를 분리했을 때 찾기 어렵지 않겠지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부가 기능이 10개쯤 되고, 이들이 모두 완벽하게 분리되어 있다면, 수정이 필요할 때 어느 곳부터 찾아봐야 할 지 난감할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 SRP 에 입각해 모듈을 설계하되, 응집도가 너무 떨어지지 않게 균형을 유지하는 것이 가장 어렵고도 중요한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>준영(Junzero)</author>
      <guid isPermaLink="true">https://til-dev.tistory.com/8</guid>
      <comments>https://til-dev.tistory.com/8#entry8comment</comments>
      <pubDate>Tue, 14 Nov 2023 08:30:54 +0900</pubDate>
    </item>
    <item>
      <title>React에서의 체크박스, State</title>
      <link>https://til-dev.tistory.com/7</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;리액트에서&amp;nbsp;체크박스의&amp;nbsp;체크&amp;nbsp;여부에&amp;nbsp;따라&amp;nbsp;카운트&amp;nbsp;상태를&amp;nbsp;+1&amp;nbsp;Or&amp;nbsp;-1&amp;nbsp;해줄&amp;nbsp;일이&amp;nbsp;있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;변경 전&lt;/h3&gt;
&lt;pre id=&quot;code_1699787230753&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const handleCheck = () =&amp;gt; {
	setIsChecked(!isChecked);
}

return (
	&amp;lt;input type=&quot;checkbox&quot; onChange={handleCheck} /&amp;gt;
  )

useEffect(() =&amp;gt; {
	isChecked ? 
         setSelectedIssues(selectedIssues + 1) :
         setSelectedIssues(selectedIssues - 1);
}, [isChecked]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;체크박스가&amp;nbsp;체크될&amp;nbsp;때마다&amp;nbsp;`isChecked`(boolean)&amp;nbsp;의&amp;nbsp;상태를&amp;nbsp;바꾸고,&lt;br /&gt;useEffect는&amp;nbsp;`isChecked`를&amp;nbsp;지켜보다가&amp;nbsp;isChecked의&amp;nbsp;true/false에&amp;nbsp;따라&lt;br /&gt;`SelectedIssues`(number)&amp;nbsp;의&amp;nbsp;상태를&amp;nbsp;+1&amp;nbsp;/&amp;nbsp;-1&amp;nbsp;해주었었다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제는?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방법의 문제는 이 체크박스를 갖고 있는 컴포넌트(A-1)의 부모(A-0)에서도 Recoil State인 `selectedIssues` 를 사용하는 경우 A-0 컴포넌트 렌더링에 따라 A-1 컴포넌트가 리렌더링 되어 useEffect에서 `isChecked` 를 다시 읽는다는 점이다.&lt;br /&gt;즉 A-1 컴포넌트가 맨 처음 렌더링 될 때, `selectedIssues` 의 값이 0 으로 들어오더라도 A-1 컴포넌트의 useEffect 에 의해 `selectedIssues`는 -1 로 세팅되어 버린다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경 후&lt;/h3&gt;
&lt;pre id=&quot;code_1699787349073&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const handleCheck = () =&amp;gt; {
	setIsChecked(!isChecked);
	isChecked ? 
         setSelectedIssues(selectedIssues - 1) :
         setSelectedIssues(selectedIssues + 1);
};
};

return (
   &amp;lt;input type=&quot;checkbox&quot; onChange={handleCheck} /&amp;gt;
  )&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;체크박스의 onChange 이벤트인 `handleCheck` 함수 안에서 `isChecked` 의 상태 뿐만 아니라, `selectedIssues` 의 상태도 함께 바꿔줬다.&lt;br /&gt;사용자가 체크박스를 클릭하는 순간에 카운트 상태가 늘거나 줄어야한다. 체크박스의 현재 체크 여부에 따라 카운트 상태를 늘리고 줄이는 것은 원래의 의도에 맞지 않다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회고&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기능 개발을 서둘러 하려다 보면 이처럼 단순한 로직 구현도 못하는 경우가 있다. 뭔가를 개발하기 전에 내가 무엇을 만들건지, 왜 이걸 만드는지, 어떻게 만들건지 이 세가지를 천천히 생각해보고 만들어보는 습관을 들이자.&lt;/p&gt;</description>
      <author>준영(Junzero)</author>
      <guid isPermaLink="true">https://til-dev.tistory.com/7</guid>
      <comments>https://til-dev.tistory.com/7#entry7comment</comments>
      <pubDate>Sun, 12 Nov 2023 20:10:46 +0900</pubDate>
    </item>
    <item>
      <title>HTTP 완벽 가이드 : 웹 프락시 정리</title>
      <link>https://til-dev.tistory.com/6</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 프락시 서버는 클라이언트의 입장에서 트랜잭션을 수행하는 중개인이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버와 클라이언트 사이에 위치해있기 때문에, 서버이면서 클라이언트이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 프락시를 구현할 때에는 둘의 역할을 모두 해내도록 구현해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게이트웨이와의 차이점은, 게이트웨이는 클라이언트와 서버가 다른 프로토콜로 통신하더라도 호환성을 맞춰주는 책임을 지는 반면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프락시는 중개 대상인 클라이언트와 서버가 같은 프로토콜을 사용하는 경우를 처리하는 책임을 진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 현실 세계에서의 필요에 의해 프로토콜 호환성을 맞춰주는 게이트웨이로서의 기능을 제공하기도 하므로, 사실상 둘의 차이점은 모호하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프락시를 사용하는 이유&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프락시는 중개자로서의 역할을 갖고 있기 때문에,&amp;nbsp; 중개자의 책임이기만 하다면 어떤 일이든 할 수 있는데, 주로 다음과 같은 이유로 쓰인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 접근 제어를 통한 보안&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 캐싱, 대리 웹서버를 통한 성능 향상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 비용 절약&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 트래픽 필터링 및 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프락시의 위치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프락시는 중개자로서 다양한 위치에 배치될 수 있는데, 다음은 그 예시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 출구 프락시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬과 인터넷 사이를 오가는 트래픽 제어를 위해 로컬 네트워크의 출구에 프락시를 위치시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해킹 방화벽, 미성년자 필터링 등이 이에 해당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 입구 프락시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ISP(Internet Service Provider)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;서버 근처에 프락시를 위치시켜 사용자들의 다운로드 속도를 개선하거나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많이 찾는 문서의 사본을 저장하는 캐싱 전략에도 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 대리 프락시 (리버스 프락시)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서버 바로 앞에 위치하여 웹 서버의 대리자처럼 행동하는 프락시이다. 대리하고자 하는 대상인 웹 서버의 이름과 IP 주소로 위장하기 때문에, 모든 요청은 서버가 아닌 이 프락시로 가게 된다. 주로 웹 서버로 향하는 요청을 일부 필터링 하거나, 캐싱, 보안 등의 목적으로 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프락시의 갯수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프락시는 클라이언트와 서버 사이에 여러 개 배치될 수 있을 뿐만 아니라, 같은 계층에도 여러 프락시가 존재할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(ex : 클라이언트 -&amp;gt; 프락시 1 -&amp;gt; 프락시 2-a OR 프락시 2-b OR 프락시 2-c -&amp;gt; 프락시 3 -&amp;gt; 서버 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Via 헤더를 이용한 프락시 추적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프락시의 갯수가 많아지다보면, 패킷이 어느 프락시를 거쳐서 전달되었는지 추적이 필요할 때가 있는데, 이때 사용하는 것이 Via 헤더이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Via 헤더는 메시지가 지나는 중간 노드의 정보를 쉼표를 기준으로 보여준다. 예를 들어,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Via: &lt;span style=&quot;background-color: #ffffff; color: #303942; text-align: start;&quot;&gt;1.1 google, 1.1 naver&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 헤더가 네트워크 요청에 있다면, 해당 요청은&amp;nbsp; HTTP 1.1 프로토콜을 사용하는 google 프록시를 지나 HTTP 1.1 프로토콜을 사용하는 naver 프록시를 지나 원 서버에 보내진 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이 헤더에 들어간 프락시 이름은 보안상의 이유로 원래의 호스트 이름이 아닌 가명을 사용하거나,  여러 프록시 이름을 하나로 합칠 수 있다는 점에서 100% 신뢰할 수 있는 건 아니지만, 일반적으로 프락시는 각 Via 항목을 유지하려 노력해야 한다.&lt;/p&gt;</description>
      <author>준영(Junzero)</author>
      <guid isPermaLink="true">https://til-dev.tistory.com/6</guid>
      <comments>https://til-dev.tistory.com/6#entry6comment</comments>
      <pubDate>Sun, 5 Nov 2023 19:07:50 +0900</pubDate>
    </item>
    <item>
      <title>싱글톤 패턴을 통해 배운 코드 통제의 중요성</title>
      <link>https://til-dev.tistory.com/5</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 지향 프로그래밍을 할 때, 객체를 생성하는 대표적인 방법은 클래스를 이용하여 생성하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 클래스로 Human 객체를 생성하는 코드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1698550068163&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Human {
	public String name;
    	public int age;
    
        public Human (String name, int age) {
            this.name = name;
            this.age = age;
        }
}


Human randomGuy = new Human(&quot;simon&quot;, 10);
Human randomGirl = new Human(&quot;marry&quot;, 12);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시에서 알 수 있는 점은, 두 Human 객체가 서로 다른 이름과 나이를 가지고 있다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 사람이니 각자만의 이름과 나이를 가지고 있는게 당연해 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 사람을 대상으로 했기 때문에, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Human 객체가 여럿 생성되는 것이 어색하지 않고, 사람 앞에&amp;nbsp;&lt;/span&gt;'서로 다른' 이라는 표현이 들어가는것 또한 이상할 게 없어 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, 이런 객체지향 프로그래밍 세상에 태양이 들어간다면 어떨까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 개념을 그대로 태양에 도입할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 이 우주에 태양은 단 하나만 존재하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'서로 다른' 이라는 형용사가 태양 이라는 고유명사 앞에 붙는 건 말이 되지 않는다. (심지어 '고유' 명사 라는 표현을 쓴다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스는 기본적으로 생성자를 통해 여러 객체를 생성할 수 있도록 디자인되어 있지만, 태양의 예시처럼 시스템 내에 단 하나의 객체만 존재해야 하는 경우에는 싱글톤 패턴을 쓸 수 있다. 위에서 이야기한 태양 객체를 싱글톤 패턴을 통해 구현해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1698551088943&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Sun {
	private static Sun instance = new Sun();  // 유일하게 생성한 태양 객체
	static float degreeAsCelsius = 6000;
    
        private Sun () {	// private 외부에서 Sun 의 객체를 생성할 수 없게 억제

        }

        public Sun getInstance() { // 유일한 Sun 객체를 얻어오는 메소드
        	if(instance == null) {
	            return new Sun();
            	}
            	return instance;
        }
    
}


boolean IsSunUnique = Sun.getInstance() == Sun.getInstance() // true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sun 클래스 외부의 시스템에서는 Sun 객체를 마음대로 생성할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이미 생성된 Sun 객체를 조회하거나, 아직 존재하지 않는 Sun 객체를 딱 한 번 만들 수 있을 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 Sun 객체는 시스템 전체에 단 하나만 존재하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 패턴에 대해서 설명한 여러 글들을 보고, 내 나름대로 이해한 바를 정리해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 new 생성자() 키워드로 객체를 새로 생성하는 코드에서 뭔가 위화감을 느낀 적이 많았는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 객체가 시스템 내에서 유일해야 하는 경우에도 객체를 추가로 생성하는 실수를 막을 수 있게 해준다는 점이 맘에 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 설계가 잘 된 프로그램은 요구사항 변화에 유연하게 반응하면서도, 코드의 변경에 있어서는 최대한의 통제를 가하는 것이 중요하다고 생각하는 요즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>준영(Junzero)</author>
      <guid isPermaLink="true">https://til-dev.tistory.com/5</guid>
      <comments>https://til-dev.tistory.com/5#entry5comment</comments>
      <pubDate>Sun, 29 Oct 2023 13:01:22 +0900</pubDate>
    </item>
    <item>
      <title>SQL 기초 문법 정리</title>
      <link>https://til-dev.tistory.com/4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 문법 플레이그라운드 링크:&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.w3schools.com/mysql/trymysql.asp?filename=trysql_select_all&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.w3schools.com/mysql/trymysql.asp?filename=trysql_select_all&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1697866356542&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;MySQL Tryit Editor v1.0&quot; data-og-description=&quot;WebSQL stores a Database locally, on the user's computer. Each user gets their own Database object. WebSQL is supported in Chrome, Safari, and Opera. If you use another browser you will still be able to use our Try SQL Editor, but a different version, usin&quot; data-og-host=&quot;www.w3schools.com&quot; data-og-source-url=&quot;https://www.w3schools.com/mysql/trymysql.asp?filename=trysql_select_all&quot; data-og-url=&quot;https://www.w3schools.com/mysql/trymysql.asp?filename=trysql_select_all&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.w3schools.com/mysql/trymysql.asp?filename=trysql_select_all&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.w3schools.com/mysql/trymysql.asp?filename=trysql_select_all&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MySQL Tryit Editor v1.0&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;WebSQL stores a Database locally, on the user's computer. Each user gets their own Database object. WebSQL is supported in Chrome, Safari, and Opera. If you use another browser you will still be able to use our Try SQL Editor, but a different version, usin&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.w3schools.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 문법은 아래와 같고, 복잡한 쿼리들은 대부분 아래에서 파생되어서 사용되는 것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1697866464671&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select 컬럼명 from 테이블명 where 조건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬 조건 추가&lt;/p&gt;
&lt;pre id=&quot;code_1697866712252&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select 컬럼명 from 테이블명 order by 정렬기준컬럼 asc -- 오름차순 (default)
select 컬럼명 from 테이블명 order by 정렬기준컬럼 desc -- 내림차순&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;산출될 데이터의 갯수 제한 (페이지네이션 등에서 쓰임)&lt;/p&gt;
&lt;pre id=&quot;code_1697866876552&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select 컬럼명 from 테이블명 limit 시작, 갯수 -- (시작이 없으면 0 이 default)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬럼명 변경하여 출력&lt;/p&gt;
&lt;pre id=&quot;code_1697867102000&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select 기존컬럼명 as '새로운컬럼명' from 테이블명 where 조건
-- 새로운 컬럼명은 단순히 출력 이름만 바꾸는 것이므로, where 등에 쓰이는 이름은 기존 컬럼명을 그대로 사용해야 함&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 응용 예시&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697866404436&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from Customers; 
select CustomerName from Customers;
select * from Orders where EmployeeId = 3;
select * from OrderDetails where Quantity &amp;lt; 5;
select * from Customers order by ContactName;
select * from OrderDetails order by ProductID asc, Quantity desc;
select * from OrderDetails limit 10;
select * from OrderDetails limit 0, 10;
select * from OrderDetails limit 30, 10;
select CustomerId as ID, CustomerName as '고객명', Address as '주소' from Customers;
select 
	CustomerId as '아이디',
    CustomerName as '고객명',
    City as '도시',
    Country as '국가'
from Customers
where Country = 'Germany' or City = 'London'
order by CustomerName
limit 0, 5;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>SQL</category>
      <author>준영(Junzero)</author>
      <guid isPermaLink="true">https://til-dev.tistory.com/4</guid>
      <comments>https://til-dev.tistory.com/4#entry4comment</comments>
      <pubDate>Sat, 21 Oct 2023 14:47:04 +0900</pubDate>
    </item>
  </channel>
</rss>