<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>방법이있지</title>
        <link>https://velog.io/</link>
        <description>뭔가 만드는 걸 좋아하는 개발자 지망생입니다. 프로야구단 LG 트윈스를 응원하고 있습니다.</description>
        <lastBuildDate>Thu, 06 Nov 2025 09:53:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>방법이있지</title>
            <url>https://velog.velcdn.com/images/strawberry-tree/profile/fb56712a-ff36-42f3-a55a-1e8c88be8df3/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 방법이있지. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/strawberry-tree" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[근황]]></title>
            <link>https://velog.io/@strawberry-tree/%EA%B7%BC%ED%99%A9</link>
            <guid>https://velog.io/@strawberry-tree/%EA%B7%BC%ED%99%A9</guid>
            <pubDate>Thu, 06 Nov 2025 09:53:42 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/ae069d5b-e202-48d5-9897-de6b05dc5aab/image.jpeg" alt=""></p>
<p>다음주에 인턴으로서 첫 출근을 한다.</p>
<p>난 백엔드보다 프론트엔드를 잘 한다고 생각했는데 막상 프론트에선 다 떨어지고 백엔드쪽 일을 하게 됐다. 음 신의 뜻이라고 생각하고 백엔드도 열심히 공부해봐야겠다.</p>
<p>다소 걱정되는 건 지금까진 난 Express.js나 NestJS 위주로만 백엔드 작업을 했지만 회사는 파이썬이랑 코틀린을 쓴다는 점이다. 물론 정글때도 코치님들께서는 실제론 언어는 금방 배우니까 문제 해결능력이 더 중요하다~ 는 뉘앙스로 말씀을 많이 해 주셨다. 일단 마음가짐이 더 중요할려나 싶다.</p>
<p>면접때 매일 인천에서 서울 왔다갔다하니 너무 피곤했다. 결국엔 단기로 방을 잡게 됐다. 사당 낙성대는 너무 비싸서 결국 구로디지털단지로 가게됐다. 참 방의 위치가 묘한 게 역이름은 구로구고 실제 주소는 관악구인데 횡단보도 하나만 지나면 금천구다.</p>
<p>그리고 지금까지 정글-취준 달려오면서 운동을 너무 안 했다. 그래서인지 너무 저질체력이 되는 것 같아, 새로 자취생활 시작하면 꾸준히 헬스장 좀 가려고 한다. PT 다시 받는것도 생각중이다... 개발자에게 제일 중요한 덕목은 똑똑함도 아니고, 문제 해결력도 아니고, 소통능력도 아니고, 체력인 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[사랑해]]></title>
            <link>https://velog.io/@strawberry-tree/%EC%82%AC%EB%9E%91%ED%95%B4</link>
            <guid>https://velog.io/@strawberry-tree/%EC%82%AC%EB%9E%91%ED%95%B4</guid>
            <pubDate>Fri, 31 Oct 2025 12:44:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/db8043bb-4b24-4f18-98d1-528996bcf87b/image.jpg" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[1021] Next복습: 사전 렌더링, Hydration]]></title>
            <link>https://velog.io/@strawberry-tree/1021-Next%EB%B3%B5%EC%8A%B5-%EC%82%AC%EC%A0%84-%EB%A0%8C%EB%8D%94%EB%A7%81</link>
            <guid>https://velog.io/@strawberry-tree/1021-Next%EB%B3%B5%EC%8A%B5-%EC%82%AC%EC%A0%84-%EB%A0%8C%EB%8D%94%EB%A7%81</guid>
            <pubDate>Tue, 21 Oct 2025 12:11:40 GMT</pubDate>
            <description><![CDATA[<p>최근 코딩테스트랑 면접준비에 진이 빠져 블로그 글을 많이 못 올렸습니다. 한 군데라도 최종합격하면 더할 나위 없이 소원이 없겠군요. ㅠ</p>
<p>이번 글은 Next.js의 제일 큰 특징인 사전 렌더링을 다룹니다.</p>
<p><a href="https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs">참고: 인프런 - 한 입 크기로 잘라먹는 Next.js</a></p>
<h1 id="프레임워크-vs-라이브러리">프레임워크 vs 라이브러리</h1>
<p>Next.js는 React 라이브러리를 기반으로 한 프레임워크라고 생각하면 됩니다.</p>
<p>이때 라이브러리와 프레임워크의 차이가 헷갈리실 수 있는데, 이런 차이가 있습니다.</p>
<ul>
<li><strong>라이브러리</strong>: 기능 구현의 주도권이 개발자에 있어서 자유로움</li>
<li><strong>프레임워크</strong>: 기능 구현의 주도권이 프레임워크에 있어서, 정해놓은 대로 구현하는 것이 권장됨</li>
</ul>
<p>이를테면, React에서는 페이지 라우팅을 할 때 <a href="https://reactrouter.com">React Router</a>나 <a href="https://tanstack.com/router/latest">Tanstack Router</a> 등 별도 라이브러리 중 적당한 친구를 골라서 사용하면 됩니다. 대신 React 자체적으로는 라우팅 기능을 제공하지 않습니다. 구현할 방법은 다양하지만, 선택하는 건 개발자의 몫이죠.</p>
<p>하지만 Next.js 사용 시, 자체적으로 제공하는 Page/App Router를 통해 페이지 라우팅을 구현하는 것이 권장됩니다. 대신 별도 설정 없이 바로 사용 가능하다는 장점이 있습니다.</p>
<p>그 외에 Next.js에는 이미지나 폰트의 용량 최적화, TypeScript 기본 지원, API Routes를 통한 백엔드 서버의 기능 구현 등등 기능이 추가되어 있습니다. 그러므로 프로젝트를 단기간에 진행해야 할 때는, 주어진 기능이 많고 정해진 틀만 지키면 되는 Next.js가 훨씬 효율적입니다.</p>
<h1 id="기존-리액트-client-side-rendering">기존 리액트 Client Side Rendering</h1>
<p>기존 글에 <a href="https://velog.io/@strawberry-tree/1012-React%EB%B3%B5%EC%8A%B5-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%9D%BC%EC%9A%B0%ED%8C%85-SSR-CSR">리액트는 Client Side Rendering으로 작동한다</a>고 언급을 했었는데요,</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/def295f9-aff3-4532-8c38-dd49788b3bd3/image.png" alt=""></p>
<p>간단히 요약하자면 클라이언트에서 직접 화면을 렌더링하는 방식이였죠.</p>
<p>우선 서버는 브라우저에 내용이 없는 <code>index.html</code> 파일을 보내고, 브라우저가 이를 렌더링합니다. 일단 빈 화면만 보이겠죠.</p>
<p>하지만 그 이후 서버가 브라우저에 JS 파일을 묶은 하나의 번들 파일인 React App을 보냅니다. 브라우저는 React App을 실행하여 콘텐츠를 렌더링하게 됩니다.</p>
<p><strong>초기 접속만 되면, 페이지 이동이 빠르고 쾌적</strong>하다는 장점이 있었습니다. 페이지를 이동할 때마다, 서버를 거치지 않고 React App만 실행하면 되니까요.</p>
<p>하지만 초기 접속을 할 때, <strong>요청 시작부터 화면 렌더링 시점</strong>까지 시간이 오래 소요된다는 단점이 있습니다. 이를 <strong>First Contentful Paint(FCP)</strong>라고도 부릅니다. 일단 빈 <code>index.html</code> 파일을 받고, 서버에서 React App도 받고, 이걸 뒤늦게 렌더링하는 과정에서 초기 시간이 다소 소요됩니다.</p>
<h1 id="넥스트의-사전-렌더링">넥스트의 사전 렌더링</h1>
<p>넥스트는 이러한 CSR의 장점은 유지하면서, FCP가 느린 단점을 보완하기 위해 서버 측에서 <strong>사전 렌더링</strong>을 하는 기능을 지원합니다.</p>
<p>이때 렌더링은, 앞선 CSR처럼 <em>브라우저에 내용을 그린다</em>는 의미가 아닌, <em>서버가 JS 코드를 HTML로 변환한다</em>라는 뜻임에 유의합시다.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/e4bf39e4-ec2d-4372-ae55-b721d65da74b/image.png" alt=""></p>
<p>유저가 초기 접속을 하면, 빈 껍데기인 <code>index.html</code>만 보내주는 리액트와 다르게, 넥스트에서는 서버가 사전에 내용이 채워진 HTML 파일을 응답하도록 설정할 수 있습니다.</p>
<ul>
<li>서버가 <strong>리액트 컴포넌트 등이 포함된 JS 코드를 실행해, 이를 HTML로 변환</strong>합니다.</li>
<li>이렇게 <strong>서버가 사전에 렌더링한 HTML</strong>을 브라우저로 보냅니다.</li>
<li>브라우저는 해당 파일을 화면에 표시합니다.</li>
</ul>
<p>이 시점에서 사용자가 화면을 볼 수 있으므로, 기존 리액트에 비해 <strong>FCP가 훨씬 빨라진다</strong>는 장점이 있습니다.</p>
<h2 id="hydration">Hydration</h2>
<p>물론 HTML 파일만 있으면 버튼 클릭과 같은 상호작용적인 요소가 먹히지 않을 겁니다. 이때 넥스트는 React 기반이라는 점 다시 기억해 봅시다. 그러므로 JS 번들 파일인 React App도 여전히 존재합니다.</p>
<p>FCP 이후 서버는 브라우저에 React App을 보내게 되고, 이후 React App이 기존 HTML 코드와 연동됩니다. 이를 황무지와 같은 HTML에 물을 준다는 느낌으로, <strong>Hydration</strong>이라고도 부릅니다. 이렇게 초기 접속으로부터 사용자가 첫 상호작용을 할 수 있기까지의 시간을 <strong>TTI(Time to Interact)</strong>로도 부릅니다.</p>
<pre><code class="language-jsx">/cv 페이지              /projects 페이지
┌──────────────┐       ┌──────────────┐
│   Header    │       │   Header    │ &lt;- 유지
├──────────────┤  --&gt;  ├──────────────┤
│             │       │             │
│  CV         │       │   Projects  │ &lt;- 너만 교체
│             │       │             │
├──────────────┤       ├──────────────┤
│   Footer    │       │   Footer    │ &lt;- 유지
└──────────────┘       └──────────────┘
+ 이 과정에서 서버 요청은 이루어지지 않음</code></pre>
<p>페이지 이동을 하는 경우 역시 기존 리액트와 마찬가지로, 서버까지 가지 않고 Hydration된 React APP을 통해 브라우저가 컴포넌트를 교체하는 CSR 방식으로 이루어집니다. 즉 <strong>페이지 이동이 빠르고 쾌적하다는 React CSR의 장점이 유지된다는 것도 중요한 특징이겠죠.</strong></p>
<h2 id="hydration-error">Hydration Error</h2>
<p>가끔가다 Next.js에선 <code>Hydration Error</code>라는 친구를 마주칠 수 있습니다. 이 에러는 서버에서 생성한 HTML과, 클라이언트에서 React가 렌더링한 결과가 일치하지 않을 때 발생합니다.</p>
<ul>
<li>JSX 리턴문 내부에 서버는 이해 못하고 브라우저만 이해하는 <code>window</code>, <code>document</code>, <code>localStorage</code>가 포함되어 있거나</li>
<li>JSX 리턴문 내부에 <strong>랜덤 값</strong>이나 <strong>현재 시간</strong>처럼, 서버 생성 / 클라이언트 렌더링 시점에 달라질 수 있는 값이 있거나</li>
<li><code>&lt;p&gt;</code> 안에 <code>&lt;div&gt;</code>를 넣는 등 HTML을 잘못 중첩했거나 (서버는 수정 안하는데, 브라우저는 자동 수정해서 불일치 발생)</li>
</ul>
<p>할 때 발생합니다. 기본적으로 서버는 컴포넌트의 JSX 리턴분에 있는 HTML을 사전 렌더링하니까, 거기에 문제되는 값이 들어가면 안 됩니다.</p>
<p>즉 랜덤 값이나 <code>window</code> 같은 브라우저 전용 코드는, 서버가 사전 렌더링하지 않는 <code>useEffect</code>를 통해 처리하면 이런 오류를 막을 수 있습니다. 서버가 렌더링 안하니까, 불일치가 뜰 일도 없겠죠.</p>
<pre><code class="language-js">// 서버엔 window가 없으므로, JSX 리턴문에 넣으면 안됨
function Component() {
  return &lt;div&gt;{window.innerWidth}&lt;/div&gt;  // 서버에는 window가 없음!
}

// useState로 분리해 놔야 함
function Component() {
  const [width, setWidth] = useState(null);

  useEffect(() =&gt; {
    setWidth(window.innerWidth);  // 브라우저에서만 실행
  }, []);

  return &lt;div&gt;{width || &#39;로딩중...&#39;}&lt;/div&gt;;
}</code></pre>
<h1 id="사전-렌더링의-여러가지-방식">사전 렌더링의 여러가지 방식</h1>
<p>재미있게도 Next.js에서 사전 렌더링을 할 수 있는 방법도 하나가 아닙니다. 좀 많습니다.</p>
<ul>
<li><strong>SSG(Static Site Generation)</strong>: 웹사이트를 빌드할 때 HTML을 미리 생성하고, 요청이 오면 서버가 미리 만들어진 HTML을 바로 전달합니다.<ul>
<li>즉 요청이 오기 전에 이미 완성본이 있으므로, 아주 빠릅니다.</li>
<li>블로그 내용이나 문서처럼 내용이 자주 바뀌지 않을 때 적당하겠죠.</li>
</ul>
</li>
<li><strong>SSR(Server Side Rendering)</strong>: 요청 시점마다 서버가 HTML을 생성합니다.<ul>
<li>매 요청마다 렌더링하므로 SSG보단 느리지만, 경매 사이트처럼 실시간 데이터를 API로 받아오는 페이지 같은 경우에는 이런 방식을 사용해야겠죠.</li>
</ul>
</li>
<li><strong>ISR(Incremental Static Regeneration)</strong>: 빌드 타임에 HTML을 생성하되, 일정 시간마다 백그라운드에서 재생성<ul>
<li>주기적으로 업데이트되는 사이트에서는 이걸 쓰면 좋습니다. 뉴스라든가, 전날 야구 스코어라든가를 이렇게 처리할 수 있겠네요.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[1012] React복습: 페이지 라우팅, SSR, CSR]]></title>
            <link>https://velog.io/@strawberry-tree/1012-React%EB%B3%B5%EC%8A%B5-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%9D%BC%EC%9A%B0%ED%8C%85-SSR-CSR</link>
            <guid>https://velog.io/@strawberry-tree/1012-React%EB%B3%B5%EC%8A%B5-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%9D%BC%EC%9A%B0%ED%8C%85-SSR-CSR</guid>
            <pubDate>Sun, 12 Oct 2025 14:44:24 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8">참고: 한 입 크기로 잘라 먹는 리액트 - 인프런</a></p>
<p>SSR은 Song Sang Rok이 아니라 Server Side Rendering을 뜻합니다.</p>
<h1 id="페이지-라우팅">페이지 라우팅</h1>
<p>라우팅이란 사용자가 요청한 경로에 따라 적절한 페이지를 보여주는 기능입니다.</p>
<p>이를테면, <a href="https://strawberry-tree.github.io/cv">https://strawberry-tree.github.io/cv</a>로 접속하면 CV 페이지가, <a href="https://strawberry-tree.github.io/projects">https://strawberry-tree.github.io/projects</a>로 접속하면 프로젝트 페이지를 보여줘야겠죠.</p>
<p>라우팅이 이루어지는 원리에 따라, 웹사이트는 <strong>MPA(multi page application)</strong>과 <strong>SPA(single page application)</strong>으로 나눌 수 있습니다.</p>
<h1 id="mpamulti-page-application">MPA(Multi Page Application)</h1>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/b2b1d0e9-6e02-4a0d-9c21-b1b4b9bb7ad6/image.png" alt=""></p>
<p>MPA 방식의 웹사이트에선, <strong>서버에 이미 여러 개의 완성된 html 페이지가 존재</strong>합니다. 브라우저가 경로를 요청하면 서버가 해당 페이지를 찾아 반환하게 됩니다.</p>
<h2 id="ssr-server-side-rendering">SSR (Server Side Rendering)</h2>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/c40ead82-6069-43d9-94c5-867d8d7e7e73/image.png" alt=""></p>
<p>일반적으로 MPA 방식에서 페이지 내용을 화면에 렌더링하는 데는 SSR이 사용되는데요, 이는 <strong>서버가 렌더링을 한다는 뜻</strong>입니다. 서버가 미리 완성된 html 파일을 만들어 브라우저에 전송하고, 브라우저가 그대로 화면에 띄우게 됩니다.</p>
<p>이때 MPA와 SSR은 흔히 함께 쓰이지만 동일 개념은 아닙니다. <strong>MPA</strong>는 서버에 여러 페이지가 존재하는 <strong>구조</strong>, <strong>SSR</strong>은 그 페이지를 서버에서 완성해서 보내주는 <strong>렌더링 방식</strong>을 의미합니다.</p>
<h2 id="mpa-방식의-단점">MPA 방식의 단점</h2>
<p>MPA 방식은 쾌적한 페이지 이동 경험에 방해가 된다는 단점이 있습니다. 그 이유를 2가지로 정리해 보자면,</p>
<h3 id="1-비효율적인-페이지-이동">1. 비효율적인 페이지 이동</h3>
<pre><code class="language-jsx">/cv 페이지              /projects 페이지
┌──────────────┐       ┌──────────────┐
│   Header    │       │   Header    │ &lt;- 동일한데 재렌더링
├──────────────┤  --&gt;  ├──────────────┤
│             │       │             │
│  CV         │       │   Projects  │ &lt;- 재렌더링
│             │       │             │
├──────────────┤       ├──────────────┤
│   Footer    │       │   Footer    │ &lt;- 동일한데 재렌더링
└──────────────┘       └──────────────┘</code></pre>
<p>MPA 방식에서는 페이지를 이동할 때마다, 화면 전체에 렌더링된 내용이 모두 제거되고, <strong>새로운 페이지를 처음부터 다시 렌더링됩니다</strong>.</p>
<p>그러니까 헤더, 푸터 등 모든 페이지에 들어가는 공통 요소도 매번 다시 그려지므로 비효율적입니다. 렌더링 과정에서 페이지 전체가 한번 깜빡이는 게 이런 이유 때문이죠. </p>
<h2 id="2-서버-부하-증가">2. 서버 부하 증가</h2>
<p>MPA 방식에서는 페이지를 이동할 때마다 <strong>서버에 새 페이지를 요청합니다</strong>. 즉, 사용자가 많아질수록 서버 부하가 심해집니다.</p>
<h1 id="spa-single-page-application">SPA (Single Page Application)</h1>
<p>React는 페이지 이동 시 MPA의 단점을 해결하기 위해, <strong>SPA 방식</strong>을 채택했습니다.</p>
<p>SPA의 경우 <strong>서버에 페이지가 단 하나 (<code>index.html</code>) 존재</strong>합니다. 추가로 React 컴포넌트나 각종 유틸리티 함수가 담긴 별도의 JavaScript 파일들도 존재합니다.</p>
<p><strong>절대로 사용자가 접근할 수 있는 페이지가 하나라는 뜻이 아닙니다.</strong> (저도 맨 처음에 이게 헷갈려서 리액트로는 페이지를 하나 밖에 못 만드나? 싶은 줄 알았습니다.)</p>
<h2 id="csr-client-side-rendering">CSR (Client Side Rendering)</h2>
<p>사용자가 어떤 경로로 접속하든, 이러한 과정을 따릅니다.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/3d99d7ab-71ab-4e12-b3ca-38ede0f176c1/image.png" alt=""></p>
<ul>
<li><strong>1단계</strong> 서버는 항상 <code>index.html</code>을 반환하고, 브라우저는 먼저 <code>index.html</code>을 렌더링한다<ul>
<li>이때 <code>index.html</code>엔 JS 파일을 실행하는 태그만 있습니다. 즉 <code>index.html</code>엔 아무 내용이 없어, 아주 잠깐 빈 화면이 뜨게 됩니다.</li>
</ul>
</li>
<li><strong>2단계</strong> 서버는 모든 React 컴포넌트/유틸리티에 사용되는 JS 파일들을 하나의 <strong>번들 파일</strong>로 묶어 브라우저로 전달한다<ul>
<li>이러한 <strong>번들링</strong> 과정을 Vite 같은 친구들이 해 줍니다</li>
<li>이러한 번들 파일을 <strong>React 앱</strong>으로도 부릅니다</li>
</ul>
</li>
<li><strong>3단계</strong> 브라우저는 번들 파일을 실행하며, <code>&lt;App&gt;</code> 컴포넌트부터 순서대로 렌더링한다</li>
</ul>
<p>이와 같이 서버가 아니라 브라우저가 화면에 뜨는 요소를 렌더링하며, <strong>이를 CSR(Client-Side Rendering)이라 합니다.</strong> MPA와 SSR의 관계랑 비슷하게, SPA는 페이지 구조, CSR은 렌더링 방식이고 동일한 개념이 아님에 유의합시다.</p>
<h2 id="페이지-이동-시">페이지 이동 시</h2>
<pre><code class="language-jsx">/cv 페이지              /projects 페이지
┌──────────────┐       ┌──────────────┐
│   Header    │       │   Header    │ &lt;- 유지
├──────────────┤  --&gt;  ├──────────────┤
│             │       │             │
│  CV         │       │   Projects  │ &lt;- 너만 교체
│             │       │             │
├──────────────┤       ├──────────────┤
│   Footer    │       │   Footer    │ &lt;- 유지
└──────────────┘       └──────────────┘
+ 이 과정에서 서버 요청은 이루어지지 않음</code></pre>
<p>기존 MPA 방식에서는 페이지를 이동할 때마다 전체 페이지를 없애고 다시 렌더링해야 했지만, SPA 방식에서는 <strong>React 앱(JS 번들 파일)을 이용해 브라우저가 자체적으로 필요한 컴포넌트만 교체</strong>합니다.</p>
<p>그러므로 헤더, 푸터 등 모든 페이지에서 동일한 컴포넌트는 그대로 남아 있고, 바뀌는 컴포넌트만 즉시 불러와 화면에 보여주게 됩니다.</p>
<p>결국에는 <strong>페이지 이동이 매끄럽고 빠르며</strong>, 서버에도 요청이 전혀 가지 않기 때문에 <strong>서버 부하도 줄어듭니다</strong>.</p>
<h2 id="실전에서는">실전에서는</h2>
<p>흔히는 <code>React Router</code> 같은 패키지로 페이지 라우팅을 구현합니다. 이런 패키지는 <strong>(1)컴포넌트가 현재 브라우저의 주소를 읽어올 수 있게</strong>, 그리고 <strong>(2)컴포넌트가 주소의 변화를 감지할 수 있게</strong> 처리해 줍니다.</p>
<pre><code class="language-jsx">function App() {
  return (
    &lt;&gt;
      &lt;Routes&gt;
        &lt;Route path=&quot;/&quot; element={&lt;Home /&gt;} /&gt;
        &lt;Route path=&quot;/new&quot; element={&lt;New /&gt;} /&gt;
        &lt;Route path=&quot;/diary&quot; element={&lt;Diary /&gt;} /&gt;
        &lt;Route path=&quot;*&quot; element={&lt;NotFound /&gt;} /&gt;
      &lt;/Routes&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>얘를 들어 이런 코드에서는, 주소가 <code>/new</code> 에서 <code>/diary</code>로 바뀌면 <code>App</code> 컴포넌트 내부의 <code>New</code> 컴포넌트가 <code>Diary</code> 컴포넌트로 교체되게 됩니다.</p>
<h1 id="ssr-csr-모두-지원하는-nextjs">SSR, CSR 모두 지원하는 <code>Next.js</code></h1>
<p>특이하게 저의 최애 프레임워크인 Next.js 같은 경우는 SSR과 CSR의 장점을 모두 활용할 수 있습니다.</p>
<p>첫 페이지 로드는 SSR로 완성된 HTML을 받아오고, 이후 페이지 이동은 CSR 방식으로 필요한 부분만 바꿔주는 식이죠.</p>
<p>예를 들어 프로젝트 목록 페이지로 이동한다면...</p>
<p><strong>1단계</strong> 사용자가 <code>/projects</code>로 이동하면, 필요한 <code>&lt;ProjectsPage&gt;</code> 컴포넌트만 불러옵니다. 헤더, 푸터 등 공통 컴포넌트는 그대로 유지됩니다.</p>
<p><strong>2단계</strong> <code>&lt;ProjectsPage&gt;</code> 안에 <code>&lt;ProjectList&gt;</code>라는 서버 컴포넌트(Server Component)가 있다고 합시다.</p>
<ul>
<li>이 <code>&lt;ProjectList&gt;</code>는 DB에서 프로젝트 목록 데이터를 가져와야 합니다.</li>
<li>서버가 DB에서 데이터를 미리 조회해서, <code>&lt;ProjectList&gt;</code>를 완성된 HTML 형태로 만들어 보내줍니다.</li>
</ul>
<p><strong>3단계</strong> 브라우저가 HTML을 받으면, 로딩 화면 없이 바로 프로젝트 목록이 표시됩니다.</p>
<h1 id="순수-csr과-비교">순수 CSR과 비교</h1>
<p>일반 React(순수 CSR)에서는:</p>
<ul>
<li>빈 화면 렌더링 → 브라우저가 API 호출 → 로딩 표시 → 내용 표시</li>
</ul>
<p>Next.js의 SSR에서는:</p>
<ul>
<li>서버가 미리 데이터 조회 + 렌더링 → 완성된 화면 바로 표시</li>
</ul>
<p>물론 서버에서 렌더링하는 시간도 걸리긴 하지만, 서버-DB 간 통신이 빠른 경우가 많아서 대부분 사용자 체감 속도가 더 빠릅니다. 특히 의미있는 콘텐츠를 빈 화면 대신 먼저 보여줄 수 있는게 장점입니다.</p>
<p>그래서 Next.js에선 페이지 이동은 빠르게(CSR), 데이터는 미리 준비해서(SSR) 보여주는 등 상황에 맞게 선택할 수 있습니다. 우와~~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[1011] React복습: 메모이제이션]]></title>
            <link>https://velog.io/@strawberry-tree/1011-React%EB%B3%B5%EC%8A%B5-%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@strawberry-tree/1011-React%EB%B3%B5%EC%8A%B5-%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98</guid>
            <pubDate>Sat, 11 Oct 2025 09:16:49 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/c31a9a20-662d-4837-8839-a913c318d2c5/image.png" alt=""></p>
<h1 id="메모이제이션">메모이제이션</h1>
<p>한 4달전쯤 크래프톤 정글에서 <a href="https://velog.io/@strawberry-tree/PS-%EB%8F%99%EC%A0%81-%EA%B3%84%ED%9A%8D%EB%B2%95-yaszv01c">다이나믹 프로그래밍</a>을 공부하면서 메모이제이션을 처음 접했는데요,</p>
<p>앞서 이미 계산한 값을 반복해서 계산하는 대신, 배열 같은 자료구조에 저장해 두고 필요할 때 다시 써먹는 방법이였죠.</p>
<p>리액트에서도 비슷하게 메모이제이션을 활용 가능합니다. <strong>값/함수/컴포넌트를 메모리에 저장해두어, 불필요한 연산과 리렌더링을 방지하는 것이죠</strong></p>
<p><a href="https://www.inflearn.com/courses/lecture?courseId=328340&amp;type=LECTURE&amp;unitId=103539&amp;subtitleLanguage=ko">참고 - 한 입 크기로 잘라 먹는 리액트, 인프런</a></p>
<h2 id="usememo---값-최적화"><code>useMemo</code> - 값 최적화</h2>
<pre><code class="language-jsx">const memoizedValue = useMemo(() =&gt; {
  return 계산할값;
}, [의존성]);</code></pre>
<p><code>useMemo</code>는 <strong>값의 불필요한 재계산을 방지</strong>하는Hook입니다. 처음 계산된 결과를 메모리에 저장하고, 이후 컴포넌트가 리렌더링되더라도 의존성 배열의 값이 바뀌지 않으면 재계산 없이 저장값을 그대로 반환합니다.</p>
<p>다만, 의존성 배열에 포함된 값이 변경되면 해당 계산을 다시 수행합니다.</p>
<pre><code class="language-jsx">import { useState, useMemo } from &quot;react&quot;;

function Example() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState(&quot;&quot;);

  const double = useMemo(() =&gt; {
    return count * 2;
  }, [count]);

  return (
    &lt;div&gt;
      &lt;p&gt;count: {count}&lt;/p&gt;
      &lt;p&gt;double: {double}&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;+1&lt;/button&gt;
      &lt;input
        value={text}
        onChange={(e) =&gt; setText(e.target.value)}
        placeholder=&quot;텍스트 입력&quot;
      /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>위 코드 같은 경우 텍스트를 <code>&lt;input&gt;</code>에 입력해 <code>text</code> state가 변경되면 <code>Example</code> 컴포넌트가 리렌더링됩니다. 하지만 <code>double</code>이 재계산되지는 않습니다. 변수 <code>count</code>는 그대로기 때문입니다.</p>
<h2 id="reactmemo---컴포넌트-최적화"><code>React.memo</code> - 컴포넌트 최적화</h2>
<pre><code class="language-jsx">export default React.memo(컴포넌트명);</code></pre>
<p><code>React.memo</code>는 <strong>컴포넌트의 불필요한 재렌더링을 방지</strong>하며, 컴포넌트에 감싸서 사용합니다. 처음 렌더링할 때 결과를 메모리에 저장하고, 이후 컴포넌트가 리렌더링되더라도 <strong><code>props</code>가 변하지 않으면 저장된 렌더링 결과를 재사용합니다.</strong></p>
<p>다만, <code>props</code>가 변경되면 컴포넌트가 재렌더링됩니다.</p>
<pre><code class="language-jsx">const Child = React.memo(({ value }) =&gt; {
  return &lt;div&gt;{value}&lt;/div&gt;;
});

function Parent() {
  const [count, setCount] = useState(0);
  return (
    &lt;&gt;
      &lt;Child value=&quot;고정된 값&quot; /&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;+&lt;/button&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>위 코드 같은 경우 <code>button</code>을 클릭하면 <code>count</code>가 변경되어 <code>Parent</code>가 리렌더링되더라도, <code>React.memo</code>로 감싼 <code>Child</code>는 리렌더링되지 않습니다. 전달되는 <code>prop</code>인 <code>value</code>가 그대로기 때문입니다.</p>
<h2 id="usecallback---함수-최적화"><code>useCallback</code> - 함수 최적화</h2>
<pre><code class="language-jsx">const memoizedFunc = useCallback(콜백함수, [의존성]);</code></pre>
<p><code>useCallback</code>은 <strong>함수의 불필요한 재생성을 방지</strong>하는 훅입니다. 함수도 JS 객체이기 때문에, 함수를 포함하는 컴포넌트가 리렌더링될 시 함수 내부 코드는 동일해도 별도의 객체로 재생성됩니다. 이러한 현상을 방지하기 위해서 <code>useCallback</code>을 사용하며, 이후 컴포넌트가 리렌더링되더라도 의존성 배열의 값이 바뀌지 않으면 함수를 다시 생성하지 않습니다.</p>
<p>다만, 의존성 배열에 포핟된 값이 변경되면 해당 함수를 재생성합니다.</p>
<p>참고로 <code>useCallback(콜백함수, 의존성배열)</code>은 <code>useMemo(() =&gt; 콜백함수, 의존성배열)</code>과 동일한 동작을 합니다. 하지만 깔끔한 코드를 위해선 <code>useCallback</code>을 쓰는 게 더 좋겠죠.</p>
<pre><code class="language-jsx">import { useState, useCallback } from &quot;react&quot;;

function Example() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() =&gt; {
    setCount((prev) =&gt; prev + 1);
  }, []);

  return &lt;button onClick={increment}&gt;+1&lt;/button&gt;;
}</code></pre>
<p>위 코드의 경우, <code>button</code>을 눌러 <code>count</code> state가 변경되어 <code>Example</code> 컴포넌트가 다시 렌더링되어도, <code>increment</code> 함수가 다시 생성되지 않습니다. 의존성 배열이 비어 있어서, 처음 렌더링될 때 빼고는 절대로 함수가 다시 생성되지 않아요.</p>
<h2 id="최적화는-언제-어떻게-해야-해요">최적화는 언제, 어떻게 해야 해요?</h2>
<p>우선 프로젝트 기능을 구현하고, 최적화는 맨 마지막에 하는 것이 권장됩니다. 이런 최적화 방법을 처음부터 적용하면, 추가로 기능을 구현할 시 코드가 복잡해져서 유지보수가 어려워질 수 있습니다.</p>
<p>그리고 꼭 모든 컴포넌트/값/함수에 최적화를 하기보단, 필수적인 코드에만 하는 게 권장됩니다. 메모이제이션 시 <strong>의존성 배열의 값이나 props가 변경되었는지 확인</strong>해야 하므로 오버헤드가 발생하기 때문입니다.</p>
<h3 id="최적화가-필요한-경우">최적화가 필요한 경우</h3>
<ul>
<li><strong>배열의 아이템</strong>처럼, 무한히 증가할 수 있는 컴포넌트. 개수가 많아질수록 배열을 순회하면서 렌더링할 때 성능에 영향을 줍니다.</li>
<li>복잡한 수학 계산, 데이터 변환, 필터링 등 <strong>오래 걸리는 작업.</strong></li>
</ul>
<h3 id="최적화가-불필요한-경우">최적화가 불필요한 경우</h3>
<ul>
<li>헤더, 푸터처럼 <strong>단순 HTML만 렌더링하는 컴포넌트</strong></li>
<li>리렌더링 빈도가 낮은 컴포넌트</li>
<li>사칙연산과 같은 간단한 계산만 이루어지는 컴포넌트</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[1010] 웹사이트 대문 바꿈]]></title>
            <link>https://velog.io/@strawberry-tree/1010-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%8C%80%EB%AC%B8-%EB%B0%94%EA%BF%88</link>
            <guid>https://velog.io/@strawberry-tree/1010-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%8C%80%EB%AC%B8-%EB%B0%94%EA%BF%88</guid>
            <pubDate>Fri, 10 Oct 2025 14:03:12 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/bbd2b1b3-4bd3-4da9-aa17-69cfad02f8bc/image.gif" alt=""></p>
<p><a href="https://strawberry-tree.github.io">웹사이트 링크</a></p>
<p>참고로 원랜 이랬어요.
<img src="https://velog.velcdn.com/images/strawberry-tree/post/e7101691-4afe-4da6-b7cd-77c47257ab3f/image.png" alt=""></p>
<p>사실 제가 짠건 아니고 AI 시킨 건데, 적어도 내용은 복습해야 할 것 같아서 올려봅니다.</p>
<p>위에선 &quot;뭔갈 만드는 걸 좋아하는&quot;, &quot;송상록&quot;, &quot;입니다&quot;를 별도의 <code>TypingEffect.jsx</code> 컴포넌트로 처리해 줍니다</p>
<pre><code class="language-jsx">// TypingEffect.jsx
&#39;use client&#39;

import { useEffect, useState } from &#39;react&#39;

interface TypingEffectProps {
  text: string              // 타이핑할 전체 텍스트
  delay?: number           // 각 글자 사이 지연 시간 (ms)
  className?: string       // 스타일 클래스
  onComplete?: () =&gt; void  // 타이핑 완료 시 실행할 콜백
  startDelay?: number      // 타이핑 시작 전 대기 시간 (ms)
}

export default function TypingEffect({
  text,
  delay = 100,
  className = &#39;&#39;,
  onComplete,
  startDelay = 0,
}: TypingEffectProps) {
  // 화면에 실제로 표시되는 텍스트
  const [displayedText, setDisplayedText] = useState(&#39;&#39;)
  // 현재까지 출력된 글자의 인덱스
  const [currentIndex, setCurrentIndex] = useState(0)
  // 시작 지연 후 타이핑이 시작되었는지 여부
  const [started, setStarted] = useState(false)

  useEffect(() =&gt; {
    // startDelay가 설정되어 있고 아직 시작 안 했으면
    if (startDelay &gt; 0 &amp;&amp; !started) {
      const startTimeout = setTimeout(() =&gt; {
        setStarted(true)    // 지연 시간 후 시작
      }, startDelay)
      // 컴포넌트 재렌더링 후 이전 타이머 남아있는 것을 방지 (cleanup)
      return () =&gt; clearTimeout(startTimeout)
    } else if (startDelay === 0) {
      setStarted(true)        // 지연이 없으면 바로 시작
    }
  }, [startDelay, started])

  // 타이핑 효과
  useEffect(() =&gt; {
    if (!started) return

    // 아직 출력할 글자가 남아있으면
    if (currentIndex &lt; text.length) {
      const timeout = setTimeout(() =&gt; {
        // 현재 인덱스의 글자를 displayedText에 추가
        setDisplayedText((prev) =&gt; prev + text[currentIndex])
        // 다음 글자로 이동
        setCurrentIndex((prev) =&gt; prev + 1)
      }, delay)

      return () =&gt; clearTimeout(timeout)
      // 모든 글자 출력 완료 시 콜백 실행
    } else if (currentIndex === text.length &amp;&amp; onComplete) {
      onComplete()
    }
  }, [currentIndex, delay, text, onComplete, started])

  // 아직 시작 안 했으면 빈 span 반환
  if (!started) {
    return &lt;span className={className}&gt;&lt;/span&gt;
  }

  return (
    &lt;span className={className}&gt;
      {displayedText}
      {currentIndex &lt; text.length &amp;&amp; (
        &lt;span className=&quot;animate-pulse text-primary-500&quot;&gt;|&lt;/span&gt;
      )}
    &lt;/span&gt;
  )
}</code></pre>
<ul>
<li><code>startDelay</code> 시간만큼 대기 후 타이핑을 시작합니다.<ul>
<li>대기 중일 땐 <code>started</code>는 <code>false</code>. <code>setStarted</code>로 제어합니다.</li>
</ul>
</li>
<li><code>delay</code> (기본 100ms) 간격으로 한글자씩 가져와 <code>displayedText</code> state에 추가합니다.<ul>
<li>현재 위치는 <code>currentIndex</code>로 추적합니다</li>
</ul>
</li>
<li>모든 글자 출력 완료 시 <code>onComplete</code>를 출력합니다.</li>
</ul>
<pre><code class="language-jsx">// MainTitle.jsx
&#39;use client&#39;

import TypingEffect from &#39;./TypingEffect&#39;
import { useState } from &#39;react&#39;

export default function MainTitle() {
  const [showSecondLine, setShowSecondLine] = useState(false)
  const [showThirdPart, setShowThirdPart] = useState(false)
  return (
    &lt;div className=&quot;space-y-4&quot;&gt;
      &lt;h1 className=&quot;min-h-[6rem] text-4xl leading-tight font-bold tracking-tight text-gray-900 sm:min-h-[8rem] sm:text-5xl md:min-h-[10rem] md:text-6xl dark:text-gray-100&quot;&gt;
        &lt;TypingEffect
          text=&quot;뭔가 만드는 걸 좋아하는&quot;
          delay={80}
          onComplete={() =&gt; setShowSecondLine(true)}
        /&gt;
        &lt;br /&gt;
        {showSecondLine &amp;&amp; (
          &lt;&gt;
            &lt;TypingEffect
              text=&quot;송상록&quot;
              delay={80}
              className=&quot;text-primary-500&quot;
              startDelay={200}
              onComplete={() =&gt; setShowThirdPart(true)}
            /&gt;
            {showThirdPart &amp;&amp; &lt;TypingEffect text=&quot;입니다&quot; delay={80} startDelay={100} /&gt;}
          &lt;/&gt;
        )}
      &lt;/h1&gt;
    &lt;/div&gt;
  )
}</code></pre>
<ul>
<li><code>&quot;뭔가 만드는 걸 좋아하는&quot;</code> 타이핑 → 완료 시 <code>showSecondLine</code>을 <code>true</code>로, 이후 <code>showSecondLine &amp;&amp;</code> 오른쪽 <code>TypingEffect</code> 렌더링.</li>
<li><code>200ms</code> 대기 후 <code>&quot;송상록&quot;</code> 타이핑 → 완료 시 <code>showThirdPart</code>을 <code>true</code>로, 이후 <code>showThirdPart &amp;&amp;</code> 오른쪽 <code>TypingEffect</code> 렌더링.</li>
<li><code>100ms</code> 대기 후 <code>&quot;입니다&quot;</code> 타이핑</li>
</ul>
<p>의존성배열에 뭐가 많은 건 <code>react-hooks/exhaustive-deps</code> 규칙 때문인데, 예상치 못한 버그를 방지하려고 <code>useEffect</code> 내 모든 변수가 들어갔다고 생각하심 됩니다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[1009] React복습: Hooks (그니까 use로 시작하는 애들)]]></title>
            <link>https://velog.io/@strawberry-tree/1009-React%EB%B3%B5%EC%8A%B5-Hooks-%EA%B7%B8%EB%8B%88%EA%B9%8C-use%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EC%95%A0%EB%93%A4</link>
            <guid>https://velog.io/@strawberry-tree/1009-React%EB%B3%B5%EC%8A%B5-Hooks-%EA%B7%B8%EB%8B%88%EA%B9%8C-use%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EC%95%A0%EB%93%A4</guid>
            <pubDate>Thu, 09 Oct 2025 06:45:47 GMT</pubDate>
            <description><![CDATA[<p>리액트를 공부하시다보면 <code>useState</code>, <code>useRef</code>, <code>useEffect</code>...처럼 <code>use</code>로 시작하는 함수들을 굉장히 많이 볼 수 있는데요, 이런 함수들을 <strong>Hooks</strong>라고 부릅니다.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/5b378091-9b15-43ba-a3c4-00c180fbbba1/image.png" alt=""></p>
<h1 id="hooks">Hooks</h1>
<h2 id="클래스--함수-컴포넌트">클래스 / 함수 컴포넌트</h2>
<p>리액트에서 컴포넌트를 작성할 때는 흔히 아래와 같은 <strong>함수형 컴포넌트</strong>를 사용합니다.
아래는 버튼을 누르면 숫자가 1 증가하는 컴포넌트의 코드입니다.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/750b1a6a-a419-41da-a06f-42078e1a73ec/image.png" alt=""> (참고로 5번 누른 이후 상황입니다)</p>
<pre><code class="language-jsx">import { useState } from &quot;react&quot;;

export default function Count() {
  const [number, setNumber] = useState(0);

  return (
    &lt;&gt;
      &lt;h1&gt;{number}&lt;/h1&gt;
      &lt;button
        onClick={() =&gt; {
          setNumber(number + 1);
        }}
      &gt;
        +1
      &lt;/button&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>그런데 클래스 형식으로도 컴포넌트를 작성할 수 있어요.
그걸 <strong>클래스 컴포넌트</strong>라 하는데, 함수 컴포넌트에 비해 문법이 많이 복잡하고 가독성도 떨어집니다.</p>
<pre><code class="language-js">import React, { Component } from &quot;react&quot;;

export default class Count extends Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 0,
    };
  }

  increment = () =&gt; {
    this.setState((prevState) =&gt; ({
      number: prevState.number + 1,
    }));
  };

  render() {
    return (
      &lt;&gt;
        &lt;h1&gt;{this.state.number}&lt;/h1&gt;
        &lt;button onClick={this.increment}&gt;+1&lt;/button&gt;
      &lt;/&gt;
    );
  }
}</code></pre>
<p>놀랍게도 기존에는 <code>useState</code>, <code>useRef</code> 같은 함수들을 클래스 컴포넌트에서만 쓸 수 있었어요.</p>
<p>하지만 <strong>Hooks</strong>가 등장하면서 이러한 함수들을 함수 컴포넌트에서도 쓸 수 있게 됐습니다.
클래스 컴포넌트의 기능을 갈고리로 낚아 채온다... 는 뜻에서 Hook이라는 이름이 붙었어요.
모든 Hook은 이름 앞에 <code>use</code>가 옵니다. 그러니까</p>
<ul>
<li><code>useState</code>는 클래스 컴포넌트의 State 기능을 낚아채온 거고</li>
<li><code>useRef</code>는 클래스 컴포넌트의 Ref 기능을 낚아채온 거다</li>
</ul>
<p>라고 생각하시면 됩니다.</p>
<h2 id="hook-사용-규칙">Hook 사용 규칙</h2>
<p>Hook을 사용할 땐 이런 규칙을 지켜야 합니다.</p>
<p><strong>(1) 함수형 컴포넌트 혹은 Custom Hook 안에서만 사용할 수 있다</strong></p>
<pre><code class="language-jsx">const [number, setNumber] = useState(0);

export default function Count() {
  // 생략
}</code></pre>
<p>즉 이렇게 <code>useState</code>를 <code>Count</code> 바깥에서 사용하시면 안 됩니다.</p>
<p>커스텀 훅이 뭔지는 뒤에서 설명할게요.</p>
<p><strong>(2) 조건문 / 반복문 안에서 사용할 수 없다</strong></p>
<pre><code class="language-jsx">if (true){
    const [number, setNumber] = useState(0);
  }</code></pre>
<p>기본적으로 조건문, 반복문은 해당 조건이 만족되어야만 안에 내용을 실행하는 특징이 있습니다.
따라서 이 안에다 Hook을 사용하면, 어떤 렌더링에서는 실행되고 어떤 렌더링에선 실행되지 않아 호출 순서가 꼬이게 됩니다. </p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/dc1b7b30-91e6-4005-aff4-1864b2c7650b/image.png" alt="">규칙을 어기시면 이런 무시무시한 경고창을 개발자도구에서 보실 수 있습니다. 바로 사이트가 뻗진 않지만 잠재적으로 고치기 어려운 버그가 생길 수 있습니다.</p>
<h1 id="usestate-vs-useref"><code>useState</code> vs <code>useRef</code></h1>
<p><a href="https://velog.io/@strawberry-tree/%EC%9B%B9%EA%B0%9C%EB%B0%9C-%ED%83%88%EC%B6%9C-D-48"><code>useState</code>에 대해선 앞선 글에서 잘 정리했으니 참고 바랍니다</a></p>
<h2 id="공통점">공통점</h2>
<p><code>useState</code>의 state, <code>useRef</code>의 ref 모두 컴포넌트 내부 변수로 활용 가능하다는 공통점이 있습니다.  그 뜻은 컴포넌트가 리렌더링되어도 값이 유지된다는 뜻입니다.</p>
<p>참고로 컴포넌트는 이러한 상황에서 리렌더링이 됩니다.</p>
<ul>
<li>자신이 관리하는 state가 변경될 때</li>
<li>부모 컴포넌트에서 제공받는 props가 변경될 때</li>
<li>부모 컴포넌트가 리렌더링될 때</li>
</ul>
<p><code>let</code>으로 변수를 선언하면, 컴포넌트가 리렌더링되면 코드가 다시 실행되어 변수 선언이 다시 이루어집니다. 즉 매번 초기값으로 리셋되는 문제가 있습니다.</p>
<h2 id="차이점">차이점</h2>
<p>state는 변경될 때마다 컴포넌트가 리렌더링되지만, ref는 변경되어도 컴포넌트가 리렌더링되지 않습니다.</p>
<h2 id="예제">예제</h2>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/96dac9d9-1f8c-4a08-99ef-41790429fe53/image.png" alt=""></p>
<pre><code class="language-jsx">import React, { useState, useRef } from &#39;react&#39;;

function Counter() {
  const [stateCount, setStateCount] = useState(0);
  const refCount = useRef(0);
  let letCount = 0;

  console.log(&#39;컴포넌트가 리렌더링됨&#39;);

  return (
    &lt;div&gt;
      &lt;h2&gt;State count: {stateCount}&lt;/h2&gt;
      &lt;button onClick={() =&gt; setStateCount(stateCount + 1)}&gt;State 증가&lt;/button&gt;

      &lt;h2&gt;Ref count: {refCount.current}&lt;/h2&gt;
      &lt;button onClick={() =&gt; {
        refCount.current += 1;
        console.log(&#39;Ref count:&#39;, refCount.current);
      }}&gt;Ref 증가&lt;/button&gt;
    &lt;/div&gt;
  );
}

export default Counter;
</code></pre>
<ul>
<li>버튼 <code>State 증가</code>를 누를 시<ul>
<li><code>setCount(count + 1)</code> 호출 후 컴포넌트가 리렌더링되어, 화면에 숫자 즉시 갱신됩니다.</li>
</ul>
</li>
<li>버튼 <code>Ref 증가</code>를 누를 시<ul>
<li><code>countRef.current+</code> 호출 시 값은 증가하지만, 컴포넌트가 리렌더링되진 않아 화면에 표시되진 않습니다.</li>
<li><code>console.log(countRef.current)</code>에서는 증가된 값을 확인할 수 있습니다.</li>
<li><code>State 증가</code>를 나중에 누르면 컴포넌트가 리렌더링되니, 그제서야 증가된 값이 반영됩니다.</li>
</ul>
</li>
</ul>
<h2 id="useref를-통한-dom-요소-조작"><code>useRef</code>를 통한 DOM 요소 조작</h2>
<p><code>useRef</code>는 DOM 요소 조작에도 흔히 사용됩니다.</p>
<p>특히 특정 DOM 요소에의 자동 포커스나, 특정 DOM 요소로의 자동 스크롤에 흔히 사용됩니다.</p>
<p>예를 들어, 회원가입창에서 Submit 버튼을 눌렀는데 이름이 미입력된 경우 이름을 강조하고 싶다고 합시다.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/93464686-9522-4640-a756-fd35ab36943f/image.png" alt=""></p>
<p>(코드의 간결함을 위해 이름 제외 다른 필드는 생략한 점 양해 바랍니다)</p>
<pre><code class="language-jsx">function Register() {
  const [input, setInput] = useState({ name: &quot;&quot; });
  const nameRef = useRef();

  const onSubmit = () =&gt; {
    if (input.name === &quot;&quot;) {
      nameRef.current.focus(); // DOM 직접 제어
    }
  };

  return (
    &lt;&gt;
      &lt;input
        ref={nameRef}
        type=&quot;text&quot;
        name=&quot;name&quot;
        value={input.name}
        onChange={(e) =&gt; setInput({ name: e.target.value })}
        placeholder=&quot;이름을 입력하세요&quot;
      /&gt;
      &lt;button onClick={onSubmit}&gt;제출&lt;/button&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>이렇게 <code>input</code> 태그의 <code>ref</code> 속성에 <code>useRef</code>로 만든 <code>inputRef</code>를 명시해 주고, 제출버튼을 눌렀을 때 실행되는 <code>onSubmit</code> 이벤트에서 <code>inputRef.current.focus()</code>를 주게끔 하는 식으로 조작이 가능합니다.</p>
<h2 id="useeffect"><code>useEffect</code></h2>
<p><code>useEffect</code>는 컴포넌트의 Side Effect를 처리하는 Hook입니다.</p>
<p>Side Effect...가 뭐냐면, 컴포넌트의 핵심 기능은 화면에 보여지는 렌더링이겠죠. 그런데 실제로는 이와 더불어 다른 작업을 수행할 수 있습니다.</p>
<ul>
<li>이를테면 API호출을 통한 fetching이라든가, DOM 조작이라든가...</li>
</ul>
<p><code>useEffect</code>는 <strong>(1)콜백함수</strong> (effect 실행 내용)와 <strong>(2)의존성 배열</strong>을 받습니다.</p>
<ul>
<li>이때 <strong>처음 컴포넌트가 렌더링될 때</strong> + <strong>의존성 배열</strong> 안의 값이 바뀔 때마다, <strong>콜백 함수</strong>가 실행됩니다. </li>
<li>그래서 일반적으로 리렌더링과 관련된 값 (state나 props 등)이 들어가는데, 리렌더링을 통해 컴포넌트를 화면에 그리고 이후 콜백함수가 실행됩니다.<pre><code>// 콜백함수, 배열
// count 또는 input 값이 바뀌면, 콜백함수 실행
useEffect(() =&gt; {
  console.log(`count: ${count} | input: ${input}`);
}, [count, input]);</code></pre></li>
</ul>
<p>이때 의존성 배열을 비워 두거나, 생략할 수도 있습니다.</p>
<ul>
<li>값이 있으면, 배열 안 값 중 하나가 변경될 때마다 실행</li>
<li>비어 있으면 (<code>[]</code>), 컴포넌트가 처음 마운트될 때 1번만 실행</li>
<li>생략하면 (콜백함수만 인수로 보냄), 모든 렌더링마다 실행 </li>
</ul>
<h1 id="custom-hook">Custom Hook</h1>
<p>직접 custom hook을 만드는 것 역시 가능합니다. 앞서 말했듯이 Hook은 함수형 컴포넌트나 Custom Hook 내부에서만 사용할 수 있습니다. 즉 중복되는 Hook 코드가 반복될 때 함수화하고 싶으시면 Custom Hook을 사용하셔야 합니다.</p>
<p>리액트는 <code>use</code>로 시작하는 함수를 만들면 자동으로 Hook으로 인식하기 때문에, 쉽게 만드실 수 있습니다.</p>
<p>아래 코드처럼 여러 입력창에서 동일한 입력 관리 로직을 사용하는 경우, Custom Hook으로 분리할 수 있겠죠.</p>
<pre><code class="language-jsx">// src/hooks/useInput.jsx

import { useState } from &quot;react&quot;;
// Custom Hook 만들기
// 그냥 함수 이름이 use로 시작하면 됨
export default function useInput() {
  const [input, setInput] = useState(&quot;&quot;);
  const onChange = (e) =&gt; {
    setInput(e.target.value);
  };

  return [input, onChange];
}</code></pre>
<pre><code class="language-jsx">// src/components/HookExam.jsx
import useInput from &quot;../hooks/useInput&quot;;

export default function HookExam() {
  const [name, onChangeName] = useInput(&quot;&quot;);
  const [email, onChangeEmail] = useInput(&quot;&quot;);

  return (
    &lt;div&gt;
      &lt;input value={name} onChange={onChangeName} placeholder=&quot;이름&quot; /&gt;
      &lt;input value={email} onChange={onChangeEmail} placeholder=&quot;이메일&quot; /&gt;
    &lt;/div&gt;
  );
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[1008] React복습: Critical Rendering Path, 가상 DOM]]></title>
            <link>https://velog.io/@strawberry-tree/1008-React%EB%B3%B5%EC%8A%B5-Critical-Rendering-Path-%EA%B0%80%EC%83%81-DOM</link>
            <guid>https://velog.io/@strawberry-tree/1008-React%EB%B3%B5%EC%8A%B5-Critical-Rendering-Path-%EA%B0%80%EC%83%81-DOM</guid>
            <pubDate>Wed, 08 Oct 2025 17:09:12 GMT</pubDate>
            <description><![CDATA[<p>React를 공부하신 분들이라면 <strong>가상(Virtual) DOM</strong>이라는 말을 한번쯤은 들으셨을 법도 한데, 이게 왜 중요한지는 가물가물하실 겁니다.</p>
<p>그래서 글로 정리해보았어요들레이요.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/aa374930-ecd4-4878-a6bb-fa1f2ac2e4a7/image.png" alt=""></p>
<h1 id="critical-rendering-path">Critical rendering path</h1>
<p>웹사이트를 화면에 표시할 때 HTML, CSS, JavaScript가 사용된다는 건 아실 거에요.</p>
<p>실제로는 HTML, CSS, JavaScript 코드가 화면에 보이는 웹사이트로 변신(?)될때까지는 일련의 과정이 이루어지는데, 이를 <strong>Critical Rendering Path</strong>라 합니다.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/0ba8be39-fcb5-43a0-ab30-f436837f2898/image.png" alt=""></p>
<p>대충 과정을 정리해 보자면</p>
<pre><code class="language-js">HTML 코드                           DOM 트리
--------------------------------    ----------------------------
&lt;html&gt;                              [html]
  &lt;body&gt;                                ├── [body]
    &lt;h1&gt;무적 LG&lt;/h1&gt;                     │     ├── [h1] &quot;무적 LG&quot;
    &lt;p&gt;끝까지 TWINS&lt;/p&gt;                        │     └── [p] &quot;끝까지 TWINS&quot;
  &lt;/body&gt;                               └──
&lt;/html&gt;
--------------------------------    ----------------------------</code></pre>
<pre><code>CSS 코드                             CSSOM 트리
--------------------------------    ----------------------------
h1 { color: pink; }                  [CSSOM]
p  { font-size: 24px; }                 ├── [h1]
                                        │     └── color: pink
                                        └── [p]
                                              └── font-size: 24px
--------------------------------    ----------------------------</code></pre><p><strong>1단계</strong> HTML 코드는 <strong>DOM(Document Object Model)</strong>, CSS 코드는 <strong>CSSOM (CSS Object Model)</strong>로 전환됩니다.</p>
<ul>
<li>DOM 및 CSS Object Model은, 각각 HTML / CSS 코드를 브라우저가 이해하기 쉬운 형태로 변환한 트리 구조의 객체를 뜻합니다.</li>
<li>이때 JavaScript는 DOM을 수정하는 역할을 합니다. 예를 들어 버튼을 누르면 표시되는 숫자가 1씩 늘어나는 기능을 구현하면, DOM 중 해당 숫자에 해당되는 부분이 수정되겠죠.</li>
</ul>
<p><strong>2단계</strong> DOM + CSS Object Model을 합쳐서 <strong>렌더 트리</strong>를 만듭니다.</p>
<ul>
<li>특별한 건 아니고 두 모델이 합쳐진 거라고 생각하면 됩니다.</li>
<li>화면에 렌더링되는 요소들의 모든 정보가 포함되어 있는, 웹사이트의 설계도이자 청사진 역할을 합니다.</li>
</ul>
<p><strong>3단계</strong> 렌더 트리 내 요소의 위치를 계산하는 <strong>Layout</strong> 단계가 진행됩니다.</p>
<ul>
<li>DOM이 변경돼서 Layout이 다시 진행되는 과정을 <strong>Reflow</strong>라고도 합니다</li>
</ul>
<p><strong>4단계</strong> 레이아웃이 계산된 요소들을 실제로 웹사이트에 그리는 <strong>Paint</strong> 단계가 진행됩니다.</p>
<ul>
<li>DOM이 변경돼서 Paint가 다시 진행되는 과정을 <strong>Repaint</strong>라고도 합니다</li>
</ul>
<h2 id="dom이-수정될-때">DOM이 수정될 때</h2>
<p>앞서 언급했듯이 JavaScript로는 DOM을 수정할 수 있습니다.</p>
<p>이런 경우 브라우저는 다시 <strong>render tree</strong>를 생성하고(2단계), 웹사이트의 레이아웃을 다시 잡는 <strong>reflow</strong>를 거치고(3단계), 렌더링 역시 다시 하여 <strong>repaint</strong>(4단계)도 거치게 됩니다.</p>
<p>문제는 <strong>reflow</strong>와 <strong>repaint</strong>에는 큰 시간적 비용이 든다는 점입니다. 그래서 DOM 수정이 많아질수록 웹사이트 성능이 급격히 떨어집니다.</p>
<p>이를테면, <code>&lt;ul&gt;</code>에 0부터 2999까지 <code>&lt;li&gt;</code>를 삽입하는 함수를 만들었다고 합시다</p>
<pre><code class="language-js">function badPractice() {
  const $ul = document.getElementById(&quot;ul&quot;);
  for (let i = 0; i &lt; 3000; i++) {
    $ul.innerHTML += `&lt;li&gt;${i}&lt;/li&gt;`;
  }
}</code></pre>
<p>위 함수에서는 무려 3000번 DOM 수정을 하게 됩니다. <strong>reflow</strong>와 <strong>repaint</strong> 역시 오래 걸리기 때문에, 실제로 <code>&lt;li&gt;</code> 태그 내용이 다 생성되려면 상당한 시간이 소요됩니다. 즉 렌더링이 많이 느려지므로, 이렇게 코드를 짜시면 안 됩니다.</p>
<pre><code class="language-js">function goodPractice(){
  let list = &quot;&quot;;
  for (let i = 0; i &lt; 3000; i++){
    list += `&lt;li&gt;${i}&lt;/li&gt;`
  }
  const $ul = document.getElementById(&quot;ul&quot;);
  $ul.innerHTML = list;
}</code></pre>
<p>대신 이렇게 <code>list</code>라는 <code>String</code> 매개변수를 선언한 뒤, <code>&lt;li&gt;</code> 태그에 해당하는 내용을 미리 저장해 두고, DOM은 단 1번 수정하는 식으로 코드를 짜시면 됩니다.</p>
<p>이렇게 한꺼번에 변경사항이 많을 경우, 매번 따로 DOM 수정을 하기보단, 한 번에 모아서 수정을 해야 합니다.</p>
<h2 id="virtual-dom">Virtual DOM</h2>
<p>React에서는 <a href="https://velog.io/@strawberry-tree/%EC%9B%B9%EA%B0%9C%EB%B0%9C-%ED%83%88%EC%B6%9C-D-48">컴포넌트와 state를 통해 업데이트를 관리한다는 점</a>을 기억합시다. <strong>컴포넌트 내 State가 바뀔 때마다 컴포넌트 UI가 업데이트되는 방식이였죠</strong>.</p>
<p>앞선 논리대로라면 여러 업데이트가 발생할 시, 모아서 한 방에 DOM에 반영해야 하는 게 좋을 겁니다. 이를 위해 리액트는 <strong>Virtual DOM</strong>을 사용하는데요, 이는 실제 브라우저가 렌더링하는 DOM을 자바스크립트 객체로 흉내낸 거라고 생각하면 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/7a552cfc-c287-41a1-b1e3-f2d13a57745a/image.png" alt=""></p>
<p>자세한 동작과정을 보자면</p>
<ul>
<li>각 컴포넌트는 자신만의 Virtual DOM 트리를 가지고 있습니다.</li>
<li><strong>컴포넌트 state가 바뀌면, 해당 컴포넌트의 Virtual DOM이 갱신됩니다.</strong></li>
<li>이전 Virtual DOM과 새로운 Virtual DOM을 비교합니다(diffing)</li>
<li>여러 컴포넌트가 동시에 바뀌면, React는 <strong>모든 Virtual DOM에서 변경 사항을 모아 한 번에 실제 DOM에 반영합니다</strong>(batch)</li>
</ul>
<p>이러면 Reflow, Repaint가 최소화되어 훨씬 빠른 렌더링이 가능해집니다. 우와~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[1008] JS문법복습: Promise / async / await]]></title>
            <link>https://velog.io/@strawberry-tree/1008-JS%EB%AC%B8%EB%B2%95%EB%B3%B5%EC%8A%B5-Promise-async-await</link>
            <guid>https://velog.io/@strawberry-tree/1008-JS%EB%AC%B8%EB%B2%95%EB%B3%B5%EC%8A%B5-Promise-async-await</guid>
            <pubDate>Wed, 08 Oct 2025 08:50:15 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/af274f1b-34a2-4512-a65d-8abffaec3c6b/image.png" alt=""></p>
<p>포트폴리오랑 이력서에 치이느라 JS문법 복습에 신경을 못 쓰고 있었네요.</p>
<p>JS 비동기 처리에 중요한 <code>Promise</code> 및 <code>async</code> / <code>await</code>에 대해 정리해본 글입니다.</p>
<h1 id="promise-비동기-처리용-객체"><code>Promise</code>: 비동기 처리용 객체</h1>
<p><code>new Promise((resolve, reject) =&gt; {...})</code>의 형태로 <code>Promise</code>를 생성할 수 있습니다.</p>
<ul>
<li><code>Promise</code>의 인수로 들어가는 이 콜백함수를 executor라고 하며, 실제 비동기 작업을 처리하는 부분이라 이런 이름이 붙었습니다.</li>
<li>그러니까 <code>setTimeout</code>, <code>fetch</code>와 같은 비동기 작업을 <code>{...}</code> 안에 넣어주면 됩니다.</li>
</ul>
<h2 id="resolve-reject"><code>resolve</code>, <code>reject</code></h2>
<p>일반적으로는 아래처럼 <code>Promise</code>를 반환하는 비동기 함수를 만들어 사용합니다.</p>
<p>한번 숫자에 10을 더하는 비동기 함수를 만들어 볼까요?</p>
<pre><code class="language-js">function addTen(num) {
  return new Promise((resolve, reject) =&gt; {
    setTimeout(() =&gt; {
      if (typeof num === &quot;number&quot;) {
        resolve(num + 10);                // 성공 시 인수에 결과값 전달
      } else {
        reject(&quot;Number형을 입력하세요!&quot;);    // 실패 시 인수에 에러 메시지 전달
      }
    }, 2000);
  });
}</code></pre>
<p>기존에는 <code>num + 10</code>을 그대로 반환하게끔 작성했겠지만, 이제 <code>Promise</code> 객체 안에 <code>resolve(num + 10)</code>을 넣고 그걸 반환을 하는 것 같은데요... 그런데 <code>resolve</code>는 무슨 역할일까요?</p>
<ul>
<li>executor는 <code>resolve</code>, <code>reject</code> 두 함수를 매개변수로 가집니다</li>
<li><code>resolve</code>는 비동기 작업이 성공적으로 완료됐을 때 호출하며, 인수에는 성공 시 표시할 결과값을 전달합니다.</li>
<li><code>reject</code>는 비동기 작업이 실패했을 때 호출하며, 인수에는 실패 시 오류 메시지를 전달합니다.</li>
<li>언제 <code>resolve</code>, <code>reject</code>를 호출할 지는 함수 짤 때 알아서 잘 생각하셔야 한다는 점에 유의합시다.</li>
</ul>
<h2 id="then-catch"><code>then</code>, <code>catch</code></h2>
<p>이렇게 <code>Promise</code>를 반환하는 함수는,<code>then</code>이나 <code>catch</code> 메서드를 이용해 후속 작업 및 예외 처리를 진행할 수 있습니다.</p>
<pre><code class="language-js">addTen(10)
  .then((value) =&gt; {
    console.log(value);
    return addTen(value);
  }) // then 메서드 안에 return 값이 있을 시, 다음 then 메서드의 매개변수로 전달됨
  .then((value) =&gt; {
    console.log(value);
    return addTen(value);
  })
  .then((value) =&gt; {
    console.log(value);
    return addTen(value);
  })
  .catch((error) =&gt; {
    console.log(error);
  }); // 도중에 reject될 시 catch 메서드로 이동

// num = 10 일시 -&gt; 2초 후 20 출력, 4초(2+2) 후 30 출력, 6초(4+2)후 40 출력
// num이 숫자가 아닐시 -&gt; 2초 후 &quot;Number형을 입력하세요!&quot; 출력</code></pre>
<ul>
<li><code>then</code>: 앞선 <code>Promise</code>가 <code>resolve</code>될 때까지 기다리고, 그 결괏값이 매개변수 (<code>value</code>)로 전달됩니다.<ul>
<li><code>then</code> 메서드 안에서 또 다른 <code>Promise</code>를 반환할 시, 다음 <code>then</code>은 그 <code>Promise</code>가 <code>resolve</code>될 때까지 기다린 후 값을 전달받습니다.</li>
</ul>
</li>
<li><code>catch</code>: 도중에 <code>reject</code> 처리된 <code>Promise</code>가 생길 시, 에러 메시지가 <code>error</code> 매개변수로 전달되고, 이후 체이닝이 종료됩니다.</li>
</ul>
<p>기존에는 이렇게 10을 더하는 비동기 연산을 3번 할 때, 콜백함수를 3단으로 호출해야 했으나, 그럴 필요가 없어졌습니다! 우와~~ 콜백 지옥에서 탈출했어요.</p>
<h1 id="async-await"><code>async</code>, <code>await</code></h1>
<p><code>.then</code> 메서드를 쓰는게 직관적이지 않은 것 같으면, 조금 더 깔끔한 <code>async</code>, <code>await</code> 키워드를 쓰는 방법도 있습니다.</p>
<h2 id="async"><code>async</code></h2>
<p><code>async</code>는 함수를 항상 <code>Promise</code>를 반환하는 비동기 함수로 만들어 줍니다.</p>
<ul>
<li>반환값이 <code>Promise</code>가 아닌 경우 자동으로 <code>resolve</code>된 <code>Promise</code>를 반환합니다.</li>
<li>이미 <code>Promise</code>를 반환하면 그런 효과는 없지만... 이따 볼 <code>await</code>를 사용하기 위해선 일단 쓰셔야 합니다.</li>
</ul>
<pre><code class="language-js">async function addTen(num) {
  // 반환값이 Promise 객체가 아닌 경우, 이를 결과값으로 가진 fulfilled / promise 객체가 반환되는 효과도 있음
  return new Promise((resolve, reject) =&gt; {
    setTimeout(() =&gt; {
      if (typeof num === &quot;number&quot;) {
        resolve(num + 10);
      } else {
        reject(&quot;Number형을 입력하세요!&quot;);
      }
    }, 2000);
  });
}</code></pre>
<h2 id="await-trycatch"><code>await</code>, <code>try~catch</code></h2>
<p><code>await</code>는 <code>async</code> 함수의 <code>Promise</code>가 처리될때까지 기다리고, 결과값을 반환하는 역할입니다. 기존에 <code>then(...)</code>안에서 처리하던 코드를, <code>await</code>를 통해 변수에 저장하고 다음 줄에 바로 사용하는 식으로 변경하실 수 있습니다.</p>
<ul>
<li>단 <code>await</code>는 <code>async</code> 함수 안에서만 쓸 수 있으니 유의합시다.</li>
<li><code>.catch()</code> 같은 경우 <code>try~catch</code>를 통한 예외 처리로 변경하시면 됩니다.</li>
</ul>
<pre><code class="language-js">async function addThreeTens(num) {
  try {
    const result1 = await addTen(num);
    console.log(result1);
    const result2 = await addTen(result1);
    console.log(result2);
    const result3 = await addTen(result2);
    console.log(result3);
  } catch (error) {
    console.log(error);
  }
}

addThreeTens(10);
// num = 10 일시 -&gt; 2초 후 20 출력, 4초(2+2) 후 30 출력, 6초(4+2)후 40 출력
// num이 숫자가 아닐시 -&gt; 2초 후 &quot;Number형을 입력하세요!&quot; 출력</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[1005] 포트폴리오 사이트 제작 / JS문법복습 - 비동기]]></title>
            <link>https://velog.io/@strawberry-tree/1005-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-JS%EB%AC%B8%EB%B2%95%EB%B3%B5%EC%8A%B5-%EB%B9%84%EB%8F%99%EA%B8%B0</link>
            <guid>https://velog.io/@strawberry-tree/1005-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-JS%EB%AC%B8%EB%B2%95%EB%B3%B5%EC%8A%B5-%EB%B9%84%EB%8F%99%EA%B8%B0</guid>
            <pubDate>Sun, 05 Oct 2025 15:43:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/690a6586-6ae7-4fe6-aa12-9587cae20d7c/image.png" alt=""></p>
<p><a href="https://strawberry-tree.github.io">포트폴리오 사이트 보러 가기</a></p>
<h1 id="포트폴리오">포트폴리오</h1>
<p>사실 대부분 포트폴리오는 피그마나 캔바를 써서 pdf로 만드시는 것 같은데</p>
<p>뭔가 홈페이지로 만드는 게 좀 더 개발자 느낌 날 것 같아서 홈페이지로 만들어봤다. 뭐 PDF로 제출하라는 곳들도 있는데 웹사이트 PDF로 바꿔주는 툴들도 있으니 그런걸 쓰면 될 것 같고</p>
<p>나만무 때 익숙해진 Next.js + TailwindCSS를 사용했는데, 내가 밑바닥부터 만들지는 않았고 인터넷에 돌아다니는 블로그 템플릿을 썼다.</p>
<p><a href="https://github.com/timlrx/tailwind-nextjs-starter-blog">이런 깃허브 템플릿이 있길래 썼다. 여러분도 튜토리얼만 잘 읽으면 따라 쓰실 수 있음</a></p>
<p>호스팅은 GitHub Pages로 했다. 무료에다가, GitHub Actions랑 연동하면 푸시하면 자동으로 빌드하고 배포해주는게 편하다.</p>
<h2 id="결과물">결과물</h2>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/137d087b-1bb2-40b6-bc19-fdfd1187cf45/image.png" alt="">
메인페이지는 아직 조촐한데 시간 나면 좀더 꾸밀 생각이다. 일단은 이력서랑 포트폴리오에 더 집중해야 되니까...</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/e8ddc583-6868-4bcc-8c36-7e242fea2dfa/image.png" alt=""></p>
<p>포트폴리오에는 정글에서 한 프로젝트 3개 (핀토스, 나만무 준비주차 때 한 클론코딩, 나만무 최종프로젝트)랑 학교에서 코딩 깔짝하면서 만든 프로젝트 2개를 넣었다.</p>
<p>아 그리고 컬러는 내가 좋아하는 핑크로 골랐다.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/b4fbbb19-1b9d-4d09-8745-b20351ee33d2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/9890b532-1c90-4066-986c-fb01144cc39b/image.png" alt=""></p>
<p>각 프로젝트별 페이지는, 마크다운 파일만 업로드하면 자동으로 사이트에 올라가는 방식이다. 나름 웹페이지의 장점을 활용하려고 해서 <a href="https://strawberry-tree.github.io/projects/wheretoput">나만무 발표영상</a>도 첨부했고, <a href="https://strawberry-tree.github.io/projects/college-life-game">5년전에 만든 게임은 직접 해 보실 수도 있다</a>.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/f7e06c3e-aa04-49e7-a79a-8fead9bf6aa4/image.png" alt=""></p>
<p>근데 아직 포트폴리오 세부내용을 완전히 적지는 못했다. 일단은 방대한 코드를 다시 읽으면서 기술적 챌린지를 판단하는 건 무리수인 것 같아서... 클로드 코드한테 이력서 내용 + 실제 코드 내용 보고 간단히나마 정리해달라고 했다. 내일 참고해서 새롭게 다시 쓰고, 사진도 추가하고 해야겠다.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/178f593e-8fd8-43ad-aaff-8f6299cfebc5/image.png" alt=""></p>
<p>이력서 페이지도 있는데, 이력서는 얼추 작성은 했지만 웹페이지에는 ㄹㅇ Ctrl+C Ctrl+V만 해 둬서 아직 CSS를 많이 입히진 못했다. 나중에 좀 더 예쁘게 다듬을 생각이다.</p>
<h1 id="비동기">비동기</h1>
<p><a href="https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8?attributionToken=gAHwfwoMCOaY_8YGELTMgJkDEAEaJDY4ZjIzM2NhLTAwMDAtMmQ1MS1hMjY0LTNjMjg2ZDNkYTU0YSoGMjM1MjU5MiSQ97Iwm9a3LcLwnhXUsp0Vjr6dFajlqi2Y1rctt7eMLfbowzA6DmRlZmF1bHRfc2VhcmNoSAFYAWABaAF6AnNp">참고: 인프런 &lt;한입크기로 잡아먹는 리액트&gt;</a></p>
<h2 id="동기">동기</h2>
<p>기본적으로 JavaScript는 <strong>여러 개의 작업을 순서대로, 한 번에 한 개씩 처리</strong>한다. (동기)</p>
<pre><code class="language-js">function taskA() {
  console.log(&quot;나는 야옹입니다&quot;);
}

console.log(&quot;당신은 누구십니까&quot;);
taskA();
console.log(&quot;아하 그렇군요&quot;);

/* 출력 결과 
// 당신은 누구십니까
// 나는 야옹입니다
// 아하 그렇군요
*/</code></pre>
<p>다만 이런 동기 처리의 경우, 중간의 한 작업의 시간이 길어지면 전체 프로그램의 실행 속도도 느려지는 문제가 있다.</p>
<p>예를 들어, 지금 이 코드의 <code>taskA</code>가 만약 1분 소요되는 작업이였다면, 1분이 지날때까지 <code>console.log(&quot;아하 그렇군요&quot;</code>)가 출력되지 못한다.</p>
<p>특히 네트워크 요청(API라든지...), 파일 읽기 같이 오래 걸리는 작업일 때 애로사항이 많아진다.</p>
<h2 id="비동기-1">비동기</h2>
<p>이렇게 도중에 시간이 오래 걸리는 작업이 있는 경우, <strong>비동기 처리하여 다음 코드를 우선 실행 할 수 있다</strong>.</p>
<p><strong>비동기</strong>란 <strong>작업을 백그라운드에서 처리하면서, 다른 작업을 먼저 실행</strong>할 수 있는 방식이다. </p>
<p>즉 비동기를 잘 활용하면, 네트워크 요청이나 파일 읽기 등 작업이 이루어지는 동안 전체 프로그램이 멈추는 것을 방지할 수 있다.</p>
<pre><code class="language-js">console.log(&quot;약속장소에 도착했다&quot;);
// setTimeout(): 비동기 함수, 일정 시간이 지난 후에 콜백함수를 실행
setTimeout(() =&gt; {
  console.log(&quot;늦어서 미안해!&quot;);
}, 3000);
// 3초(3000ms) 타이머만 실행 후 다음 라인으로 넘어감, 기다리지 않음
console.log(&quot;얜 왜이렇게 늦게 와?&quot;);

/* 출력 결과
// 약속장소에 도착했다 (즉시)
// 얜 왜이렇게 늦게 와? (즉시)
// 늦어서 미안해! (3초 후)
*/</code></pre>
<p>위 코드의 <code>setTimeout</code>은 비동기 함수로, 콜백함수랑 시간(ms)을 인자로 받는다. 우선 인자로 받은 시간만큼 타이머를 실행한 후, 바로 다음 라인으로 넘어간다. 이후 시간이 경과하면 콜백함수가 실행된다.</p>
<pre><code class="language-js">console.log(&quot;데이터 요청 시작&quot;);

fetch(&quot;https://api.example.com/users&quot;)
  .then((response) =&gt; response.json())
  .then((data) =&gt; {
    console.log(&quot;데이터 받음:&quot;, data);
  });

console.log(&quot;다른 작업 실행 중...&quot;);

/* 출력 결과
// 데이터 요청 시작
// 다른 작업 실행 중...
// 데이터 받음: [사용자 데이터...] (API 응답 후 출력)
*/</code></pre>
<p>실제로는 <code>setTimeout</code> 대신 <code>fetch</code>와 같은 API 요청 함수가 중간에 들어가는 경우가 많은데, 이러한 함수를 비동기 처리하면, 처리가 완료될 때까지 이후의 라인을 우선 실행할 수 있다는 소리다. </p>
<p>물론 <code>fetch</code>에다가 콜백 함수를 넣어, API 요청이 완료된 후 후속 작업을 지정하는 것도 가능하다.</p>
<h2 id="js는-싱글쓰레드">js는 싱글쓰레드</h2>
<p>C#, Java 같은 언어는 멀티쓰레드를 통해 비동기 처리를 실행한다. <strong>쓰레드는 프로그램이 작업을 실행하는 흐름의 단위</strong>인데, 이런 언어들론 병렬로 여러 작업을 실행할 수 있다....정도로 이해하면 된다.</p>
<p>하지만 애석하게도 JavaScript 엔진은 <strong>싱글쓰레드다. 한 번에 하나의 작업만 처리할 수 있다는 뜻</strong>이다. 그런데도 이런 비동기 처리가 가능하다. 왤까?</p>
<p>이는 비동기 작업은 JavaScript 엔진이 아닌, <strong>Web API(브라우저가 제공하는 별도 공간이라고만 알아두기)</strong>에서 실행되기 때문이다.</p>
<p>클로드 형님께서 그려준 아래 그림을 보고 이해해보자.</p>
<pre><code class="language-JS">  JS ENGINE (Call Stack)          WEB APIs              Callback Queue
┌─────────────────────────┐    ┌──────────────────┐       ┌─────────────────┐
│                       │    │                  │    │                 │
│  1️⃣ console.log()     │    │                  │    │                 │
│     &quot;약속장소에...&quot;      │    │                  │    │                 │
│  ✓ 실행 완료            │    │                  │    │                 │
│                       │    │                  │    │                 │
│  2️⃣ setTimeout()      │───▶│  Timer 시작      │    │                 │
│     ✓ Web API에 위임    │    │  (3000ms)       │    │                 │
│     ✓ 즉시 다음으로      │    │   ⏱️ 카운트다운   │     │                 │
│                       │    │                  │    │                 │
│  3️⃣ console.log()     │    │   ⏱️ 카운트다운   │     │                 │
│     &quot;얜 왜이렇게...&quot;     │    │                  │    │                 │
│  ✓ 실행 완료            │    │   ⏱️ 카운트다운   │    │                 │
│                        │   │                  │    │                 │
│  [비어있음]              │    │  ✓ 3초 완료!     │───▶│  () =&gt; {...}    │
│   대기 중...            │    │                  │    │  콜백 대기 중   │
│                        │    │                 │    │                 │
│                        │    │                 │    │                 │
│  4️⃣ () =&gt; console.log()  │◀───────────────────────────│  콜백 전달      │
│     &quot;늦어서 미안해!&quot;     │    │                  │    │                 │
│  ✓ 실행!               │    │                  │    │  [비어있음]     │
│                        │   │                  │    │                 │
└─────────────────────────┘    └──────────────────┘     └─────────────────┘
         ▲                                                      │
         │                                                      │
         └──────────────────────────────────────────────────────┘
                        Event Loop (계속 감시)
                &quot;Call Stack 비었어? 그럼 Queue에서 가져올게!&quot;</code></pre>
<p>일반적인 동기 함수(e.g., <code>console.log()</code>)는 자바스크립트 엔진의 <strong>콜 스택</strong>에서 실행된다. </p>
<p>하지만 <code>setTimeout()</code>같은 비동기 함수를 만나면, 이러한 절차가 이루어진다.</p>
<ol>
<li><p>자바스크립트 엔진은 비동기 작업을 <strong>Web API</strong>에 맡긴다. 이와 동시에 <code>setTimeout()</code> 종료 후 실행될 콜백 함수도 전달한다.</p>
</li>
<li><p>Web API가 3초 타이머를 실행한다.</p>
</li>
</ol>
<p>3.<code>setTimeout()</code>의 3초 타이머가 종료되면, WEB API가 콜백 함수를 콜백 큐에 추가한다. </p>
<ul>
<li>콜백 큐는 역시 Web API처럼 브라우저에서 관리한다.</li>
</ul>
<ol start="4">
<li>이때 브라우저의 <strong>Event Loop</strong>이라는 친구가 계속 콜 스택이 비어 있는지 확인한다. 비어 있으면 콜백 큐에서 함수를 가져와, 콜 스택에서 다시 실행하는 식이다. </li>
</ol>
<ul>
<li>얘 덕분에 콜백 함수가 기존에 실행되는 동기 작업을 강제 중단시키고 새치기하는 일은 없게 된다.</li>
</ul>
<h2 id="콜백-지옥">콜백 지옥</h2>
<p>다만 비동기 함수 때 콜백 함수를 사용할 땐 애로사항이 많다.</p>
<p>이를테면, 비동기 작업이 끝나고 다른 비동기 작업을 연달아 실행하는 경우가 있다고 하자.</p>
<p>그러면 콜백 함수 자리에 다른 비동기 함수를 넣어 줘야 하는데, 이러한 작업이 많아질수록 코드가 복잡해진다.</p>
<p>실제로 이런 코드를 적을 일은 없지만, 1초 간격으로 음식을 먹었다고 출력하는 코드를 짰다고 가정해 보자.</p>
<pre><code class="language-js">// 콜백 함수

function eatMandu(callback) {
  setTimeout(() =&gt; {
    const ateMandu = &quot;만두도 먹고&quot;;
    callback(ateMandu);
  }, 1000);
}

function eatPizza(food, callback) {
  setTimeout(() =&gt; {
    const atePizza = `${food} 피자도 먹고`;
    callback(atePizza);
  }, 1000);
}

function eatJjajang(food, callback) {
  setTimeout(() =&gt; {
    const ateJjajang = `${food} 짜장면도 먹고`;
    callback(ateJjajang);
  }, 1000);
}

// 비동기 작업의 결과를 다른 비동기 작업의 인수로 활용할 수 있음
// 보다시피 복잡함 - 콜백 지옥 우려
eatMandu((ateMandu) =&gt; {
  console.log(ateMandu);
  eatPizza(ateMandu, (atePizza) =&gt; {
    console.log(atePizza);
    eatJjajang(atePizza, (ateJjajang) =&gt; {
      console.log(ateJjajang);
    });
  });
});

// 출력결과
// 만두도 먹고 (1초 후 출력)
// 만두도 먹고 피자도 먹고 (2초 후 출력)
// 만두도 먹고 피자도 먹고 짜장면도 먹고 (3초 후 출력)</code></pre>
<p>보시다시피 비동기작업이 3번 연달아 일어나니 코드가 복잡해지는 것을 볼 수 있다.</p>
<p>이러한 현상을 콜백 지옥이라 하는데, <code>Promise</code>나 <code>async/await</code>라는 친구로 해결할 수 있다. 거기서부턴 내일 정리할게용~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[1004] JS문법복습 - 원시 vs 객체 타입, 배열 메서드, Date 객체]]></title>
            <link>https://velog.io/@strawberry-tree/1004A-JS%EB%AC%B8%EB%B2%95%EB%B3%B5%EC%8A%B5-%EC%9B%90%EC%8B%9C-vs-%EA%B0%9D%EC%B2%B4-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@strawberry-tree/1004A-JS%EB%AC%B8%EB%B2%95%EB%B3%B5%EC%8A%B5-%EC%9B%90%EC%8B%9C-vs-%EA%B0%9D%EC%B2%B4-%ED%83%80%EC%9E%85</guid>
            <pubDate>Sat, 04 Oct 2025 11:55:15 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/fad9e9c2-264a-4d1a-b731-ebd9fadbba88/image.png" alt=""></p>
<p>일단 오늘도 <a href="https://www.rallit.com/hub/resumes/466135/%EC%86%A1%EC%83%81%EB%A1%9D">이력서 다듬기</a>를 했는데, 뭔가 지금까지 한 프로젝트들이 잘 정리가 안 되니까 이력서도 어떻게 정리해야 할지 헷갈리는 감이 있다.</p>
<p>그래서 내일은 이력서 말고 포트폴리오를 작업하면서 지금까지 한 프로젝트들을 좀 다시 정리해볼 생각이다.</p>
<p><a href="https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8?attributionToken=gAHwfwoMCOaY_8YGELTMgJkDEAEaJDY4ZjIzM2NhLTAwMDAtMmQ1MS1hMjY0LTNjMjg2ZDNkYTU0YSoGMjM1MjU5MiSQ97Iwm9a3LcLwnhXUsp0Vjr6dFajlqi2Y1rctt7eMLfbowzA6DmRlZmF1bHRfc2VhcmNoSAFYAWABaAF6AnNp">참고: 인프런 &lt;한입크기로 잡아먹는 리액트&gt;</a></p>
<h1 id="원시-vs-객체-타입">원시 vs 객체 타입</h1>
<p>JavaScript에서 변수에 할당되는 값들은 메모리에 저장이 된다.</p>
<p>이때 실제로 값이 저장되는 방식은, 값이 <strong>원시 타입</strong>인지, <strong>객체 타입</strong>인지에 따라 달라진다.</p>
<h2 id="원시-타입-불변-타입">원시 타입 (불변 타입)</h2>
<ul>
<li><code>number</code>, <code>string</code>, <code>boolean</code>, <code>null</code>, <code>undefined</code> 등 타입은 원시 타입이다.</li>
<li><strong>값 자체가 메모리에 저장</strong>되며, 변수는 실제 값을 가리킨다.</li>
</ul>
<pre><code class="language-js">
  변수 선언:  let p1 = 1;

     변수          메모리
   ┌──────┐     ┌──────┐
   │  p1  │----&gt;│  1   │  메모리 주소: 0x001
   └──────┘     └──────┘

  복사:  let p2 = p1;

     변수          메모리
   ┌──────┐     ┌──────┐
   │  p1  │----&gt;│  1   │  메모리 주소: 0x001
   └──────┘     └──────┘
   ┌──────┐     ┌──────┐
   │  p2  │----&gt;│  1   │  메모리 주소: 0x002 (새로운 복사본!)
   └──────┘     └──────┘</code></pre>
<ul>
<li><code>p2</code>에 <code>p1</code> 값을 할당하면, 메모리상 값이 복사되어 독립적인 메모리에 저장된다.</li>
<li>따라서 <code>p2</code>와 <code>p1</code>은 서로 영향을 주지 않는다.</li>
</ul>
<pre><code class="language-js">  재할당:  p2 = 2;

     변수          메모리
   ┌──────┐     ┌──────┐
   │  p1  │----&gt;│  1   │  메모리 주소: 0x001 (변경 없음)
   └──────┘     └──────┘
                ┌──────┐
   ┌──────┐     │  1   │  메모리 주소: 0x002 (버려짐)
   │  p2  │-╳   └──────┘
   └──────┘ ║   ┌──────┐
            ╚═=&gt;│  2   │  메모리 주소: 0x003 (새 값!)
                └──────┘</code></pre>
<ul>
<li><code>p2 = 2</code>와 같이 변수에 값을 재할당하더라도, 기존의 값 <code>1</code>은 메모리에서 수정되지 않는다.<ul>
<li>그래서 원시 타입을 <strong>불변 타입</strong>으로도 부른다.</li>
</ul>
</li>
<li>단지 메모리에 새로운 값 <code>2</code>가 새로 저장되고, 변수 <code>p2</code>가 새로운 값을 가리킬 뿐이다.</li>
</ul>
<h2 id="객체-타입">객체 타입</h2>
<ul>
<li><code>object</code>, <code>array</code>, <code>function</code> 등 타입은 객체 타입이다.</li>
<li><strong>실제 객체</strong> 및 해당 객체의 <strong>참조값(주소)</strong>이 모두 메모리에 저장되며, 변수는 참조값을 가리킨다.<ul>
<li>배열의 원소나 객체의 프로퍼티가 추가/삭제/수정될 수 있기 때문에 이러한 방식으로 저장된다고 한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-js">변수 선언:  let o1 = { place: &quot;서울&quot; };

     변수          참조값           실제 객체
   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o1 │----&gt;│ 0xA001 │-----&gt;│ place: &quot;서울&quot;   │  메모리: 0xA001
   └──────┘     └────────┘      └─────────────────┘</code></pre>
<ul>
<li>변수 <code>o1</code>에는 실제 객체를 가리키는 참조값이 저장되며, 실제 데이터는 별도 메모리에 존재한다.</li>
</ul>
<pre><code class="language-js">  얕은 복사:  let o2 = o1;

     변수          참조값           실제 객체
   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o1 │----&gt;│ 0xA001 │--┐   │ place: &quot;서울&quot;   │  메모리: 0xA001
   └──────┘     └────────┘  │   └─────────────────┘
   ┌──────┐     ┌────────┐  │          ▲
   │  o2 │----&gt;│ 0xA001 │--┘         │
   └──────┘     └────────┘             │
                                동일한 객체를 가리킴!</code></pre>
<ul>
<li><code>o2</code>에 <code>o1</code> 값을 할당할 때, 메모리상 실제 객체가 아닌 <strong>참조값이 복사된다. (얕은 복사)</strong></li>
<li>따라서 <code>o2</code>와 <code>o1</code>은 <strong>동일한 객체를 가리키므로</strong> 서로 영향을 줄 수 있다.</li>
</ul>
<pre><code class="language-js">  값 수정:  o2.place = &quot;인천&quot;;

     변수          참조값           실제 객체
   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o1  │----&gt;│ 0xA001 │--┐   │ place: &quot;인천&quot;   │  메모리: 0xA001
   └──────┘     └────────┘  │   └─────────────────┘
   ┌──────┐     ┌────────┐  │          ▲
   │  o2  │----&gt;│ 0xA001 │--┘         │
   └──────┘     └────────┘             │
                                 둘 다 영향받음!

  console.log(o1.place)    // &quot;인천&quot;
</code></pre>
<ul>
<li><code>o2</code>의 <code>place</code>를 <code>인천</code>으로 바꾸면, 실제객체(<code>0xA001</code>)의 데이터가 바뀌므로<code>o1.place</code>도 <code>인천</code>으로 변경된다.<ul>
<li>그래서 원시 타입을 <strong>가변 타입</strong>으로도 부른다.</li>
</ul>
</li>
</ul>
<h2 id="깊은-복사">깊은 복사</h2>
<ul>
<li>만약 원래 객체를 복사해야 하는데 위처럼 의도치 않게 값이 삭제되는 일을 막으려면, <code>...</code>(스프레드 연산자)로 깊은 복사를 수행해야 한다.</li>
</ul>
<pre><code class="language-js">  선언:  let o3 = { place: &quot;서울&quot; };
         let o4 = { ...o3 };

     변수          참조값           실제 객체
   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o3  │----&gt;│ 0xB001 │-----&gt;│ place: &quot;서울&quot;   │  메모리: 0xB001
   └──────┘     └────────┘      └─────────────────┘

   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o4  │----&gt;│ 0xB002 │-----&gt;│ place: &quot;서울&quot;   │  메모리: 0xB002
   └──────┘     └────────┘      └─────────────────┘
                                     (새 객체!)</code></pre>
<ul>
<li><code>...</code>(스프레드연산자)는 배열/객체의 여러 값을 개별로 흩뿌려 주는 역할을 수행한다.</li>
<li>위와 같이 <code>let o4 = {...o3}</code>로 복사 시, 메모리에 새로운 객체가 생성되며, 프로퍼티만 따로 복사된다.<ul>
<li>이를 <strong>깊은 복사</strong>라 한다.</li>
</ul>
</li>
</ul>
<pre><code class="language-js">  값 수정:  o4.place = &quot;대전&quot;;

     변수          참조값           실제 객체
   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o3  │----&gt;│ 0xB001 │-----&gt;│ place: &quot;서울&quot;   │  변경 없음!
   └──────┘     └────────┘      └─────────────────┘

   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o4  │----&gt;│ 0xB002 │-----&gt;│ place: &quot;대전&quot;   │  o4만 변경!
   └──────┘     └────────┘      └─────────────────┘</code></pre>
<ul>
<li><code>o3</code>, <code>o4</code>는 별개 객체를 가리키므로, <code>o4.place</code>를 <code>대전</code>으로 변경해도 <code>o3.place</code>는 변경되지 않는다.</li>
</ul>
<h2 id="깊은-비교">깊은 비교</h2>
<ul>
<li>객체를 비교할 땐, 실제 객체의 값이 아닌 참조값 기준으로 비교가 이루어진다.</li>
</ul>
<pre><code class="language-js">
  설정:  let o5 = { place: &quot;대구&quot; };
         let o6 = o5;        // 얕은 복사
         let o7 = { ...o5 }; // 깊은 복사


  [얕은 비교 - 참조값 비교]

    o5 === o6  →  true

    ┌────────┐      ┌─────────────────┐
    │ 0xC001 │-----&gt;│ place: &quot;대구&quot;   │
    └────────┘      └─────────────────┘
         ▲                   ▲
         │                   │
      o5, o6 모두 같은 참조값!


    o5 === o7  →  false

    ┌────────┐      ┌─────────────────┐
    │ 0xC001 │-----&gt;│ place: &quot;대구&quot;   │  o5
    └────────┘      └─────────────────┘

    ┌────────┐      ┌─────────────────┐
    │ 0xC002 │-----&gt;│ place: &quot;대구&quot;   │  o7
    └────────┘      └─────────────────┘

    참조값이 다름!</code></pre>
<ul>
<li><code>o6</code>은 <code>o5</code>와 같은 객체를 가리켜 (동일 참조값) 비교 결과가 <code>true</code>가 된다. </li>
<li><code>o7</code>은 스프레드연산자를 통해 생성된 새로운 객체기 때문에, <code>o5</code>와 프로퍼티의 키/값이 완전히 동일하더라도 참조값이 다르므로 비교 결과가 <code>false</code>가 된다.</li>
<li>이러한 <code>===</code>를 이용한 참조값 기준 비교를 <strong>얕은 비교</strong>라고 한다.</li>
</ul>
<pre><code class="language-js">  [깊은 비교 - 내부 값 비교]

    JSON.stringify(o5) === JSON.stringify(o7)  →  true

    &quot;{&quot;place&quot;:&quot;대구&quot;}&quot; === &quot;{&quot;place&quot;:&quot;대구&quot;}&quot;

    문자열로 변환하여 내부 값 비교!</code></pre>
<ul>
<li>만약 실제 객체의 값 기준으로 비교를 하고 싶다면, <code>JSON.stringify</code>를 사용해 객체의 값을 문자로 변경한 뒤 내부 값을 비교해야 한다. 이를 <strong>깊은 비교</strong>라고 한다.</li>
</ul>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>타입</th>
<th>저장 방식</th>
<th>복사 시</th>
<th>수정 시</th>
</tr>
</thead>
<tbody><tr>
<td>원시 타입 (불변)</td>
<td>값 자체 저장</td>
<td>새 값 복사됨 (독립적)</td>
<td>새 메모리에 저장 (원본 불변)</td>
</tr>
<tr>
<td>객체 타입 (가변)</td>
<td>참조값 저장</td>
<td>참조값 복사됨 (공유됨)</td>
<td>원본 객체 수정 (모두 영향)</td>
</tr>
</tbody></table>
<h1 id="배열-메서드">배열 메서드</h1>
<ul>
<li>너무 많지만 리액트에서 흔히 사용되는 메서드들 위주로만 정리해보았다.</li>
</ul>
<h2 id="필수-3형제">필수 3형제</h2>
<h3 id="map"><code>map</code></h3>
<ul>
<li>모든 요소에 대해 콜백 함수 수행 후, 그 반환값으로 이루어진 새로운 배열을 반환한다.<pre><code class="language-js">let arr = [1, 2, 3];
let arr_squared = arr.map((item, idx, arr) =&gt; {
console.log(`${idx}번째 요소: ${item}`);
return item ** 2;
});
// 0번째 요소: 1
// 1번째 요소: 2
// 2번째 요소: 3
console.log(arr_squared); // [1, 4, 9]
</code></pre>
</li>
</ul>
<p>let arr = [
  { name: &quot;롯데리아&quot;, category: &quot;햄버거&quot; },
  { name: &quot;김가네&quot;, category: &quot;김밥&quot; },
  { name: &quot;설빙&quot;, category: &quot;디저트&quot; },
  { name: &quot;맥도날드&quot;, category: &quot;햄버거&quot; },
];
let food_names = arr.map((item) =&gt; item.name);
console.log(food_names); // [ &#39;롯데리아&#39;, &#39;김가네&#39;, &#39;설빙&#39;, &#39;맥도날드&#39; ]</p>
<pre><code>
- 리액트에서는 배열을 JSX 요소 배열로 변환하여, 화면에 리스트를 렌더링할 때 주로 사용된다.
  - 상품 목록, 드롭다운 옵션, 테이블 행 등...
```js
{users.map(user =&gt; &lt;UserCard key={user.id} user={user} /&gt;)}</code></pre><h3 id="filter"><code>filter</code></h3>
<ul>
<li><p>콜백 함수 결과가 <code>true</code>인 요소들만 이루어진 새로운 배열을 반환한다.</p>
<pre><code class="language-js">// 콜백함수 결과가 true인 요소들로만 이루어진 새로운 배열 반환
let arr = [
{ name: &quot;롯데리아&quot;, category: &quot;햄버거&quot; },
{ name: &quot;김가네&quot;, category: &quot;김밥&quot; },
{ name: &quot;설빙&quot;, category: &quot;디저트&quot; },
{ name: &quot;맥도날드&quot;, category: &quot;햄버거&quot; },
];
console.log(arr.filter((item) =&gt; item.category === &quot;햄버거&quot;));
// [ { name: &#39;롯데리아&#39;, category: &#39;햄버거&#39; }, { name: &#39;맥도날드&#39;, category: &#39;햄버거&#39; } ]</code></pre>
</li>
<li><p>리액트에서는 검색, 카테고리에 따라 조건에 맞는 데이터만 추출해서 보여줄 때 주로 사용된다.</p>
<pre><code class="language-js">const activeUsers = users.filter(user =&gt; user.isActive);</code></pre>
</li>
</ul>
<h3 id="find"><code>find</code></h3>
<pre><code class="language-js">// 배열에서 콜백함수 결과가 true인 첫 요소를 반환. 없으면 undefined 반환
let arr = [1, 2, 3];
console.log(arr.find((item) =&gt; item &gt; 1)); // 2

let objectArr = [{ name: &quot;누렁이&quot; }, { name: &quot;탱고&quot; }, { name: &quot;빵아지&quot; }];
console.log(objectArr.find((item) =&gt; item.name === &quot;탱고&quot;));
// { name: &#39;탱고&#39; }</code></pre>
<ul>
<li>리액트에선 파라미터, ID를 기반으로 데이터를 찾아 상세 페이지를 렌더링할 때 주로 사용된다.<pre><code class="language-js">const user = users.find(u =&gt; u.id === userId);</code></pre>
</li>
</ul>
<h2 id="덜-중요한데-쓰이긴-함">덜 중요한데 쓰이긴 함</h2>
<h3 id="slice"><code>slice</code></h3>
<ul>
<li>배열의 특정 범위를 잘라, 새로운 배열로 반환한다.</li>
</ul>
<pre><code class="language-js">let arr = [1, 2, 3, 4, 5];
console.log(arr.slice(1, 4)); // [2, 3, 4] (인덱스 1부터 4 전까지)
console.log(arr.slice(2)); // [3, 4, 5] (인덱스 2부터 끝까지)
console.log(arr.slice(-2)); // [4, 5] (뒤에서부터 2개)
console.log(arr); // [1, 2, 3, 4, 5] (원본 배열은 변경되지 않음)</code></pre>
<ul>
<li>리액트에선 페이지네이션으로 현재 페이지에 해당하는 데이터만 잘라내거나, 원본 배열을 유지하며 특정 요소를 삭제할 때 사용한다.<pre><code class="language-js">// 페이지네이션 예시
const currentPage = 2;
const itemsPerPage = 10;
const displayItems = items.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);</code></pre>
</li>
</ul>
<pre><code class="language-js">const newArr = [...arr.slice(0, index), ...arr.slice(index + 1)];</code></pre>
<h3 id="sorttosorted"><code>sort</code>/<code>toSorted</code></h3>
<ul>
<li>사전 순으로 배열 요소를 정렬한다. <strong>원본 배열이 변경된다!!</strong></li>
<li>사전 순이므로, 숫자값 등을 비교할 시 별도 비교 콜백 함수로 명시해야 한다.<ul>
<li>매개변수 2개를 받으며, <code>-1</code> 반환 시 앞 변수, <code>1</code> 반환 시 뒷 변수가 앞에 오게끔 정렬된다.</li>
</ul>
</li>
</ul>
<pre><code class="language-js">// 사전순으로 배열 요소를 정렬. 원본 배열이 변경됨
let arr = [5, 10, 3];
arr.sort();
console.log(arr); // [10, 3, 5] (사전순임에 유의)

// 숫자값 기준 시, 비교 콜백함수로 명시해야 함
arr.sort((a, b) =&gt; {
  if (a &gt; b) {
    // 양수 반환 -&gt; 뒤의 매개변수(b)가 먼저 옴
    return 1;
  } else if (a &lt; b) {
    // 음수 반환 -&gt; 앞의 매개변수(a)가 먼저 옴
    return -1;
  } else {
    // 0 반환 -&gt; 두 값의 자리를 바꾸지 않음
    return 0;
  }
});
console.log(arr); // [3, 5, 10]

let arr_desc = [5, 10, 3];

// 내림차순일 시 그 반대로...
arr_desc.sort((a, b) =&gt; {
  if (a &gt; b) {
    return -1;
  } else if (a &lt; b) {
    return 1;
  } else {
    return 0;
  }
});</code></pre>
<ul>
<li>리액트에서는 최신순, 가격순 등 정렬 기능을 구현할 때 사용된다.<ul>
<li><strong>반드시 복사본을 만든 후 정렬해야 한다.</strong><pre><code class="language-js">const sorted = [...items].sort((a, b) =&gt; a.order - b.order);</code></pre>
</li>
</ul>
</li>
<li>원본이 유지되고 새로운 배열을 반환하는 <code>toSorted</code>를 사용할 수도 있다.<pre><code class="language-js">const sorted = items.toSorted((a, b) =&gt; a.price - b.price);</code></pre>
</li>
</ul>
<h2 id="알면-좋은-나머지-메서드-요약">알면 좋은 나머지 메서드 요약</h2>
<p>내가 다 정리하기 귀찮아서 클로드가 요약해줬다.</p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
<th>자주 쓰나?</th>
<th>React에서 언제?</th>
<th>대안</th>
</tr>
</thead>
<tbody><tr>
<td><code>findIndex</code></td>
<td>조건 만족하는 첫 요소의 인덱스 반환</td>
<td>⭐⭐⭐</td>
<td>특정 요소 수정/삭제 시</td>
<td>-</td>
</tr>
<tr>
<td><code>includes</code></td>
<td>특정 요소 포함 여부 확인</td>
<td>⭐⭐⭐</td>
<td>선택 여부, 권한 체크</td>
<td>-</td>
</tr>
<tr>
<td><code>forEach</code></td>
<td>모든 요소 순회하며 작업 수행 (반환값 필요 없을 때)</td>
<td>⭐⭐</td>
<td>useEffect 내 부수효과 처리</td>
<td><code>map</code> 선호</td>
</tr>
<tr>
<td><code>join</code></td>
<td>배열을 문자열로 결합</td>
<td>⭐⭐</td>
<td>태그, 주소 등 문자열 표시</td>
<td>-</td>
</tr>
<tr>
<td><code>concat</code></td>
<td>두 배열 합치기</td>
<td>⭐</td>
<td>무한 스크롤 데이터 추가</td>
<td><code>[...arr1, ...arr2]</code> 선호</td>
</tr>
<tr>
<td><code>indexOf</code></td>
<td>요소의 인덱스 반환</td>
<td>⭐</td>
<td>원시값 인덱스 찾기</td>
<td><code>includes</code>, <code>findIndex</code> 더 직관적</td>
</tr>
<tr>
<td><code>push</code></td>
<td>마지막에 요소 추가 (원본 변경)</td>
<td>❌</td>
<td>사용 금지</td>
<td><code>[...items, newItem]</code></td>
</tr>
<tr>
<td><code>pop</code></td>
<td>마지막 요소 제거 (원본 변경)</td>
<td>❌</td>
<td>사용 금지</td>
<td><code>items.slice(0, -1)</code></td>
</tr>
<tr>
<td><code>shift</code></td>
<td>첫 요소 제거 (원본 변경)</td>
<td>❌</td>
<td>사용 금지</td>
<td><code>items.slice(1)</code></td>
</tr>
<tr>
<td><code>unshift</code></td>
<td>첫 요소 추가 (원본 변경)</td>
<td>❌</td>
<td>사용 금지</td>
<td><code>[newItem, ...items]</code></td>
</tr>
</tbody></table>
<h3 id="리액트에서-push-pop-shift-unshift-쓰면-안되는-이유">리액트에서 <code>push</code>, <code>pop</code>, <code>shift</code>, <code>unshift</code> 쓰면 안되는 이유</h3>
<ul>
<li>리액트에서는 상태 변화를 참조값(메모리주소) 비교로 감지한다.</li>
<li>아예 새로운 배열을 만든 경우 참조값이 변경돼서 변화를 감지할 수 있다.</li>
<li>하지만 원본 배열을 직접 수정하면, 참조값은 그대로이므로 변화를 감지할 수 없다.</li>
</ul>
<pre><code class="language-js">// 올바르지 않은 방법
const [items, setItems] = useState([1, 2, 3]);

const handleAdd = () =&gt; {
  items.push(4);        // 원본 배열 수정
  setItems(items);      // 같은 참조(주소) -&gt; React는 변화 없다고 판단
};
// 화면이 업데이트되지 않음

// 새로운 배열 생성 - React가 변화 감지
const handleAdd = () =&gt; {
  setItems([...items, 4]);  // 새 배열 생성 -&gt; 다른 참조(주소)
};
// 화면이 정상적으로 업데이트됨</code></pre>
<h1 id="date-객체와-날짜">Date 객체와 날짜</h1>
<h2 id="date-객체">Date 객체</h2>
<ul>
<li>후술할 각종 메서드를 사용할 수 있다.<pre><code class="language-js">let date1 = new Date(); // 매개변수 없을 시현재 시간
console.log(date1);
// Sat Oct 04 2025 22:17:15 GMT+0900 (한국 표준시)
// 결과는 실행 시점에 따라 다름
</code></pre>
</li>
</ul>
<p>let date2 = new Date(2000, 6, 22, 12, 30, 59);
// 월은 0부터 시작함에 유의 (0: 1월, 1: 2월, ..., 6: 7월)
console.log(date2); // Sat Jul 22 2000 12:30:59 GMT+0900 (한국 표준시)</p>
<pre><code>
## 타임 스탬프
- 특정 시간이 1970년 1월 1일 00:00:00 UTC로부터 몇 밀리초가 지났는지 나타낸 값
- 참고로 대한민국은 UTC보다 9시간 빠르다.

```js
let ts1 = date2.getTime();
console.log(ts1); // 964296659000

// 타임 스탬프로부터 Date 객체 생성 가능
let date3 = new Date(ts1);
console.log(date3); // Sat Jul 22 2000 12:30:59 GMT+0900 (한국 표준시)</code></pre><h2 id="시간-요소의-추출-수정">시간 요소의 추출, 수정</h2>
<ul>
<li>추출은 <code>get</code>, 수정은 <code>set</code>을 기억하자.<pre><code class="language-js">// 시간 요소 추출
let year = date2.getFullYear();
let month = date2.getMonth() + 1; // 월은 0부터 시작하므로 +1
let day = date2.getDate();
let hours = date2.getHours();
let minutes = date2.getMinutes();
let seconds = date2.getSeconds();
console.log(
`${year}년 ${month}월 ${day}일 ${hours}시 ${minutes}분 ${seconds}초`
);
// 2000년 7월 22일 12시 30분 59초
</code></pre>
</li>
</ul>
<p>// 시간 요소 수정
date2.setFullYear(2025);
date2.setMonth(9); // 10월 (0부터 시작)
date2.setDate(4);
date2.setHours(22);
date2.setMinutes(15);
date2.setSeconds(30);
console.log(date2); // Sat Oct 04 2025 22:15:30 GMT+0900 (한국 표준시)</p>
<pre><code>
## 시간 포맷팅
```js
console.log(date2.toDateString()); // Sat Oct 04 2025
console.log(date2.toLocaleString()); // 2025. 10. 4. 오후 10:15:30
console.log(date2.toLocaleDateString()); // 2025. 10. 4.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[1003] JS문법복습 - ??, 호이스팅, 콜백함수]]></title>
            <link>https://velog.io/@strawberry-tree/1003-JS%EB%AC%B8%EB%B2%95-%EB%B3%B5%EC%8A%B5-%ED%98%B8%EC%9D%B4%EC%8A%A4%ED%8C%85-%EC%BD%9C%EB%B0%B1%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@strawberry-tree/1003-JS%EB%AC%B8%EB%B2%95-%EB%B3%B5%EC%8A%B5-%ED%98%B8%EC%9D%B4%EC%8A%A4%ED%8C%85-%EC%BD%9C%EB%B0%B1%ED%95%A8%EC%88%98</guid>
            <pubDate>Fri, 03 Oct 2025 13:35:09 GMT</pubDate>
            <description><![CDATA[<p>지금까지 자바스크립트는 &#39;대충 파이썬이랑 비슷하게 간단한거구만&#39; + &#39;모르는 건 클로드 코드가 알아서 해 주겠지ㅇㅇ&#39; 정도로 생각해서 공부를 별로 안 했지만</p>
<p>정글 최종프로젝트를 하면서, 자바스크립트에 헷갈리는 게 많다보니 타입스크립트든 리액트든 Next.js도 점점 어려워지는 걸 씨게 느꼈다...</p>
<p>그래서 복습하면서 특히나 헷갈리는 개념 위주로 정리를 해보기로 했다.</p>
<p><a href="https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8?attributionToken=gAHwfwoMCOaY_8YGELTMgJkDEAEaJDY4ZjIzM2NhLTAwMDAtMmQ1MS1hMjY0LTNjMjg2ZDNkYTU0YSoGMjM1MjU5MiSQ97Iwm9a3LcLwnhXUsp0Vjr6dFajlqi2Y1rctt7eMLfbowzA6DmRlZmF1bHRfc2VhcmNoSAFYAWABaAF6AnNp">참고: 인프런 &lt;한입크기로 잡아먹는 리액트&gt;</a></p>
<p>(cf. 마음같아선 하루종일 인강 들으면서 빨리 끝내고싶지만, 이력서 / 포트폴리오 준비랑 병행하느라 그러진 못할것같다)</p>
<h1 id="-null-병합-연산자">(<code>??</code>) null 병합 연산자</h1>
<ul>
<li>왼쪽 값이 <code>null</code> 또는 <code>undefined</code>일 때만 오른쪽 값을 반환한다.</li>
</ul>
<pre><code class="language-js">// 존재하는 값을 추려내는 기능 (null, undefined 제외)
// e.g., 회원가입
let userName = &quot;코딩왕 송상록&quot;;
let displayName = userName ?? &quot;익명의 두루미&quot;; // 회원이름이 없으면 익명의 두루미로 표시
console.log(displayName); // 코딩왕 송상록</code></pre>
<h2 id="과의-비교"><code>||</code>과의 비교</h2>
<ul>
<li><code>||</code> (논리 OR 연산자) 같은 경우는 왼쪽 값이 <code>null</code>, <code>undefined</code>가 아닌 다른 <code>falsy</code> 값(<code>0</code>, <code>&quot;&quot;</code> - 빈 문자열, <code>false</code>)일 때도 체크한다.</li>
<li>하지만 <code>??</code>은 오직 <code>null</code>, <code>undefined</code> 둘만 체크한다.</li>
</ul>
<pre><code class="language-js">let userName = &quot;&quot;;
let displayName = userName || &quot;익명의 두루미&quot;; 
console.log(displayName); // &quot;익명의 두루미&quot; -&gt; &#39;&#39;(빈 문자열)도 falsy라서 OR에서는 오른쪽 값 반환

let displayName2 = userName ?? &quot;익명의 두루미&quot;;
console.log(displayName2); // &quot;&quot; -&gt; null이나 undefined일 때만 오른쪽 값 반환</code></pre>
<h1 id="호이스팅">호이스팅</h1>
<ul>
<li>Javascript 특유의 함수 호출법으로, 함수를 선언되기 전에 호출하는 방식이다.</li>
<li><code>function 함수이름(){}</code> 꼴의 함수 선언식으로 선언된 함수에서만 적용 가능하다.</li>
<li>변수에 익명 함수를 할당한 함수 표현식에는 적용 불가능하다. <ul>
<li>이건 <code>let</code>, <code>const</code> 따위로 선언한 변수를 선언 전에 사용할 수 없는 거랑 동일한 이유라고 생각하심 된다.</li>
<li>이걸 복잡한 말로 <strong>Temporal Dead Zone(TDZ)</strong>이라 부른다. 실제로는 선언 전에도 변수 확인은 가능하지만, 초기화되어 있지 않아 정상적인 접근이 불가능하다.</li>
</ul>
</li>
</ul>
<pre><code class="language-js">// 호이스팅: &#39;끌어올린다&#39;는 뜻. 함수가 선언되기 전에 호출해도 문제 없음.

console.log(getArea(10, 5));

// 매개변수와 인수
function getArea(width, height) {
  const area = width * height;
  console.log(`가로 ${width}, 세로 ${height}인 사각형의 넓이는 ${area}입니다.`);
  return area;
}

// 단, 함수 표현식을 사용하는 경우... 변수에 함수를 할당하는 방식이므로, 호이스팅 불가능.
console.log(mandu());    // ReferenceError 발생
let mandu = function () {
  console.log(&quot;만두먹고싶다&quot;);
};</code></pre>
<h2 id="python에선-됐었나">Python에선 됐었나?</h2>
<pre><code class="language-python">print(mandu())  # NameError: name &quot;mandu&quot; is not defined

def mandu():
    return &quot;만두먹고싶다&quot;</code></pre>
<p>안 되니까 코테 볼 때 헷갈리지 말자.</p>
<h1 id="콜백-함수">콜백 함수</h1>
<ul>
<li>자신이 아닌 다른 함수에, 인수로써 전달된 함수를 의미한다.</li>
<li>자바스크립트 특유의 비동기 처리에 흔히 사용되는데, 이건 내일 들을 강의 내용이라서 그때 더 정리해 보려고 한다.<ul>
<li>일단은 &#39;작업이 완료된 뒤 실행될 함수&#39;를 콜백 함수로 흔히 전달한다고만 알아두면 된다.</li>
</ul>
</li>
</ul>
<pre><code class="language-js">// 자신이 아닌 다른 함수에, 인수로써 전달된 함수를 의미함

function repeat(count, callback) {
  for (let idx = 0; idx &lt; count; idx++) {
    callback(idx);
  }
}
// 0, 1, 2, 3, 4 순으로 출력.
repeat(5, (idx) =&gt; {
  console.log(idx);
});

// 0, 2, 4, 6, 8 순으로 출력.
repeat(5, (idx) =&gt; {
  console.log(idx * 2);
});

// 0, 3, 6, 9, 12 순으로 출력.
repeat(5, (idx) =&gt; {
  console.log(idx * 3);
});</code></pre>
<h2 id="콜백-지옥">콜백 지옥</h2>
<ul>
<li>콜백이 여러 단계로 중첩돼서 코드 가독성이 쓰레기가 되는 현상을 뜻한다.</li>
<li>만약에 로그인하고 사용자 프로필 구하고 친구들 정보 구하고 게시글 정보 구하는 비동기 과정을 콜백 함수로 작성하면 아래처럼 되는데... 이렇게 쓰면 여러모로 나중에 유지보수가 어려워진다.</li>
<li>이런 걸 해결하려고 <code>async/await</code>, <code>Promises</code> 같은 게 생겼다는 데, 내일 범위라서 내일 글에서 다룰 예정이다.</li>
</ul>
<pre><code class="language-js">loginUser(id, password, (user) =&gt; {
  getUserProfile(user, (profile) =&gt; {
    getFriends(profile, (friends) =&gt; {
      getPosts(friends, (posts) =&gt; {
        console.log(posts);
      });
    });
  });
});</code></pre>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/fe46771e-edb5-4671-8ad5-905d04e8df27/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[1001] 정글 끝나고 할거]]></title>
            <link>https://velog.io/@strawberry-tree/%EC%A0%95%EA%B8%80-%EB%81%9D%EB%82%98%EA%B3%A0-%ED%95%A0%EA%B1%B0</link>
            <guid>https://velog.io/@strawberry-tree/%EC%A0%95%EA%B8%80-%EB%81%9D%EB%82%98%EA%B3%A0-%ED%95%A0%EA%B1%B0</guid>
            <pubDate>Tue, 30 Sep 2025 17:16:05 GMT</pubDate>
            <description><![CDATA[<h1 id="이력서-포트폴리오">이력서, 포트폴리오</h1>
<p>일단 정글 공식일정상 협력사 (+인턴) 지원은 명절 이후부터라고 한다.</p>
<p>지원하려는 협력사의 성격이나 직무에 따라 전략이 달라질 수도 있을 것 같아, 지금 말고 해당 시점부터 작업하려고 한다.</p>
<p>포트폴리오는 깃허브 jekyll 같은 거 써서 웹사이트로 만들까 싶다.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/522dc148-2136-4470-8c86-16d412485f7d/image.jpg" alt=""></p>
<p>문제는 정글 오기전에 학교에서 프로그래밍 과제를 했을땐 기술적 챌린지 그런 건 신경 안쓰고 프로젝트를 진행해서, 뭘 강조해야할지 모르겠다는 거다. (사진의) 정글 나만무 최종프로젝트 말곤 딱히 개발자가 되어야겠단 생각 전에 했던지라 그런 부분들을 간과했다</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/d05d0830-8632-4ce6-a736-6518515567a2/image.jpg" alt=""></p>
<p>이건 대학 2학년때, 그니까 5년전에 교양수업에서 만든 웹게임이다. AI도 없을 때 만들었어서 삽질을 많이 하긴 했는데, 구체적으로 어떤 삽질을 했나 기억 안난다... 같이 만드신 선배님 잘 살아계시나요?</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/b3616d1d-3896-4865-93d9-9b3b5499e7e1/image.jpg" alt=""></p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/d28a8f55-3369-4fb5-9a90-2ce7f1731b58/image.jpg" alt=""></p>
<p>웹프로그래밍 밖으로 범주를 넓히자면, langchain 프롬프팅 미니프로젝트를 과제로 몇번 했지만, 아무래도 기술적 챌린지보단 사회과학적/교육학적 결론을 내는 것에 집중했다보니, 이력서와 핏이 맞을 지 모르겠다.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/0ed4aa8c-dceb-4efe-843f-06a901e47bc6/image.jpg" alt=""></p>
<p>그렇다고 개발이랑 아예 상관없는, 내 과거 교육봉사 및 진로 멘토링 이력을 적기도 그렇고 흠. 고민이 많다. 일단 써 보는 것에 의의를 둬야 할듯.</p>
<p>프로젝트 더 많이 하고 싶은데 그런 건 어디서 구해야할지 모르겠다.</p>
<h1 id="코딩테스트">코딩테스트</h1>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/cccad939-a217-41a9-aa36-a804472f6e6b/image.jpg" alt=""></p>
<p>정글 입학하기 전에 사둔 책을 다시 한번 보며 복습할 생각이다. 하루에 한두문제 정도 푸는 게 적당할 것 같다.</p>
<h1 id="웹개발">웹개발</h1>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/7f480173-f253-4ebd-a1b7-2cc64953c5b5/image.avif" alt=""></p>
<p>일단은 프론트에 가까운 풀스택 개발자를 목표로 하고 있다. 옛날에 사둔 인프랩 이정환님 리액트 강의를 완주하는 게 목표다. 다 들으면 타입스크립트랑 넥스트 강의도 들어볼 예정이다.</p>
<p>꼭 이건 취업 목적만은 아닌게, 난 나만무 최종프로젝트 때 내가 쓴 리액트 코드의 절반은 동작원리를 모르겠다,,, 그래서 필수라고 생각했다.</p>
<h1 id="기술-면접">기술 면접</h1>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/c218984e-fc0e-4c11-8e32-d6c914856e55/image.jpg" alt=""></p>
<p>정글에서 핀토스를 아무리 열심히 하고 CSAPP을 아무리 열심히 읽었다고 해도, 그걸 다시 보라고 하면 못 보겠다;;; 정글에서 하루 10시간 넘게 공부할 때도 너무 방대했는데 지금은 더할 거다. 기술면접 말고도 준비할 게 많은데 더 효율적인 방법이 필요하다.</p>
<p>그래서 일단은 면접대비용 요약서를 보면서, CSAPP이나 핀토스의 내용 중 필요한 부분이 있으면 과거에 블로그에 정리한 내용들 참고하면서 복습하는 게 좋을 것 같다.</p>
<h1 id="위치">위치</h1>
<p>슬프게도 정글에선 더 이상 집중이 안 돼서 나가서 공부할 것 같다. 오랜만에 봄날의 서재를 들릴 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/7a23580e-3417-4238-ae57-b5847133b70d/image.jpg" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[내일발표합니다]]></title>
            <link>https://velog.io/@strawberry-tree/%EB%82%B4%EC%9D%BC%EB%B0%9C%ED%91%9C%ED%95%A9%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@strawberry-tree/%EB%82%B4%EC%9D%BC%EB%B0%9C%ED%91%9C%ED%95%A9%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Fri, 26 Sep 2025 10:04:26 GMT</pubDate>
            <description><![CDATA[<p>굿럭!</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/a33912db-8d4d-41ce-baed-c336735a92c3/image.jpg" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도면 벽 검출 구현 - OpenCV.js와 함께]]></title>
            <link>https://velog.io/@strawberry-tree/%EB%8F%84%EB%A9%B4-%EB%B2%BD-%EA%B2%80%EC%B6%9C-%EA%B5%AC%ED%98%84-OpenCV.js%EC%99%80-%ED%95%A8%EA%BB%98</link>
            <guid>https://velog.io/@strawberry-tree/%EB%8F%84%EB%A9%B4-%EB%B2%BD-%EA%B2%80%EC%B6%9C-%EA%B5%AC%ED%98%84-OpenCV.js%EC%99%80-%ED%95%A8%EA%BB%98</guid>
            <pubDate>Sat, 20 Sep 2025 07:22:56 GMT</pubDate>
            <description><![CDATA[<h1 id="도면-벽-검출">도면 벽 검출</h1>
<img width="1000" height="650" alt="image" src="https://github.com/user-attachments/assets/0be7289f-65da-444e-9255-bb2f7216d920" />

<p>&lt;어따놀래&gt;는 사용자가 도면의 사진을 업로드하면 벽을 검출하여 표시하는 기능을 제공합니다.</p>
<p>단 본 프로젝트의 지향점이 AI의 성능 강화보단 전반적인 웹 서비스 구현에 초점을 맞추었기 때문에, 벽 검출 기능에 일부 오차가 있을 수 있습니다.</p>
<p>물론 직접 인공지능 모형을 훈련하거나 하는 식으로 해결할 순 있겠지만, 도면만들기보단 시뮬레이터에 집중해야 할 필요가 있겠다고 생각했습니다. (특히 정글이 AI 부트캠프는 아닌만큼, 선택과 집중을...)</p>
<p>대신 축척 설정 및 벽 추가 / 삭제 등 에디터 기능 역시 구현되어 있으므로, 벽 검출에 오차가 있더라도 사용자가 바로 수정할 수 있습니다.</p>
<h1 id="hough-transformation">Hough Transformation</h1>
<p><a href="https://github.com/KJ-9th-NMM-Team2/wheretoput/blob/main/next/lib/wallDetection.ts">소스 코드</a></p>
<p>벽 검출에는 공개 컴퓨터 비전 라이브러리인 OpenCV.js의 Hough Transformation을 사용하였습니다.</p>
<p>Hough Transformation은 도형 및 사진에서 선을 검출해 주는 알고리즘입니다. 동작 과정을 차례로 설명해 보겠습니다.</p>
<h2 id="0-원본-이미지">0. 원본 이미지</h2>
<img width="589" height="572" alt="image" src="https://github.com/user-attachments/assets/391e1dfd-427d-435a-88b6-92f757bfdb3c" />


<h2 id="1-그레이스케일-변환">1. 그레이스케일 변환</h2>
<img width="512" height="492" alt="image" src="https://github.com/user-attachments/assets/cddb964e-b4db-4b97-88cc-f510761433cc" />

<pre><code class="language-javascript">window.cv.cvtColor(srcImg, gray, window.cv.COLOR_RGBA2GRAY);</code></pre>
<ul>
<li>벽 검출에 불필요한 색상 정보를 없애고 그레이스케일로 변환합니다.</li>
</ul>
<h2 id="2-이진화">2. 이진화</h2>
<img width="506" height="524" alt="image" src="https://github.com/user-attachments/assets/def48137-0806-4f77-844b-d3eb218bf557" />


<pre><code class="language-javascript">window.cv.threshold(gray,
  binary,
  0,
  255,
  window.cv.THRESH_BINARY_INV + window.cv.THRESH_OTSU
);</code></pre>
<ul>
<li>실제로는 흑백 이미지도 0부터 255까지의 밝기를 가집니다. Otsu 이진화는 그레이스케일 이미지를 완전한 검은색(0) 또는 흰색(255)로 변환하는 과정입니다.</li>
<li>0부터 255까지 모든 밝기 값의 빈도를 구한 뒤, 빈도의 분산이 최대가 되는 임계값을 구합니다.<ul>
<li>분산은 &#39;어두운 그룹과 밝은 그룹의 평균 밝기 차이가 가장 커지도록 이미지를 둘로 나누는 기준 값&#39;을 말합니다.</li>
</ul>
</li>
<li>이후 임계값보다 어두우면 흰색(0, 벽을 나타냄), 밝으면 검은색(255, 벽이 아닌 곳을 나타냄)으로 처리됩니다.</li>
</ul>
<h2 id="3-모폴로지-연산-노이즈-계산">3. 모폴로지 연산 (노이즈 계산)</h2>
<img width="590" height="612" alt="image" src="https://github.com/user-attachments/assets/6c371f9c-2d4c-42be-810f-9471a6f30b08" />

<pre><code class="language-javascript">const kernel = window.cv.Mat.ones(3, 3, window.cv.CV_8U);
window.cv.morphologyEx(binary, cleaned, window.cv.MORPH_OPEN, kernel);</code></pre>
<ul>
<li>모폴로지 연산은 이미지의 점들을 조금씩 늘리거나 줄여서, 작은 얼룩을 없앨 수 있습니다.</li>
<li>선 내 작은 구멍을 메우고 연결해 주는 역할을 수행합니다.</li>
<li>3 x 3 행렬로 주변 9개 점들을 한 번에 보면서 작은 점들을 지우고, 끊어진 선들을 이어줍니다.</li>
</ul>
<h2 id="4-canny-엣지-검출을-통한-윤곽선-계산">4. Canny 엣지 검출을 통한 윤곽선 계산</h2>
<img width="571" height="613" alt="image" src="https://github.com/user-attachments/assets/0a78507d-933e-4e36-9b91-2bac5cc82003" />

<pre><code class="language-javascript">window.cv.Canny(cleaned, edges, canny1, canny2, 3, false);</code></pre>
<ul>
<li>Canny 엣지검출은 이미지에서 가장자리, 즉 윤곽선을 검출하는 알고리즘입니다.</li>
<li>Canny 임계값은 벽과 배경 사이 밝기 차이를 측정합니다.</li>
<li><code>canny1(50)</code>보다 낮으면 경계선이 아니라고 판단하고, <code>canny2(150)</code>보다 높으면 확실한 경계선으로 판단합니다. 50과 150 사이의 값은 주변에 확실한 경계선이 있을 때만 경계선으로 인정합니다.</li>
</ul>
<h2 id="5-probabilistic-hough를-통한-벽-선분-추출">5. Probabilistic Hough를 통한 벽 선분 추출</h2>
<img width="545" height="580" alt="image" src="https://github.com/user-attachments/assets/f47d13ae-b591-444f-b981-4868fa540875" />

<pre><code class="language-javascript">window.cv.HoughLinesP(
  edges,
  lines,
  1,                    // rho: 거리 해상도
  Math.PI / 180,        // theta: 각도 해상도 (1도)
  houghTh,              // 임계값 (80)
  minLen,               // 최소 선분 길이 (30)
  maxGap                // 선분 간 최대 간격 (20)
);</code></pre>
<ul>
<li>Hough Transform을 통해 흩어진 점을을 이어 선분을 검출할 수 있습니다.</li>
<li><code>rho</code>는 선의 위치를 1픽셀 단위로, <code>theta</code>는 선의 기울기를 1도 단위로 세밀하게 측정하여 정확한 직선을 찾아냅니다. </li>
<li><code>houghTh</code>(80)는 몇 개의 점이 일직선 위에 있어야 선으로 인정할지 정하는 값입니다. 80개 이상의 점이 일직선상에 있으면 벽으로 판단합니다.</li>
</ul>
<h2 id="6-선분-필터링">6. 선분 필터링</h2>
<img width="567" height="555" alt="image" src="https://github.com/user-attachments/assets/9e0250f2-f1ee-46da-9e31-e56558bcbb6e" />

<pre><code class="language-javascript">filterLines(lines, minLength = 80, angleThreshold = 5)</code></pre>
<ul>
<li>실제 벽으로 보기 어려운 선분을 제거합니다.</li>
<li>너무 짧은 선분(80px 미만)이나, 수직/수평이 아닌 선분 (회전각도가 5도 이상)인 경우 제거됩니다.</li>
</ul>
<h2 id="7-평행선-병합">7. 평행선 병합</h2>
<img width="549" height="596" alt="image" src="https://github.com/user-attachments/assets/540c0fbc-6247-4295-a436-79a477be94d5" />

<pre><code class="language-javascript">mergeParallelLines(lines, maxDistance = 15, angleThreshold = 5)</code></pre>
<ul>
<li>지금까지의 과정에서는 한 벽에서 여러 개의 선이 검출될 수 있습니다. 따라서 평행한 선을 하나의 선으로 병합할 필요가 있습니다.</li>
<li>위 사진의 분홍색 선은, 원래 선들 중 병합된 선을 의미합니다.</li>
<li><code>angleThreshold</code>: 5도 이내의 각도 차이만 평행선으로 인정합니다.</li>
<li><code>maxDistance</code>: 15px 이내 거리의 선만 병합 대상입니다.</li>
<li>겹침 구간이 10% 이상인 선분만 병합하여, 정확도를 높입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[벽자석 기능 구현 - 좌표계 변환 및 바운딩 박스의 활용]]></title>
            <link>https://velog.io/@strawberry-tree/%EB%B2%BD%EC%9E%90%EC%84%9D-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-%EC%A2%8C%ED%91%9C%EA%B3%84-%EB%B3%80%ED%99%98-%EB%B0%8F-%EB%B0%94%EC%9A%B4%EB%94%A9-%EB%B0%95%EC%8A%A4%EC%9D%98-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@strawberry-tree/%EB%B2%BD%EC%9E%90%EC%84%9D-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-%EC%A2%8C%ED%91%9C%EA%B3%84-%EB%B3%80%ED%99%98-%EB%B0%8F-%EB%B0%94%EC%9A%B4%EB%94%A9-%EB%B0%95%EC%8A%A4%EC%9D%98-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Sat, 20 Sep 2025 05:50:38 GMT</pubDate>
            <description><![CDATA[<h1 id="벽-자석-기능">벽 자석 기능</h1>
<p>실제 인테리어에서는 가구를 벽에 붙여 배치하는 경우가 많기 때문에, 이를 돕는 벽 자석 기능을 구현했습니다.</p>
<h2 id="파일-구조">파일 구조</h2>
<p>벽 자석 기능은 다음 파일들에 구현되어 있습니다:</p>
<ul>
<li><a href="https://github.com/KJ-9th-NMM-Team2/wheretoput/blob/main/next/components/sim/mainsim/hooks/useObjectControls.js"><code>useObjectControls.js:39-396</code> - 메인 벽 자석 로직 (<code>findNearestWallSnap</code> 함수)</a></li>
<li><a href="https://github.com/KJ-9th-NMM-Team2/wheretoput/blob/main/next/components/sim/slices/wallSlice.js"><code>wallSlice.js:11-17</code> - 벽 자석 기능 상태 관리</a></li>
<li><a href="https://github.com/KJ-9th-NMM-Team2/wheretoput/blob/main/next/components/sim/mainsim/control/CameraControlPanel.jsx"><code>CameraControlPanel.jsx:164-182</code> - 벽 자석 ON/OFF 토글 UI</a></li>
</ul>
<img width="355" height="377" alt="image" src="https://github.com/user-attachments/assets/c0be51ef-901b-4040-84d1-f23ed6ab5f8b" />
<img width="310" height="386" alt="image" src="https://github.com/user-attachments/assets/d7ada093-c7b2-43bd-9524-2d4f19e40a80" />

<p>벽 자석은 가구를 벽의 모서리 300mm(three.js 기준 0.3) 이내로 이동시키면, 자동으로 가구를 벽에서 50mm 떨어진 위치로 보정하는 기능입니다. 위치가 보정된 이후에도 사용자가 추가적으로 미세 조정할 수 있습니다.</p>
<p>이때 자석 효과가 활성화된 벽은 노란색으로 하이라이트되어, 사용자가 쉽게 인식할 수 있습니다.</p>
<img width="486" height="456" alt="image" src="https://github.com/user-attachments/assets/701441f8-62a4-4d0a-9d9d-fe62805cab7e" />

<p>또한 직각으로 만나는 두 벽의 교차점 500mm(three.js 기준 0.5) 이내 범위로 가구를 이동시키면, 두 벽이 만나는 직각 코너에 정확히 맞춰 자동 배치됩니다. 코너 스냅이 활성화되면 벽이 주황색으로 하이라이트됩니다.</p>
<img width="173" height="155" alt="image" src="https://github.com/user-attachments/assets/b9533ed1-05ff-4552-898f-c7642e49e0f5" />

<p>벽 자석 기능은 화면 좌측 Display 패널의 &quot;벽 자석&quot; 토글 스위치를 통해 활성화 및 비활성화가 가능합니다.</p>
<h1 id="작동-로직">작동 로직</h1>
<h2 id="1-자석-발동-거리-설정">1. 자석 발동 거리 설정</h2>
<pre><code>const SNAP_DISTANCE = 0.3; // 300mm 이내에서 자석 효과
const WALL_OFFSET = 0.05; // 벽에서 50mm 떨어진 위치로 보정
const CORNER_SNAP_DISTANCE = 0.5; // 직각 코너의 경우, 500mm 이내</code></pre><p>&lt;어따놀래&gt;에서는 실제 1mm를 three.js <code>0.001</code> 단위로 환산해 사용합니다.</p>
<p>즉 가구가 300mm(한 벽) / 500mm(직각 코너) 이내로 접근하면, 자석 효과가 발동해 가구가 벽에서 50mm 떨어진 위치에 맞춰집니다.</p>
<p>직각 코너에서는 두 벽과 동시에 거리 조건을 만족해야 하는 특성상 일반 벽 자석보다 발동이 어려우므로, 의도적으로 감지 거리를 300mm에서 500mm로 늘려 사용성을 향상시켰습니다.</p>
<p>또한 보정된 벽과의 거리를 0mm가 아닌 50mm로 설정한 이유는, 실제 인테리어에서도 청소 공간 확보 및 벽면 손상 방지를 위해 벽과 가구 사이에 약간의 틈을 두는 점을 반영했습니다.</p>
<h2 id="2-벽면별-거리-판정-로직">2. 벽면별 거리 판정 로직</h2>
<p>각 벽의 4개 면(앞, 뒤, 좌, 우)에 대해 개별적으로 거리를 계산하고, 자석 거리 이내에 있는지 확인합니다:</p>
<pre><code class="language-javascript">// useObjectControls.js:140-149 (벽 앞면 예시)
// Z축 방향 자석 발동 가능 여부 먼저 확인
if (Math.abs(localX) &lt;= wallHalfWidth + rotatedFurnitureWidth) {
  // 벽 앞면과 가구 뒷면의 거리 계산
  const furnitureBackEdge = localZ - rotatedFurnitureDepth;
  const wallFrontEdge = wallHalfDepth;
  const frontDistance = Math.abs(furnitureBackEdge - wallFrontEdge);

  // 거리 조건과 위치 조건을 동시에 만족하는지 확인
  if (furnitureBackEdge &gt; wallFrontEdge &amp;&amp; frontDistance &lt; SNAP_DISTANCE) {
    // 자석 발동 후보로 등록
  }
}</code></pre>
<p>이 로직이 벽의 4개 면 모두에 대해 실행되며, 조건을 만족하는 면들이 <code>allCandidates</code> 배열에 수집됩니다.</p>
<h2 id="3-벽과-가구의-거리-계산을-위한-로컬-좌표계-변환">3. 벽과 가구의 거리 계산을 위한 로컬 좌표계 변환</h2>
<p>벽이 회전되어 있는 경우, 벽과 가구의 정확한 거리를 측정하기 까다롭습니다.</p>
<p>예를 들어 45도 회전된 벽의 경우, 가구와의 거리를 X, Z 좌표로 직접 계산하면 복잡한 삼각함수 계산이 필요합니다.</p>
<img width="711" height="399" alt="image" src="https://github.com/user-attachments/assets/c4ad1097-9b9a-4c73-96d3-54d8994272a0" />

<p><a href="https://www.sefindia.org/forum/viewtopic.php?p=65116">이미지 출처</a></p>
<p>따라서 <strong>벽을 기준으로 하는 새로운 좌표계(로컬 좌표계)</strong>를 만들어, 마치 벽이 수평인 것처럼 간단하게 계산할 수 있도록 했습니다. 이는 전체 공간(월드 좌표계)에서 벽 중심의 작은 공간(로컬 좌표계)으로 관점을 바꾸는 것입니다.
(월드 좌표계는 전체 공간 기준의 좌표계, 로컬 좌표계는 벽을 기준으로 다시 계산한 좌표계로 이해하시면 됩니다.)</p>
<pre><code class="language-js">// 벽을 기준으로 가구의 상대 좌표 계산
const relativePos = new THREE.Vector3(
  position.x - wallPos.x,
  0,
  position.z - wallPos.z
);</code></pre>
<p>먼저 가구의 월드 좌표에서 벽의 월드 좌표를 빼서, 가구가 벽으로부터 얼마나 떨어져 있는지를 나타내는 상대 벡터(<code>relativePos</code>)를 구합니다.</p>
<p>하지만 이 <code>relativePos</code>는 여전히 월드 좌표계 기준이므로 벽의 회전이 반영되지 않은 상태입니다.</p>
<pre><code class="language-js">// 벽 회전에 따라 좌표 변환
const wallCos = Math.cos(wallRotation);
const wallSin = Math.sin(wallRotation);

const localX = relativePos.x * wallCos + relativePos.z * wallSin;
const localZ = -relativePos.x * wallSin + relativePos.z * wallCos;</code></pre>
<p>따라서 벽의 회전 각도(<code>wallRotation</code>)을 고려하여, 가구가 실제로 벽 기준으로 어느 쪽에 있는지를 다시 계산합니다.</p>
<p>이때 앞서 구한 <code>relativePos</code>에 삼각함수를 사용한 회전 변환을 적용합니다.</p>
<p><code>localX</code>는 벽을 기준으로 한 좌우 거리로, 양수인 경우 벽의 오른쪽, 음수인 경우 벽의 왼쪽에 가구가 있음을 의미합니다.</p>
<p><code>localZ</code>는 벽을 기준으로 한 앞뒤 거리로, 양수인 경우 벽의 앞쪽, 음수인 경우 벽의 뒤쪽에 가구가 있음을 의미합니다.</p>
<p>(three.js에서는 관례와 다르게 높이를 나타내는 축이 z축이 아닌 y축으로 처리된다는 점에 유의 바랍니다.)</p>
<h2 id="4-회전된-가구의-바운딩-박스-계산">4. 회전된 가구의 바운딩 박스 계산</h2>
<img width="426" height="314" alt="image" src="https://github.com/user-attachments/assets/10e428cb-e31f-4294-8297-6dd4bb53c818" />

<p>앞서 구한 <code>localX</code>, <code>localZ</code>는 벽과 <strong>가구의 중심점</strong> 간의 상대적 위치입니다.</p>
<p>하지만 실제 벽 자석에서는 가구의 <strong>중심점</strong>이 아닌 <strong>가구의 가장자리</strong>가 벽에 닿는 거리를 계산해야 합니다. 즉, 가구 중심에서 벽까지의 거리가 아니라 가구의 면(edge)에서 벽까지의 최단거리를 측정해야 합니다. 이를 위해 <code>furnitureHalfWidth</code>, <code>furnitureHalfDepth</code> 같은 가구의 실제 크기를 반영한 바운딩 박스 계산이 필수입니다.</p>
<p>이를 위해, 가구를 감싸는 박스인 바운딩 박스를 따로 계산했습니다. (위 사진의 파란색 박스가 bounding box입니다).</p>
<img width="399" height="248" alt="image" src="https://github.com/user-attachments/assets/5e9e0108-e0f8-4a8a-9c2d-33b4ea6667b9" />

<p><a href="https://madmann91.github.io/2024/02/10/converting-oriented-bounding-boxes-to-axis-aligned-ones.html">이미지 출처</a></p>
<p>가구가 회전된 경우, 월드 좌표계에서 보는 바운딩 박스의 실제 크기가 달라집니다.</p>
<p>예를 들어 원본 크기가 width 2000mm, depth 1000mm인 가구를 Y축 기준으로 90도 회전시키면:</p>
<ul>
<li>회전 전: X축 방향 2000mm, Z축 방향 1000mm</li>
<li>회전 후: X축 방향 1000mm, Z축 방향 2000mm</li>
</ul>
<p>이러한 회전을 고려하지 않으면 가구를 회전해도 벽 자석이 원래 크기로 잘못 계산하여 부정확한 스냅이 발생합니다.</p>
<p>따라서 회전 변환 행렬을 적용한 Oriented Bounding Box(OBB)를 계산하여 실제 공간에서의 정확한 바운딩 박스 크기(<code>rotatedFurnitureWidth</code>, <code>rotatedFurnitureDepth</code>)를 구해야 합니다.</p>
<pre><code class="language-javascript">// 가구의 회전을 벽의 로컬 좌표계에서 계산
const relativeRotation = furnitureRotationY - wallRotation;
const furnitureCos = Math.cos(relativeRotation);
const furnitureSin = Math.sin(relativeRotation);

// 회전된 가구의 실제 바운딩 박스 크기
const rotatedFurnitureWidth =
  Math.abs(furnitureHalfWidth * furnitureCos) +
  Math.abs(furnitureHalfDepth * furnitureSin);
const rotatedFurnitureDepth =
  Math.abs(furnitureHalfWidth * furnitureSin) +
  Math.abs(furnitureHalfDepth * furnitureCos);</code></pre>
<p>벽을 기준으로 가구가 얼마나 회전했는지(<code>relativeRotation</code>)를 구한 다음, 삼각함수(<code>furnitureCos</code>, <code>furnitureSin</code>)를 이용해 회전된 가구가 실제로 차지하는 폭(<code>rotatedFurnitureWidth</code>)과 깊이(<code>rotatedFurnitureDepth</code>)를 계산합니다.</p>
<h2 id="5-벽면별-자석-위치-계산">5. 벽면별 자석 위치 계산</h2>
<p>실제 자석 계산은 각 벽의 4개 면(앞, 뒤, 좌, 우)에 대해 개별적으로 수행됩니다. 아래는 벽 앞면과의 자석 발동 여부를 계산한 코드입니다:</p>
<pre><code class="language-javascript">// 벽 앞면 자석 발동 계산 예시
const furnitureBackEdge = localZ - rotatedFurnitureDepth; // 가구 뒷면 위치
const wallFrontEdge = wallHalfDepth; // 벽 앞면 위치
const frontDistance = Math.abs(furnitureBackEdge - wallFrontEdge); // 거리 계산

// 거리 조건과 위치 조건 확인
if (furnitureBackEdge &gt; wallFrontEdge &amp;&amp; frontDistance &lt; SNAP_DISTANCE) {
  // 자석 발동 위치 계산: 벽 앞면에서 WALL_OFFSET만큼 떨어진 곳
  const snapLocalZ = wallFrontEdge + WALL_OFFSET + rotatedFurnitureDepth;

  // 로컬 좌표를 월드 좌표로 변환
  const snapWorldPos = {
    x: wallPos.x + (localX * wallCos - snapLocalZ * wallSin),
    y: position.y,
    z: wallPos.z + (localX * wallSin + snapLocalZ * wallCos),
  };

  // 자석 발동 후보로 등록
  allCandidates.push({
    distance: frontDistance,
    snapPosition: snapWorldPos,
    wall: wall,
    face: &quot;front&quot;,
  });
}</code></pre>
<p><strong>계산 과정 요약</strong></p>
<ol>
<li><strong>가구와 벽면의 거리 계산</strong>: <code>furnitureBackEdge</code>와 <code>wallFrontEdge</code> 사이의 실제 거리(<code>frontDistance</code>)</li>
<li><strong>조건 확인</strong>: <code>frontDistance</code>가 <code>SNAP_DISTANCE</code>(300mm) 이내이고, 가구가 벽의 올바른 쪽에 위치하는지 확인</li>
<li><strong>자석 발동 위치 계산</strong>: <code>snapLocalZ</code>로 벽에서 <code>WALL_OFFSET</code>(50mm) 떨어진 최종 위치 계산</li>
<li><strong>좌표 변환</strong>: <code>wallCos</code>, <code>wallSin</code>을 사용해 로컬 좌표(<code>snapLocalZ</code>)를 월드 좌표(<code>snapWorldPos</code>)로 변환</li>
</ol>
<p>이 과정이 벽의 모든 면에 대해 반복되어 가능한 모든 자석 발동 후보들이 <code>allCandidates</code> 배열에 수집됩니다.</p>
<h2 id="6-직각-벽-감지-및-코너-자석-활성화">6. 직각 벽 감지 및 코너 자석 활성화</h2>
<p>코너 자석이 활성화되려면 다음 조건들을 만족해야 합니다:</p>
<p><strong>1. 두 개 이상의 가까운 벽 감지</strong></p>
<pre><code class="language-javascript">// useObjectControls.js:250-252
const nearCandidates = allCandidates.filter(
  (candidate) =&gt; candidate.distance &lt; CORNER_SNAP_DISTANCE // 0.5 (500mm) 이내
);</code></pre>
<p><code>allCandidates</code> 배열에서 <code>CORNER_SNAP_DISTANCE</code>(500mm) 이내의 벽들을 <code>nearCandidates</code>로 필터링합니다.</p>
<p><strong>2. 직각 관계 확인</strong></p>
<pre><code class="language-javascript">// useObjectControls.js:284-288
const angleDiff = Math.abs(wall1.rotation[1] - wall2.rotation[1]);
const normalizedDiff = angleDiff % (2 * Math.PI);
const isRightAngle =
  Math.abs(normalizedDiff - Math.PI / 2) &lt; 0.1 ||
  Math.abs(normalizedDiff - (3 * Math.PI) / 2) &lt; 0.1;</code></pre>
<p><code>wall1.rotation[1]</code>과 <code>wall2.rotation[1]</code>의 차이(<code>angleDiff</code>)를 계산하여 두 벽이 90도 각도로 만나는지 확인합니다. <code>normalizedDiff</code>로 정규화하고, <code>isRightAngle</code>로 0.1라디안 (약 5.7도)의 오차를 허용합니다.</p>
<p>두 벽이 서로 다른 방향(하나는 가로, 하나는 세로)을 향해야 코너 자석이 가능합니다. 같은 방향을 향하는 평행한 벽들은 코너가 아니므로 제외됩니다.</p>
<h1 id="추가-설명">추가 설명</h1>
<h2 id="벽이-많거나-가구가-여러-개일-때-성능이-저하될-가능성은-없을까요">벽이 많거나 가구가 여러 개일 때 성능이 저하될 가능성은 없을까요?</h2>
<ul>
<li>벽 자석 발동 여부 계산은 매 프레임마다 실행되지 않고, 가구를 드래그할 때만 실행됩니다.</li>
<li>가구를 움직이지 않을 때는 계산이 발생하지 않아, 성능에 영향은 없습니다.</li>
<li>한 번에 드래그되는 가구 1개에 대해서만 벽 자석 계산이 실행됩니다.</li>
</ul>
<h2 id="조건을-만족하는-벽이-많을-때-어떤-우선순위로-결정되나요">조건을 만족하는 벽이 많을 때, 어떤 우선순위로 결정되나요?</h2>
<ul>
<li>코너 자석과 일반 벽 자석을 모두 만족하는 경우, 코너 자석이 최우선으로 적용됩니다.</li>
<li>일반 자석의 경우, 가장 가까운 벽을 기준으로 작동합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVP 거의 끝나가는중]]></title>
            <link>https://velog.io/@strawberry-tree/MVP-%EA%B1%B0%EC%9D%98-%EB%81%9D%EB%82%98%EA%B0%80%EB%8A%94%EC%A4%91</link>
            <guid>https://velog.io/@strawberry-tree/MVP-%EA%B1%B0%EC%9D%98-%EB%81%9D%EB%82%98%EA%B0%80%EB%8A%94%EC%A4%91</guid>
            <pubDate>Mon, 08 Sep 2025 17:04:54 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/52a64bc3-649c-4bad-b907-c2f8711308b8/image.jpg" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[나도 이제 집주인]]></title>
            <link>https://velog.io/@strawberry-tree/%EB%82%98%EB%8F%84-%EC%9D%B4%EC%A0%9C-%EC%A7%91%EC%A3%BC%EC%9D%B8</link>
            <guid>https://velog.io/@strawberry-tree/%EB%82%98%EB%8F%84-%EC%9D%B4%EC%A0%9C-%EC%A7%91%EC%A3%BC%EC%9D%B8</guid>
            <pubDate>Tue, 02 Sep 2025 06:45:55 GMT</pubDate>
            <description><![CDATA[<p>무려 33평짜리 아파트임.
우왕.</p>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/3e8390c9-3021-4918-aa23-c70dd6b977dc/image.jpg" alt="">
<img src="https://velog.velcdn.com/images/strawberry-tree/post/b8747edf-1322-41fa-b1c7-7b3956dea674/image.jpg" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[웹개발] TypeScript 기본문법 및 React와 사용하는 방법]]></title>
            <link>https://velog.io/@strawberry-tree/%EC%9B%B9%EA%B0%9C%EB%B0%9C-TypeScript-%EA%B8%B0%EB%B3%B8%EB%AC%B8%EB%B2%95-%EB%B0%8F-React%EC%99%80-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@strawberry-tree/%EC%9B%B9%EA%B0%9C%EB%B0%9C-TypeScript-%EA%B8%B0%EB%B3%B8%EB%AC%B8%EB%B2%95-%EB%B0%8F-React%EC%99%80-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sat, 23 Aug 2025 10:58:35 GMT</pubDate>
            <description><![CDATA[<h1 id="타입스크립트를-사용하는-이유">타입스크립트를 사용하는 이유</h1>
<ul>
<li>자바스크립트는 어떤 타입이든 받아줌 -&gt; 이로 인해 오류 발생 가능</li>
</ul>
<pre><code class="language-js">let age = 25;
age = &quot;스물다섯&quot;;
console.log(age + 5); // &quot;스물다섯5&quot;</code></pre>
<ul>
<li><p><code>age</code>에는 숫자만 들어갈 수 있게 의도했는데, 문자열이 들어가게 되면 의도치 않은 결과 발생할 수 있음</p>
</li>
<li><p>타입스크립트에선 변수, 함수, 객체에 타입을 명시적으로 선언</p>
<ul>
<li>컴파일 단계에서 타입 오류를 잡아주므로, 이런 버그를 줄일 수 있음</li>
</ul>
</li>
</ul>
<h1 id="타입스크립트-기본-문법">타입스크립트 기본 문법</h1>
<h2 id="설치">설치</h2>
<pre><code class="language-bash">npm install -g typescript</code></pre>
<h2 id="타입스크립트의-type">타입스크립트의 type</h2>
<ul>
<li>총 6개의 타입이 존재한다.<ul>
<li><code>number</code></li>
<li><code>string</code></li>
<li><code>boolean</code></li>
<li><code>null</code></li>
<li><code>undefined</code></li>
<li><code>any</code> -&gt; 그 외의 타입</li>
</ul>
</li>
</ul>
<pre><code class="language-ts">// let (변수명):(타입명)
let a: number = 15;
a = &quot;hello world&quot;;
let b: string = &quot;무적LG&quot;;</code></pre>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/5f44db94-89a4-457c-af64-bd0b081c5d4a/image.png" alt=""></p>
<h2 id="any-타입"><code>any</code> 타입</h2>
<pre><code class="language-ts">// any: 뭐가 올지 모름
let c: any = 4;
c = &quot;날려버려 날려버려 안타 신민재&quot;; // 다른 타입의 값 넣어도 에러 없음</code></pre>
<ul>
<li>단, 지나친 <code>any</code>를 사용하는 것은 지양해야 함</li>
<li>그럼 TypeScript를 쓰는 의미가 없으니까</li>
</ul>
<h2 id="2개-이상-타입-지정">2개 이상 타입 지정</h2>
<pre><code class="language-ts">// 여러 타입 지정 가능 - 얘네 중 하나
let c: number | string = &quot;홍창기 안타 날려 홍창기&quot;;
c = 51;</code></pre>
<ul>
<li>여러 타입 중 하나가 올 수 있는 경우</li>
</ul>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/9eb16eb3-53c0-438a-bd5b-ef617b2180d6/image.png" alt=""></p>
<h2 id="배열">배열</h2>
<pre><code class="language-ts">// 배열
let d: string[] = [&quot;박해민&quot;, &quot;신민재&quot;, &quot;홍창기&quot;, &quot;문성주&quot;];
d.push(&quot;김현수&quot;);</code></pre>
<ul>
<li><code>(타입)[]</code> 과 같은 식으로 가능
<img src="https://velog.velcdn.com/images/strawberry-tree/post/95f51ff1-dfa2-43c8-99dd-536e0d865b85/image.png" alt=""></li>
</ul>
<pre><code class="language-ts">let d: (string | number)[] = [&quot;박해민&quot;, &quot;신민재&quot;, &quot;홍창기&quot;, &quot;문성주&quot;];
d.push(&quot;김현수&quot;);
d.push(13);</code></pre>
<ul>
<li><code>|</code>를 통한 여러 타입 선언도 가능</li>
</ul>
<h2 id="함수">함수</h2>
<pre><code class="language-ts">// 함수
// e.g., number 인수 2개를 받고, number를 반환한다.
function addNumber(a: number, b: number): number {
  return a + b;
}
console.log(addNumber(7, 22)); // 29</code></pre>
<ul>
<li>인수 및 반환값에도 타입을 지정할 수 있음</li>
</ul>
<pre><code class="language-ts">const addNumber = (a: number, b: number): number =&gt; {
  return a + b;
};</code></pre>
<ul>
<li>화살표 함수인 경우에도 선언 가능</li>
</ul>
<h2 id="객체">객체</h2>
<ul>
<li>객체는 C 구조체마냥, 타입을 직접 만들 수 있음</li>
<li>방법은 두 가지. <code>interface</code> 와 <code>type</code>이 있음</li>
<li>이런 객체를 선언한다고 가정하면</li>
</ul>
<pre><code class="language-js">let data = {
  name: &quot;임찬규&quot;,
  number: 1,
  size: { height: 186, weight: 88 },
  friends: [
    { name: &quot;손주영&quot;, number: 29 },
    { name: &quot;김진성&quot;, number: 42 },
    { name: &quot;유영찬&quot;, number: 54 },
    { name: &quot;송승기&quot;, number: 13 },
  ],
};</code></pre>
<h3 id="type"><code>type</code></h3>
<ul>
<li>객체에 들어갈 필드의 타입 지정 후, 나만의 타입을 지정 가능</li>
</ul>
<pre><code class="language-ts">type Player = {
  name: string;
  number: number;
  size: { height: number; weight: number }; // 객체 안 객체
  friends: { name: string; number: number }[]; // 객체의 배열
};

let data: Player = {
  name: &quot;임찬규&quot;,
  number: 1,
  size: { height: 186, weight: 88 },
  friends: [
    { name: &quot;손주영&quot;, number: 29 },
    { name: &quot;김진성&quot;, number: 42 },
    { name: &quot;유영찬&quot;, number: 54 },
    { name: &quot;송승기&quot;, number: 13 },
  ],
};</code></pre>
<ul>
<li>위처럼 해도 되고, 객체 안 객체를 별도의 객체로 만들어 줘도 됨</li>
</ul>
<pre><code class="language-ts">type Size = {
  height: number;
  weight: number;
};

type Friend = {
  name: string;
  number: number;
};

type Player = {
  name: string;
  number: number;
  size: Size;
  friends: Friend[];
};</code></pre>
<h3 id="interface"><code>interface</code></h3>
<ul>
<li><code>type 타입명 = {...}</code> 이 아니라 <code>interface 타입명 {...}</code> 식으로 선언된다는 점이 큰 차이.</li>
</ul>
<pre><code class="language-ts">interface Size {
  height: number;
  weight: number;
}

interface Friend {
  name: string;
  number: number;
}

interface Player {
  name: string;
  number?: number;
  size: Size;
  friends: Friend[];
}

let data: Player = {
  name: &quot;임찬규&quot;,
  number: 1,
  size: { height: 186, weight: 88 },
  friends: [
    { name: &quot;손주영&quot;, number: 29 },
    { name: &quot;김진성&quot;, number: 42 },
    { name: &quot;유영찬&quot;, number: 54 },
    { name: &quot;송승기&quot;, number: 13 },
  ],
};</code></pre>
<ul>
<li><code>number?</code> 처럼 물음표를 붙이면, 해당 필드는 와도 되고, 안 와도 됨.</li>
</ul>
<p><strong><code>extends</code></strong></p>
<ul>
<li><code>interface</code>에서는 <code>extends</code>라고, 기존 <code>interface</code>에 뭘 더하는 문법을 사용할 수 있음</li>
</ul>
<pre><code class="language-ts">interface Food {
  name: string;
  price: number;
}

interface Pizza extends Food {
  pieces: number;
  comment: string;
}

let pizza: Pizza = {
  name: &quot;불고기 피자&quot;,
  price: 13000,
  pieces: 8,
  comment: &quot;너무 맛있어요&quot;,
};</code></pre>
<h1 id="타입스크립트-코드-컴파일">타입스크립트 코드 컴파일</h1>
<ul>
<li>웹 브라우저는 JavaScript만 이해하지, TypeScript를 이해하지 못함<ul>
<li>TypeScript는 인간의 편리를 위한 언어</li>
</ul>
</li>
<li>즉, TypeScript는 JavaScript로 컴파일해야 함</li>
</ul>
<pre><code class="language-ts">// test.ts
function addNumber(a: number, b: number): number {
  return a + b;
}
console.log(addNumber(7, 22));</code></pre>
<pre><code class="language-bash"># test.ts -&gt; test.js 파일 생성
tsc test.ts</code></pre>
<pre><code class="language-js">// 컴파일된 test.js 파일
function addNumber(a, b) {
  return a + b;
}
console.log(addNumber(7, 22));</code></pre>
<ul>
<li>타입 오류 발생 시, 컴파일은 되지만 아래와 같이 알려줌</li>
</ul>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/cfd670ab-965f-48bd-8f39-473ee879e35a/image.png" alt=""></p>
<pre><code class="language-bash">test.ts:2:1 - error TS2322: Type &#39;string&#39; is not assignable to type &#39;number&#39;.

2 a = &quot;fsdfsdsfd&quot;;
  ~
Found 1 error in test.ts:2</code></pre>
<h2 id="tsconfigjson"><code>tsconfig.json</code></h2>
<ul>
<li><code>ts</code> -&gt; <code>js</code> config 위한 <code>json</code> 파일</li>
<li>자바스크립트 버전도 다양한 만큼, 이를 설정하기 위함</li>
</ul>
<pre><code class="language-json">{
  &quot;compilerOptions&quot;: {
    &quot;outDir&quot;: &quot;dist&quot;, // 컴파일된 JS 파일을 저장할 디렉토리
    &quot;target&quot;: &quot;es6&quot;, // 어떤버전의 JS로 변환할지 지정. 제일 최신은 ES6
    &quot;module&quot;: &quot;commonjs&quot;, // Express는 commonjs, React는 es6로 설정
    &quot;lib&quot;: [&quot;es6&quot;], // 컴파일 시 사용할 라이브러리 지정
    &quot;sourceMap&quot;: true // 디버깅용. JS코드에서 에러 발생시, 원래 작성한 TS에서 위치를 정확히 확인하기 위함.
  }
}</code></pre>
<ul>
<li>이후 <code>tsc (파일명)</code>로 빌드하면, 해당폴더의 <code>tsconfig.json</code>에서 지정한 컴파일러 옵션을 적용함</li>
<li>config 옵션은 사용하는 라이브러리 등에 따라 달라질 수 있음. 보통 이를 그대로 따르면 됨</li>
</ul>
<h1 id="react--typescript">React + TypeScript</h1>
<ul>
<li>기존처럼 <code>npm create vite@latest</code>을 실행하시되, <code>TypeScript</code> 선택.</li>
</ul>
<pre><code class="language-bash">&gt; create-vite

│
◇  Project name:
│  react-typescript
│
◇  Select a framework:
│  React
│
◇  Select a variant:
│  TypeScript
│
◇  Scaffolding project in C:\Users\drink\Documents\jungle\week15\typescript\react\react-typescript...
│
└  Done. Now run:

  cd react-typescript
  npm install
  npm run dev</code></pre>
<ul>
<li>기본 파일도 <code>.tsx</code>고, <code>tsconfig.json</code> 파일도 기본적으로 생성된 것을 확인할 수 있음.</li>
<li>대신 코드 자체는 아직 타입스크립트 형태로 바뀌지 않은 상황</li>
</ul>
<h2 id="state-타입스크립트에서-사용하기"><code>state</code> 타입스크립트에서 사용하기</h2>
<ul>
<li><code>useState</code> 함수에 무슨 타입이 들어가지?</li>
<li>리액트는 기본적으로는 JS 기반. 뭔 타입이 들어와도 괜찮음.</li>
<li>JS를 위한 함수를 TS에 맞게 수정할 수는 없는 노릇. 대신 제네릭이라는 친구를 사용.</li>
<li><strong>제네릭</strong> <code>&lt;...&gt;</code>: 내가 <code>useState</code>를 호출하는 순간, 타입을 정해 준다.</li>
</ul>
<pre><code class="language-ts">// 내가 `myPlayer` state에 앞서 만든 Player type를 저장하는 경우
// `useState&lt;Player&gt;`와 같이 사용.
const [myPlayer, setMyPlayer] = useState&lt;Player&gt;(data);</code></pre>
<ul>
<li>여기서 <code>myPlayer</code>는 <code>Player</code> 타입</li>
<li><code>setMyPlayer</code>는 <code>Player</code>를 인수로 받고 반환값이 없는 함수 타입이 됨.</li>
</ul>
<h2 id="props-타입스크립트에서-사용하기"><code>props</code> 타입스크립트에서 사용하기</h2>
<ul>
<li>컴포넌트에 보내는 <code>props</code> 역시 <code>type</code>나 <code>interface</code>로 타입을 만들어 사용</li>
</ul>
<pre><code class="language-ts">const App = () =&gt; {
  const [myPlayer, setMyPlayer] = useState&lt;Player&gt;(data);
  const changeName = (name: string) =&gt; {
    setMyPlayer((myPlayer) =&gt; ({ ...myPlayer, name }));
  };

  return (
    &lt;&gt;
      &lt;CheckPlayer data={myPlayer} changeName={changeName} /&gt;
    &lt;/&gt;
  );
};</code></pre>
<ul>
<li>e.g., 내가 <code>Player</code> 형의 <code>data</code>와, <code>string</code>을 받고 반환값이 없는 함수 <code>changeName</code>을 <code>CheckPlayer</code> 컴포넌트에 전달하고 싶다면?</li>
</ul>
<pre><code class="language-ts">interface CheckPlayerProps {
  data: Player;
  changeName(name: string): void;
}</code></pre>
<ul>
<li><code>Props</code>를 <code>interface</code>나 <code>type</code>로 만들어 주고<ul>
<li>이때 <code>interface</code>에 함수를 넣는 경우, <code>(변수명: 변수타입, ...): 반환값타입</code> 식으로 작성</li>
<li>반환값 없는 경우 <code>void</code>로</li>
</ul>
</li>
</ul>
<pre><code class="language-ts">const CheckPlayer = (props: CheckPlayerProps) =&gt; {
  return (
    &lt;&gt;
      &lt;h1&gt;
        #{props.data.number}: {props.data.name}
      &lt;/h1&gt;
    &lt;/&gt;
  );
};</code></pre>
<ul>
<li>이후 <code>CheckPlayer</code>의 매개변수 타입을 <code>CheckPlayerProps</code>로 설정한다</li>
</ul>
<h2 id="예제코드">예제코드</h2>
<pre><code class="language-ts">// App.tsx
import &quot;./App.css&quot;;
import { useState } from &quot;react&quot;;

interface Size {
  height: number;
  weight: number;
}

interface Friend {
  name: string;
  number: number;
}

interface Player {
  name: string;
  number: number;
  size: Size;
  friends: Friend[];
}

const data: Player = {
  name: &quot;임찬규&quot;,
  number: 1,
  size: { height: 186, weight: 88 },
  friends: [
    { name: &quot;손주영&quot;, number: 29 },
    { name: &quot;김진성&quot;, number: 42 },
    { name: &quot;유영찬&quot;, number: 54 },
    { name: &quot;송승기&quot;, number: 13 },
  ],
};

interface CheckPlayerProps {
  data: Player;
  changeName(name: string): void;
}

const CheckPlayer = (props: CheckPlayerProps) =&gt; {
  const [newName, setNewName] = useState&lt;string&gt;(props.data.name);
  return (
    &lt;&gt;
      &lt;h1&gt;
        #{props.data.number}: {props.data.name}
      &lt;/h1&gt;
      &lt;div&gt;
        키 {props.data.size.height}, 몸무게 {props.data.size.weight}
      &lt;/div&gt;
      &lt;h2&gt;동료 선수들&lt;/h2&gt;
      &lt;ul&gt;
        {props.data.friends.map((friend: Friend, idx: number) =&gt; (
          &lt;li key={idx}&gt;
            #{friend.number} {friend.name}
          &lt;/li&gt;
        ))}
      &lt;/ul&gt;
      &lt;input
        type=&quot;text&quot;
        value={newName}
        onChange={(e) =&gt; setNewName(e.target.value)}
        placeholder=&quot;변경할 이름 입력&quot;
      /&gt;
      &lt;button onClick={() =&gt; props.changeName(newName)}&gt;이름 변경&lt;/button&gt;
    &lt;/&gt;
  );
};

const App = () =&gt; {
  const [myPlayer, setMyPlayer] = useState&lt;Player&gt;(data);
  const changeName = (name: string) =&gt; {
    setMyPlayer((myPlayer) =&gt; ({ ...myPlayer, name }));
  };

  return (
    &lt;&gt;
      &lt;CheckPlayer data={myPlayer} changeName={changeName} /&gt;
    &lt;/&gt;
  );
};

export default App;
</code></pre>
<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/762a691b-9d79-4a2f-972a-d88e4cddb9d6/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[나만무팀장의하루.jpg]]></title>
            <link>https://velog.io/@strawberry-tree/%EB%82%98%EB%A7%8C%EB%AC%B4%ED%8C%80%EC%9E%A5%EC%9D%98%ED%95%98%EB%A3%A8.jpg</link>
            <guid>https://velog.io/@strawberry-tree/%EB%82%98%EB%A7%8C%EB%AC%B4%ED%8C%80%EC%9E%A5%EC%9D%98%ED%95%98%EB%A3%A8.jpg</guid>
            <pubDate>Thu, 21 Aug 2025 16:01:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/strawberry-tree/post/573d16a9-b3a0-4898-bcc1-d9274a38bcb8/image.jpg" alt=""></p>
<p>우리 팀 사랑합니다 ㅎㅎㅎ</p>
]]></description>
        </item>
    </channel>
</rss>