<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>나만의 개발 기록</title>
    <link>https://aamoos.tistory.com/</link>
    <description>웹개발을 하면서 기억하고 싶은 내용을 정리하는 블로그입니다</description>
    <language>ko</language>
    <pubDate>Mon, 24 Aug 2026 16:03:20 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>JaeSung Kim</managingEditor>
    <image>
      <title>나만의 개발 기록</title>
      <url>https://tistory1.daumcdn.net/tistory/3049955/attach/0cb037e12f8e48e9932620fd1aee0cff</url>
      <link>https://aamoos.tistory.com</link>
    </image>
    <item>
      <title># 면접 CS 정리 Day 13 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/810</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 13 (풀스택 / Java·Spring·React)&lt;/h1&gt;
&lt;h2&gt;Q1. static&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;static&lt;/code&gt;이 변수/메서드에 붙으면? static 메서드에서 인스턴스 필드를 바로 쓸 수 있나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;&lt;code&gt;static&lt;/code&gt; 멤버는 &lt;strong&gt;클래스에 속함&lt;/strong&gt;. &lt;code&gt;new&lt;/code&gt; 없이 &lt;code&gt;클래스명.메서드()&lt;/code&gt;로 호출 가능.&lt;br&gt;&lt;code&gt;static&lt;/code&gt; 변수는 인스턴스마다 생기지 않고 &lt;strong&gt;공유&lt;/strong&gt;된다.&lt;/p&gt;
&lt;p&gt;static 메서드에서는 &lt;strong&gt;&lt;code&gt;this&lt;/code&gt;가 없어&lt;/strong&gt; 인스턴스 필드를 &lt;strong&gt;직접 접근할 수 없다&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Q2. REST stateless&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;REST API에서 stateless가 뭔지? JWT와 어떻게 연결되나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;서버가 요청 사이에 &lt;strong&gt;클라이언트 상태를 기억하지 않는다&lt;/strong&gt;. 각 요청만으로 처리 가능해야 한다.&lt;/p&gt;
&lt;p&gt;세션은 서버에 로그인 상태를 두므로 stateful에 가깝고,&lt;br&gt;JWT는 토큰으로 인증 정보를 넘기고 서버는 &lt;strong&gt;검증만&lt;/strong&gt; 해서 stateless API에 잘 맞는다.&lt;/p&gt;
&lt;p&gt;세션 만료·access 짧게는 &lt;strong&gt;별개 이슈&lt;/strong&gt; (만료 정책 / 탈취 피해 축소).&lt;/p&gt;
&lt;h2&gt;Q3. DNS&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;DNS가 뭔지, &lt;code&gt;https://api.example.com&lt;/code&gt; 접속 시 하는 일.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;도메인 이름을 &lt;strong&gt;IP 주소&lt;/strong&gt;로 바꿔 주는 시스템.&lt;br&gt;&lt;code&gt;api.example.com&lt;/code&gt; → 서버 IP를 찾아 TCP 연결로 이어진다.&lt;/p&gt;
&lt;h2&gt;Q4. JPA 영속성 컨텍스트&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;영속성 컨텍스트가 뭔지? 같은 트랜잭션에서 같은 엔티티를 두 번 조회하면 쿼리 1번인 이유.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;JPA가 엔티티를 관리하는 &lt;strong&gt;1차 캐시(작업 공간)&lt;/strong&gt;.&lt;br&gt;조회: 1차 캐시 → 없으면 DB.&lt;br&gt;같은 id 재조회 시 캐시에서 반환되어 SQL이 1번만 나간다.&lt;/p&gt;
&lt;h2&gt;Q5. Redis 캐시&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;Redis를 DB 대신이 아니라 캐시로 쓰는 이유.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;&lt;strong&gt;메모리(In-memory)&lt;/strong&gt; 기반이라 DB보다 훨씬 빠르다.&lt;br&gt;Redis 먼저 조회 → 없으면 DB 조회 후 Redis에 저장.&lt;br&gt;자주 읽는 데이터의 &lt;strong&gt;응답 속도·DB 부하&lt;/strong&gt;를 줄인다.&lt;/p&gt;
&lt;h2&gt;Q6. Controlled vs Uncontrolled input&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;값을 누가 관리하는지로 차이.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Controlled&lt;/strong&gt;: 값을 &lt;strong&gt;React &lt;code&gt;state&lt;/code&gt;&lt;/strong&gt;가 관리 (&lt;code&gt;value&lt;/code&gt; + &lt;code&gt;onChange&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Uncontrolled&lt;/strong&gt;: 값을 &lt;strong&gt;브라우저(DOM)&lt;/strong&gt;이 관리 (&lt;code&gt;defaultValue&lt;/code&gt; + &lt;code&gt;ref&lt;/code&gt;로 읽기)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;React도 state 변경 시 실제 DOM을 갱신한다.&lt;br&gt;Uncontrolled는 input 값을 state에 올리지 않고 DOM에 맡기는 방식.&lt;/p&gt;
&lt;h2&gt;Q7. useMemo vs useCallback&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;각각 무엇을 캐시하나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useMemo&lt;/code&gt;: &lt;strong&gt;계산 결과(값)&lt;/strong&gt; 캐시&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useCallback&lt;/code&gt;: &lt;strong&gt;함수 참조&lt;/strong&gt; 캐시&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;컴포넌트 캐시 = &lt;code&gt;React.memo&lt;/code&gt;, API 캐시 = React Query 등.&lt;/p&gt;
&lt;h2&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;static = 클래스 단위, 인스턴스 필드 직접 접근 X&lt;/li&gt;
&lt;li&gt;REST stateless = 서버가 요청 간 상태 안 들고 있음&lt;/li&gt;
&lt;li&gt;DNS = 도메인 → IP&lt;/li&gt;
&lt;li&gt;영속성 컨텍스트 = JPA 1차 캐시&lt;/li&gt;
&lt;li&gt;Controlled = state / Uncontrolled = DOM&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;다음 목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;stateless를 “세션 만료”랑 섞지 않기&lt;/li&gt;
&lt;li&gt;Controlled/Uncontrolled를 input 값 소유 주체로 설명하기&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타/CS 질문</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/810</guid>
      <comments>https://aamoos.tistory.com/810#entry810comment</comments>
      <pubDate>Mon, 24 Aug 2026 10:43:25 +0900</pubDate>
    </item>
    <item>
      <title># 면접 CS 정리 Day 12 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/809</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 12 (풀스택 / Java·Spring·React)&lt;/h1&gt;
&lt;h2&gt;Q1. 스택 vs 힙&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;JVM에서 스택이랑 힙 차이? 지역 변수 / 객체는 어디에?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;스택&lt;/strong&gt;: 지역 변수, 메서드 호출 정보. 스레드마다 있음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;힙&lt;/strong&gt;: &lt;code&gt;new&lt;/code&gt;로 만든 &lt;strong&gt;객체&lt;/strong&gt;. GC가 회수&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;User u = new User()&lt;/code&gt; → 참조 &lt;code&gt;u&lt;/code&gt;는 스택, 객체는 힙.&lt;br&gt;자료구조 LIFO 스택이랑 헷갈리지 말 것.&lt;/p&gt;
&lt;h2&gt;Q2. int vs Integer&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;차이 한 줄. Integer가 null일 때 언박싱하면?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int&lt;/code&gt;: 기본형, &lt;strong&gt;null 불가&lt;/strong&gt;, 기본값 &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Integer&lt;/code&gt;: 래퍼 객체, &lt;strong&gt;null 가능&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;null&lt;/code&gt; Integer를 &lt;code&gt;int&lt;/code&gt;로 언박싱하면 &lt;strong&gt;&lt;code&gt;NullPointerException&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Q3. useMemo vs useCallback&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;각각 무엇을 캐시하나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useMemo&lt;/code&gt;: &lt;strong&gt;계산 결과(값)&lt;/strong&gt; 캐시&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useCallback&lt;/code&gt;: &lt;strong&gt;함수 참조&lt;/strong&gt; 캐시&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;컴포넌트 재사용은 &lt;code&gt;React.memo&lt;/code&gt;, API 캐시는 React Query 쪽.&lt;br&gt;&lt;code&gt;useCallback(fn, deps)&lt;/code&gt; ≈ &lt;code&gt;useMemo(() =&amp;gt; fn, deps)&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Q4. 데드락&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;데드락이 뭔지 + 발생 조건 하나.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;서로가 가진 락을 기다리며 &lt;strong&gt;아무도 못 끝나는&lt;/strong&gt; 상태.&lt;br&gt;동시 접근만으로 생기진 않고, 락을 &lt;strong&gt;엇갈려&lt;/strong&gt; 잡을 때 난다.&lt;/p&gt;
&lt;p&gt;예방: 락 순서를 같게 (항상 작은 id부터).&lt;/p&gt;
&lt;h2&gt;Q5. TCP 3-way handshake&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;뭐 하는 과정인지. SYN / ? / ?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;TCP 연결을 양쪽으로 맞추는 과정.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;클라이언트 → 서버: &lt;code&gt;SYN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;서버 → 클라이언트: &lt;code&gt;SYN+ACK&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;클라이언트 → 서버: &lt;code&gt;ACK&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Q6. @Transactional(readOnly = true)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;조회에 붙이는 이유? 쓰기 메서드에 붙이면?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;조회 시 플러시/더티체킹을 줄이고 DB에 읽기 힌트를 준다.&lt;br&gt;트랜잭션이 &lt;strong&gt;안 먹는 게 아니라&lt;/strong&gt;, 읽기 전용이라 쓰기가 실패하거나 반영이 안 될 수 있다.&lt;/p&gt;
&lt;h2&gt;Q7. Dirty / Non-repeatable / Phantom&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;한 줄씩 + 막는 최소 격리수준.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dirty Read: 커밋 전 읽기 → &lt;code&gt;READ COMMITTED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Non-repeatable Read: 같은 행 값 변경 → &lt;code&gt;REPEATABLE READ&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Phantom Read: 행이 늘거나 줄어듦 → &lt;code&gt;SERIALIZABLE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;지역변수·참조 = 스택 / 객체 = 힙&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Integer&lt;/code&gt; null 언박싱 = NPE&lt;/li&gt;
&lt;li&gt;useMemo = 값 / useCallback = 함수&lt;/li&gt;
&lt;li&gt;3-way = SYN → SYN+ACK → ACK&lt;/li&gt;
&lt;li&gt;Phantom = SERIALIZABLE&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;다음 목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;useMemo를 “컴포넌트 캐시”, useCallback을 “API 캐시”로 말하지 않기&lt;/li&gt;
&lt;li&gt;데드락 = 동시 접근이 아니라 &lt;strong&gt;락 교차 대기&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타/CS 질문</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/809</guid>
      <comments>https://aamoos.tistory.com/809#entry809comment</comments>
      <pubDate>Wed, 19 Aug 2026 13:40:55 +0900</pubDate>
    </item>
    <item>
      <title># 면접 CS 정리 Day 11 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/808</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 11 (풀스택 / Java·Spring·React)&lt;/h1&gt;
&lt;h2&gt;Q1. interface vs abstract class&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;interface랑 abstract class 차이? 다중 상속, 필드, 생성자 기준.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;interface: &lt;strong&gt;여러 개&lt;/strong&gt; 구현 가능, 생성자 &lt;strong&gt;없음&lt;/strong&gt;, 상수 위주&lt;/li&gt;
&lt;li&gt;abstract class: 클래스는 &lt;strong&gt;하나만&lt;/strong&gt; 상속, 생성자 &lt;strong&gt;있음&lt;/strong&gt;, 인스턴스 필드 가능&lt;/li&gt;
&lt;li&gt;둘 다 직접 &lt;code&gt;new&lt;/code&gt; 불가. abstract 생성자는 자식이 &lt;code&gt;super()&lt;/code&gt;로 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;역할(계약)만이면 interface, 공통 구현·상태를 물려줄 때면 abstract class.&lt;/p&gt;
&lt;h2&gt;Q2. @Component / @Service / @Repository&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;셋 다 Bean 등록인데 왜 나누나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;&lt;code&gt;@Service&lt;/code&gt; / &lt;code&gt;@Repository&lt;/code&gt;는 &lt;code&gt;@Component&lt;/code&gt;의 &lt;strong&gt;특화(스테레오타입)&lt;/strong&gt;. 셋 다 스캔되어 Bean이 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;계층 의미: Repository=DB, Service=비즈니스, Component=기타&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Repository&lt;/code&gt;는 DB 예외를 &lt;code&gt;DataAccessException&lt;/code&gt;으로 &lt;strong&gt;변환&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;인터페이스라서 붙이는 게 아니라 &lt;strong&gt;역할&lt;/strong&gt; 따라 붙인다.&lt;/p&gt;
&lt;h2&gt;Q3. Virtual DOM&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;Virtual DOM이 뭔지, 실제 DOM을 매번 안 건드리는 이유.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;실제 DOM을 흉내 낸 &lt;strong&gt;가벼운 JS 객체 트리&lt;/strong&gt;.&lt;br&gt;이전/이후를 diff해서 &lt;strong&gt;바뀐 부분만&lt;/strong&gt; 실제 DOM에 반영한다.&lt;br&gt;실제 DOM 조작이 비싸서 전체를 다시 그리지 않는다.&lt;/p&gt;
&lt;h2&gt;Q4. HTTPS&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;HTTPS가 HTTP보다 안전한 이유? HTTP + ? = HTTPS인지.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;&lt;strong&gt;HTTPS = HTTP + TLS.&lt;/strong&gt; (SSL은 옛 이름)&lt;br&gt;클라이언트↔서버 구간을 &lt;strong&gt;암호화&lt;/strong&gt;하고, 인증서로 서버를 검증하며 위변조도 막는다.&lt;/p&gt;
&lt;h2&gt;Q5. OFFSET 페이징&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;LIMIT 10 OFFSET 100000&lt;/code&gt;이 느릴 수 있는 이유 + 대안.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;OFFSET은 앞 행을 &lt;strong&gt;건너뛰려고 읽고 버린다&lt;/strong&gt;. 페이지가 깊을수록 느리다.&lt;br&gt;대안: &lt;strong&gt;커서(keyset) 페이징&lt;/strong&gt; — &lt;code&gt;WHERE id &amp;lt; 마지막id ORDER BY id DESC LIMIT 10&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Q6. REQUIRED vs REQUIRES_NEW&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;트랜잭션이 이미 있을 때 각각?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;REQUIRED&lt;/code&gt;: 기존에 &lt;strong&gt;참여&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REQUIRES_NEW&lt;/code&gt;: &lt;strong&gt;새 트랜잭션&lt;/strong&gt; (기존은 잠시 보류)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;없을 때는 둘 다 새로 시작.&lt;br&gt;&lt;code&gt;있으면 쓰고 없으면 안 씀&lt;/code&gt; = &lt;code&gt;SUPPORTS&lt;/code&gt;. 이름 철자: &lt;code&gt;REQUIRES_NEW&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Q7. Dirty / Non-repeatable / Phantom&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;세 현상 + 막는 최소 격리수준.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dirty Read: 커밋 전 읽기 → &lt;code&gt;READ COMMITTED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Non-repeatable Read: 같은 행 값 변경 → &lt;code&gt;REPEATABLE READ&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Phantom Read: 행이 늘거나 줄어듦 → &lt;code&gt;SERIALIZABLE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;SUPPORTS&lt;/code&gt;는 트랜잭션 &lt;strong&gt;전파&lt;/strong&gt;이지 격리수준이 아니다.&lt;/p&gt;
&lt;h2&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;interface = 다중 구현, 생성자 없음 / abstract = 단일 상속, 생성자 있음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Repository&lt;/code&gt; = Component 특화 + 예외 변환&lt;/li&gt;
&lt;li&gt;HTTPS = HTTP + TLS&lt;/li&gt;
&lt;li&gt;OFFSET 깊을수록 느림 → 커서 페이징&lt;/li&gt;
&lt;li&gt;Phantom 막는 수준 = &lt;code&gt;SERIALIZABLE&lt;/code&gt; (SUPPORTS 아님)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;다음 목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;격리수준 3종을 현상 한 줄 + 이름까지 세트로 말하기&lt;/li&gt;
&lt;li&gt;SUPPORTS(전파) vs SERIALIZABLE(격리) 섞지 않기&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타/CS 질문</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/808</guid>
      <comments>https://aamoos.tistory.com/808#entry808comment</comments>
      <pubDate>Tue, 18 Aug 2026 10:37:26 +0900</pubDate>
    </item>
    <item>
      <title># 면접 CS 정리 Day 10 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/807</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 10 (풀스택 / Java·Spring·React)&lt;/h1&gt;
&lt;h2&gt;Q1. 오버로딩 vs 오버라이딩&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;오버로딩이랑 오버라이딩 차이? 컴파일/런타임 중 언제 결정되나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;오버로딩&lt;/strong&gt;: 같은 이름, &lt;strong&gt;파라미터(개수/타입/순서)&lt;/strong&gt;가 다름 → &lt;strong&gt;컴파일&lt;/strong&gt; 때 결정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;오버라이딩&lt;/strong&gt;: 부모 메서드를 &lt;strong&gt;재정의&lt;/strong&gt; → &lt;strong&gt;런타임&lt;/strong&gt;에 실제 객체 타입 기준 (동적 바인딩)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;반환 타입만 다르게는 오버로딩이 안 된다.&lt;/p&gt;
&lt;h2&gt;Q2. REQUIRED vs REQUIRES_NEW&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;REQUIRED&lt;/code&gt;(기본)랑 &lt;code&gt;REQUIRES_NEW&lt;/code&gt; 차이? 트랜잭션이 이미 있을 때.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;REQUIRED&lt;/strong&gt;: 있으면 &lt;strong&gt;참여&lt;/strong&gt;, 없으면 &lt;strong&gt;새로 시작&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;REQUIRES_NEW&lt;/strong&gt;: &lt;strong&gt;항상 새 트랜잭션&lt;/strong&gt;. 기존은 잠시 보류. 커밋/롤백이 바깥과 &lt;strong&gt;따로&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;있으면 쓰고 없으면 안 쓴다&lt;/code&gt;는 &lt;code&gt;SUPPORTS&lt;/code&gt;다. REQUIRED가 아니다.&lt;/p&gt;
&lt;h2&gt;Q3. Dirty / Non-repeatable / Phantom&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;세 현상 한 줄씩 + 막는 최소 격리수준.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dirty Read&lt;/strong&gt;: 남의 &lt;strong&gt;커밋 전&lt;/strong&gt; 데이터를 읽음 → &lt;code&gt;READ COMMITTED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-repeatable Read&lt;/strong&gt;: 같은 행을 다시 읽었더니 &lt;strong&gt;값이 바뀜&lt;/strong&gt; → &lt;code&gt;REPEATABLE READ&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phantom Read&lt;/strong&gt;: 같은 조건 재조회 시 &lt;strong&gt;행이 늘거나 줄어듦&lt;/strong&gt; → &lt;code&gt;SERIALIZABLE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dirty = 미확정 값 / Non-repeatable = 그 행의 값 / Phantom = 행 개수.&lt;/p&gt;
&lt;h2&gt;Q4. @RequestParam / @PathVariable / @RequestBody&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;각각 어디서 값을 받나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@RequestParam&lt;/code&gt;: 쿼리스트링 (&lt;code&gt;?type=admin&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@PathVariable&lt;/code&gt;: URL 경로 (&lt;code&gt;/users/3&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequestBody&lt;/code&gt;: JSON 본문 (보통 DTO)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;GET /users/3?type=admin&lt;/code&gt; → PathVariable=&lt;code&gt;3&lt;/code&gt;, RequestParam=&lt;code&gt;type&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Q5. useState vs useRef&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;값 변경 시 리렌더 여부로 차이.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useState&lt;/code&gt;: 값 변경 시 &lt;strong&gt;리렌더&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useRef&lt;/code&gt;: &lt;code&gt;.current&lt;/code&gt;가 바뀌어도 &lt;strong&gt;리렌더 안 함&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;화면 상태 = state, DOM 접근·렌더와 무관한 값 = ref.&lt;/p&gt;
&lt;h2&gt;Q6. 비관적 락 vs 낙관적 락&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;차이 + 각각 언제 쓰는지.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;비관적&lt;/strong&gt;: 조회 시 &lt;strong&gt;미리 잠금&lt;/strong&gt; (&lt;code&gt;SELECT … FOR UPDATE&lt;/code&gt;). 충돌이 &lt;strong&gt;잦을 때&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;낙관적&lt;/strong&gt;: 저장 시 &lt;strong&gt;버전&lt;/strong&gt;으로 충돌 감지. 충돌이 &lt;strong&gt;드물 때&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;비관적 = 먼저 잡고 수정 / 낙관적 = 일단 수정하고 버전 검증.&lt;/p&gt;
&lt;h2&gt;Q7. @Transactional + this 호출&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;같은 클래스에서 &lt;code&gt;this.xxx()&lt;/code&gt;로 호출하면 왜 트랜잭션이 안 먹을 수 있나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;트랜잭션은 &lt;strong&gt;프록시&lt;/strong&gt;가 호출을 가로챌 때 적용된다.&lt;br&gt;&lt;code&gt;this.xxx()&lt;/code&gt;는 프록시를 안 지나서 &lt;code&gt;@Transactional&lt;/code&gt;이 적용되지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;해결: 트랜잭션 메서드를 &lt;strong&gt;다른 Bean&lt;/strong&gt;으로 나눠 주입받아 호출.&lt;/p&gt;
&lt;h2&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;오버로딩 = 컴파일 / 오버라이딩 = 런타임&lt;/li&gt;
&lt;li&gt;REQUIRED = 있으면 참여 / REQUIRES_NEW = 항상 새로&lt;/li&gt;
&lt;li&gt;Dirty=미커밋 / Non-repeatable=값변경 / Phantom=행수변경&lt;/li&gt;
&lt;li&gt;useState 리렌더 O / useRef 리렌더 X&lt;/li&gt;
&lt;li&gt;&lt;code&gt;this&lt;/code&gt; 호출 = 프록시 우회 → 트랜잭션 안 먹을 수 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;다음 목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;REQUIRED를 SUPPORTS랑 헷갈리지 않기&lt;/li&gt;
&lt;li&gt;Dirty / Non-repeatable / Phantom을 격리수준 이름까지 말하기&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타/CS 질문</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/807</guid>
      <comments>https://aamoos.tistory.com/807#entry807comment</comments>
      <pubDate>Fri, 14 Aug 2026 15:05:40 +0900</pubDate>
    </item>
    <item>
      <title># 면접 CS 정리 Day 9 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/806</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 9 (풀스택 / Java·Spring·React)&lt;/h1&gt;
&lt;h2&gt;Q1. equals + hashCode&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;HashMap&lt;/code&gt;에 커스텀 객체를 key로 쓸 때 &lt;code&gt;equals&lt;/code&gt;와 &lt;code&gt;hashCode&lt;/code&gt;를 같이 오버라이드해야 하는 이유는?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;&lt;code&gt;HashMap&lt;/code&gt;은 &lt;code&gt;hashCode&lt;/code&gt;로 &lt;strong&gt;버킷 위치&lt;/strong&gt;를 찾고, 그 안에서 &lt;code&gt;equals&lt;/code&gt;로 &lt;strong&gt;같은 key인지&lt;/strong&gt; 판단한다.&lt;br&gt;&lt;code&gt;equals&lt;/code&gt;가 true면 &lt;code&gt;hashCode&lt;/code&gt;도 같아야 하므로 둘 다 오버라이드한다.&lt;/p&gt;
&lt;h2&gt;Q2. Checked / Unchecked + 롤백&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;Checked&lt;/code&gt; / &lt;code&gt;Unchecked&lt;/code&gt; Exception 차이? &lt;code&gt;@Transactional&lt;/code&gt; 기본 롤백은?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Checked&lt;/strong&gt;: &lt;code&gt;IOException&lt;/code&gt; 등 — 컴파일러가 처리 강제&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unchecked&lt;/strong&gt;: &lt;code&gt;RuntimeException&lt;/code&gt; 계열 — 보통 &lt;code&gt;BusinessException&lt;/code&gt;도 여기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;@Transactional&lt;/code&gt; 기본 롤백 = &lt;strong&gt;unchecked만&lt;/strong&gt;.&lt;br&gt;checked도 롤백하려면 &lt;code&gt;@Transactional(rollbackFor = Exception.class)&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Q3. 복합 인덱스&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;복합 인덱스 &lt;code&gt;(user_id, created_at)&lt;/code&gt;에서 &lt;code&gt;WHERE created_at = ?&lt;/code&gt;만 쓰면 왜 잘 못 타나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;복합 인덱스는 &lt;strong&gt;왼쪽 컬럼부터&lt;/strong&gt; 의미가 있다.&lt;br&gt;앞 컬럼(&lt;code&gt;user_id&lt;/code&gt;) 없이 뒤 컬럼만 쓰면 인덱스를 잘 못 탄다.&lt;/p&gt;
&lt;h2&gt;Q4. 401 vs 403&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;401&lt;/code&gt; vs &lt;code&gt;403&lt;/code&gt; 한 줄씩.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;401&lt;/code&gt;: &lt;strong&gt;인증&lt;/strong&gt; 실패 (로그인/토큰 없음·만료)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;403&lt;/code&gt;: 인증은 됐지만 &lt;strong&gt;권한&lt;/strong&gt; 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q5. N+1&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;N+1 문제가 뭔지, MyBatis/JPA에서 줄이는 방법 하나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;목록 1번 조회 후 연관 데이터를 건마다 N번 추가 조회하는 문제.  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MyBatis: &lt;code&gt;JOIN&lt;/code&gt; 또는 &lt;code&gt;IN&lt;/code&gt; 일괄 조회&lt;/li&gt;
&lt;li&gt;JPA: &lt;code&gt;fetch join&lt;/code&gt;, &lt;code&gt;@EntityGraph&lt;/code&gt; 등&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q6. access / refresh Token&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;JWT에서 accessToken을 짧게, refreshToken을 길게 두는 이유는?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;access가 탈취돼도 &lt;strong&gt;짧은 만료&lt;/strong&gt;로 피해 구간을 줄이고,&lt;br&gt;만료 시 refresh로 access를 재발급하기 위해서다.&lt;/p&gt;
&lt;h2&gt;Q7. Filter vs Interceptor&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;Filter&lt;/code&gt; vs &lt;code&gt;Interceptor&lt;/code&gt; 인증은 어디에 두는지 + 이유 한 줄?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Filter: 서블릿 단 (&lt;code&gt;DispatcherServlet&lt;/code&gt; &lt;strong&gt;앞&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Interceptor: Spring MVC (컨트롤러 전후)&lt;/li&gt;
&lt;li&gt;인증: Spring Security가 &lt;strong&gt;Filter 체인&lt;/strong&gt;에서 동작하므로 보통 Filter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;css/js로 외우지 말고 &lt;strong&gt;서블릿 vs MVC&lt;/strong&gt;로 구분한다.&lt;/p&gt;
&lt;h2&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;HashMap key = &lt;code&gt;hashCode&lt;/code&gt;(버킷) + &lt;code&gt;equals&lt;/code&gt;(동일 판단)&lt;/li&gt;
&lt;li&gt;기본 롤백 = unchecked&lt;/li&gt;
&lt;li&gt;복합 인덱스 = 좌측 접두어&lt;/li&gt;
&lt;li&gt;401 인증 / 403 권한&lt;/li&gt;
&lt;li&gt;인증 = Filter (Security)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;다음 목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;equals/hashCode를 “버킷 + 동일 키 판단” 두 단계로 말하기&lt;/li&gt;
&lt;li&gt;Filter를 css/js가 아니라 서블릿 단으로 설명하기&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타/CS 질문</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/806</guid>
      <comments>https://aamoos.tistory.com/806#entry806comment</comments>
      <pubDate>Thu, 13 Aug 2026 11:07:48 +0900</pubDate>
    </item>
    <item>
      <title># 면접 CS 정리 Day 8 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/805</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 8 (풀스택 / Java·Spring·React)&lt;/h1&gt;
&lt;h2&gt;Q1. String / StringBuilder / StringBuffer&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;String&lt;/code&gt; / &lt;code&gt;StringBuilder&lt;/code&gt; / &lt;code&gt;StringBuffer&lt;/code&gt; 차이?&lt;br&gt;루프에서 문자열을 &lt;code&gt;+&lt;/code&gt;로 이어 붙이면 왜 안 좋은지?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;String&lt;/code&gt;: &lt;strong&gt;불변&lt;/strong&gt;. 더하면 새 객체 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StringBuilder&lt;/code&gt;: 가변, 동기화 없음 → &lt;strong&gt;일반 추천&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StringBuffer&lt;/code&gt;: 가변, 동기화 있음 → 멀티스레드용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;루프에서 &lt;code&gt;str = str + &amp;quot;x&amp;quot;&lt;/code&gt;는 매번 새 &lt;code&gt;String&lt;/code&gt;이 생겨 느리고 GC 부담이 크다.&lt;br&gt;반복 연결은 &lt;code&gt;StringBuilder&lt;/code&gt;를 쓴다.&lt;/p&gt;
&lt;h2&gt;Q2. Bean 스코프 + singleton 위험&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;Bean 기본 스코프는? &lt;code&gt;singleton&lt;/code&gt;에 요청별 상태 필드를 두면 왜 위험한지?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;기본 스코프는 &lt;strong&gt;&lt;code&gt;singleton&lt;/code&gt;&lt;/strong&gt; (컨테이너당 1개 공유).  &lt;/p&gt;
&lt;p&gt;모든 요청이 같은 인스턴스를 쓰므로 &lt;code&gt;currentUserId&lt;/code&gt; 같은&lt;br&gt;요청별 상태를 필드에 두면 사용자 정보가 섞일 수 있다.&lt;/p&gt;
&lt;h2&gt;Q3. 생성자 주입&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;생성자 주입을 필드 주입보다 선호하는 이유 2가지?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;테스트 시 mock을 생성자로 넣기 쉬움&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final&lt;/code&gt;로 필수 의존성을 명시·불변 유지&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;의존성 누락 시 객체 생성이 실패해 문제를 빨리 발견한다.&lt;/p&gt;
&lt;h2&gt;Q4. EXPLAIN&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;EXPLAIN&lt;/code&gt;에서 슬로우 쿼리일 때 먼저 볼 키워드 2개?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;type&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;ALL&lt;/code&gt;이면 full scan 의심&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;key&lt;/code&gt;&lt;/strong&gt;: 사용한 인덱스 (&lt;code&gt;NULL&lt;/code&gt;이면 인덱스를 못 탐)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;보너스: &lt;strong&gt;&lt;code&gt;rows&lt;/code&gt;&lt;/strong&gt; (예상 행 수)&lt;/p&gt;
&lt;h2&gt;Q5. XSS / CSRF&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;XSS / CSRF 차이 한 줄씩 + 각각 막는 방법 하나씩?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;XSS&lt;/strong&gt;: 페이지에 악의적 스크립트 삽입·실행&lt;br&gt;→ 출력 Escape, Lucy 필터, 쿠키 &lt;code&gt;HttpOnly&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSRF&lt;/strong&gt;: 로그인된 사용자에게 원치 않는 요청을 보냄&lt;br&gt;→ CSRF 토큰, 쿠키 &lt;code&gt;SameSite&lt;/code&gt;, Spring Security CSRF&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q6. useEffect 의존성&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;useEffect&lt;/code&gt; 의존성 &lt;code&gt;[]&lt;/code&gt; / 없음 / &lt;code&gt;[id]&lt;/code&gt; 각각 언제 실행?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[]&lt;/code&gt;: 마운트 시 1번&lt;/li&gt;
&lt;li&gt;없음: &lt;strong&gt;매 렌더마다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[id]&lt;/code&gt;: 마운트 + &lt;code&gt;id&lt;/code&gt; 변경 시&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q7. GET으로 상태 변경 금지&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;GET&lt;/code&gt;으로 서버 상태를 바꾸면 안 되는 이유?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;&lt;code&gt;GET&lt;/code&gt;은 &lt;strong&gt;safe&lt;/strong&gt;해야 한다 → 조회만, 상태 변경 X.  &lt;/p&gt;
&lt;p&gt;브라우저 프리페치·크롤러·캐시가 GET을 자동 호출할 수 있고,&lt;br&gt;URL/로그에 남아 의도치 않은 변경이 생길 수 있다.  &lt;/p&gt;
&lt;p&gt;변경은 &lt;code&gt;POST&lt;/code&gt;/&lt;code&gt;PUT&lt;/code&gt;/&lt;code&gt;PATCH&lt;/code&gt;/&lt;code&gt;DELETE&lt;/code&gt;.&lt;br&gt;&lt;code&gt;POST&lt;/code&gt;가 무조건 더 보안인 것은 아니고, &lt;strong&gt;메서드 의미&lt;/strong&gt;가 다르다.&lt;/p&gt;
&lt;h2&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;반복 문자열 = &lt;code&gt;StringBuilder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;singleton + 요청별 필드 = 위험&lt;/li&gt;
&lt;li&gt;EXPLAIN: &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CSRF = 토큰 / SameSite&lt;/li&gt;
&lt;li&gt;GET = safe (조회만)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;다음 목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;GET 금지 이유를 “POST가 더 보안”이 아니라 safe/프리페치로 말하기&lt;/li&gt;
&lt;li&gt;StringBuilder vs StringBuffer (동기화 유무) 구분&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타/CS 질문</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/805</guid>
      <comments>https://aamoos.tistory.com/805#entry805comment</comments>
      <pubDate>Wed, 12 Aug 2026 09:27:49 +0900</pubDate>
    </item>
    <item>
      <title># 면접 CS 정리 Day 7 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/803</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 7 (풀스택 / Java·Spring·React)&lt;/h1&gt;
&lt;h2&gt;Q1. Optional&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;Optional&lt;/code&gt;을 왜 쓰는지? &lt;code&gt;null&lt;/code&gt; 대신 쓰는 이유를 한 줄로.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;“값이 없을 수 있음”을 &lt;strong&gt;타입으로 명시&lt;/strong&gt;하고,&lt;br&gt;&lt;code&gt;isPresent&lt;/code&gt; / &lt;code&gt;orElse&lt;/code&gt; / &lt;code&gt;ifPresent&lt;/code&gt; 등으로 빈 값 처리를 강제하려고 쓴다.&lt;/p&gt;
&lt;p&gt;참고: 자동으로 NPE를 막아 주진 않는다. &lt;code&gt;get()&lt;/code&gt;을 막 쓰면 예외가 날 수 있다.&lt;br&gt;주로 &lt;strong&gt;반환값&lt;/strong&gt;에 쓰고, 필드/파라미터 남용은 피한다.&lt;/p&gt;
&lt;h2&gt;Q2. 트랜잭션 전파 REQUIRED&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;REQUIRED&lt;/code&gt; 전파(기본)에서 트랜잭션이 있을 때 / 없을 때 각각 어떻게 되나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;트랜잭션 &lt;strong&gt;없음&lt;/strong&gt; → 새로 시작&lt;/li&gt;
&lt;li&gt;트랜잭션 &lt;strong&gt;있음&lt;/strong&gt; → 기존 트랜잭션에 &lt;strong&gt;참여&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;한 줄: &lt;strong&gt;있으면 쓰고, 없으면 만든다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Q3. CORS&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;CORS가 뭔지 한 줄, Spring에서 정석 해결은?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;브라우저가 &lt;strong&gt;다른 Origin&lt;/strong&gt;(스킴 + 호스트 + 포트) 요청을 제한하는 정책.  &lt;/p&gt;
&lt;p&gt;Spring에서 허용 Origin/메서드/헤더를 설정한다.&lt;br&gt;(&lt;code&gt;CorsConfiguration&lt;/code&gt;, Security CORS, 또는 API별 origin 설정)&lt;/p&gt;
&lt;p&gt;같은 Origin이 아니라 &lt;strong&gt;다른 Origin&lt;/strong&gt;일 때 발생한다.&lt;/p&gt;
&lt;h2&gt;Q4. HTTP 상태코드&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;200&lt;/code&gt; / &lt;code&gt;201&lt;/code&gt; / &lt;code&gt;400&lt;/code&gt; / &lt;code&gt;500&lt;/code&gt; 각각 언제 쓰는지 한 줄씩.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;200&lt;/code&gt;: 요청 성공 (조회·수정 등 일반 성공)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;201&lt;/code&gt;: &lt;strong&gt;생성 성공&lt;/strong&gt; (Created)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;400&lt;/code&gt;: 클라이언트 오류 (잘못된 요청)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;500&lt;/code&gt;: 서버 내부 오류&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q5. AOP와 @Transactional&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;Spring AOP가 뭔지 한 줄, &lt;code&gt;@Transactional&lt;/code&gt;이랑 어떻게 연결되는지 한 줄.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;AOP는 로깅·트랜잭션처럼 API마다 반복되는 &lt;strong&gt;공통 관심사&lt;/strong&gt;를 분리해 적용하는 기법이다.&lt;br&gt;&lt;code&gt;@Transactional&lt;/code&gt;은 AOP &lt;strong&gt;프록시&lt;/strong&gt;로 메서드 진입/종료 시 트랜잭션을 시작하고 커밋/롤백한다.&lt;/p&gt;
&lt;h2&gt;Q6. MyBatis #{} vs ${}&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;MyBatis &lt;code&gt;#{}&lt;/code&gt; / &lt;code&gt;${}&lt;/code&gt; 차이 한 줄씩.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#{}&lt;/code&gt;: PreparedStatement 바인딩 → &lt;strong&gt;SQL Injection 방어&lt;/strong&gt;에 유리 (기본)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;${}&lt;/code&gt;: 문자열 그대로 치환 → 위험. 테이블명 등 꼭 필요할 때만&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q7. 중복 생성 방지 + POST 멱등&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;프론트 연타로 주문이 2번 생길 수 있을 때,&lt;br&gt;DB에서 막는 방법 하나 + HTTP 관점에서 POST가 위험한 이유 한 줄.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DB: &lt;code&gt;(user_id, create_dt)&lt;/code&gt; 등 조합에 &lt;strong&gt;UNIQUE 제약&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;HTTP: &lt;code&gt;POST&lt;/code&gt;는 호출마다 새로 생성될 수 있어 &lt;strong&gt;멱등이 아님&lt;/strong&gt;&lt;br&gt;→ 재시도/연타 시 중복 생성이 나기 쉽다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(프론트 클릭 방지만으론 부족, DB가 최종 방어선)&lt;/p&gt;
&lt;h2&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Optional = “없을 수 있음”을 타입으로 명시&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REQUIRED&lt;/code&gt; = 있으면 참여, 없으면 생성&lt;/li&gt;
&lt;li&gt;CORS = &lt;strong&gt;다른&lt;/strong&gt; Origin&lt;/li&gt;
&lt;li&gt;AOP 프록시 = &lt;code&gt;@Transactional&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;POST는 멱등 X → UNIQUE 등으로 중복 방어&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;다음 목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CORS “같은 Origin” 오해 교정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REQUIRED&lt;/code&gt; vs &lt;code&gt;REQUIRES_NEW&lt;/code&gt; 차이 한 번 더&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타/CS 질문</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/803</guid>
      <comments>https://aamoos.tistory.com/803#entry803comment</comments>
      <pubDate>Wed, 12 Aug 2026 09:25:37 +0900</pubDate>
    </item>
    <item>
      <title># 면접 CS 정리 Day 7 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/802</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 7 (풀스택 / Java·Spring·React)&lt;/h1&gt;
&lt;h2&gt;Q1. Optional&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;Optional&lt;/code&gt;을 왜 쓰는지? &lt;code&gt;null&lt;/code&gt; 대신 쓰는 이유를 한 줄로.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;“값이 없을 수 있음”을 &lt;strong&gt;타입으로 명시&lt;/strong&gt;하고,&lt;br&gt;&lt;code&gt;isPresent&lt;/code&gt; / &lt;code&gt;orElse&lt;/code&gt; / &lt;code&gt;ifPresent&lt;/code&gt; 등으로 빈 값 처리를 강제하려고 쓴다.&lt;/p&gt;
&lt;p&gt;참고: 자동으로 NPE를 막아 주진 않는다. &lt;code&gt;get()&lt;/code&gt;을 막 쓰면 예외가 날 수 있다.&lt;br&gt;주로 &lt;strong&gt;반환값&lt;/strong&gt;에 쓰고, 필드/파라미터 남용은 피한다.&lt;/p&gt;
&lt;h2&gt;Q2. 트랜잭션 전파 REQUIRED&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;REQUIRED&lt;/code&gt; 전파(기본)에서 트랜잭션이 있을 때 / 없을 때 각각 어떻게 되나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;트랜잭션 &lt;strong&gt;없음&lt;/strong&gt; → 새로 시작&lt;/li&gt;
&lt;li&gt;트랜잭션 &lt;strong&gt;있음&lt;/strong&gt; → 기존 트랜잭션에 &lt;strong&gt;참여&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;한 줄: &lt;strong&gt;있으면 쓰고, 없으면 만든다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Q3. CORS&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;CORS가 뭔지 한 줄, Spring에서 정석 해결은?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;브라우저가 &lt;strong&gt;다른 Origin&lt;/strong&gt;(스킴 + 호스트 + 포트) 요청을 제한하는 정책.  &lt;/p&gt;
&lt;p&gt;Spring에서 허용 Origin/메서드/헤더를 설정한다.&lt;br&gt;(&lt;code&gt;CorsConfiguration&lt;/code&gt;, Security CORS, 또는 API별 origin 설정)&lt;/p&gt;
&lt;p&gt;같은 Origin이 아니라 &lt;strong&gt;다른 Origin&lt;/strong&gt;일 때 발생한다.&lt;/p&gt;
&lt;h2&gt;Q4. HTTP 상태코드&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;200&lt;/code&gt; / &lt;code&gt;201&lt;/code&gt; / &lt;code&gt;400&lt;/code&gt; / &lt;code&gt;500&lt;/code&gt; 각각 언제 쓰는지 한 줄씩.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;200&lt;/code&gt;: 요청 성공 (조회·수정 등 일반 성공)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;201&lt;/code&gt;: &lt;strong&gt;생성 성공&lt;/strong&gt; (Created)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;400&lt;/code&gt;: 클라이언트 오류 (잘못된 요청)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;500&lt;/code&gt;: 서버 내부 오류&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q5. AOP와 @Transactional&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;Spring AOP가 뭔지 한 줄, &lt;code&gt;@Transactional&lt;/code&gt;이랑 어떻게 연결되는지 한 줄.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;AOP는 로깅·트랜잭션처럼 API마다 반복되는 &lt;strong&gt;공통 관심사&lt;/strong&gt;를 분리해 적용하는 기법이다.&lt;br&gt;&lt;code&gt;@Transactional&lt;/code&gt;은 AOP &lt;strong&gt;프록시&lt;/strong&gt;로 메서드 진입/종료 시 트랜잭션을 시작하고 커밋/롤백한다.&lt;/p&gt;
&lt;h2&gt;Q6. MyBatis #{} vs ${}&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;MyBatis &lt;code&gt;#{}&lt;/code&gt; / &lt;code&gt;${}&lt;/code&gt; 차이 한 줄씩.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#{}&lt;/code&gt;: PreparedStatement 바인딩 → &lt;strong&gt;SQL Injection 방어&lt;/strong&gt;에 유리 (기본)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;${}&lt;/code&gt;: 문자열 그대로 치환 → 위험. 테이블명 등 꼭 필요할 때만&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q7. 중복 생성 방지 + POST 멱등&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;프론트 연타로 주문이 2번 생길 수 있을 때,&lt;br&gt;DB에서 막는 방법 하나 + HTTP 관점에서 POST가 위험한 이유 한 줄.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DB: &lt;code&gt;(user_id, create_dt)&lt;/code&gt; 등 조합에 &lt;strong&gt;UNIQUE 제약&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;HTTP: &lt;code&gt;POST&lt;/code&gt;는 호출마다 새로 생성될 수 있어 &lt;strong&gt;멱등이 아님&lt;/strong&gt;&lt;br&gt;→ 재시도/연타 시 중복 생성이 나기 쉽다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(프론트 클릭 방지만으론 부족, DB가 최종 방어선)&lt;/p&gt;
&lt;h2&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Optional = “없을 수 있음”을 타입으로 명시&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REQUIRED&lt;/code&gt; = 있으면 참여, 없으면 생성&lt;/li&gt;
&lt;li&gt;CORS = &lt;strong&gt;다른&lt;/strong&gt; Origin&lt;/li&gt;
&lt;li&gt;AOP 프록시 = &lt;code&gt;@Transactional&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;POST는 멱등 X → UNIQUE 등으로 중복 방어&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;다음 목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CORS “같은 Origin” 오해 교정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REQUIRED&lt;/code&gt; vs &lt;code&gt;REQUIRES_NEW&lt;/code&gt; 차이 한 번 더&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/802</guid>
      <comments>https://aamoos.tistory.com/802#entry802comment</comments>
      <pubDate>Tue, 11 Aug 2026 09:55:12 +0900</pubDate>
    </item>
    <item>
      <title># 면접 CS 정리 Day 6 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/801</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 6 (풀스택 / Java·Spring·React)&lt;/h1&gt;
&lt;h2&gt;Q1. == vs equals&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;==&lt;/code&gt;이랑 &lt;code&gt;equals&lt;/code&gt; 차이 한 줄씩. (문자열 비교 기준)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;==&lt;/code&gt;: &lt;strong&gt;주소(참조)&lt;/strong&gt; 비교&lt;/li&gt;
&lt;li&gt;&lt;code&gt;equals&lt;/code&gt;: &lt;strong&gt;값(내용)&lt;/strong&gt; 비교&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;문자열은 &lt;code&gt;equals&lt;/code&gt;로 비교한다.&lt;/p&gt;
&lt;h2&gt;Q2. TCP vs UDP&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;TCP랑 UDP 차이 한 줄씩, HTTP는 보통 둘 중 뭐 위인지?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TCP&lt;/strong&gt;: 연결 지향, &lt;strong&gt;신뢰성&lt;/strong&gt; 있는 전송 (순서·재전송)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UDP&lt;/strong&gt;: 연결 없음, 신뢰성보다 &lt;strong&gt;속도&lt;/strong&gt;. 스트리밍 등에 사용&lt;/li&gt;
&lt;li&gt;HTTP는 보통 &lt;strong&gt;TCP&lt;/strong&gt; 위 (HTTP/1.1, HTTP/2)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;보너스: HTTP/3는 QUIC(UDP 위). 모르면 “HTTP는 보통 TCP”만 말해도 OK.&lt;/p&gt;
&lt;h2&gt;Q3. synchronized&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;synchronized&lt;/code&gt;가 뭐 하는 키워드인지 한 줄, 너무 많이 쓰면 생기는 문제 하나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;코드 블록/메서드를 &lt;strong&gt;한 번에 한 스레드만&lt;/strong&gt; 실행하게 잠근다.  &lt;/p&gt;
&lt;p&gt;많이 쓰면 스레드 대기가 늘어 &lt;strong&gt;성능이 떨어지고&lt;/strong&gt;, 잘못 잠그면 &lt;strong&gt;데드락&lt;/strong&gt;이 날 수 있다.&lt;br&gt;(주된 문제는 메모리 낭비가 아님)&lt;/p&gt;
&lt;h2&gt;Q4. Session vs JWT&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;세션(Session) 방식 인증이랑 JWT 차이? 장단 한 가지씩.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;세션&lt;/strong&gt;: 서버가 로그인 상태를 보관  &lt;ul&gt;
&lt;li&gt;장점: 로그아웃·강제 만료 제어가 쉬움  &lt;/li&gt;
&lt;li&gt;단점: 서버/세션저장소 부하, 다중 서버면 세션 공유 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JWT&lt;/strong&gt;: 클라이언트가 토큰을 보유, 서버는 검증 위주  &lt;ul&gt;
&lt;li&gt;장점: 서버 확장에 유리  &lt;/li&gt;
&lt;li&gt;단점: 토큰 폐기가 어렵고 탈취 시 위험&lt;br&gt;→ access는 짧게, refresh로 재발급&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;세션은 부하를 “줄이는” 방식이 아니라, 서버에 상태를 두므로 &lt;strong&gt;부하가 생길 수 있다&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Q5. ArrayList vs HashSet&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;ArrayList&lt;/code&gt; vs &lt;code&gt;HashSet&lt;/code&gt; 중복 / 순서 기준으로.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ArrayList&lt;/code&gt;: &lt;strong&gt;중복 허용&lt;/strong&gt;, &lt;strong&gt;순서 보장&lt;/strong&gt;, 인덱스로 접근&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HashSet&lt;/code&gt;: &lt;strong&gt;중복 불허&lt;/strong&gt;, &lt;strong&gt;순서 보장 안 함&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q6. React key&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;React 리스트에서 &lt;code&gt;key&lt;/code&gt;에 &lt;code&gt;index&lt;/code&gt; 대신 &lt;code&gt;id&lt;/code&gt;를 쓰는 이유?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;삽입·삭제·순서 변경 시 &lt;code&gt;index&lt;/code&gt; key는 React가 항목을 잘못 매칭할 수 있다.&lt;br&gt;그래서 고유한 &lt;code&gt;id&lt;/code&gt;를 쓴다.&lt;/p&gt;
&lt;h2&gt;Q7. GC&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;GC(Garbage Collection)가 뭔지 한 줄.&lt;br&gt;개발자가 객체를 직접 메모리에서 안 지워도 되는 이유는?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;더 이상 참조되지 않는 객체를 GC가 &lt;strong&gt;자동으로 회수&lt;/strong&gt;한다.&lt;br&gt;그래서 C처럼 &lt;code&gt;free&lt;/code&gt;를 직접 호출하지 않아도 된다.&lt;/p&gt;
&lt;h2&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;==&lt;/code&gt; 주소 / &lt;code&gt;equals&lt;/code&gt; 값&lt;/li&gt;
&lt;li&gt;HTTP는 보통 TCP&lt;/li&gt;
&lt;li&gt;&lt;code&gt;synchronized&lt;/code&gt; = 한 스레드만, 과다 사용 시 성능↓·데드락&lt;/li&gt;
&lt;li&gt;세션 = 서버 상태 / JWT = 클라이언트 토큰&lt;/li&gt;
&lt;li&gt;ArrayList 중복O·순서O / HashSet 중복X·순서X&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;다음 목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Session vs JWT를 “부하·확장·폐기” 키워드로 말하기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;synchronized&lt;/code&gt; 부작용을 메모리로 말하지 않기&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타/CS 질문</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/801</guid>
      <comments>https://aamoos.tistory.com/801#entry801comment</comments>
      <pubDate>Mon, 10 Aug 2026 10:41:39 +0900</pubDate>
    </item>
    <item>
      <title># 면접 CS 정리 Day 5 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/800</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 5 (풀스택 / Java·Spring·React)&lt;/h1&gt;
&lt;h2&gt;Q1. == vs equals (문자열)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;자바에서 문자열 비교할 때 &lt;code&gt;==&lt;/code&gt; 쓰지 말고 &lt;code&gt;equals&lt;/code&gt; 쓰는 이유?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;==&lt;/code&gt;: &lt;strong&gt;주소(참조)&lt;/strong&gt; 비교&lt;/li&gt;
&lt;li&gt;&lt;code&gt;equals&lt;/code&gt;: &lt;strong&gt;값(내용)&lt;/strong&gt; 비교&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;값이 같아도 객체가 다르면 &lt;code&gt;==&lt;/code&gt;는 false, &lt;code&gt;equals&lt;/code&gt;는 true.&lt;br&gt;문자열은 항상 &lt;code&gt;equals&lt;/code&gt;로 비교한다.&lt;/p&gt;
&lt;h2&gt;Q2. IoC / DI&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;Spring IoC랑 DI가 뭔지 한 줄씩, 왜 &lt;code&gt;new&lt;/code&gt;로 직접 안 만들고 주입받는지?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IoC&lt;/strong&gt;: 객체 생성·조립 제어를 &lt;strong&gt;컨테이너&lt;/strong&gt;에 넘김&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DI&lt;/strong&gt;: 필요한 의존 객체를 컨테이너가 &lt;strong&gt;주입&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;new&lt;/code&gt; 대신 주입하는 이유:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;결합도 감소&lt;/li&gt;
&lt;li&gt;테스트 시 mock 교체 용이&lt;/li&gt;
&lt;li&gt;생명주기·AOP(트랜잭션 등) 적용이 쉬움&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(메모리 절약만의 이유는 아님)&lt;/p&gt;
&lt;h2&gt;Q3. PUT vs PATCH&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;PUT&lt;/code&gt;이랑 &lt;code&gt;PATCH&lt;/code&gt; 차이?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PUT&lt;/code&gt;: 리소스 &lt;strong&gt;전체 교체&lt;/strong&gt; (빠진 필드가 null로 덮일 수 있음)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PATCH&lt;/code&gt;: &lt;strong&gt;부분 수정&lt;/strong&gt; (넘어온 필드만 변경)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q4. final 키워드&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;자바 &lt;code&gt;final&lt;/code&gt;이 변수 / 메서드 / 클래스에 붙으면 각각 뭐가 막히는지?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;변수: &lt;strong&gt;재할당 불가&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;메서드: &lt;strong&gt;오버라이드 불가&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;클래스: &lt;strong&gt;상속 불가&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;final&lt;/code&gt;이라고 해서 객체 내부 필드까지 무조건 불변은 아니다.&lt;/p&gt;
&lt;h2&gt;Q5. 전역 예외 처리&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;API 예외를 컨트롤러마다 &lt;code&gt;try-catch&lt;/code&gt; 안 하고 공통 JSON으로 내리려면 Spring에서 뭘 쓰는지?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;&lt;code&gt;@RestControllerAdvice&lt;/code&gt; + &lt;code&gt;@ExceptionHandler&lt;/code&gt;로 전역 처리한다.&lt;br&gt;비즈니스 예외(보통 unchecked)를 던지고, 여기서 잡아 상태코드·JSON 포맷을 통일한다.&lt;/p&gt;
&lt;h2&gt;Q6. 인덱스 + 컬럼 함수&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;인덱스가 있는데도 &lt;code&gt;WHERE YEAR(created_at) = 2024&lt;/code&gt;처럼&lt;br&gt;컬럼에 함수를 씌우면 왜 느려질 수 있는지?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;인덱스는 보통 &lt;strong&gt;컬럼 원본 값&lt;/strong&gt; 기준으로 만들어진다.&lt;br&gt;컬럼에 함수를 씌우면 인덱스와 바로 매칭되지 않아 &lt;strong&gt;인덱스를 못 타고&lt;/strong&gt; 풀스캔이 나기 쉽다.&lt;/p&gt;
&lt;p&gt;개선 예:&lt;br&gt;&lt;code&gt;created_at &amp;gt;= &amp;#39;2024-01-01&amp;#39; AND created_at &amp;lt; &amp;#39;2025-01-01&amp;#39;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Q7. state vs props&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;React에서 &lt;code&gt;state&lt;/code&gt;랑 &lt;code&gt;props&lt;/code&gt; 차이 한 줄씩.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;state&lt;/code&gt;: 컴포넌트 &lt;strong&gt;내부 상태&lt;/strong&gt;, 해당 컴포넌트가 변경 가능&lt;/li&gt;
&lt;li&gt;&lt;code&gt;props&lt;/code&gt;: &lt;strong&gt;부모가 자식에게&lt;/strong&gt; 내려주는 값, 자식은 읽기 전용&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;==&lt;/code&gt; = 주소 / &lt;code&gt;equals&lt;/code&gt; = 값 (&lt;strong&gt;반대 주의&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;IoC = 제어를 컨테이너에 / DI = 의존성 주입&lt;/li&gt;
&lt;li&gt;PUT 전체 / PATCH 부분&lt;/li&gt;
&lt;li&gt;final: 변수=재할당X, 메서드=오버라이드X, 클래스=상속X&lt;/li&gt;
&lt;li&gt;컬럼에 함수 씌우면 인덱스 못 탈 수 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;다음 목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;==&lt;/code&gt; / &lt;code&gt;equals&lt;/code&gt; 매일 한 번씩 복습 (반복 오답)&lt;/li&gt;
&lt;li&gt;IoC/DI를 “테스트·결합도” 관점으로 말하기&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타/CS 질문</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/800</guid>
      <comments>https://aamoos.tistory.com/800#entry800comment</comments>
      <pubDate>Wed, 5 Aug 2026 09:38:57 +0900</pubDate>
    </item>
    <item>
      <title># 면접 CS 정리 Day 4 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/799</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 4 (풀스택 / Java·Spring·React)&lt;/h1&gt;
&lt;h2&gt;Q1. HTTP 멱등성&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;HTTP에서 멱등(idempotent)이 뭔지?&lt;br&gt;&lt;code&gt;GET&lt;/code&gt; / &lt;code&gt;PUT&lt;/code&gt; / &lt;code&gt;POST&lt;/code&gt; / &lt;code&gt;DELETE&lt;/code&gt; 중 멱등인 것과 아닌 것은?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;멱등 = 같은 요청을 &lt;strong&gt;여러 번&lt;/strong&gt; 해도 결과가 &lt;strong&gt;한 번 한 것과 같음&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET&lt;/code&gt;: 멱등 O (조회만)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PUT&lt;/code&gt;: 멱등 O (같은 내용으로 덮어쓰기)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE&lt;/code&gt;: 멱등 O (보통, 지웠으면 또 지워도 없는 상태)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST&lt;/code&gt;: 멱등 X (호출할 때마다 새로 생성될 수 있음)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Q2. 쿠키 HttpOnly / Secure&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;쿠키의 &lt;code&gt;HttpOnly&lt;/code&gt;랑 &lt;code&gt;Secure&lt;/code&gt; 옵션이 각각 뭐 하는 건지?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HttpOnly&lt;/code&gt;: &lt;strong&gt;JS에서 쿠키 읽기 불가&lt;/strong&gt; → XSS로 세션 탈취 어렵게&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Secure&lt;/code&gt;: &lt;strong&gt;HTTPS에서만&lt;/strong&gt; 쿠키 전송 → 평문 HTTP 도청 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;인증/세션 쿠키에 자주 같이 붙인다.&lt;br&gt;(보너스: &lt;code&gt;SameSite&lt;/code&gt;는 CSRF 완화)&lt;/p&gt;
&lt;h2&gt;Q3. Filter vs Interceptor&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;Filter&lt;/code&gt; vs &lt;code&gt;Interceptor&lt;/code&gt; 한 줄씩 차이 + 인증은 보통 어디에?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Filter: 서블릿 단 (&lt;code&gt;DispatcherServlet&lt;/code&gt; &lt;strong&gt;앞&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Interceptor: Spring MVC (컨트롤러 &lt;strong&gt;전후&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;인증: Spring Security → &lt;strong&gt;Filter&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;css/js로 외우지 말고 &lt;strong&gt;서블릿 vs MVC&lt;/strong&gt;로 구분한다.&lt;/p&gt;
&lt;h2&gt;Q4. @Transactional + this 호출&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;@Transactional&lt;/code&gt; 메서드를 같은 클래스에서 &lt;code&gt;this.xxx()&lt;/code&gt;로 호출하면&lt;br&gt;왜 트랜잭션이 안 먹을 수 있나?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;트랜잭션은 &lt;strong&gt;프록시&lt;/strong&gt;가 호출을 가로챌 때 적용된다.&lt;br&gt;&lt;code&gt;this.xxx()&lt;/code&gt;는 프록시를 안 지나서 &lt;code&gt;@Transactional&lt;/code&gt;이 적용되지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;해결: 트랜잭션 메서드를 &lt;strong&gt;다른 Bean&lt;/strong&gt;으로 나눠 주입받아 호출.&lt;/p&gt;
&lt;h2&gt;Q5. ArrayList vs LinkedList&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;ArrayList&lt;/code&gt; vs &lt;code&gt;LinkedList&lt;/code&gt; 언제 뭘 쓰고,&lt;br&gt;실무에서 LinkedList를 잘 안 쓰는 이유는?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ArrayList&lt;/code&gt;: 배열 기반, 인덱스 접근 빠름. &lt;strong&gt;실무 기본&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LinkedList&lt;/code&gt;: 노드 연결, 중간 삽입/삭제 자체는 싸 보여도&lt;br&gt;&lt;strong&gt;위치 찾기&lt;/strong&gt;가 O(n)이고 캐시 지역성도 불리해서 잘 안 씀&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;둘 다 순서는 보장한다. 순서가 없어서 LinkedList를 안 쓰는 게 아니다.&lt;/p&gt;
&lt;h2&gt;Q6. SSR vs CSR&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;SSR이랑 CSR 차이 한 줄, 각각 장점 한 줄.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SSR&lt;/strong&gt; (Server-Side Rendering): &lt;strong&gt;서버&lt;/strong&gt;에서 HTML 생성&lt;br&gt;→ 첫 화면·SEO에 유리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSR&lt;/strong&gt; (Client-Side Rendering): &lt;strong&gt;브라우저&lt;/strong&gt;에서 JS로 렌더링&lt;br&gt;→ 화면 전환이 부드러움&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SSR = 무조건 새로고침은 아니다. Next.js는 SSR과 CSR을 섞어 쓸 수 있다.&lt;/p&gt;
&lt;h2&gt;Q7. Connection Pool&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;Connection Pool이 뭔지, 왜 요청마다 DB Connection을 새로 안 만드는지?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;미리 DB 연결을 &lt;strong&gt;여러 개&lt;/strong&gt; 만들어 두고, 요청 때 &lt;strong&gt;빌려 쓰고 반납&lt;/strong&gt;하는 방식.&lt;br&gt;Connection 생성(TCP·인증) 비용이 커서 매번 만들면 느리고 DB 부하가 크다.&lt;/p&gt;
&lt;p&gt;요청이 끝날 때 DB를 끊는 게 아니라 &lt;strong&gt;풀에 반납&lt;/strong&gt;한다.&lt;br&gt;예: HikariCP (Spring Boot 기본)&lt;/p&gt;
&lt;h2&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;멱등: GET·PUT·DELETE O / POST X&lt;/li&gt;
&lt;li&gt;HttpOnly = JS 차단 / Secure = HTTPS만&lt;/li&gt;
&lt;li&gt;인증 = Filter, &lt;code&gt;this&lt;/code&gt; 호출 = 트랜잭션 프록시 우회&lt;/li&gt;
&lt;li&gt;실무 List 기본 = ArrayList&lt;/li&gt;
&lt;li&gt;Connection Pool = 빌려 쓰고 반납&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;다음 목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;멱등성 + POST 재시도(중복) 방지까지 연결해서 말하기&lt;/li&gt;
&lt;li&gt;LinkedList “순서 없음” 오해 교정 복습&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타/CS 질문</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/799</guid>
      <comments>https://aamoos.tistory.com/799#entry799comment</comments>
      <pubDate>Mon, 3 Aug 2026 09:27:35 +0900</pubDate>
    </item>
    <item>
      <title># Docker Compose로 Mattermost 사내 메신저 구축하기</title>
      <link>https://aamoos.tistory.com/798</link>
      <description>&lt;h1&gt;Docker Compose로 Mattermost 사내 메신저 구축하기 (Teams 대체)&lt;/h1&gt;
&lt;p&gt;Teams를 더 이상 쓰지 않게 되면서, 무료 오픈소스 메신저를 자체 서버에 설치해보기로 했다. 여러 후보(Rocket.Chat, Zulip, Matrix) 중 &lt;strong&gt;Mattermost&lt;/strong&gt;를 선택했다. Slack과 가장 비슷한 UX를 제공하고, 오픈소스 Team Edition은 인원 제한 없이 완전 무료이기 때문이다.&lt;/p&gt;
&lt;h2&gt;왜 Mattermost인가&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;2015년부터 개발된 오픈소스 프로젝트로 커뮤니티/문서가 풍부하다.&lt;/li&gt;
&lt;li&gt;채팅, 채널, 파일 업로드, 검색, 멘션, DM, 이모지 반응 등 기본 기능은 전부 무료(Team Edition)이며 인원 제한이 없다.&lt;/li&gt;
&lt;li&gt;LDAP/SSO, 고가용성 클러스터링, 컴플라이언스 리포트 같은 기능만 유료(Professional/Enterprise) 티어에서 제공된다. 일반적인 사내 채팅+파일공유 용도라면 무료판으로 충분하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;docker-compose.yml 작성&lt;/h2&gt;
&lt;p&gt;Mattermost(Team Edition) + PostgreSQL 조합으로 구성했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: &amp;quot;3.8&amp;quot;

services:
  postgres:
    image: postgres:15-alpine
    container_name: mattermost-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: mmuser
      POSTGRES_PASSWORD: &amp;lt;비밀번호&amp;gt;
      POSTGRES_DB: mattermost
    volumes:
      - /mnt/data/mattermost/postgres:/var/lib/postgresql/data
    networks:
      - mattermost-network

  mattermost:
    image: mattermost/mattermost-team-edition:latest
    container_name: mattermost
    restart: unless-stopped
    depends_on:
      - postgres
    ports:
      - &amp;quot;8065:8065&amp;quot;
    environment:
      MM_SQLSETTINGS_DRIVERNAME: postgres
      MM_SQLSETTINGS_DATASOURCE: &amp;quot;postgres://mmuser:&amp;lt;비밀번호&amp;gt;@postgres:5432/mattermost?sslmode=disable&amp;amp;connect_timeout=10&amp;quot;
      MM_SERVICESETTINGS_SITEURL: &amp;quot;http://서버IP:8065&amp;quot;
      MM_FILESETTINGS_DRIVERNAME: &amp;quot;local&amp;quot;
      MM_FILESETTINGS_DIRECTORY: &amp;quot;/mattermost/data&amp;quot;
    volumes:
      - /mnt/data/mattermost/config:/mattermost/config
      - /mnt/data/mattermost/data:/mattermost/data
      - /mnt/data/mattermost/logs:/mattermost/logs
      - /mnt/data/mattermost/plugins:/mattermost/plugins
      - /mnt/data/mattermost/client-plugins:/mattermost/client/plugins
      - /mnt/data/mattermost/bleve-indexes:/mattermost/bleve-indexes
    networks:
      - mattermost-network

networks:
  mattermost-network:
    driver: bridge&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;포인트&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;포트 충돌 방지&lt;/strong&gt;: 서버에 이미 여러 서비스(Jenkins, GitLab, Elasticsearch 등)가 떠 있었기 때문에, &lt;code&gt;docker ps&lt;/code&gt;로 사용 중인 포트를 먼저 확인하고 8065 포트가 비어있는지 체크했다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;볼륨 경로&lt;/strong&gt;: 루트 파티션 용량이 넉넉하지 않아서(&lt;code&gt;df -h&lt;/code&gt; 확인 결과 71% 사용 중), 별도로 마운트된 대용량 디스크(&lt;code&gt;/mnt/data&lt;/code&gt;) 아래에 데이터를 저장하도록 경로를 잡았다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DB 격리&lt;/strong&gt;: postgres 서비스는 호스트에 포트를 노출하지 않고 컨테이너 내부 네트워크에서만 통신하도록 설정했다. 컨테이너 이름과 데이터 경로가 기존에 떠 있던 다른 postgres/mariadb 컨테이너들과 전혀 겹치지 않아 기존 서비스에 영향이 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;트러블슈팅 1: 권한 에러 (permission denied)&lt;/h2&gt;
&lt;p&gt;처음 &lt;code&gt;docker compose up -d&lt;/code&gt; 실행 시 아래 에러가 반복적으로 발생했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error: failed to load configuration: could not create config file: open /mattermost/config/config.json: permission denied&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;원인&lt;/strong&gt;: Mattermost 컨테이너는 root가 아닌 &lt;code&gt;UID 2000&lt;/code&gt; 사용자로 실행되는데, &lt;code&gt;mkdir -p&lt;/code&gt;로 만든 호스트 폴더의 소유자가 root(또는 sudo 실행 계정)라서 컨테이너 내부에서 쓰기 권한이 없었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo chown -R 2000:2000 /mnt/data/mattermost/config
sudo chown -R 2000:2000 /mnt/data/mattermost/data
sudo chown -R 2000:2000 /mnt/data/mattermost/logs
sudo chown -R 2000:2000 /mnt/data/mattermost/plugins
sudo chown -R 2000:2000 /mnt/data/mattermost/client-plugins
sudo chown -R 2000:2000 /mnt/data/mattermost/bleve-indexes&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이후 재기동하니 정상적으로 마이그레이션이 진행되고 서버가 떴다.&lt;/p&gt;
&lt;h2&gt;초기 설정: 관리자 계정 &amp;amp; 팀 초대&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;첫 계정 생성 시 &lt;strong&gt;사용자명(Username)은 영문 소문자 + 숫자만 가능&lt;/strong&gt;하다. 한글 사용자명 입력 시 에러가 난다.&lt;/li&gt;
&lt;li&gt;팀원 초대는 두 가지 방식이 있다.&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;초대 링크 복사&lt;/strong&gt; → 카톡 등으로 직접 전달&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;관리자 도구 → 사이트 구성 → &amp;quot;누구나 이 팀에 참여할 수 있습니다&amp;quot;&lt;/strong&gt; 토글 활성화 → 서버 주소만 알려주면 팀원이 알아서 가입&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;이메일 초대는 SMTP 서버가 설정되어 있지 않으면 전송되지 않는다(&amp;quot;SMTP가 시스템 콘솔에서 구성되지 않았습니다&amp;quot;). 소규모 팀이면 SMTP 설정 없이 링크/오픈서버 방식으로 충분하다.&lt;/li&gt;
&lt;li&gt;데스크톱 앱에서 초대 링크 클립보드 복사가 안 되는 경우가 있었는데, 이때는 System Console에서 &lt;strong&gt;&amp;quot;공개 서버&amp;quot;를 활성화&lt;/strong&gt;하고 서버 주소만 공유하는 방식으로 우회했다. (관리자 도구 → 사이트 구성 → 인증 → 가입 → 공개 서버: 활성화)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;트러블슈팅 2: 개인 메시지가 실시간으로 안 뜸&lt;/h2&gt;
&lt;p&gt;DM을 보내도 바로 화면에 안 뜨고, 다른 채널로 갔다 와야 보이는 증상이 있었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;확인한 것들&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker stats&lt;/code&gt;로 CPU/메모리 확인 → 리소스는 충분히 여유로움 (CPU 0.1%대)&lt;/li&gt;
&lt;li&gt;컨테이너 로그에서 웹소켓 에러 확인 → 특이 에러 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 증상은 전형적으로 &lt;strong&gt;웹소켓 연결 문제&lt;/strong&gt;일 때 나타난다. Mattermost는 실시간 메시지 전달을 위해 웹소켓을 쓰는데, &lt;code&gt;MM_SERVICESETTINGS_SITEURL&lt;/code&gt; 값이 실제 접속 주소와 다르면 웹소켓 연결이 불안정해질 수 있어 이 값을 서버 실제 IP로 맞춰주는 것이 첫 번째 점검 포인트였다.&lt;/p&gt;
&lt;h2&gt;트러블슈팅 3: 모바일 푸시 알림이 안 옴&lt;/h2&gt;
&lt;p&gt;모바일 앱에서 &amp;quot;이 서버의 알림을 수신할 수 없습니다&amp;quot; 메시지가 계속 떴다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1차 조치&lt;/strong&gt;: System Console → 환경 → 푸시 알림 서버에서 다음과 같이 설정&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모바일 푸시 알림: &lt;code&gt;TPNS 연결을 사용하여 iOS 및 Android 앱에 알림을 전송합니다&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;푸시 알림 서버: &lt;code&gt;https://push.mattermost.com&lt;/code&gt; (기본값인 &lt;code&gt;push-test.mattermost.com&lt;/code&gt;이 아니라 test가 빠진 정식 URL로 변경)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 값을 바꾸고 컨테이너를 재시작해도 동일한 에러가 계속 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;진짜 원인&lt;/strong&gt;: 서버 주소가 &lt;code&gt;http://&lt;/code&gt;(HTTPS 아님)였기 때문이다. Mattermost 공식 모바일 앱은 보안 정책상 &lt;strong&gt;서버가 HTTPS로 연결되어 있어야만 푸시 알림 등록을 허용&lt;/strong&gt;한다. 즉 TPNS 설정 자체는 문제가 아니었고, 사이트 자체가 HTTPS가 아닌 것이 근본 원인이었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결 방향&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사내망 + IP 접속이라면 → 자체 서명 SSL 인증서 + Nginx 리버스 프록시 (기기에서 인증서 신뢰 예외 처리 필요)&lt;/li&gt;
&lt;li&gt;사용 가능한 도메인이 있다면 → Let&amp;#39;s Encrypt 무료 인증서로 정식 HTTPS 적용 (권장)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;무료 여부&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;채팅, 채널, DM, 파일 업로드&lt;/td&gt;
&lt;td&gt;무료 (Team Edition)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데스크톱/모바일 알림&lt;/td&gt;
&lt;td&gt;무료, 단 모바일 푸시는 HTTPS 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSO/LDAP, 고가용성, 컴플라이언스&lt;/td&gt;
&lt;td&gt;유료 (Professional/Enterprise)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;소규모 팀의 사내 채팅용으로는 무료 Team Edition만으로 충분했고, docker-compose 하나로 기존에 운영 중이던 다른 서비스들과 완전히 격리된 상태로 손쉽게 추가할 수 있었다. 다만 &lt;strong&gt;모바일 푸시 알림까지 정상적으로 받으려면 HTTPS 적용이 사실상 필수&lt;/strong&gt;라는 점은 미리 알아두면 좋을 것 같다.&lt;/p&gt;
&lt;p&gt;파일 공유/동기화가 추가로 필요하다면 OneDrive 대체제로 &lt;strong&gt;Nextcloud&lt;/strong&gt;를 같은 방식(docker-compose)으로 조합해서 쓸 수 있다.&lt;/p&gt;</description>
      <category>DevOps/Docker</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/798</guid>
      <comments>https://aamoos.tistory.com/798#entry798comment</comments>
      <pubDate>Fri, 31 Jul 2026 10:01:33 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS 실전] 9. FAQ &amp;middot; 시리즈 총정리</title>
      <link>https://aamoos.tistory.com/797</link>
      <description>&lt;h1&gt;[Next.js + TS 실전] 9. FAQ · 시리즈 총정리&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;입문 8편 + 실전 8편으로 작은 메모 앱을 끝까지 밀어 봤습니다.&lt;br&gt;마지막은 &lt;strong&gt;한눈에 지도&lt;/strong&gt;, &lt;strong&gt;자주 막히는 FAQ&lt;/strong&gt;, &lt;strong&gt;다음에 뭐 할지&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;이 글에서 하는 일&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;시리즈 전체 목차 정리&lt;/li&gt;
&lt;li&gt;지금 앱이 할 수 있는 것 체크&lt;/li&gt;
&lt;li&gt;실제로 자주 터지는 에러 Q&amp;amp;A&lt;/li&gt;
&lt;li&gt;학습 로드맵 짧게&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;새 기능 추가는 없습니다.&lt;br&gt;&lt;strong&gt;복습·북마크용&lt;/strong&gt; 글입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시리즈 지도&lt;/h2&gt;
&lt;h3&gt;입문 &lt;code&gt;[Next.js + TS]&lt;/code&gt;&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;편&lt;/th&gt;
&lt;th&gt;주제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;왜 Next.js + TypeScript인가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;프로젝트 만들고 폴더 구조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;컴포넌트 타입 (Memo, props)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;라우팅과 레이아웃&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Server vs Client Component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;데이터 가져오기 (async, loading/error)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;폼과 Server Action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;정리 + 다음 로드맵&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;실전 &lt;code&gt;[Next.js + TS 실전]&lt;/code&gt;&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;편&lt;/th&gt;
&lt;th&gt;주제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;메모 수정과 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Prisma + SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Vercel 배포 (SQLite 한계)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;폼 검증 (zod + useActionState)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;로그인 (Clerk) + userId&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Neon Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;삭제 확인 UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;목록 검색과 정렬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;FAQ · 총정리 ← 지금&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;우리가 만든 것&lt;/h2&gt;
&lt;p&gt;메모 앱 기준으로 대략 이 정도입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/                 소개
/memos            목록 (+ 검색·정렬)
/memos/new        작성 (+ zod 검증)
/memos/[id]       상세 (+ 삭제 확인)
/memos/[id]/edit  수정&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;기술 축:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;App Router + TypeScript&lt;/li&gt;
&lt;li&gt;Server Component 기본, 필요한 곳만 Client&lt;/li&gt;
&lt;li&gt;Prisma (SQLite → Neon 가능)&lt;/li&gt;
&lt;li&gt;Server Action CRUD&lt;/li&gt;
&lt;li&gt;Clerk 로그인 + 내 메모만&lt;/li&gt;
&lt;li&gt;Vercel 배포 이슈 이해&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;스스로 점검 체크리스트&lt;/h2&gt;
&lt;h3&gt;기본&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;app/page.tsx&lt;/code&gt; = &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;app/memos/page.tsx&lt;/code&gt; = &lt;code&gt;/memos&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;code&gt;layout.tsx&lt;/code&gt;와 &lt;code&gt;page.tsx&lt;/code&gt; 차이를 설명할 수 있다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; props / &lt;code&gt;Memo&lt;/code&gt; 타입을 붙일 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;서버·클라이언트&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 기본은 Server Component다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; hooks / &lt;code&gt;onClick&lt;/code&gt;이 있으면 Client로 분리한다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 페이지 전체가 아니라 버튼·폼만 Client로 뺄 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;데이터·폼&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 목록은 서버에서 &lt;code&gt;await&lt;/code&gt;로 가져온다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 쓰기는 Server Action + &lt;code&gt;revalidatePath&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; zod로 검증하고 실패 시 상태 객체를 반환한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;인증·배포&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 미들웨어는 세션 연결, 보호는 &lt;code&gt;auth.protect()&lt;/code&gt; / userId 쿼리&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; SQLite 파일은 Vercel과 안 맞는 이유를 안다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; 비밀 값은 &lt;code&gt;.env&lt;/code&gt; / Vercel env에만 둔다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하나라도 막히면 해당 편만 다시 보면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;FAQ (실제로 자주 막히는 것)&lt;/h2&gt;
&lt;h3&gt;Q1. &lt;code&gt;/memos&lt;/code&gt;가 404예요&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;원인 후보:&lt;/strong&gt; 목록 파일에 상세 코드를 붙여 넣음.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;파일&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;app/memos/page.tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/memos&lt;/code&gt; 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;app/memos/[id]/page.tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/memos/1&lt;/code&gt; 상세&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;상세(수정/삭제) 코드는 &lt;strong&gt;&lt;code&gt;[id]/page.tsx&lt;/code&gt;&lt;/strong&gt; 에 넣어야 합니다.&lt;br&gt;목록에 &lt;code&gt;notFound()&lt;/code&gt;가 있으면 &lt;code&gt;/memos&lt;/code&gt;도 404가 납니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Q2. &lt;code&gt;package.json&lt;/code&gt;이 깨져서 npm이 안 돼요&lt;/h3&gt;
&lt;p&gt;시드 설정을 &lt;strong&gt;별도 객체로 하나 더&lt;/strong&gt; 붙이면 JSON이 깨집니다.&lt;/p&gt;
&lt;p&gt;잘못된 예:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;  &amp;quot;devDependencies&amp;quot;: { ... },
  {
    &amp;quot;prisma&amp;quot;: { &amp;quot;seed&amp;quot;: &amp;quot;...&amp;quot; }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;올바른 예 — 맨 바깥 키:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;  &amp;quot;devDependencies&amp;quot;: { ... },
  &amp;quot;prisma&amp;quot;: {
    &amp;quot;seed&amp;quot;: &amp;quot;npx tsx prisma/seed.ts&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;Q3. &lt;code&gt;userId&lt;/code&gt; 마이그레이션이 실패해요&lt;/h3&gt;
&lt;p&gt;에러 예:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Added the required column &lt;code&gt;userId&lt;/code&gt; without a default value. There are N rows...&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이미 있는 행에 &lt;code&gt;userId&lt;/code&gt;를 뭘로 채울지 몰라서입니다.&lt;/p&gt;
&lt;p&gt;로컬 연습이면:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx prisma migrate reset&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;데이터 삭제 후 스키마를 다시 맞춥니다.&lt;br&gt;그다음 시드에 &lt;code&gt;userId&lt;/code&gt;도 넣어야 합니다. (&lt;code&gt;&amp;quot;seed-user&amp;quot;&lt;/code&gt; 등)&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Q4. 시드 메모가 로그인 목록에 안 보여요&lt;/h3&gt;
&lt;p&gt;정상일 수 있습니다.&lt;br&gt;시드 &lt;code&gt;userId&lt;/code&gt;와 Clerk 로그인 &lt;code&gt;userId&lt;/code&gt;가 다르기 때문입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;로그인 후 새로 작성한 메모&lt;/strong&gt;로 확인하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Q5. &lt;code&gt;createRouteMatcher&lt;/code&gt;가 deprecated래요&lt;/h3&gt;
&lt;p&gt;최신 Clerk는 미들웨어에서 경로로 막지 말고,&lt;br&gt;&lt;strong&gt;페이지/레이아웃에서 &lt;code&gt;auth.protect()&lt;/code&gt;&lt;/strong&gt; 하라고 바뀌었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;proxy.ts&lt;/code&gt; (또는 &lt;code&gt;middleware.ts&lt;/code&gt;) → &lt;code&gt;clerkMiddleware()&lt;/code&gt;만&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app/memos/layout.tsx&lt;/code&gt; → &lt;code&gt;await auth.protect()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;Q6. &lt;code&gt;SignedOut&lt;/code&gt; / &lt;code&gt;SignedIn&lt;/code&gt;을 찾을 수 없어요&lt;/h3&gt;
&lt;p&gt;Clerk Core 3 / 최신 SDK에서는 &lt;strong&gt;&lt;code&gt;Show&lt;/code&gt;&lt;/strong&gt; 로 통합되었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;Show when=&amp;quot;signed-out&amp;quot;&amp;gt;...&amp;lt;/Show&amp;gt;
&amp;lt;Show when=&amp;quot;signed-in&amp;quot;&amp;gt;...&amp;lt;/Show&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ClerkProvider&lt;/code&gt;는 &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;을 감싸지 말고 &lt;strong&gt;&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; 안&lt;/strong&gt;에 두는 형태를 권장합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Q7. &lt;code&gt;parsed.error.flatten()&lt;/code&gt;이 deprecated래요&lt;/h3&gt;
&lt;p&gt;Zod 4:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// 예전
parsed.error.flatten().fieldErrors

// 지금
z.flattenError(parsed.error).fieldErrors&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;Q8. &lt;code&gt;revalidatePath&lt;/code&gt;는 뭐예요?&lt;/h3&gt;
&lt;p&gt;“이 경로 화면을 &lt;strong&gt;다시 준비해라&lt;/strong&gt;”는 신호입니다.&lt;br&gt;메모를 추가/수정/삭제한 뒤 목록·상세가 옛 데이터로 남지 않게 합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;redirect&lt;/code&gt; = 이동, &lt;code&gt;revalidatePath&lt;/code&gt; = 그 경로 캐시/데이터 갱신.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Q9. Prisma를 루트에 설치하고 memo-app에서 import했어요&lt;/h3&gt;
&lt;p&gt;앱은 &lt;code&gt;memo-app&lt;/code&gt; 기준입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;패키지: &lt;code&gt;memo-app&lt;/code&gt;에서 &lt;code&gt;npm install&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;스키마: &lt;code&gt;memo-app/prisma&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;명령: &lt;code&gt;cd memo-app&lt;/code&gt; 후 &lt;code&gt;npx prisma ...&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;루트(&lt;code&gt;toyproject&lt;/code&gt;)와 섞이면 &lt;code&gt;PrismaClient&lt;/code&gt; export 에러가 납니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Q10. Vercel에 올렸더니 데이터가 안 남아요 / DB 에러&lt;/h3&gt;
&lt;p&gt;SQLite &lt;code&gt;file:./dev.db&lt;/code&gt;는 서버리스와 안 맞습니다.&lt;br&gt;Neon 등 외부 Postgres + Vercel &lt;code&gt;DATABASE_URL&lt;/code&gt;이 필요합니다. (실전 3·6편)&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Q11. 티스토리에 붙였더니 코드 블록이 안 먹어요&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;마크다운 모드인지 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;```prisma&lt;/code&gt; 같은 생소한 언어 태그보다 &lt;code&gt;ts&lt;/code&gt; / &lt;code&gt;bash&lt;/code&gt; 또는 태그 없는 fence&lt;/li&gt;
&lt;li&gt;JSX가 많은 블록은 미리보기에서 한 번 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;채팅에서 복사할 때 코드펜스가 중간에 끊기면, &lt;strong&gt;&lt;code&gt;.md&lt;/code&gt; 파일을 통째로&lt;/strong&gt; 복사하는 편이 안전합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;한 줄로 다시 보는 핵심&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;주제&lt;/th&gt;
&lt;th&gt;한 줄&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Next&lt;/td&gt;
&lt;td&gt;React로 앱 만드는 프레임워크 (폴더 = URL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TS&lt;/td&gt;
&lt;td&gt;데이터·props 계약&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server / Client&lt;/td&gt;
&lt;td&gt;기본 서버, 인터랙션만 클라이언트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action&lt;/td&gt;
&lt;td&gt;폼이 부르는 서버 함수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prisma&lt;/td&gt;
&lt;td&gt;DB 쿼리를 타입과 함께&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clerk&lt;/td&gt;
&lt;td&gt;로그인 + &lt;code&gt;userId&lt;/code&gt;로 내 데이터만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neon&lt;/td&gt;
&lt;td&gt;배포용 Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검색&lt;/td&gt;
&lt;td&gt;상태는 URL 쿼리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;다음에 공부하면 좋은 것&lt;/h2&gt;
&lt;p&gt;우선순위 예시:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;테스트&lt;/strong&gt; — zod 스키마 / Action 단위 테스트&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이미지 업로드&lt;/strong&gt; — 파일 저장·표시&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;권한·역할&lt;/strong&gt; — admin / member&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;캐시·성능&lt;/strong&gt; — &lt;code&gt;fetch&lt;/code&gt; 옵션, 목록 페이지네이션&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auth.js&lt;/strong&gt; — Clerk와 같은 구조를 직접 비교&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;새 시리즈를 쓴다면&lt;br&gt;“메모 앱 고도화” 또는 “포트폴리오용 기능 추가”로 끊는 편이 읽기 좋습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;이 시리즈의 목표는 문서를 다 외우는 게 아니었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;작은 앱 하나를 &lt;strong&gt;만들고 → 저장하고 → 고치고 → 로그인하고 → 배포 이슈를 이해&lt;/strong&gt;하는 것.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;막힐 때마다 이 FAQ로 돌아와 보세요.&lt;br&gt;에러 메시지 한 줄이 보통 위 질문 중 하나에 연결됩니다.&lt;/p&gt;
&lt;p&gt;질문·추가하고 싶은 FAQ가 있으면 댓글로 남겨 주세요.&lt;br&gt;같은 메모 앱 기준으로 이어서 정리할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;전체 목차 (다시)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;입문 1~8&lt;/strong&gt; · &lt;strong&gt;실전 1~9&lt;/strong&gt;&lt;br&gt;지금 이 글이 실전 시리즈의 매듭입니다.&lt;/p&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/797</guid>
      <comments>https://aamoos.tistory.com/797#entry797comment</comments>
      <pubDate>Fri, 31 Jul 2026 09:51:53 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS 실전] 8. 목록 검색과 정렬</title>
      <link>https://aamoos.tistory.com/796</link>
      <description>&lt;h1&gt;[Next.js + TS 실전] 8. 목록 검색과 정렬&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;메모가 늘어나면 목록만으로는 찾기 어렵습니다.&lt;br&gt;이번 편은 &lt;strong&gt;제목 검색&lt;/strong&gt;과 &lt;strong&gt;정렬&lt;/strong&gt;을 URL 쿼리로 붙입니다.&lt;br&gt;목표: &lt;code&gt;?q=&lt;/code&gt; · &lt;code&gt;?sort=&lt;/code&gt; · 서버에서 Prisma 필터.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;왜 검색 상태를 URL에 두는지 이해하기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getMemos&lt;/code&gt;에 검색·정렬 옵션 추가하기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memos&lt;/code&gt; 페이지에서 &lt;code&gt;searchParams&lt;/code&gt; 읽기&lt;/li&gt;
&lt;li&gt;검색 폼 + 정렬 링크 만들기&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Client에서 배열을 &lt;code&gt;filter&lt;/code&gt;해도 되지만,&lt;br&gt;이번엔 &lt;strong&gt;서버 컴포넌트 + DB 쿼리&lt;/strong&gt;로 갑니다. 데이터가 커져도 같은 패턴입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 URL에 두나&lt;/h2&gt;
&lt;p&gt;예:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/memos?q=Next&amp;amp;sort=oldest&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;장점:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;새로고침해도 검색어 유지&lt;/li&gt;
&lt;li&gt;링크를 공유할 수 있음&lt;/li&gt;
&lt;li&gt;뒤로 가기와 자연스럽게 맞음&lt;/li&gt;
&lt;li&gt;서버 컴포넌트가 &lt;code&gt;searchParams&lt;/code&gt;로 바로 읽음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;useState&lt;/code&gt;만 쓰면 위 이점이 약해집니다.&lt;br&gt;목록 필터의 기본은 &lt;strong&gt;쿼리스트링&lt;/strong&gt;이 무난합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1) 데이터 함수에 옵션 추가&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;lib/memos.ts&lt;/code&gt;의 &lt;code&gt;getMemos&lt;/code&gt;를 확장합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type GetMemosOptions = {
  q?: string;
  sort?: &amp;quot;newest&amp;quot; | &amp;quot;oldest&amp;quot;;
};

export async function getMemos(
  userId: string,
  options: GetMemosOptions = {}
): Promise&amp;lt;Memo[]&amp;gt; {
  const q = options.q?.trim();
  const sort = options.sort ?? &amp;quot;newest&amp;quot;;

  const rows = await prisma.memo.findMany({
    where: {
      userId,
      ...(q
        ? {
            title: {
              contains: q,
              // Postgres면 대소문자 무시에 mode: &amp;quot;insensitive&amp;quot; 가능
              // SQLite는 contains만으로 시작
            },
          }
        : {}),
    },
    orderBy: {
      createdAt: sort === &amp;quot;oldest&amp;quot; ? &amp;quot;asc&amp;quot; : &amp;quot;desc&amp;quot;,
    },
  });

  return rows.map(toMemo);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;포인트:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;userId&lt;/code&gt;는 그대로 (내 메모만)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q&lt;/code&gt;가 있으면 제목 &lt;code&gt;contains&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sort&lt;/code&gt;로 &lt;code&gt;asc&lt;/code&gt; / &lt;code&gt;desc&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Neon(Postgres)을 쓰면 이렇게 대소문자 무시가 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;title: { contains: q, mode: &amp;quot;insensitive&amp;quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SQLite에서는 &lt;code&gt;mode: &amp;quot;insensitive&amp;quot;&lt;/code&gt;가 지원되지 않을 수 있습니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Clerk를 아직 안 붙였다면 &lt;code&gt;userId&lt;/code&gt; 인자는 프로젝트 상태에 맞게 조정하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2) 목록 페이지에서 searchParams 읽기&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;app/memos/page.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import Link from &amp;quot;next/link&amp;quot;;
import { auth } from &amp;quot;@clerk/nextjs/server&amp;quot;;
import { redirect } from &amp;quot;next/navigation&amp;quot;;
import MemoCard from &amp;quot;@/components/MemoCard&amp;quot;;
import { getMemos } from &amp;quot;@/lib/memos&amp;quot;;

type MemosPageProps = {
  searchParams: Promise&amp;lt;{ q?: string; sort?: string }&amp;gt;;
};

export default async function MemosPage({ searchParams }: MemosPageProps) {
  const { userId } = await auth();
  if (!userId) redirect(&amp;quot;/&amp;quot;);

  const params = await searchParams;
  const q = params.q ?? &amp;quot;&amp;quot;;
  const sort = params.sort === &amp;quot;oldest&amp;quot; ? &amp;quot;oldest&amp;quot; : &amp;quot;newest&amp;quot;;

  const memos = await getMemos(userId, { q, sort });

  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;메모 목록&amp;lt;/h1&amp;gt;

      &amp;lt;p&amp;gt;
        &amp;lt;Link href=&amp;quot;/memos/new&amp;quot;&amp;gt;새 메모 작성&amp;lt;/Link&amp;gt;
      &amp;lt;/p&amp;gt;

      &amp;lt;form&amp;gt;
        &amp;lt;label&amp;gt;
          제목 검색
          &amp;lt;br /&amp;gt;
          &amp;lt;input type=&amp;quot;search&amp;quot; name=&amp;quot;q&amp;quot; defaultValue={q} placeholder=&amp;quot;검색어&amp;quot; /&amp;gt;
        &amp;lt;/label&amp;gt;
        {&amp;quot; &amp;quot;}
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;검색&amp;lt;/button&amp;gt;
        {q &amp;amp;&amp;amp; (
          &amp;lt;&amp;gt;
            {&amp;quot; &amp;quot;}
            &amp;lt;Link href={`/memos?sort=${sort}`}&amp;gt;초기화&amp;lt;/Link&amp;gt;
          &amp;lt;/&amp;gt;
        )}
      &amp;lt;/form&amp;gt;

      &amp;lt;p&amp;gt;
        정렬:{&amp;quot; &amp;quot;}
        &amp;lt;Link href={`/memos?q=${encodeURIComponent(q)}&amp;amp;sort=newest`}&amp;gt;최신순&amp;lt;/Link&amp;gt;
        {&amp;quot; | &amp;quot;}
        &amp;lt;Link href={`/memos?q=${encodeURIComponent(q)}&amp;amp;sort=oldest`}&amp;gt;오래된순&amp;lt;/Link&amp;gt;
      &amp;lt;/p&amp;gt;

      &amp;lt;section&amp;gt;
        {memos.length === 0 ? (
          &amp;lt;p&amp;gt;조건에 맞는 메모가 없습니다.&amp;lt;/p&amp;gt;
        ) : (
          memos.map((memo) =&amp;gt; &amp;lt;MemoCard key={memo.id} memo={memo} /&amp;gt;)
        )}
      &amp;lt;/section&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;포인트:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;searchParams&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URL &lt;code&gt;?q=&amp;amp;sort=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;에 action 없음&lt;/td&gt;
&lt;td&gt;같은 &lt;code&gt;/memos&lt;/code&gt;로 GET 제출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name=&amp;quot;q&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;쿼리 키 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;defaultValue={q}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;검색 후에도 입력값 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;encodeURIComponent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;한글 검색어 링크 안전&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;auth&lt;/code&gt; / &lt;code&gt;redirect&lt;/code&gt;는 Clerk 기준입니다.&lt;br&gt;미사용이면 기존 &lt;code&gt;getMemos()&lt;/code&gt; 호출 방식에 옵션만 추가하면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;동작 흐름&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/memos?q=Next&amp;amp;sort=oldest
        ↓
page가 searchParams 읽음
        ↓
getMemos(userId, { q: &amp;quot;Next&amp;quot;, sort: &amp;quot;oldest&amp;quot; })
        ↓
Prisma where + orderBy
        ↓
필터된 목록 렌더&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;useEffect&lt;/code&gt;로 클라이언트 fetch를 돌릴 필요가 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;검색 폼이 GET인 이유&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;form&amp;gt;
  &amp;lt;input name=&amp;quot;q&amp;quot; /&amp;gt;
  &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;검색&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;기본 method는 &lt;code&gt;GET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;제출하면 &lt;code&gt;/memos?q=입력값&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Server Action이 아님 (데이터 변경이 아니라 &lt;strong&gt;조회&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;쓰기(추가/수정/삭제)는 Action,&lt;br&gt;읽기 필터는 쿼리 — 역할이 갈립니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;(선택) 디바운스 실시간 검색&lt;/h2&gt;
&lt;p&gt;입력할 때마다 바로 필터하려면 Client 컴포넌트가 필요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useState&lt;/code&gt; + &lt;code&gt;useRouter().push(?q=...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;또는 클라이언트에서 이미 받은 목록을 &lt;code&gt;filter&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;메모가 적을 때는 편하지만,&lt;br&gt;이번 편 목표인 “서버+URL”과 결이 다릅니다.&lt;br&gt;필요해지면 그때 추가하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 하는 실수&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;searchParams&lt;/code&gt;를 await 안 함&lt;/strong&gt;&lt;br&gt;→ 최근 Next에서는 Promise인 경우가 많음&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 정렬 링크에서 &lt;code&gt;q&lt;/code&gt;를 빠뜨림&lt;/strong&gt;&lt;br&gt;→ 정렬 바꾸면 검색어가 사라짐. 링크에 &lt;code&gt;q&lt;/code&gt; 유지&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;contains&lt;/code&gt;에 빈 문자열&lt;/strong&gt;&lt;br&gt;→ &lt;code&gt;trim()&lt;/code&gt; 후 없을 때는 where에 넣지 않기&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 목록을 Client로 통째로 올리고 fetch&lt;/strong&gt;&lt;br&gt;→ 이 편 패턴이면 서버 &lt;code&gt;getMemos&lt;/code&gt;로 충분&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Postgres &lt;code&gt;mode: &amp;quot;insensitive&amp;quot;&lt;/code&gt;를 SQLite에 그대로&lt;/strong&gt;&lt;br&gt;→ 에러 날 수 있음. DB에 맞게&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;목록 필터 상태는 URL 쿼리가 기본이다&lt;/li&gt;
&lt;li&gt;검색은 GET 폼, 쓰기는 Server Action&lt;/li&gt;
&lt;li&gt;Prisma &lt;code&gt;where&lt;/code&gt; / &lt;code&gt;orderBy&lt;/code&gt;로 서버에서 거른다&lt;/li&gt;
&lt;li&gt;검색어와 정렬을 링크에 같이 실어 상태를 유지한다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;다음으로 이어가면 좋은 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;입문 + 실전 FAQ / 시리즈 총정리&lt;/strong&gt; — 지금까지 막혔던 포인트 모음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;검색·정렬까지 오면 메모 앱의 “일상 UX”는 한 바퀴 돈 셈입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;시리즈 목차 (실전)&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;메모 수정과 삭제&lt;/li&gt;
&lt;li&gt;Prisma + SQLite로 DB 붙이기&lt;/li&gt;
&lt;li&gt;Vercel에 배포하기&lt;/li&gt;
&lt;li&gt;폼 검증하기 (zod)&lt;/li&gt;
&lt;li&gt;로그인 붙이기 (Clerk)&lt;/li&gt;
&lt;li&gt;Neon Postgres 연결하기&lt;/li&gt;
&lt;li&gt;삭제 확인 UI&lt;/li&gt;
&lt;li&gt;목록 검색과 정렬 ← 지금&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/796</guid>
      <comments>https://aamoos.tistory.com/796#entry796comment</comments>
      <pubDate>Fri, 31 Jul 2026 09:19:10 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS 실전] 7. 삭제 확인 UI</title>
      <link>https://aamoos.tistory.com/795</link>
      <description>&lt;h1&gt;[Next.js + TS 실전] 7. 삭제 확인 UI&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;지금은 상세 페이지에서 &lt;strong&gt;삭제&lt;/strong&gt;를 누르면 바로 지워집니다.&lt;br&gt;이번 편은 실수로 누르는 걸 막는 &lt;strong&gt;확인 단계&lt;/strong&gt;를 붙입니다.&lt;br&gt;목표: &lt;code&gt;window.confirm&lt;/code&gt; → 작은 Client 버튼 컴포넌트.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;왜 확인이 필요한지 이해하기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DeleteMemoButton&lt;/code&gt; 클라이언트 컴포넌트 만들기&lt;/li&gt;
&lt;li&gt;상세 페이지 폼에 연결하기&lt;/li&gt;
&lt;li&gt;(선택) 간단한 모달로 바꾸기&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Server Action·DB는 그대로입니다.&lt;br&gt;&lt;strong&gt;누르는 순간만&lt;/strong&gt; Client에서 한 번 막습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;지금 코드의 문제&lt;/h2&gt;
&lt;p&gt;상세 페이지 대략:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;form action={deleteMemoAction}&amp;gt;
  &amp;lt;input type=&amp;quot;hidden&amp;quot; name=&amp;quot;id&amp;quot; value={memo.id} /&amp;gt;
  &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;한 번 클릭 = 즉시 삭제 + &lt;code&gt;/memos&lt;/code&gt;로 이동.&lt;/p&gt;
&lt;p&gt;실무에서는 보통:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;삭제 클릭&lt;/li&gt;
&lt;li&gt;“정말 삭제할까요?”&lt;/li&gt;
&lt;li&gt;확인 → 그때 서버로 전송&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;접근 두 가지&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;window.confirm&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;코드 최소, 바로 동작&lt;/td&gt;
&lt;td&gt;브라우저 기본 UI라 못생김&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;모달&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;디자인·문구 자유&lt;/td&gt;
&lt;td&gt;컴포넌트가 조금 김&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;이번 편 추천은 &lt;strong&gt;confirm 먼저&lt;/strong&gt;입니다.&lt;br&gt;패턴만 익히고, 나중에 모달로 갈아끼우면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1) DeleteMemoButton 만들기&lt;/h2&gt;
&lt;p&gt;경로: &lt;code&gt;components/DeleteMemoButton.tsx&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;quot;use client&amp;quot;;

type DeleteMemoButtonProps = {
  memoTitle: string;
};

export default function DeleteMemoButton({ memoTitle }: DeleteMemoButtonProps) {
  function handleClick(e: React.MouseEvent&amp;lt;HTMLButtonElement&amp;gt;) {
    const ok = window.confirm(
      `&amp;quot;${memoTitle}&amp;quot; 메모를 삭제할까요?\n삭제하면 되돌릴 수 없습니다.`
    );

    if (!ok) {
      e.preventDefault();
    }
  }

  return (
    &amp;lt;button type=&amp;quot;submit&amp;quot; onClick={handleClick}&amp;gt;
      삭제
    &amp;lt;/button&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;포인트:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;#39;use client&amp;#39;&lt;/code&gt; → &lt;code&gt;onClick&lt;/code&gt; / &lt;code&gt;window&lt;/code&gt; 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type=&amp;quot;submit&amp;quot;&lt;/code&gt; → 확인했을 때만 폼이 제출됨&lt;/li&gt;
&lt;li&gt;&lt;code&gt;preventDefault()&lt;/code&gt; → 취소하면 서버 Action이 안 불림&lt;/li&gt;
&lt;li&gt;페이지 전체가 아니라 &lt;strong&gt;버튼만&lt;/strong&gt; Client (5편 패턴)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2) 상세 페이지에 연결&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;app/memos/[id]/page.tsx&lt;/code&gt;의 삭제 부분만 바꿉니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import Link from &amp;quot;next/link&amp;quot;;
import { notFound } from &amp;quot;next/navigation&amp;quot;;
import { deleteMemoAction } from &amp;quot;@/app/memos/actions&amp;quot;;
import { getMemoById } from &amp;quot;@/lib/memos&amp;quot;;
import DeleteMemoButton from &amp;quot;@/components/DeleteMemoButton&amp;quot;;

type MemoDetailPageProps = {
  params: Promise&amp;lt;{ id: string }&amp;gt;;
};

export default async function MemoDetailPage({ params }: MemoDetailPageProps) {
  const { id } = await params;
  const memo = await getMemoById(id);

  if (!memo) {
    notFound();
  }

  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;{memo.title}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{memo.content}&amp;lt;/p&amp;gt;
      &amp;lt;time dateTime={memo.createdAt}&amp;gt;{memo.createdAt}&amp;lt;/time&amp;gt;

      &amp;lt;p&amp;gt;
        &amp;lt;Link href={`/memos/${memo.id}/edit`}&amp;gt;수정&amp;lt;/Link&amp;gt;
      &amp;lt;/p&amp;gt;

      &amp;lt;form action={deleteMemoAction}&amp;gt;
        &amp;lt;input type=&amp;quot;hidden&amp;quot; name=&amp;quot;id&amp;quot; value={memo.id} /&amp;gt;
        &amp;lt;DeleteMemoButton memoTitle={memo.title} /&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Clerk를 붙였다면 &lt;code&gt;getMemoById(id, userId)&lt;/code&gt; 형태일 수 있습니다.&lt;br&gt;이번 편에서 바꾸는 건 &lt;strong&gt;삭제 버튼&lt;/strong&gt;뿐입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;동작 확인&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;/memos/어떤id&lt;/code&gt; 상세로 이동&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;삭제&lt;/strong&gt; 클릭&lt;/li&gt;
&lt;li&gt;확인 창에서 &lt;strong&gt;취소&lt;/strong&gt; → 그대로 상세에 머무름&lt;/li&gt;
&lt;li&gt;다시 &lt;strong&gt;삭제&lt;/strong&gt; → &lt;strong&gt;확인&lt;/strong&gt; → 목록으로 이동하고 메모 사라짐&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;(선택) 간단한 모달&lt;/h2&gt;
&lt;p&gt;confirm이 밋밋하면 같은 자리에 모달을 넣을 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;quot;use client&amp;quot;;

import { useState } from &amp;quot;react&amp;quot;;

type DeleteMemoButtonProps = {
  memoTitle: string;
};

export default function DeleteMemoButton({ memoTitle }: DeleteMemoButtonProps) {
  const [open, setOpen] = useState(false);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button type=&amp;quot;button&amp;quot; onClick={() =&amp;gt; setOpen(true)}&amp;gt;
        삭제
      &amp;lt;/button&amp;gt;

      {open &amp;amp;&amp;amp; (
        &amp;lt;div
          role=&amp;quot;dialog&amp;quot;
          aria-modal=&amp;quot;true&amp;quot;
          style={{
            position: &amp;quot;fixed&amp;quot;,
            inset: 0,
            background: &amp;quot;rgba(0,0,0,0.4)&amp;quot;,
            display: &amp;quot;grid&amp;quot;,
            placeItems: &amp;quot;center&amp;quot;,
          }}
        &amp;gt;
          &amp;lt;div style={{ background: &amp;quot;white&amp;quot;, padding: 16, minWidth: 280 }}&amp;gt;
            &amp;lt;p&amp;gt;
              &amp;lt;strong&amp;gt;{memoTitle}&amp;lt;/strong&amp;gt; 을(를) 삭제할까요?
            &amp;lt;/p&amp;gt;
            &amp;lt;p&amp;gt;삭제하면 되돌릴 수 없습니다.&amp;lt;/p&amp;gt;

            &amp;lt;button type=&amp;quot;button&amp;quot; onClick={() =&amp;gt; setOpen(false)}&amp;gt;
              취소
            &amp;lt;/button&amp;gt;
            {&amp;quot; &amp;quot;}
            &amp;lt;button type=&amp;quot;submit&amp;quot; onClick={() =&amp;gt; setOpen(false)}&amp;gt;
              삭제하기
            &amp;lt;/button&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      )}
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;주의:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;바깥 &lt;strong&gt;삭제&lt;/strong&gt;는 &lt;code&gt;type=&amp;quot;button&amp;quot;&lt;/code&gt; (폼 즉시 제출 방지)&lt;/li&gt;
&lt;li&gt;모달 안 &lt;strong&gt;삭제하기&lt;/strong&gt;만 &lt;code&gt;type=&amp;quot;submit&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;이 버튼들은 반드시 &lt;code&gt;&amp;lt;form action={deleteMemoAction}&amp;gt;&lt;/code&gt; 안에 있어야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;접근성·포커스 트랩까지 가면 라이브러리(모달)를 쓰는 편이 낫습니다.&lt;br&gt;학습용으로는 위 수준이면 충분합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 Action은 그대로인가&lt;/h2&gt;
&lt;p&gt;확인 UI는 &lt;strong&gt;브라우저 UX&lt;/strong&gt;입니다.&lt;br&gt;진짜 삭제는 여전히 서버의 &lt;code&gt;deleteMemoAction&lt;/code&gt;이 합니다.&lt;/p&gt;
&lt;p&gt;그래서:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;확인을 우회하는 요청이 올 수도 있음 → Action에서 id/권한 검증은 유지&lt;/li&gt;
&lt;li&gt;UI만 Client, 삭제로직은 Server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;보안을 confirm에 맡기지 마세요.&lt;br&gt;confirm은 &lt;strong&gt;실수 방지&lt;/strong&gt;용입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 하는 실수&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. 페이지 전체를 &lt;code&gt;&amp;#39;use client&amp;#39;&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;→ 버튼만 Client로 분리&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. confirm 취소를 안 막고 submit&lt;/strong&gt;&lt;br&gt;→ &lt;code&gt;preventDefault()&lt;/code&gt; 필요&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 모달의 삭제 버튼을 form 밖에 둠&lt;/strong&gt;&lt;br&gt;→ &lt;code&gt;action&lt;/code&gt;이 연결되지 않음&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;type=&amp;quot;submit&amp;quot;&lt;/code&gt;인 첫 버튼이 모달을 안 거침&lt;/strong&gt;&lt;br&gt;→ 열기 버튼은 &lt;code&gt;type=&amp;quot;button&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;삭제는 확인 한 단계가 있는 편이 안전하다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;window.confirm&lt;/code&gt;으로 최소 구현 가능하다&lt;/li&gt;
&lt;li&gt;인터랙션만 Client 컴포넌트로 뺀다&lt;/li&gt;
&lt;li&gt;확인 UI ≠ 권한/검증 (Server Action은 그대로)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;다음으로 이어가면 좋은 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;목록 검색·정렬&lt;/li&gt;
&lt;li&gt;입문+실전 &lt;strong&gt;FAQ / 시리즈 총정리&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;시리즈 목차 (실전)&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;메모 수정과 삭제&lt;/li&gt;
&lt;li&gt;Prisma + SQLite로 DB 붙이기&lt;/li&gt;
&lt;li&gt;Vercel에 배포하기&lt;/li&gt;
&lt;li&gt;폼 검증하기 (zod)&lt;/li&gt;
&lt;li&gt;로그인 붙이기 (Clerk)&lt;/li&gt;
&lt;li&gt;Neon Postgres 연결하기&lt;/li&gt;
&lt;li&gt;삭제 확인 UI ← 지금&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/795</guid>
      <comments>https://aamoos.tistory.com/795#entry795comment</comments>
      <pubDate>Fri, 31 Jul 2026 09:10:05 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS 실전] 6. Neon Postgres 연결하기</title>
      <link>https://aamoos.tistory.com/794</link>
      <description>&lt;h1&gt;[Next.js + TS 실전] 6. Neon Postgres 연결하기&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;실전 3편에서 “Vercel에는 SQLite 파일 DB가 안 맞다”고 했습니다.&lt;br&gt;이번 편은 &lt;strong&gt;Neon(Postgres)&lt;/strong&gt; 으로 바꿔, 배포 환경에서도 메모가 남게 합니다.&lt;br&gt;목표: provider 변경 · 마이그레이션 · 로컬/Vercel &lt;code&gt;DATABASE_URL&lt;/code&gt;.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;왜 Neon인지 짧게 이해하기&lt;/li&gt;
&lt;li&gt;Neon 프로젝트 만들고 연결 문자열 받기&lt;/li&gt;
&lt;li&gt;Prisma &lt;code&gt;provider&lt;/code&gt;를 &lt;code&gt;postgresql&lt;/code&gt;로 바꾸기&lt;/li&gt;
&lt;li&gt;마이그레이션을 Postgres에 적용하기&lt;/li&gt;
&lt;li&gt;Vercel 환경 변수에 &lt;code&gt;DATABASE_URL&lt;/code&gt; 넣기&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;앱 코드(&lt;code&gt;lib/memos.ts&lt;/code&gt;, Action, 페이지)는 &lt;strong&gt;거의 안 바꿉니다.&lt;/strong&gt;&lt;br&gt;바꾸는 건 DB 종류와 연결 정보입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 Neon인가&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;선택&lt;/th&gt;
&lt;th&gt;한 줄&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Neon&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;서버리스 Postgres, 무료 티어, Prisma와 궁합 좋음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;Postgres + 부가 기능이 많음 (이번 편 범위 밖)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로컬 SQLite&lt;/td&gt;
&lt;td&gt;로컬 학습용으로 계속 써도 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Neon을 고른 이유:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vercel 같은 서버리스와 잘 맞음&lt;/li&gt;
&lt;li&gt;Connection string만 바꾸면 Prisma가 거의 그대로 동작&lt;/li&gt;
&lt;li&gt;무료로 시작하기 쉬움&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;UI 메뉴 이름은 자주 바뀝니다.&lt;br&gt;&lt;strong&gt;프로젝트 생성 → Connection string 복사&lt;/strong&gt; 만 기억하면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;전체 그림&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;로컬 / Vercel 앱
      ↓
 DATABASE_URL (환경 변수)
      ↓
 Prisma Client
      ↓
 Neon Postgres (클라우드)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SQLite 때:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DATABASE_URL=&amp;quot;file:./dev.db&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Neon 때:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DATABASE_URL=&amp;quot;postgresql://...&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;1) Neon에서 DB 만들기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://neon.tech&quot;&gt;https://neon.tech&lt;/a&gt; 가입&lt;/li&gt;
&lt;li&gt;새 프로젝트 생성&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection string&lt;/strong&gt; 복사&lt;br&gt;(보통 &lt;code&gt;postgresql://...&lt;/code&gt; 형태)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;예시 형태 (값은 본인 것):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;postgresql://USER:PASSWORD@HOST/neondb?sslmode=require&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;비밀번호가 들어 있습니다. GitHub에 올리지 마세요.&lt;br&gt;&lt;code&gt;.env&lt;/code&gt; / Vercel Environment Variables에만 둡니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;2) 로컬 &lt;code&gt;.env&lt;/code&gt; 바꾸기&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;memo-app/.env&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DATABASE_URL=&amp;quot;postgresql://USER:PASSWORD@HOST/neondb?sslmode=require&amp;quot;
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이전 SQLite용 &lt;code&gt;file:./dev.db&lt;/code&gt;는 주석 처리하거나 지웁니다.&lt;/p&gt;
&lt;p&gt;로컬에서도 Neon을 쓰면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PC를 꺼도 데이터가 클라우드에 남음&lt;/li&gt;
&lt;li&gt;팀원과 같은 DB를 볼 수 있음 (주의: 연습용 키 관리)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;원하면 로컬은 SQLite, 배포만 Neon으로 나눌 수도 있습니다.&lt;br&gt;이번 편은 &lt;strong&gt;로컬도 Neon으로 통일&lt;/strong&gt;해서 흐름을 단순하게 갑니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3) Prisma 스키마: provider 변경&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;prisma/schema.prisma&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;generator client {
  provider = &amp;quot;prisma-client-js&amp;quot;
}

datasource db {
  provider = &amp;quot;postgresql&amp;quot;
  url      = env(&amp;quot;DATABASE_URL&amp;quot;)
}

model Memo {
  id        String   @id @default(cuid())
  title     String
  content   String
  userId    String
  createdAt DateTime @default(now())

  @@index([userId])
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;바뀐 핵심은 한 줄입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;provider = &amp;quot;sqlite&amp;quot;  →  provider = &amp;quot;postgresql&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;Memo&lt;/code&gt; 모델은 그대로입니다.&lt;br&gt;(&lt;code&gt;@@index([userId])&lt;/code&gt;는 있으면 조회에 도움이 됩니다.)&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4) 마이그레이션을 Neon에 적용&lt;/h2&gt;
&lt;p&gt;SQLite용 마이그레이션 히스토리가 있으면 Postgres와 충돌할 수 있습니다.&lt;br&gt;연습 단계에서는 아래 둘 중 하나를 고르세요.&lt;/p&gt;
&lt;h3&gt;방법 A) 새 DB에 스키마만 밀어넣기 (가장 단순)&lt;/h3&gt;
&lt;p&gt;테이블이 비어 있는 Neon이라면:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx prisma db push
npx prisma generate&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;db push&lt;/code&gt;는 마이그레이션 파일 없이 스키마를 DB에 맞춥니다.&lt;br&gt;프로토타입·학습에 편합니다.&lt;/p&gt;
&lt;h3&gt;방법 B) migrate로 정식 적용&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx prisma migrate dev --name postgres-init&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기존 SQLite 마이그레이션 폴더 때문에 꼬이면:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;prisma/migrations&lt;/code&gt;를 백업/삭제 (로컬 연습 한정)&lt;/li&gt;
&lt;li&gt;다시 &lt;code&gt;migrate dev --name init&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;실서비스에서는 히스토리를 함부로 지우면 안 됩니다.&lt;br&gt;&lt;strong&gt;혼자 쓰는 메모 앱 연습&lt;/strong&gt;이라면 A 또는 “migrations 초기화 후 다시”가 현실적입니다.&lt;/p&gt;
&lt;p&gt;시드:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx prisma db seed&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;시드의 &lt;code&gt;userId: &amp;quot;seed-user&amp;quot;&lt;/code&gt;는 Clerk 로그인 유저와 다릅니다.&lt;br&gt;목록 확인은 &lt;strong&gt;로그인 후 새 메모 작성&lt;/strong&gt;으로 하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5) 동작 확인 (로컬)&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run dev&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;로그인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memos/new&lt;/code&gt;에서 메모 작성&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://console.neon.tech&quot;&gt;Neon Console&lt;/a&gt; → Tables에서 행이 보이는지 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;보이면 &lt;strong&gt;클라우드 DB 연결 성공&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6) Vercel에 DATABASE_URL 넣기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Vercel Project → &lt;strong&gt;Settings → Environment Variables&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;추가:&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATABASE_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Neon 연결 문자열&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clerk 공개 키&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CLERK_SECRET_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clerk 비밀 키&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Production / Preview 체크&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redeploy&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;빌드 스크립트 권장 (&lt;code&gt;package.json&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;scripts&amp;quot;: {
    &amp;quot;dev&amp;quot;: &amp;quot;next dev&amp;quot;,
    &amp;quot;build&amp;quot;: &amp;quot;prisma generate &amp;amp;&amp;amp; next build&amp;quot;,
    &amp;quot;start&amp;quot;: &amp;quot;next start&amp;quot;,
    &amp;quot;postinstall&amp;quot;: &amp;quot;prisma generate&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vercel 빌드 머신에서 Prisma Client가 생성되게 합니다.&lt;/p&gt;
&lt;p&gt;프로덕션 마이그레이션은 보통:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx prisma migrate deploy&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;을 CI나 배포 전에 한 번 실행합니다.&lt;br&gt;&lt;code&gt;db push&lt;/code&gt;로만 맞춰 둔 상태면, 배포 전에도 스키마가 Neon에 이미 있어야 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;코드는 왜 거의 안 바꾸나&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;lib/memos.ts&lt;/code&gt;는 여전히:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await prisma.memo.findMany({ where: { userId }, ... })&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prisma가 SQL 방언(SQLite vs Postgres)을 감춥니다.&lt;br&gt;그래서 실전 2편에서 &lt;strong&gt;데이터 레이어를 분리&lt;/strong&gt;해 둔 것이 여기서 이득이 됩니다.&lt;/p&gt;
&lt;p&gt;가끔 달라지는 것:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;일부 raw SQL&lt;/li&gt;
&lt;li&gt;타입/기본값 디테일&lt;/li&gt;
&lt;li&gt;연결 풀 (서버리스에서는 Neon의 pooled URL을 쓰는 경우도 있음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;입문 다음 단계에서는 Connection string 하나로 충분합니다.&lt;br&gt;연결이 불안정하면 Neon 대시보드의 &lt;strong&gt;pooled connection&lt;/strong&gt; 문자열을 써 보세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 하는 실수&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;.env&lt;/code&gt;의 따옴표/복붙 깨짐&lt;/strong&gt;&lt;br&gt;→ 비밀번호에 특수문자가 있으면 URL 인코딩 필요할 수 있음&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Vercel에 &lt;code&gt;DATABASE_URL&lt;/code&gt; 안 넣음&lt;/strong&gt;&lt;br&gt;→ 로컬만 되고 배포만 실패&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. SQLite &lt;code&gt;dev.db&lt;/code&gt;를 그대로 커밋/배포 기대&lt;/strong&gt;&lt;br&gt;→ Neon으로 바꾼 뒤에는 의미 없음&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;provider&lt;/code&gt;만 바꾸고 migrate/&lt;code&gt;db push&lt;/code&gt;를 안 함&lt;/strong&gt;&lt;br&gt;→ 테이블이 없어 런타임 에러&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Clerk 키는 넣었는데 DB URL만 빠짐 (또는 반대)&lt;/strong&gt;&lt;br&gt;→ 로그인 되는데 저장 실패 / 저장은 되는데 로그인 실패&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. 시드 메모가 로그인 목록에 안 보임&lt;/strong&gt;&lt;br&gt;→ &lt;code&gt;userId: &amp;quot;seed-user&amp;quot;&lt;/code&gt;라서 정상. 새 글로 확인&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Vercel 배포 앱의 DB는 &lt;strong&gt;외부 Postgres&lt;/strong&gt;가 맞다&lt;/li&gt;
&lt;li&gt;Neon = 시작하기 쉬운 서버리스 Postgres&lt;/li&gt;
&lt;li&gt;Prisma는 &lt;code&gt;provider&lt;/code&gt; + &lt;code&gt;DATABASE_URL&lt;/code&gt;만 바꿔도 앱 코드를 유지하기 쉽다&lt;/li&gt;
&lt;li&gt;비밀 값은 &lt;code&gt;.env&lt;/code&gt; / Vercel env에만 둔다&lt;/li&gt;
&lt;li&gt;로컬에서 한 번 쓰고, 같은 URL(또는 별도 prod URL)을 배포에 연결한다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;다음에 이어가면 좋은 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;삭제 확인 UI / 목록 검색 (짧은 UX 보강)&lt;/li&gt;
&lt;li&gt;시리즈 FAQ·총정리&lt;/li&gt;
&lt;li&gt;로컬 SQLite + 배포 Neon을 &lt;code&gt;.env&lt;/code&gt;로 나누는 팀 워크플로&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;시리즈 목차 (실전)&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;메모 수정과 삭제&lt;/li&gt;
&lt;li&gt;Prisma + SQLite로 DB 붙이기&lt;/li&gt;
&lt;li&gt;Vercel에 배포하기&lt;/li&gt;
&lt;li&gt;폼 검증하기 (zod)&lt;/li&gt;
&lt;li&gt;로그인 붙이기 (Clerk)&lt;/li&gt;
&lt;li&gt;Neon Postgres 연결하기 ← 지금&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/794</guid>
      <comments>https://aamoos.tistory.com/794#entry794comment</comments>
      <pubDate>Fri, 31 Jul 2026 09:03:42 +0900</pubDate>
    </item>
    <item>
      <title># 면접 CS 정리 Day 3 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/793</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 3 (풀스택 / Java·Spring·React)&lt;/h1&gt;
&lt;h2&gt;Q1. 401 vs 403&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;HTTP 상태코드 &lt;code&gt;401&lt;/code&gt;이랑 &lt;code&gt;403&lt;/code&gt; 차이?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;401&lt;/code&gt;: &lt;strong&gt;인증&lt;/strong&gt;이 안 됨 (로그인/토큰 없음·만료)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;403&lt;/code&gt;: 인증은 됐지만 &lt;strong&gt;권한&lt;/strong&gt;이 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;한 줄: 401 = 누구인지 모름 / 403 = 누군진 알지만 못 함.&lt;/p&gt;
&lt;h2&gt;Q2. 복합 인덱스 좌측 접두어&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;복합 인덱스 &lt;code&gt;(user_id, created_at)&lt;/code&gt;이 있을 때&lt;br&gt;아래 중 인덱스를 잘 타는 것 / 못 타는 것은?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A. &lt;code&gt;WHERE user_id = ?&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;B. &lt;code&gt;WHERE created_at = ?&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;C. &lt;code&gt;WHERE user_id = ? AND created_at = ?&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A: &lt;strong&gt;탐&lt;/strong&gt; (왼쪽 컬럼)&lt;/li&gt;
&lt;li&gt;B: &lt;strong&gt;못 탐&lt;/strong&gt; (앞 컬럼 스킵)&lt;/li&gt;
&lt;li&gt;C: &lt;strong&gt;가장 잘 탐&lt;/strong&gt; (인덱스 전부 활용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;복합 인덱스는 &lt;strong&gt;왼쪽 컬럼부터&lt;/strong&gt; 써야 한다.&lt;/p&gt;
&lt;h2&gt;Q3. Dirty Read&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;Dirty Read가 뭔지 한 줄, 막는 격리수준 이름은?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;Dirty Read = 다른 트랜잭션이 &lt;strong&gt;커밋하기 전 데이터&lt;/strong&gt;를 읽는 것.&lt;br&gt;막는 수준: &lt;strong&gt;&lt;code&gt;READ COMMITTED&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Q4. HashMap vs ConcurrentHashMap&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;&lt;code&gt;HashMap&lt;/code&gt;이랑 &lt;code&gt;ConcurrentHashMap&lt;/code&gt; 차이? 멀티스레드에서 뭘 써야 하는지.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;&lt;code&gt;HashMap&lt;/code&gt;은 스레드 안전하지 않아서, 여러 스레드가 동시에 쓰면&lt;br&gt;데이터가 깨지거나 예외가 날 수 있다.&lt;br&gt;공유 Map이면 &lt;strong&gt;&lt;code&gt;ConcurrentHashMap&lt;/code&gt;&lt;/strong&gt;을 쓴다.&lt;/p&gt;
&lt;h2&gt;Q5. @Controller vs @RestController&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;Spring &lt;code&gt;@Controller&lt;/code&gt;랑 &lt;code&gt;@RestController&lt;/code&gt; 차이?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Controller&lt;/code&gt;: 보통 &lt;strong&gt;화면(뷰)&lt;/strong&gt; 반환&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RestController&lt;/code&gt;: &lt;code&gt;@Controller&lt;/code&gt; + &lt;code&gt;@ResponseBody&lt;/code&gt;&lt;br&gt;→ 반환값이 뷰 이름이 아니라 JSON 등 &lt;strong&gt;응답 본문&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;REST API는 &lt;code&gt;@RestController&lt;/code&gt;를 주로 쓴다.&lt;/p&gt;
&lt;h2&gt;Q6. XSS vs CSRF&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;XSS랑 CSRF 차이 한 줄씩.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;XSS&lt;/strong&gt;: 페이지에 &lt;strong&gt;악의적 스크립트를 삽입&lt;/strong&gt;해 실행시키는 공격&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSRF&lt;/strong&gt;: 로그인된 사용자 브라우저로 &lt;strong&gt;원치 않는 요청&lt;/strong&gt;을 보내게 하는 공격&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;XSS 완화 예: 출력 이스케이프, 쿠키 &lt;code&gt;HttpOnly&lt;/code&gt;&lt;br&gt;CSRF 완화 예: CSRF 토큰, 쿠키 &lt;code&gt;SameSite&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Q7. N+1 문제&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;문제&lt;/strong&gt;&lt;br&gt;N+1 문제가 뭔지, 목록 조회할 때 어떻게 발생하고 어떻게 줄이는지?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;답&lt;/strong&gt;&lt;br&gt;목록 조회 &lt;strong&gt;1번&lt;/strong&gt; 후, 각 행마다 연관 데이터를 &lt;strong&gt;N번&lt;/strong&gt; 추가 조회하는 문제.&lt;br&gt;(총 1+N번 쿼리)&lt;/p&gt;
&lt;p&gt;줄이는 방법:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JPA: &lt;code&gt;fetch join&lt;/code&gt;, &lt;code&gt;@EntityGraph&lt;/code&gt;, 배치 조회(&lt;code&gt;IN&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;MyBatis: JOIN 한 방, id 목록 &lt;code&gt;IN&lt;/code&gt; 조회 후 조립&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;401 = 인증 X / 403 = 권한 X&lt;/li&gt;
&lt;li&gt;복합 인덱스 &lt;code&gt;(A,B)&lt;/code&gt; → &lt;code&gt;A&lt;/code&gt; OK, &lt;code&gt;B&lt;/code&gt;만 NO&lt;/li&gt;
&lt;li&gt;Dirty Read → &lt;code&gt;READ COMMITTED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;멀티스레드 Map → &lt;code&gt;ConcurrentHashMap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;N+1 = 목록 1 + 건별 N → fetch join / JOIN / IN&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;다음 목표&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;복합 인덱스 A도 탄다는 점까지 항상 말하기&lt;/li&gt;
&lt;li&gt;XSS/CSRF 방어 수단 한 가지씩 더 정리&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기타/CS 질문</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/793</guid>
      <comments>https://aamoos.tistory.com/793#entry793comment</comments>
      <pubDate>Fri, 31 Jul 2026 08:25:13 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS 실전] 5. 로그인 붙이기 (Clerk)</title>
      <link>https://aamoos.tistory.com/792</link>
      <description>&lt;h1&gt;[Next.js + TS 실전] 5. 로그인 붙이기 (Clerk)&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;지금까지 메모 앱은 &lt;strong&gt;누구나&lt;/strong&gt; 같은 목록을 보고 고칠 수 있습니다.&lt;br&gt;이번 편은 &lt;strong&gt;로그인&lt;/strong&gt;을 붙이고, &lt;strong&gt;내 메모만&lt;/strong&gt; 보이게 만듭니다.&lt;br&gt;도구: Clerk (Next.js App Router와 연동이 단순함)&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;왜 직접 인증을 안 짜는지 짧게 이해하기&lt;/li&gt;
&lt;li&gt;Clerk 프로젝트·키·패키지 준비&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ClerkProvider&lt;/code&gt; + 미들웨어 + &lt;code&gt;auth.protect()&lt;/code&gt;로 보호&lt;/li&gt;
&lt;li&gt;내비에 로그인/유저 버튼 넣기&lt;/li&gt;
&lt;li&gt;Prisma &lt;code&gt;Memo&lt;/code&gt;에 &lt;code&gt;userId&lt;/code&gt; 추가 → &lt;strong&gt;내 메모만&lt;/strong&gt; CRUD&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;UI 클릭 경로는 Clerk 대시보드가 자주 바뀝니다.&lt;br&gt;&lt;strong&gt;키 발급 → env → 코드 연결&lt;/strong&gt; 흐름만 정확히 따라가면 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;왜 Clerk인가&lt;/h2&gt;
&lt;p&gt;직접 만들면 대략 이만큼입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;회원가입 / 로그인 / 세션 / 쿠키&lt;/li&gt;
&lt;li&gt;비밀번호 해시, 이메일 인증&lt;/li&gt;
&lt;li&gt;로그아웃, 비밀번호 찾기…&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;학습 목표가 “인증 프로토콜”이 아니라면,&lt;br&gt;&lt;strong&gt;검증된 서비스 + SDK&lt;/strong&gt;로 붙이는 편이 앱에 집중하기 좋습니다.&lt;/p&gt;
&lt;p&gt;Clerk를 고른 이유:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Next App Router 공식 가이드가 친절함&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;SignInButton /&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;UserButton /&amp;gt;&lt;/code&gt; 같은 UI가 바로 있음&lt;/li&gt;
&lt;li&gt;서버에서 &lt;code&gt;auth()&lt;/code&gt;로 현재 유저 id를 얻기 쉬움&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;나중에 Auth.js 등으로 바꿔도 “보호된 라우트 + userId로 데이터 필터” 개념은 같습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;전체 그림&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;브라우저
  ↓
Clerk (로그인/세션)
  ↓
Next 미들웨어 (세션 연결)
  ↓
app/memos/layout (auth.protect)
  ↓
Server Action / Prisma
  ↓
Memo.userId === 현재 유저만 조회·수정·삭제
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;로그인만 하고 DB를 안 나누면,&lt;br&gt;여전히 &lt;strong&gt;남의 메모가 전부&lt;/strong&gt; 보입니다.&lt;br&gt;이번 편의 진짜 목표는 &lt;strong&gt;userId 연결&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1) Clerk 계정·애플리케이션&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://clerk.com&quot;&gt;https://clerk.com&lt;/a&gt; 가입&lt;/li&gt;
&lt;li&gt;새 Application 생성 (Next.js 선택)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API Keys&lt;/strong&gt;에서 아래 두 값을 복사&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브라우저에 노출돼도 되는 공개 키&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CLERK_SECRET_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;서버 전용. GitHub에 올리면 안 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;memo-app/.env&lt;/code&gt; (또는 &lt;code&gt;.env.local&lt;/code&gt;)에 추가:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DATABASE_URL=&quot;file:./dev.db&quot;
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;.gitignore&lt;/code&gt;에 &lt;code&gt;.env&lt;/code&gt; / &lt;code&gt;.env.local&lt;/code&gt;이 있는지 다시 확인하세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2) 패키지 설치&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;npm install @clerk/nextjs
&lt;/code&gt;&lt;/pre&gt;

&lt;hr&gt;
&lt;h2&gt;3) 미들웨어 + 페이지(레이아웃)에서 보호&lt;/h2&gt;
&lt;p&gt;Next.js &lt;strong&gt;16&lt;/strong&gt;은 네트워크 경계 파일 이름이 &lt;code&gt;proxy.ts&lt;/code&gt;인 경우가 있고,&lt;br&gt;&lt;strong&gt;15 이하&lt;/strong&gt;는 보통 &lt;code&gt;middleware.ts&lt;/code&gt;입니다.&lt;br&gt;(공식 문서가 &lt;code&gt;middleware.ts&lt;/code&gt;를 말하면, 버전 기준으로 파일명만 맞추면 됩니다.)&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;proxy.ts&lt;/code&gt; — Clerk 연결만&lt;/h3&gt;
&lt;p&gt;경로: &lt;code&gt;memo-app/proxy.ts&lt;/code&gt; (또는 &lt;code&gt;middleware.ts&lt;/code&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { clerkMiddleware } from &amp;quot;@clerk/nextjs/server&amp;quot;;

export default clerkMiddleware();

export const config = {
  matcher: [
    &amp;quot;/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)&amp;quot;,
    &amp;quot;/(api|trpc)(.*)&amp;quot;,
  ],
};&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;최신 &lt;code&gt;@clerk/nextjs&lt;/code&gt;에서는 &lt;code&gt;createRouteMatcher&lt;/code&gt;로 미들웨어에서 경로를 막는 방식이 &lt;strong&gt;deprecated&lt;/strong&gt;입니다.&lt;br&gt;미들웨어는 Clerk 세션만 연결하고, &lt;strong&gt;보호는 각 페이지/레이아웃에서&lt;/strong&gt; 합니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;&lt;code&gt;app/memos/layout.tsx&lt;/code&gt; — /memos 전체 보호&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { auth } from &amp;quot;@clerk/nextjs/server&amp;quot;;

export default async function MemosLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  await auth.protect();
  return children;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;의미:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/memos&lt;/code&gt;, &lt;code&gt;/memos/new&lt;/code&gt;, &lt;code&gt;/memos/1&lt;/code&gt; … → 이 레이아웃을 거치므로 로그인 필요&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt; (홈) → 레이아웃 밖이라 비로그인 가능&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auth.protect()&lt;/code&gt; → 없으면 로그인 화면으로&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4) 레이아웃에 ClerkProvider + 버튼&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;app/layout.tsx&lt;/code&gt; (루트 레이아웃):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import Link from &amp;quot;next/link&amp;quot;;
import type { Metadata } from &amp;quot;next&amp;quot;;
import {
  ClerkProvider,
  SignInButton,
  SignUpButton,
  Show,
  UserButton,
} from &amp;quot;@clerk/nextjs&amp;quot;;
import &amp;quot;./globals.css&amp;quot;;

export const metadata: Metadata = {
  title: &amp;quot;Memo App&amp;quot;,
  description: &amp;quot;Next.js + TypeScript 실습&amp;quot;,
};

export default function RootLayout({
  children,
}: Readonly&amp;lt;{ children: React.ReactNode }&amp;gt;) {
  return (
    &amp;lt;html lang=&amp;quot;ko&amp;quot;&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;ClerkProvider&amp;gt;
          &amp;lt;header&amp;gt;
            &amp;lt;nav&amp;gt;
              &amp;lt;Link href=&amp;quot;/&amp;quot;&amp;gt;홈&amp;lt;/Link&amp;gt;
              {&amp;quot; | &amp;quot;}
              &amp;lt;Link href=&amp;quot;/memos&amp;quot;&amp;gt;메모&amp;lt;/Link&amp;gt;
              {&amp;quot; | &amp;quot;}
              &amp;lt;Show when=&amp;quot;signed-out&amp;quot;&amp;gt;
                &amp;lt;SignInButton /&amp;gt; &amp;lt;SignUpButton /&amp;gt;
              &amp;lt;/Show&amp;gt;
              &amp;lt;Show when=&amp;quot;signed-in&amp;quot;&amp;gt;
                &amp;lt;UserButton /&amp;gt;
              &amp;lt;/Show&amp;gt;
            &amp;lt;/nav&amp;gt;
          &amp;lt;/header&amp;gt;
          {children}
        &amp;lt;/ClerkProvider&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;컴포넌트&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ClerkProvider&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;앱 전체에 Clerk 컨텍스트 (&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; 안)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Show when=&amp;quot;signed-out&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;로그아웃일 때 보일 UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Show when=&amp;quot;signed-in&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;로그인일 때 보일 UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SignInButton&lt;/code&gt; / &lt;code&gt;SignUpButton&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;로그인·가입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UserButton&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;프로필·로그아웃&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;예전 &lt;code&gt;SignedIn&lt;/code&gt; / &lt;code&gt;SignedOut&lt;/code&gt;은 최신 Clerk에서 &lt;strong&gt;&lt;code&gt;Show&lt;/code&gt;&lt;/strong&gt; 로 바뀌었습니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;code&gt;npm run dev&lt;/code&gt; 후 홈에서 로그인 → &lt;code&gt;/memos&lt;/code&gt; 접근이 되면 1차 성공입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5) Prisma: 메모에 주인(&lt;code&gt;userId&lt;/code&gt;) 붙이기&lt;/h2&gt;
&lt;p&gt;스키마를 바꿉니다. &lt;code&gt;prisma/schema.prisma&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model Memo {
  id        String   @id @default(cuid())
  title     String
  content   String
  userId    String
  createdAt DateTime @default(now())

  @@index([userId])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;마이그레이션:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx prisma migrate dev --name add-memo-user-id
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;이미 시드/메모가 있다면 &lt;code&gt;userId&lt;/code&gt; 때문에 충돌할 수 있습니다.&lt;br&gt;연습 단계면 DB를 비우고 다시 만들어도 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx prisma migrate reset
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;(&lt;code&gt;reset&lt;/code&gt;은 데이터 삭제 + 마이그레이션 + 시드. 로컬에서만.)&lt;/p&gt;
&lt;p&gt;시드(&lt;code&gt;prisma/seed.ts&lt;/code&gt;)에도 임시 &lt;code&gt;userId&lt;/code&gt;가 필요합니다.&lt;br&gt;예: &lt;code&gt;&amp;quot;seed-user&amp;quot;&lt;/code&gt; — 나중에 실제 Clerk 유저 id와는 다릅니다.&lt;br&gt;실습은 &lt;strong&gt;로그인 후 새로 만든 메모&lt;/strong&gt;로 확인하는 편이 낫습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6) 데이터 레이어: 내 것만&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;lib/memos.ts&lt;/code&gt;를 userId 기준으로 바꿉니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import type { Memo } from &amp;quot;@/types/memo&amp;quot;;
import { prisma } from &amp;quot;@/lib/prisma&amp;quot;;

function toMemo(row: {
  id: string;
  title: string;
  content: string;
  createdAt: Date;
}): Memo {
  return {
    id: row.id,
    title: row.title,
    content: row.content,
    createdAt: row.createdAt.toISOString().slice(0, 10),
  };
}

export async function getMemos(userId: string): Promise&amp;lt;Memo[]&amp;gt; {
  const rows = await prisma.memo.findMany({
    where: { userId },
    orderBy: { createdAt: &amp;quot;desc&amp;quot; },
  });
  return rows.map(toMemo);
}

export async function getMemoById(
  id: string,
  userId: string
): Promise&amp;lt;Memo | undefined&amp;gt; {
  const row = await prisma.memo.findFirst({
    where: { id, userId },
  });
  return row ? toMemo(row) : undefined;
}

export async function addMemo(
  userId: string,
  input: { title: string; content: string }
): Promise&amp;lt;Memo&amp;gt; {
  const row = await prisma.memo.create({
    data: {
      userId,
      title: input.title,
      content: input.content,
    },
  });
  return toMemo(row);
}

export async function updateMemo(
  id: string,
  userId: string,
  input: { title: string; content: string }
): Promise&amp;lt;Memo | undefined&amp;gt; {
  try {
    const row = await prisma.memo.update({
      where: { id },
      data: input,
    });
    // 남의 메모면 위에서 이미 다른 유저 것일 수 있으니, 소유권 확인
    if (row.userId !== userId) {
      return undefined;
    }
    return toMemo(row);
  } catch {
    return undefined;
  }
}

export async function deleteMemo(
  id: string,
  userId: string
): Promise&amp;lt;boolean&amp;gt; {
  const existing = await prisma.memo.findFirst({
    where: { id, userId },
  });
  if (!existing) return false;

  await prisma.memo.delete({ where: { id } });
  return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;더 안전한 update 패턴 (추천):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const result = await prisma.memo.updateMany({
  where: { id, userId },
  data: input,
});
if (result.count === 0) return undefined;
return getMemoById(id, userId);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;updateMany&lt;/code&gt; + &lt;code&gt;where: { id, userId }&lt;/code&gt;면 &lt;strong&gt;남의 id를 알아도&lt;/strong&gt; 수정이 안 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7) Action / 페이지에서 &lt;code&gt;auth()&lt;/code&gt;로 userId&lt;/h2&gt;
&lt;p&gt;서버에서 현재 유저:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { auth } from &quot;@clerk/nextjs/server&quot;;

const { userId } = await auth();
if (!userId) {
  throw new Error(&quot;로그인이 필요합니다&quot;);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;createMemo&lt;/code&gt; 예시 핵심만:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { userId } = await auth();
if (!userId) {
  return { ok: false, message: &quot;로그인이 필요합니다&quot;, values };
}

// ... zod 검증 후
await addMemo(userId, parsed.data);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;목록 페이지:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { auth } from &quot;@clerk/nextjs/server&quot;;
import { redirect } from &quot;next/navigation&quot;;
import { getMemos } from &quot;@/lib/memos&quot;;

export default async function MemosPage() {
  const { userId } = await auth();
  if (!userId) redirect(&quot;/&quot;);

  const memos = await getMemos(userId);
  // ... 기존 UI
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;상세·수정도 &lt;code&gt;getMemoById(id, userId)&lt;/code&gt;로 바꿉니다.&lt;br&gt;없으면 &lt;code&gt;notFound()&lt;/code&gt; — 남 메모 URL을 쳐도 404.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;updateMemoAction&lt;/code&gt; / &lt;code&gt;deleteMemoAction&lt;/code&gt;에도 똑같이 &lt;code&gt;userId&lt;/code&gt;를 넘기세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;동작 확인 체크리스트&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;로그아웃 상태에서 &lt;code&gt;/memos&lt;/code&gt; → 로그인으로 유도&lt;/li&gt;
&lt;li&gt;가입/로그인 후 메모 작성&lt;/li&gt;
&lt;li&gt;목록에 &lt;strong&gt;내 메모만&lt;/strong&gt; 보임&lt;/li&gt;
&lt;li&gt;(가능하면) 다른 계정으로 로그인 → 이전 계정 메모가 안 보임&lt;/li&gt;
&lt;li&gt;다른 유저 메모 id를 URL에 넣으면 404&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;한 계정만 있어도 1~3이면 기본 연동은 성공입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;자주 하는 실수&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;CLERK_SECRET_KEY&lt;/code&gt;를 클라이언트에 노출&lt;/strong&gt;&lt;br&gt;→ &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; 붙이지 말 것&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 미들웨어만 하고 DB는 그대로&lt;/strong&gt;&lt;br&gt;→ 로그인은 됐는데 데이터는 공유. &lt;code&gt;userId&lt;/code&gt; 필수&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;auth()&lt;/code&gt;를 await 안 함&lt;/strong&gt;&lt;br&gt;→ 최신 SDK는 &lt;code&gt;await auth()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. update를 &lt;code&gt;where: { id }&lt;/code&gt;만 함&lt;/strong&gt;&lt;br&gt;→ id를 알면 남의 글 수정 가능. 반드시 &lt;code&gt;userId&lt;/code&gt; 조건&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. env를 넣고 서버를 안 재시작&lt;/strong&gt;&lt;br&gt;→ &lt;code&gt;.env&lt;/code&gt; 변경 후 &lt;code&gt;npm run dev&lt;/code&gt; 다시&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. Root에 Clerk를 설치하고 memo-app에서 import&lt;/strong&gt;&lt;br&gt;→ 패키지·env는 &lt;strong&gt;memo-app&lt;/strong&gt; 기준&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;인증은 직접 구현보다 서비스+SDK가 실무에서 흔하다&lt;/li&gt;
&lt;li&gt;미들웨어 = Clerk 세션 연결 (&lt;code&gt;clerkMiddleware&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;접근 보호 = 레이아웃/페이지에서 &lt;code&gt;auth.protect()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auth().userId&lt;/code&gt; = 서버에서 “지금 누구인지”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;보호의 핵심은 DB 쿼리에 userId를 넣는 것&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;UI 컴포넌트(&lt;code&gt;SignInButton&lt;/code&gt; 등)와 데이터 소유권은 같이 설계한다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;다음으로 이어가면 좋은 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Neon Postgres + Vercel에 &lt;code&gt;CLERK_*&lt;/code&gt; 환경 변수까지 넣어 &lt;strong&gt;배포본 로그인&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;조직(Organization) / 역할(role) — 팀 메모&lt;/li&gt;
&lt;li&gt;Auth.js로 같은 구조를 직접 비교&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;시리즈 목차 (실전)&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;메모 수정과 삭제&lt;/li&gt;
&lt;li&gt;Prisma + SQLite로 DB 붙이기&lt;/li&gt;
&lt;li&gt;Vercel에 배포하기&lt;/li&gt;
&lt;li&gt;폼 검증하기 (zod)&lt;/li&gt;
&lt;li&gt;로그인 붙이기 (Clerk) ← 지금&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/792</guid>
      <comments>https://aamoos.tistory.com/792#entry792comment</comments>
      <pubDate>Thu, 30 Jul 2026 13:05:03 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS 실전] 4. 폼 검증하기 (zod)</title>
      <link>https://aamoos.tistory.com/791</link>
      <description>&lt;h1&gt;[Next.js + TS 실전] 4. 폼 검증하기 (zod)&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 검증은 &lt;code&gt;if (!title)&lt;/code&gt; + &lt;code&gt;throw new Error&lt;/code&gt; 수준이었습니다.&lt;br /&gt;이번 편은 &lt;b&gt;필드별 메시지&lt;/b&gt;를 폼 아래에 보여 줍니다.&lt;br /&gt;목표: zod 스키마 &amp;middot; Server Action 반환값 &amp;middot; &lt;code&gt;useActionState&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왜 &lt;code&gt;throw&lt;/code&gt;만으로는 UX가 약한지 이해하기&lt;/li&gt;
&lt;li&gt;zod로 메모 입력 규칙 정하기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createMemo&lt;/code&gt;가 성공/실패 결과를 &lt;b&gt;반환&lt;/b&gt;하게 바꾸기&lt;/li&gt;
&lt;li&gt;클라이언트 폼에서 &lt;code&gt;useActionState&lt;/code&gt;로 에러 표시하기&lt;/li&gt;
&lt;li&gt;수정 폼에도 같은 패턴 적용하기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB&amp;middot;배포는 건드리지 않습니다.&lt;br /&gt;&lt;b&gt;이미 있는 작성/수정 폼의 품질&lt;/b&gt;만 올립니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금 방식의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Action 대략:&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;if (!title || !content) {
  throw new Error(&quot;제목과 내용을 모두 입력하세요&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 필드가 문제인지 모호하다&lt;/li&gt;
&lt;li&gt;화면에는 &lt;code&gt;error.tsx&lt;/code&gt;로 크게 깨지거나, 메시지가 폼과 떨어져 보인다&lt;/li&gt;
&lt;li&gt;HTML &lt;code&gt;required&lt;/code&gt;만 믿으면 브라우저마다 다르고, 서버 검증과 중복된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 UX:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;제목
[                    ]
&amp;rarr; 제목은 1자 이상 입력하세요

내용
[                    ]
&amp;rarr; 내용은 1자 이상 입력하세요&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 흐름&lt;/h2&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;폼 제출
  &amp;darr;
createMemo (Server Action)
  &amp;darr;
zod로 검증
  ├─ 실패 &amp;rarr; { ok: false, errors, values } 반환 (페이지 유지)
  └─ 성공 &amp;rarr; DB 저장 후 redirect
  &amp;darr;
useActionState가 state를 받아 에러 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 변화:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실패할 때 &lt;b&gt;throw하지 않고 객체를 반환&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;폼이 그 객체를 읽어 메시지를 그림&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) zod 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;memo-app&lt;/code&gt;에서:&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install zod&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zod = TypeScript와 잘 맞는 &lt;b&gt;스키마 검증 라이브러리&lt;/b&gt;입니다.&lt;br /&gt;&amp;ldquo;이 값은 이런 모양이어야 한다&amp;rdquo;를 한곳에서 선언합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) 스키마 파일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로: &lt;code&gt;lib/memo-schema.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import { z } from &quot;zod&quot;;

export const memoFormSchema = z.object({
  title: z
    .string()
    .trim()
    .min(1, &quot;제목은 1자 이상 입력하세요&quot;)
    .max(100, &quot;제목은 100자 이하여야 합니다&quot;),
  content: z
    .string()
    .trim()
    .min(1, &quot;내용은 1자 이상 입력하세요&quot;)
    .max(5000, &quot;내용은 5000자 이하여야 합니다&quot;),
});

export type MemoFormInput = z.infer&amp;lt;typeof memoFormSchema&amp;gt;;

export type MemoFormState = {
  ok: boolean;
  message?: string;
  errors?: {
    title?: string[];
    content?: string[];
  };
  values?: {
    title: string;
    content: string;
  };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;z.object&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;폼 필드 묶음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;min&lt;/code&gt; / &lt;code&gt;max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;길이 규칙 + 한국어 메시지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;z.infer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;스키마에서 TS 타입 자동 추출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MemoFormState&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Action이 폼에 돌려줄 상태&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;errors.title&lt;/code&gt;을 배열로 둔 이유: zod가 필드당 메시지를 배열로 주기 때문입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) &lt;code&gt;createMemo&lt;/code&gt;를 &amp;ldquo;결과 반환&amp;rdquo;형으로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/memos/actions.ts&lt;/code&gt;의 생성 액션을 바꿉니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;&quot;use server&quot;;

import { z } from &quot;zod&quot;;
import { redirect } from &quot;next/navigation&quot;;
import { revalidatePath } from &quot;next/cache&quot;;
import { addMemo, deleteMemo, updateMemo } from &quot;@/lib/memos&quot;;
import { memoFormSchema, type MemoFormState } from &quot;@/lib/memo-schema&quot;;

export async function createMemo(
  _prevState: MemoFormState,
  formData: FormData
): Promise&amp;lt;MemoFormState&amp;gt; {
  const values = {
    title: String(formData.get(&quot;title&quot;) ?? &quot;&quot;),
    content: String(formData.get(&quot;content&quot;) ?? &quot;&quot;),
  };

  const parsed = memoFormSchema.safeParse(values);

  if (!parsed.success) {
    return {
      ok: false,
      // Zod 4: parsed.error.flatten() 대신 z.flattenError
      errors: z.flattenError(parsed.error).fieldErrors,
      values,
    };
  }

  await addMemo(parsed.data);

  revalidatePath(&quot;/memos&quot;);
  redirect(&quot;/memos&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바뀐 점:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인자가 &lt;code&gt;(prevState, formData)&lt;/code&gt; &amp;mdash; &lt;code&gt;useActionState&lt;/code&gt; 규약&lt;/li&gt;
&lt;li&gt;&lt;code&gt;safeParse&lt;/code&gt; &amp;mdash; 실패해도 throw하지 않음&lt;/li&gt;
&lt;li&gt;실패 시 &lt;code&gt;errors&lt;/code&gt; + 사용자가 친 &lt;code&gt;values&lt;/code&gt;를 돌려줌 (입력 유지)&lt;/li&gt;
&lt;li&gt;성공 시만 &lt;code&gt;redirect&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;deleteMemoAction&lt;/code&gt;은 이번 편에서 그대로 둬도 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) 작성 폼을 Client로 (에러 표시)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;useActionState&lt;/code&gt;는 훅이라 &lt;b&gt;클라이언트 컴포넌트&lt;/b&gt;가 필요합니다.&lt;br /&gt;페이지 전체를 클라이언트로 올리기보다, &lt;b&gt;폼만&lt;/b&gt; 분리합니다. (5편 패턴)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로: &lt;code&gt;components/MemoForm.tsx&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;&quot;use client&quot;;

import { useActionState } from &quot;react&quot;;
import type { MemoFormState } from &quot;@/lib/memo-schema&quot;;

type MemoFormProps = {
  action: (
    prevState: MemoFormState,
    formData: FormData
  ) =&amp;gt; Promise&amp;lt;MemoFormState&amp;gt;;
  initialTitle?: string;
  initialContent?: string;
  submitLabel?: string;
  hiddenFields?: Record&amp;lt;string, string&amp;gt;;
};

const initialState: MemoFormState = { ok: true };

export default function MemoForm({
  action,
  initialTitle = &quot;&quot;,
  initialContent = &quot;&quot;,
  submitLabel = &quot;저장&quot;,
  hiddenFields,
}: MemoFormProps) {
  const [state, formAction, pending] = useActionState(action, initialState);

  const titleValue = state.values?.title ?? initialTitle;
  const contentValue = state.values?.content ?? initialContent;

  return (
    &amp;lt;form action={formAction}&amp;gt;
      {hiddenFields &amp;amp;&amp;amp;
        Object.entries(hiddenFields).map(([name, value]) =&amp;gt; (
          &amp;lt;input key={name} type=&quot;hidden&quot; name={name} value={value} /&amp;gt;
        ))}

      &amp;lt;p&amp;gt;
        &amp;lt;label&amp;gt;
          제목
          &amp;lt;br /&amp;gt;
          &amp;lt;input
            type=&quot;text&quot;
            name=&quot;title&quot;
            defaultValue={titleValue}
            key={`title-${titleValue}`}
          /&amp;gt;
        &amp;lt;/label&amp;gt;
        {state.errors?.title?.[0] &amp;amp;&amp;amp; (
          &amp;lt;span style={{ color: &quot;crimson&quot; }}&amp;gt;{state.errors.title[0]}&amp;lt;/span&amp;gt;
        )}
      &amp;lt;/p&amp;gt;

      &amp;lt;p&amp;gt;
        &amp;lt;label&amp;gt;
          내용
          &amp;lt;br /&amp;gt;
          &amp;lt;textarea
            name=&quot;content&quot;
            rows={5}
            defaultValue={contentValue}
            key={`content-${contentValue}`}
          /&amp;gt;
        &amp;lt;/label&amp;gt;
        {state.errors?.content?.[0] &amp;amp;&amp;amp; (
          &amp;lt;span style={{ color: &quot;crimson&quot; }}&amp;gt;{state.errors.content[0]}&amp;lt;/span&amp;gt;
        )}
      &amp;lt;/p&amp;gt;

      &amp;lt;button type=&quot;submit&quot; disabled={pending}&amp;gt;
        {pending ? &quot;저장 중...&quot; : submitLabel}
      &amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;useActionState(action, initialState)&lt;/code&gt; &amp;rarr; &lt;code&gt;[state, formAction, pending]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defaultValue&lt;/code&gt; + &lt;code&gt;key&lt;/code&gt;로 검증 실패 후 입력값 유지&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pending&lt;/code&gt;으로 저장 중 버튼 비활성&lt;/li&gt;
&lt;li&gt;HTML &lt;code&gt;required&lt;/code&gt;는 빼도 됩니다 (서버+zod가 진실)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSX를 &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; 안에 넣을 때 &lt;code&gt;&amp;amp;lt;&lt;/code&gt; &lt;code&gt;&amp;amp;gt;&lt;/code&gt;로 이스케이프했습니다.&lt;br /&gt;실제 파일에는 일반 &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;을 쓰세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5) 새 메모 페이지 연결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/memos/new/page.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import MemoForm from &quot;@/components/MemoForm&quot;;
import { createMemo } from &quot;@/app/memos/actions&quot;;

export default function NewMemoPage() {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;새 메모&amp;lt;/h1&amp;gt;
      &amp;lt;MemoForm action={createMemo} /&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;/memos/new&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;제목&amp;middot;내용 비우고 저장&lt;/li&gt;
&lt;li&gt;필드 아래 빨간 메시지&lt;/li&gt;
&lt;li&gt;올바르게 입력하면 &lt;code&gt;/memos&lt;/code&gt;로 이동&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6) 수정도 같은 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;updateMemoAction&lt;/code&gt;도 &lt;code&gt;(prevState, formData)&lt;/code&gt;로 맞춥니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export async function updateMemoAction(
  _prevState: MemoFormState,
  formData: FormData
): Promise&amp;lt;MemoFormState&amp;gt; {
  const id = String(formData.get(&quot;id&quot;) ?? &quot;&quot;).trim();
  const values = {
    title: String(formData.get(&quot;title&quot;) ?? &quot;&quot;),
    content: String(formData.get(&quot;content&quot;) ?? &quot;&quot;),
  };

  if (!id) {
    return { ok: false, message: &quot;메모 id가 없습니다&quot;, values };
  }

  const parsed = memoFormSchema.safeParse(values);

  if (!parsed.success) {
    return {
      ok: false,
      errors: z.flattenError(parsed.error).fieldErrors,
      values,
    };
  }

  const updated = await updateMemo(id, parsed.data);

  if (!updated) {
    return { ok: false, message: &quot;메모를 찾을 수 없습니다&quot;, values };
  }

  revalidatePath(&quot;/memos&quot;);
  revalidatePath(`/memos/${id}`);
  redirect(`/memos/${id}`);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/memos/[id]/edit/page.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { notFound } from &quot;next/navigation&quot;;
import MemoForm from &quot;@/components/MemoForm&quot;;
import { updateMemoAction } from &quot;@/app/memos/actions&quot;;
import { getMemoById } from &quot;@/lib/memos&quot;;

type EditMemoPageProps = {
  params: Promise&amp;lt;{ id: string }&amp;gt;;
};

export default async function EditMemoPage({ params }: EditMemoPageProps) {
  const { id } = await params;
  const memo = await getMemoById(id);

  if (!memo) {
    notFound();
  }

  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;메모 수정&amp;lt;/h1&amp;gt;
      &amp;lt;MemoForm
        action={updateMemoAction}
        initialTitle={memo.title}
        initialContent={memo.content}
        hiddenFields={{ id: memo.id }}
        submitLabel=&quot;수정 저장&quot;
      /&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성&amp;middot;수정이 &lt;b&gt;같은 &lt;code&gt;MemoForm&lt;/code&gt;&lt;/b&gt;을 씁니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;useActionState&lt;/code&gt; 짧게&lt;/h2&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;const [state, formAction, pending] = useActionState(action, initialState);&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;마지막 Action 반환값 (에러&amp;middot;values)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;formAction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;form action={...}&amp;gt;&lt;/code&gt;에 넘김&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pending&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;제출 진행 중 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전 &lt;code&gt;useFormState&lt;/code&gt;와 같은 계열입니다.&lt;br /&gt;React 19 / 최신 Next에서는 &lt;code&gt;useActionState&lt;/code&gt;를 쓰면 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 스키마를 Action에 두나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 &lt;code&gt;required&lt;/code&gt;만으로도 빈 값은 막을 수 있습니다.&lt;br /&gt;그래도 &lt;b&gt;서버에서 다시&lt;/b&gt; 검증합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DevTools로 &lt;code&gt;required&lt;/code&gt;를 지우고 제출할 수 있음&lt;/li&gt;
&lt;li&gt;API&amp;middot;Action은 폼 말고도 호출될 수 있음&lt;/li&gt;
&lt;li&gt;규칙(최대 글자 등)을 &lt;b&gt;한 스키마&lt;/b&gt;로 공유 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에서 zod를 한 번 더 돌리는 &amp;ldquo;즉시 검증&amp;rdquo;은 나중에 추가해도 됩니다.&lt;br /&gt;입문 다음 단계에서는 &lt;b&gt;서버 검증 + 메시지 표시&lt;/b&gt;만으로 충분합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 하는 실수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Action 시그니처를 예전 &lt;code&gt;(formData)&lt;/code&gt;만 유지&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;useActionState&lt;/code&gt;는 &lt;code&gt;(prevState, formData)&lt;/code&gt; 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 실패 시 계속 &lt;code&gt;throw&lt;/code&gt;&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;error.tsx&lt;/code&gt;로 튕김. 필드 에러는 &lt;code&gt;return { errors }&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 페이지 전체를 &lt;code&gt;'use client'&lt;/code&gt;&lt;/b&gt;&lt;br /&gt;&amp;rarr; 폼만 Client, 페이지는 Server 유지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. &lt;code&gt;safeParse&lt;/code&gt; 대신 &lt;code&gt;parse&lt;/code&gt;&lt;/b&gt;&lt;br /&gt;&amp;rarr; 실패 시 throw. 폼 UX에는 &lt;code&gt;safeParse&lt;/code&gt;가 맞음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. &lt;code&gt;redirect&lt;/code&gt;를 try/catch로 감싸서 에러로 착각&lt;/b&gt;&lt;br /&gt;&amp;rarr; Next의 &lt;code&gt;redirect&lt;/code&gt;는 특수 throw를 씁니다. 성공 경로에서만 호출&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검증 규칙은 zod 스키마로 한곳에 둔다&lt;/li&gt;
&lt;li&gt;실패는 throw보다 &lt;b&gt;상태 객체 반환&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useActionState&lt;/code&gt;로 에러를 필드 옆에 보여 준다&lt;/li&gt;
&lt;li&gt;폼만 Client Component로 분리한다&lt;/li&gt;
&lt;li&gt;작성&amp;middot;수정은 같은 폼 컴포넌트를 재사용한다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 목차 (실전)&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;메모 수정과 삭제&lt;/li&gt;
&lt;li&gt;Prisma + SQLite로 DB 붙이기&lt;/li&gt;
&lt;li&gt;Vercel에 배포하기&lt;/li&gt;
&lt;li&gt;폼 검증하기 (zod) &amp;larr; 지금&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/791</guid>
      <comments>https://aamoos.tistory.com/791#entry791comment</comments>
      <pubDate>Thu, 30 Jul 2026 10:57:55 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS 실전] 3. Vercel에 배포하기</title>
      <link>https://aamoos.tistory.com/790</link>
      <description>&lt;h1&gt;[Next.js + TS 실전] 3. Vercel에 배포하기&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실전 2편까지 로컬에서 Prisma + SQLite로 메모가 저장됩니다.&lt;br /&gt;이번 편은 &lt;b&gt;다른 사람이 URL로 접속&lt;/b&gt;할 수 있게 배포합니다.&lt;br /&gt;목표: GitHub &amp;rarr; Vercel 배포 &amp;middot; 환경 변수 &amp;middot; SQLite 한계와 대안.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;배포 전 로컬 빌드 확인&lt;/li&gt;
&lt;li&gt;GitHub에 올리고 Vercel 연결&lt;/li&gt;
&lt;li&gt;환경 변수 설정 이해&lt;/li&gt;
&lt;li&gt;&lt;b&gt;왜 Vercel에서 SQLite가 힘든지&lt;/b&gt; 알기&lt;/li&gt;
&lt;li&gt;현실적인 DB 선택지 정리 (Postgres 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;버튼 몇 번이면 끝&amp;rdquo;만 말하지 않습니다.&lt;br /&gt;로컬 SQLite 앱을 그대로 Vercel에 올리면 &lt;b&gt;어디서 막히는지&lt;/b&gt;까지 같이 봅니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배포가 되면 뭐가 바뀌나&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;로컬&lt;/th&gt;
&lt;th&gt;배포 후&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;접속&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localhost:3000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://something.vercel.app&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버&lt;/td&gt;
&lt;td&gt;내 PC&lt;/td&gt;
&lt;td&gt;Vercel 클라우드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prisma/dev.db&lt;/code&gt; 파일&lt;/td&gt;
&lt;td&gt;&lt;b&gt;파일 DB는 보통 유지 안 됨&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면(Next.js) 배포와 &lt;b&gt;데이터 저장&lt;/b&gt;은 별개입니다.&lt;br /&gt;이번 편의 핵심도 그 구분입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) 배포 전: 로컬에서 빌드해 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;memo-app&lt;/code&gt;에서:&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 없이 끝나면 배포 가능성이 높습니다.&lt;br /&gt;여기서 깨지면 Vercel에서도 거의 같은 에러가 납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 서버만 켜 보는 것과 다릅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;npm run dev&lt;/code&gt; &amp;rarr; 개발용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm run build&lt;/code&gt; &amp;rarr; 프로덕션 결과물 만들기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) GitHub에 올리기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel은 GitHub 저장소와 연결하는 방식이 가장 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 git이 없다면 &lt;code&gt;memo-app&lt;/code&gt;에서:&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;git init
git add .
git commit -m &quot;Add memo app with Prisma&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.gitignore&lt;/code&gt;에 아래가 있는지 확인하세요.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;.env
.env*.local
prisma/dev.db
prisma/dev.db-journal
node_modules
.next
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub에 새 저장소를 만들고 push:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;git remote add origin https://github.com/본인계정/memo-app.git
git branch -M main
git push -u origin main
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.env&lt;/code&gt;는 올리지 않습니다.&lt;br /&gt;DB URL은 Vercel 대시보드에서 따로 넣습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) Vercel에 프로젝트 연결&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://vercel.com&quot;&gt;https://vercel.com&lt;/a&gt; 가입/로그인 (GitHub 연동 권장)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Add New&amp;hellip; &amp;rarr; Project&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;방금 push한 저장소 Import&lt;/li&gt;
&lt;li&gt;Root Directory가 &lt;code&gt;memo-app&lt;/code&gt;이면 (모노레포인 경우) 그 폴더로 지정&lt;/li&gt;
&lt;li&gt;Framework Preset: &lt;b&gt;Next.js&lt;/b&gt; (보통 자동)&lt;/li&gt;
&lt;li&gt;Deploy&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 배포는 &lt;b&gt;DB 없이도&lt;/b&gt; 프론트가 뜰 수 있지만,&lt;br /&gt;메모 CRUD는 DB URL이 없으면 런타임에서 실패합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) 왜 SQLite는 Vercel과 안 맞나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 SQLite는 &lt;b&gt;디스크의 한 파일&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel(서버리스)은 대략 이렇게 동작합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청마다 짧은 수명의 함수가 뜸&lt;/li&gt;
&lt;li&gt;파일 시스템은 &lt;b&gt;읽기 위주 / 임시&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;인스턴스가 여러 개일 수 있음&lt;/li&gt;
&lt;li&gt;재배포하면 로컬 파일이 날아감&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;file:./dev.db&lt;/code&gt; 방식은:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쓰기가 안 되거나&lt;/li&gt;
&lt;li&gt;써도 다음 요청/재배포에 사라지거나&lt;/li&gt;
&lt;li&gt;인스턴스마다 다른 DB를 보는&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄로:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Vercel에 올릴 앱의 &amp;ldquo;진짜 DB&amp;rdquo;는 파일이 아니라, 외부 데이터베이스 서비스가 필요하다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 학습용 SQLite는 여전히 훌륭합니다.&lt;br /&gt;배포 단계에서만 교체하면 됩니다. (실전 2편에서 &lt;code&gt;lib/memos.ts&lt;/code&gt;만 바꾼 이유)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5) 배포용 DB 선택지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입문&amp;middot;실전 시리즈 기준으로 현실적인 선택:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;선택&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;th&gt;추천&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Neon / Supabase (Postgres)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Prisma와 궁합 좋음, 무료 티어 있음&lt;/td&gt;
&lt;td&gt;⭐ 가장 무난&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Turso (libSQL)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;SQLite 호환에 가까운 원격 DB&lt;/td&gt;
&lt;td&gt;SQLite 감각 유지하고 싶을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Prisma Postgres&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Prisma 생태계 연동&lt;/td&gt;
&lt;td&gt;Prisma 문서 따라가기 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로컬 SQLite만&lt;/td&gt;
&lt;td&gt;배포 CRUD 불가에 가깝음&lt;/td&gt;
&lt;td&gt;화면만 배포 데모용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편에서는 방향을 &lt;b&gt;Postgres(Neon 등)로 바꾼다&lt;/b&gt;고 잡고,&lt;br /&gt;구체적인 클릭 경로는 서비스 UI가 자주 바뀌므로 &lt;b&gt;공통 절차&lt;/b&gt;만 적습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6) Postgres로 바꾸는 큰 그림&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) 클라우드에서 DB 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Neon / Supabase 등에서 프로젝트를 만들고&lt;br /&gt;&lt;b&gt;Connection string&lt;/b&gt;을 복사합니다. 예:&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;postgresql://USER:PASSWORD@HOST/DB?sslmode=require
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) 스키마 provider 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;prisma/schema.prisma&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;datasource db {
  provider = &quot;postgresql&quot;
  url      = env(&quot;DATABASE_URL&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Memo&lt;/code&gt; 모델은 그대로 둬도 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) 로컬 &lt;code&gt;.env&lt;/code&gt;도 잠시 Postgres로&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DATABASE_URL=&quot;postgresql://...&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음:&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;npx prisma migrate dev --name postgres-init
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(4) Vercel 환경 변수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel Project &amp;rarr; &lt;b&gt;Settings &amp;rarr; Environment Variables&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATABASE_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Postgres 연결 문자열&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Production / Preview에 체크하고 저장한 뒤 &lt;b&gt;Redeploy&lt;/b&gt;.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(5) 코드는?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;lib/memos.ts&lt;/code&gt;, Server Action, 페이지는 &lt;b&gt;그대로&lt;/b&gt;인 경우가 많습니다.&lt;br /&gt;바뀌는 건 주로:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;provider&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DATABASE_URL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;migrate 대상 DB&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 데이터 레이어를 나눠 둔 보답입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7) Vercel 빌드에서 Prisma 자주 하는 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;package.json&lt;/code&gt;에 빌드 직전 generate를 넣는 경우가 많습니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;next dev&quot;,
    &quot;build&quot;: &quot;prisma generate &amp;amp;&amp;amp; next build&quot;,
    &quot;start&quot;: &quot;next start&quot;,
    &quot;postinstall&quot;: &quot;prisma generate&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;postinstall&lt;/code&gt;: &lt;code&gt;npm install&lt;/code&gt; 직후 클라이언트 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;build&lt;/code&gt; 앞 &lt;code&gt;prisma generate&lt;/code&gt;: Vercel 빌드 환경에서 타입/엔진 준비&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션을 배포 파이프라인에 넣을지는 팀마다 다릅니다.&lt;br /&gt;혼자 연습이면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;로컬(또는 CI)에서 &lt;code&gt;prisma migrate deploy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;앱은 Vercel에 배포&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서로 시작하는 편이 단순합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8) 배포 후 체크리스트&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;https://프로젝트.vercel.app&lt;/code&gt; 접속&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memos&lt;/code&gt; 목록&lt;/li&gt;
&lt;li&gt;새 메모 작성 &amp;rarr; 새로고침 후에도 남는지&lt;/li&gt;
&lt;li&gt;수정 / 삭제&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안 되면 Vercel &lt;b&gt;Deployments &amp;rarr; 해당 배포 &amp;rarr; Logs / Functions&lt;/b&gt; 에서&lt;br /&gt;&lt;code&gt;DATABASE_URL&lt;/code&gt;, Prisma 에러를 확인합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 하는 실수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. &lt;code&gt;.env&lt;/code&gt;를 GitHub에 올림&lt;/b&gt;&lt;br /&gt;&amp;rarr; 비밀 유출. 올렸다면 키 교체 + git 기록 정리 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Vercel에 &lt;code&gt;DATABASE_URL&lt;/code&gt;을 안 넣음&lt;/b&gt;&lt;br /&gt;&amp;rarr; 로컬에만 있어서 배포 환경에서 DB 연결 실패&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. SQLite 파일 그대로 배포 기대&lt;/b&gt;&lt;br /&gt;&amp;rarr; 서버리스와 구조적으로 안 맞음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Root Directory 잘못 지정&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;toyproject&lt;/code&gt; 루트를 배포해 &lt;code&gt;memo-app&lt;/code&gt;을 못 찾음&lt;br /&gt;&amp;rarr; Vercel에서 Root를 &lt;code&gt;memo-app&lt;/code&gt;으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. &lt;code&gt;npm run build&lt;/code&gt;를 로컬에서 안 해보고 바로 배포&lt;/b&gt;&lt;br /&gt;&amp;rarr; 같은 에러를 Vercel 로그에서만 헤맴&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Vercel 배포 = Git 연결 + 빌드 + (필요 시) 환경 변수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;화면 배포&lt;/b&gt;와 &lt;b&gt;DB&lt;/b&gt;는 별개다&lt;/li&gt;
&lt;li&gt;SQLite 파일은 로컬용, Vercel에는 외부 DB가 맞다&lt;/li&gt;
&lt;li&gt;Prisma면 &lt;code&gt;provider&lt;/code&gt; + &lt;code&gt;DATABASE_URL&lt;/code&gt;만 바꿔도 앱 코드는 유지하기 쉽다&lt;/li&gt;
&lt;li&gt;배포 전에 &lt;code&gt;npm run build&lt;/code&gt;로 먼저 검증한다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습을 어디까지 하면 되나 (추천)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 없다면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GitHub + Vercel로 &lt;b&gt;앱 화면&lt;/b&gt;만 배포해 보기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memos&lt;/code&gt;가 DB 때문에 에러 나는 걸 &lt;b&gt;로그로 확인&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;그래서 외부 DB가 필요하다&amp;rdquo;를 글로 정리&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 있다면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;4&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Neon(또는 Supabase) Postgres 연결&lt;/li&gt;
&lt;li&gt;migrate + Vercel &lt;code&gt;DATABASE_URL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CRUD가 배포 URL에서 동작하는지 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그에는 둘 다 솔직히 적어도 좋습니다.&lt;br /&gt;완벽 배포보다 &lt;b&gt;왜 SQLite가 안 되는지&lt;/b&gt;를 쓰는 편이 독자에게 더 가치가 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈를 마치며 (실전 1~3)&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;편&lt;/th&gt;
&lt;th&gt;한 줄&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;수정&amp;middot;삭제로 CRUD 완성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Prisma + SQLite로 영속화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Vercel 배포 + 외부 DB 필요성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입문 8편 + 실전 3편이면:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 경험 &amp;rarr; Next App Router + TS &amp;rarr; CRUD &amp;rarr; DB &amp;rarr; 배포 이슈까지&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 축이 이어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에 이어서 쓰기 좋은 주제:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Neon Postgres 연결 실습 (스크린샷 중심)&lt;/li&gt;
&lt;li&gt;폼 검증 (zod + &lt;code&gt;useActionState&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;로그인 (Clerk / Auth.js)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 목차 (실전)&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;메모 수정과 삭제&lt;/li&gt;
&lt;li&gt;Prisma + SQLite로 DB 붙이기&lt;/li&gt;
&lt;li&gt;Vercel에 배포하기 &amp;larr; 지금&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/790</guid>
      <comments>https://aamoos.tistory.com/790#entry790comment</comments>
      <pubDate>Thu, 30 Jul 2026 10:48:06 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS 실전] 2. Prisma + SQLite로 DB 붙이기</title>
      <link>https://aamoos.tistory.com/789</link>
      <description>&lt;h1&gt;[Next.js + TS 실전] 2. Prisma + SQLite로 DB 붙이기&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실전 1편까지 CRUD는 완성됐습니다.&lt;br /&gt;문제는 &lt;b&gt;서버를 재시작하면 메모가 사라진다는 것&lt;/b&gt;입니다.&lt;br /&gt;이번 편은 Prisma + SQLite로 &lt;b&gt;진짜 저장&lt;/b&gt;을 붙입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Prisma + SQLite 설치&amp;middot;초기화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Memo&lt;/code&gt; 테이블(스키마) 정의&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lib/prisma.ts&lt;/code&gt; 클라이언트 만들기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lib/memos.ts&lt;/code&gt;를 배열 &amp;rarr; DB 쿼리로 교체&lt;/li&gt;
&lt;li&gt;시드(초기 데이터) 넣고 재시작해도 남는지 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지&amp;middot;Server Action 코드는 &lt;b&gt;거의 안 건드립니다.&lt;/b&gt;&lt;br /&gt;바뀌는 건 주로 &lt;code&gt;lib/memos.ts&lt;/code&gt;입니다. 이게 이번 편의 핵심입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 Prisma + SQLite인가&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;선택&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SQLite&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;파일 하나로 동작. 로컬 학습에 제일 단순&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Prisma&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;스키마 &amp;rarr; 타입 생성. TypeScript와 궁합이 좋음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 PostgreSQL로 바꿀 때도, 스키마의 &lt;code&gt;provider&lt;/code&gt;와 &lt;code&gt;DATABASE_URL&lt;/code&gt;만 바꾸면 비슷한 코드로 갈 수 있습니다.&lt;br /&gt;지금은 &lt;b&gt;로컬에서 영속화&lt;/b&gt;가 목표입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 그림&lt;/h2&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;페이지 / Server Action
        &amp;darr;
   lib/memos.ts   &amp;larr; 함수 이름은 그대로
        &amp;darr;
   lib/prisma.ts
        &amp;darr;
   SQLite 파일 (prisma/dev.db)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;getMemos&lt;/code&gt;, &lt;code&gt;addMemo&lt;/code&gt; 같은 &lt;b&gt;함수 시그니처를 유지&lt;/b&gt;하면&lt;br /&gt;&lt;code&gt;actions.ts&lt;/code&gt;와 페이지는 그대로 두어도 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) 패키지 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;memo-app&lt;/code&gt; 폴더에서:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;npm install @prisma/client
npm install -D prisma
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기화 (SQLite):&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;npx prisma init --datasource-provider sqlite
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성되는 것:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;prisma/schema.prisma
.env
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.env&lt;/code&gt; 예시:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DATABASE_URL=&quot;file:./dev.db&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;file:./dev.db&lt;/code&gt;는 &lt;code&gt;prisma/&lt;/code&gt; 폴더 기준 상대 경로입니다.&lt;br /&gt;실제 DB 파일은 &lt;code&gt;prisma/dev.db&lt;/code&gt;에 생깁니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.env&lt;/code&gt;는 Git에 올리지 마세요.&lt;br /&gt;&lt;code&gt;create-next-app&lt;/code&gt;이 &lt;code&gt;.gitignore&lt;/code&gt;에 &lt;code&gt;.env*&lt;/code&gt;를 넣어 둔 경우가 많습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) 스키마 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;prisma/schema.prisma&lt;/code&gt;를 아래처럼 맞춥니다.&lt;/p&gt;
&lt;pre class=&quot;puppet&quot;&gt;&lt;code&gt;generator client {
  provider = &quot;prisma-client-js&quot;
}

datasource db {
  provider = &quot;sqlite&quot;
  url      = env(&quot;DATABASE_URL&quot;)
}

model Memo {
  id        String   @id @default(cuid())
  title     String
  content   String
  createdAt DateTime @default(now())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기본 키&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@default(cuid())&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;고유 id 자동 생성 (&lt;code&gt;Date.now()&lt;/code&gt; 대체)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DateTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DB에는 날짜 타입, 화면에는 문자열로 변환해서 씀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@default(now())&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;생성 시각 자동&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블을 만들고 Prisma Client를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;npx prisma migrate dev --name init
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;prisma/dev.db&lt;/code&gt; 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prisma/migrations/&lt;/code&gt; 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@prisma/client&lt;/code&gt; 타입이 갱신됨&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) Prisma Client 싱글톤&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 개발 모드에서는 Hot Reload 때문에&lt;br /&gt;&lt;code&gt;PrismaClient&lt;/code&gt;를 매번 &lt;code&gt;new&lt;/code&gt; 하면 연결이 쌓일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로: &lt;code&gt;lib/prisma.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { PrismaClient } from &quot;@prisma/client&quot;;

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== &quot;production&quot;) {
  globalForPrisma.prisma = prisma;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발에서만 &lt;code&gt;globalThis&lt;/code&gt;에 붙여 두는 패턴입니다.&lt;br /&gt;공식 문서에서도 비슷한 예시를 씁니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) &lt;code&gt;lib/memos.ts&lt;/code&gt;를 DB로 교체&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열과 &lt;code&gt;delay&lt;/code&gt;를 지우고, Prisma로 바꿉니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import type { Memo } from &quot;@/types/memo&quot;;
import { prisma } from &quot;@/lib/prisma&quot;;

function toMemo(row: {
  id: string;
  title: string;
  content: string;
  createdAt: Date;
}): Memo {
  return {
    id: row.id,
    title: row.title,
    content: row.content,
    createdAt: row.createdAt.toISOString().slice(0, 10),
  };
}

export async function getMemos(): Promise&amp;lt;Memo[]&amp;gt; {
  const rows = await prisma.memo.findMany({
    orderBy: { createdAt: &quot;desc&quot; },
  });

  return rows.map(toMemo);
}

export async function getMemoById(id: string): Promise&amp;lt;Memo | undefined&amp;gt; {
  const row = await prisma.memo.findUnique({
    where: { id },
  });

  return row ? toMemo(row) : undefined;
}

export async function addMemo(input: {
  title: string;
  content: string;
}): Promise&amp;lt;Memo&amp;gt; {
  const row = await prisma.memo.create({
    data: {
      title: input.title,
      content: input.content,
    },
  });

  return toMemo(row);
}

export async function updateMemo(
  id: string,
  input: { title: string; content: string }
): Promise&amp;lt;Memo | undefined&amp;gt; {
  try {
    const row = await prisma.memo.update({
      where: { id },
      data: {
        title: input.title,
        content: input.content,
      },
    });

    return toMemo(row);
  } catch {
    return undefined;
  }
}

export async function deleteMemo(id: string): Promise&amp;lt;boolean&amp;gt; {
  try {
    await prisma.memo.delete({
      where: { id },
    });
    return true;
  } catch {
    return false;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;함수 이름&amp;middot;반환 타입은 그대로&lt;/b&gt; &amp;rarr; Action/페이지 수정 최소&lt;/li&gt;
&lt;li&gt;&lt;code&gt;toMemo&lt;/code&gt;로 DB &lt;code&gt;Date&lt;/code&gt; &amp;rarr; 화면용 &lt;code&gt;string&lt;/code&gt; 변환&lt;/li&gt;
&lt;li&gt;없는 id update/delete는 Prisma가 에러 &amp;rarr; &lt;code&gt;try/catch&lt;/code&gt;로 &lt;code&gt;undefined&lt;/code&gt; / &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;가짜 &lt;code&gt;delay&lt;/code&gt;는 제거 (로딩 UI는 DB 속도에 맡김)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;types/memo.ts&lt;/code&gt;의 &lt;code&gt;Memo&lt;/code&gt; 타입은 &lt;b&gt;그대로 둡니다.&lt;/b&gt;&lt;br /&gt;DB 모델과 화면 모델을 &lt;code&gt;toMemo&lt;/code&gt;로 연결하는 방식입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5) 초기 데이터(시드) 넣기 (선택)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB가 비어 있으면 목록이 텅 빕니다.&lt;br /&gt;연습용 데이터를 넣고 싶다면:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;prisma/seed.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { PrismaClient } from &quot;@prisma/client&quot;;

const prisma = new PrismaClient();

async function main() {
  await prisma.memo.createMany({
    data: [
      {
        title: &quot;Next.js 시작&quot;,
        content: &quot;App Router로 프로젝트 만들었다.&quot;,
      },
      {
        title: &quot;TypeScript props&quot;,
        content: &quot;MemoCard에 타입을 붙였다.&quot;,
      },
    ],
  });
}

main()
  .then(() =&amp;gt; prisma.$disconnect())
  .catch(async (e) =&amp;gt; {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;package.json&lt;/code&gt; &lt;b&gt;맨 바깥&lt;/b&gt;에 &lt;code&gt;prisma&lt;/code&gt; 키를 추가합니다.&lt;br /&gt;(주의: &lt;code&gt;{ &quot;prisma&quot;: ... }&lt;/code&gt;를 파일에 따로 하나 더 붙이면 JSON이 깨집니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;memo-app&quot;,
  &quot;scripts&quot;: { &quot;...&quot;: &quot;...&quot; },
  &quot;dependencies&quot;: { &quot;...&quot;: &quot;...&quot; },
  &quot;devDependencies&quot;: { &quot;...&quot;: &quot;...&quot; },
  &quot;prisma&quot;: {
    &quot;seed&quot;: &quot;npx tsx prisma/seed.ts&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;tsx&lt;/code&gt;가 없으면:&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install -D tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시드 전에 반드시 마이그레이션을 먼저 하세요. (&lt;code&gt;Memo&lt;/code&gt; 테이블이 있어야 함)&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;npx prisma migrate dev --name init
npx prisma db seed
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 Prisma Studio로 직접 넣어도 됩니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;npx prisma studio
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 테이블을 GUI로 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6) 동작 확인&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;npm run dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memos/new&lt;/code&gt;에서 메모 추가&lt;/li&gt;
&lt;li&gt;터미널에서 &lt;b&gt;서버 중지 후 다시&lt;/b&gt; &lt;code&gt;npm run dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memos&lt;/code&gt;에 방금 메모가 &lt;b&gt;남아 있으면&lt;/b&gt; 성공&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전(메모리 배열)이라면 여기서 사라졌을 겁니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Action / 페이지는?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 &lt;b&gt;수정할 필요 없습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;createMemo&lt;/code&gt;, &lt;code&gt;updateMemoAction&lt;/code&gt;, &lt;code&gt;deleteMemoAction&lt;/code&gt;은&lt;br /&gt;이미 &lt;code&gt;addMemo&lt;/code&gt; / &lt;code&gt;updateMemo&lt;/code&gt; / &lt;code&gt;deleteMemo&lt;/code&gt;만 호출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;lib/memos.ts&lt;/code&gt; 구현만 바뀌었을 뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 좋은 설계입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI &amp;middot; Action &amp;middot; 데이터 레이어를 나누면,&lt;br /&gt;저장소만 바꿔도 화면 코드는 버틸 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;.gitignore&lt;/code&gt; 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래가 무시되는지 확인하세요.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;.env
.env*.local
prisma/dev.db
prisma/dev.db-journal
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 파일과 환경 변수는 저장소에 올리지 않는 편이 안전합니다.&lt;br /&gt;팀원/다른 PC에서는 &lt;code&gt;migrate&lt;/code&gt;로 스키마를 다시 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;npx prisma migrate dev
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 하는 실수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. &lt;code&gt;migrate&lt;/code&gt;를 안 돌림&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;prisma.memo&lt;/code&gt;가 없거나 테이블이 없어 에러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. &lt;code&gt;lib/prisma.ts&lt;/code&gt; 없이 매 파일에서 &lt;code&gt;new PrismaClient()&lt;/code&gt;&lt;/b&gt;&lt;br /&gt;&amp;rarr; 개발 중 연결 폭주 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. &lt;code&gt;createdAt&lt;/code&gt;을 Date 그대로 JSX에 뿌림&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;toMemo&lt;/code&gt;로 문자열 변환 권장 (기존 UI와 맞춤)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. &lt;code&gt;.env&lt;/code&gt;의 &lt;code&gt;DATABASE_URL&lt;/code&gt; 오타&lt;/b&gt;&lt;br /&gt;&amp;rarr; DB 파일을 못 찾아 실패&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 예전에 쓰던 id &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;를 하드코딩한 링크&lt;/b&gt;&lt;br /&gt;&amp;rarr; 이제 id는 &lt;code&gt;cuid()&lt;/code&gt;라 긴 문자열입니다. UI의 Link는 &lt;code&gt;memo.id&lt;/code&gt;를 쓰면 문제 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. loading이 너무 짧아져서 &amp;ldquo;로딩이 안 보여요&amp;rdquo;&lt;/b&gt;&lt;br /&gt;&amp;rarr; 정상입니다. 원하면 &lt;code&gt;loading.tsx&lt;/code&gt;는 그대로 두고, 느린 네트워크에서만 눈에 띕니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 배열은 학습용, 실서비스는 DB가 필요하다&lt;/li&gt;
&lt;li&gt;Prisma = 스키마 + 타입 안전한 쿼리&lt;/li&gt;
&lt;li&gt;SQLite = 로컬에 파일 하나로 시작하기 좋다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 레이어만 바꾸면&lt;/b&gt; CRUD UI/Action은 재사용된다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;toMemo&lt;/code&gt;로 DB 모델과 화면 타입을 연결한다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Next.js + TS 실전] 3. Vercel에 배포하기&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌드&amp;middot;환경 변수&lt;/li&gt;
&lt;li&gt;SQLite의 한계와 배포 시 선택지&lt;/li&gt;
&lt;li&gt;메모 앱을 URL로 열어보기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 DB까지 붙였으니, 다음은 &lt;b&gt;다른 사람이 접속할 수 있게&lt;/b&gt; 만드는 단계입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 한 줄: Vercel 같은 서버리스 환경에서는 SQLite 파일 DB가 잘 안 맞을 수 있습니다.&lt;br /&gt;다음 편에서 &amp;ldquo;왜&amp;rdquo;와 대안(예: Postgres)을 같이 정리합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 목차 (실전)&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;메모 수정과 삭제&lt;/li&gt;
&lt;li&gt;Prisma + SQLite로 DB 붙이기 &amp;larr; 지금&lt;/li&gt;
&lt;li&gt;Vercel 배포 (예정)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입문&amp;middot;실전 1편과의 연결&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;이전&lt;/th&gt;
&lt;th&gt;이번&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;const memos = [...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prisma.memo.findMany&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Date.now()&lt;/code&gt; id&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cuid()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버 재시작 &amp;rarr; 데이터 소멸&lt;/td&gt;
&lt;td&gt;서버 재시작 &amp;rarr; 데이터 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delay()&lt;/code&gt;로 로딩 연습&lt;/td&gt;
&lt;td&gt;실제 I/O&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 이름은 같고, &lt;b&gt;속이 DB로 바뀐 것&lt;/b&gt;이 이번 편의 전부입니다.&lt;/p&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/789</guid>
      <comments>https://aamoos.tistory.com/789#entry789comment</comments>
      <pubDate>Thu, 30 Jul 2026 10:15:15 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS 실전] 1. 메모 수정과 삭제</title>
      <link>https://aamoos.tistory.com/788</link>
      <description>&lt;h1&gt;[Next.js + TS 실전] 1. 메모 수정과 삭제&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입문 시리즈(1~8편)에서 &lt;b&gt;추가&lt;/b&gt;까지 했다면, 이번 편은 &lt;b&gt;수정&amp;middot;삭제&lt;/b&gt;로 CRUD를 마칩니다.&lt;br /&gt;목표: &lt;code&gt;updateMemo&lt;/code&gt; / &lt;code&gt;deleteMemo&lt;/code&gt; &amp;middot; Server Action 확장 &amp;middot; 수정 페이지.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;lib/memos.ts&lt;/code&gt;에 수정&amp;middot;삭제 함수 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;actions.ts&lt;/code&gt;에 Server Action 2개 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memos/[id]/edit&lt;/code&gt; 수정 페이지 만들기&lt;/li&gt;
&lt;li&gt;상세 페이지에 수정 링크 &amp;middot; 삭제 버튼 붙이기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7편에서 만든 &lt;b&gt;추가(create)&lt;/b&gt; 흐름을 그대로 확장합니다.&lt;br /&gt;새 개념보다 &lt;b&gt;같은 패턴 반복&lt;/b&gt;이 목표입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CRUD 중 지금 어디까지?&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;상태&lt;/th&gt;
&lt;th&gt;이번 편&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create&lt;/td&gt;
&lt;td&gt;✅ 7편&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read&lt;/td&gt;
&lt;td&gt;✅ 4~6편&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Create를 이미 Server Action으로 했으니, Update/Delete도 &lt;b&gt;폼 + Server Action&lt;/b&gt;으로 가면 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) 데이터 함수: 수정&amp;middot;삭제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;lib/memos.ts&lt;/code&gt; 하단에 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;export async function updateMemo(
  id: string,
  input: { title: string; content: string }
): Promise&amp;lt;Memo | undefined&amp;gt; {
  await delay(300);

  const index = memos.findIndex((memo) =&amp;gt; memo.id === id);
  if (index === -1) {
    return undefined;
  }

  memos[index] = {
    ...memos[index],
    title: input.title,
    content: input.content,
  };

  return memos[index];
}

export async function deleteMemo(id: string): Promise&amp;lt;boolean&amp;gt; {
  await delay(300);

  const index = memos.findIndex((memo) =&amp;gt; memo.id === id);
  if (index === -1) {
    return false;
  }

  memos.splice(index, 1);
  return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;updateMemo&lt;/code&gt; &amp;rarr; 못 찾으면 &lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deleteMemo&lt;/code&gt; &amp;rarr; 못 찾으면 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createdAt&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;는 그대로 두고 &lt;b&gt;제목&amp;middot;내용만&lt;/b&gt; 바꿉니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) Server Action 확장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/memos/actions.ts&lt;/code&gt;에 두 함수를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;&quot;use server&quot;;

import { redirect } from &quot;next/navigation&quot;;
import { revalidatePath } from &quot;next/cache&quot;;
import { addMemo, deleteMemo, updateMemo } from &quot;@/lib/memos&quot;;

export async function createMemo(formData: FormData) {
  const title = String(formData.get(&quot;title&quot;) ?? &quot;&quot;).trim();
  const content = String(formData.get(&quot;content&quot;) ?? &quot;&quot;).trim();

  if (!title || !content) {
    throw new Error(&quot;제목과 내용을 모두 입력하세요&quot;);
  }

  await addMemo({ title, content });

  revalidatePath(&quot;/memos&quot;);
  redirect(&quot;/memos&quot;);
}

export async function updateMemoAction(formData: FormData) {
  const id = String(formData.get(&quot;id&quot;) ?? &quot;&quot;).trim();
  const title = String(formData.get(&quot;title&quot;) ?? &quot;&quot;).trim();
  const content = String(formData.get(&quot;content&quot;) ?? &quot;&quot;).trim();

  if (!id || !title || !content) {
    throw new Error(&quot;제목과 내용을 모두 입력하세요&quot;);
  }

  const updated = await updateMemo(id, { title, content });

  if (!updated) {
    throw new Error(&quot;메모를 찾을 수 없습니다&quot;);
  }

  revalidatePath(&quot;/memos&quot;);
  revalidatePath(`/memos/${id}`);
  redirect(`/memos/${id}`);
}

export async function deleteMemoAction(formData: FormData) {
  const id = String(formData.get(&quot;id&quot;) ?? &quot;&quot;).trim();

  if (!id) {
    throw new Error(&quot;삭제할 메모 id가 없습니다&quot;);
  }

  const deleted = await deleteMemo(id);

  if (!deleted) {
    throw new Error(&quot;메모를 찾을 수 없습니다&quot;);
  }

  revalidatePath(&quot;/memos&quot;);
  redirect(&quot;/memos&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7편과 비교:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;create&lt;/th&gt;
&lt;th&gt;update&lt;/th&gt;
&lt;th&gt;delete&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;입력&lt;/td&gt;
&lt;td&gt;title, content&lt;/td&gt;
&lt;td&gt;id, title, content&lt;/td&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성공 후&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/memos&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/memos/[id]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/memos&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;revalidate&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/memos&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/memos&lt;/code&gt;, &lt;code&gt;/memos/[id]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/memos&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정은 &lt;b&gt;목록 + 상세&lt;/b&gt; 둘 다 갱신하는 게 안전합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) 수정 페이지: &lt;code&gt;/memos/[id]/edit&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폴더와 파일을 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;app/memos/[id]/edit/page.tsx&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;import { notFound } from &quot;next/navigation&quot;;
import { getMemoById } from &quot;@/lib/memos&quot;;
import { updateMemoAction } from &quot;@/app/memos/actions&quot;;

type EditMemoPageProps = {
  params: Promise&amp;lt;{ id: string }&amp;gt;;
};

export default async function EditMemoPage({ params }: EditMemoPageProps) {
  const { id } = await params;
  const memo = await getMemoById(id);

  if (!memo) {
    notFound();
  }

  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;메모 수정&amp;lt;/h1&amp;gt;

      &amp;lt;form action={updateMemoAction}&amp;gt;
        &amp;lt;input type=&quot;hidden&quot; name=&quot;id&quot; value={memo.id} /&amp;gt;

        &amp;lt;p&amp;gt;
          &amp;lt;label&amp;gt;
            제목
            &amp;lt;br /&amp;gt;
            &amp;lt;input
              type=&quot;text&quot;
              name=&quot;title&quot;
              defaultValue={memo.title}
              required
            /&amp;gt;
          &amp;lt;/label&amp;gt;
        &amp;lt;/p&amp;gt;

        &amp;lt;p&amp;gt;
          &amp;lt;label&amp;gt;
            내용
            &amp;lt;br /&amp;gt;
            &amp;lt;textarea
              name=&quot;content&quot;
              rows={5}
              defaultValue={memo.content}
              required
            /&amp;gt;
          &amp;lt;/label&amp;gt;
        &amp;lt;/p&amp;gt;

        &amp;lt;button type=&quot;submit&quot;&amp;gt;저장&amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;새 메모&lt;/b&gt;와 거의 같은 폼, &lt;code&gt;defaultValue&lt;/code&gt;로 기존 값 채움&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hidden&lt;/code&gt; input으로 &lt;code&gt;id&lt;/code&gt;를 Action에 전달&lt;/li&gt;
&lt;li&gt;&lt;code&gt;'use client'&lt;/code&gt; 없음 &amp;rarr; 서버 컴포넌트 + Server Action 조합&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/memos/1/edit&lt;/code&gt; 에서 제목을 바꾸고 저장하면 상세 페이지 내용이 바뀌어야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) 상세 페이지에 수정&amp;middot;삭제 UI&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/memos/[id]/page.tsx&lt;/code&gt;를 아래처럼 확장합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Link from &quot;next/link&quot;;
import { notFound } from &quot;next/navigation&quot;;
import { deleteMemoAction } from &quot;@/app/memos/actions&quot;;
import { getMemoById } from &quot;@/lib/memos&quot;;

type MemoDetailPageProps = {
  params: Promise&amp;lt;{ id: string }&amp;gt;;
};

export default async function MemoDetailPage({ params }: MemoDetailPageProps) {
  const { id } = await params;
  const memo = await getMemoById(id);

  if (!memo) {
    notFound();
  }

  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;{memo.title}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{memo.content}&amp;lt;/p&amp;gt;
      &amp;lt;time dateTime={memo.createdAt}&amp;gt;{memo.createdAt}&amp;lt;/time&amp;gt;

      &amp;lt;p&amp;gt;
        &amp;lt;Link href={`/memos/${memo.id}/edit`}&amp;gt;수정&amp;lt;/Link&amp;gt;
      &amp;lt;/p&amp;gt;

      &amp;lt;form action={deleteMemoAction}&amp;gt;
        &amp;lt;input type=&quot;hidden&quot; name=&quot;id&quot; value={memo.id} /&amp;gt;
        &amp;lt;button type=&quot;submit&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;수정&lt;/b&gt; &amp;rarr; Link로 edit 페이지 이동&lt;/li&gt;
&lt;li&gt;&lt;b&gt;삭제&lt;/b&gt; &amp;rarr; 별도 &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; + &lt;code&gt;deleteMemoAction&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제는 &lt;code&gt;onClick&lt;/code&gt; + &lt;code&gt;fetch&lt;/code&gt;가 아니라 &lt;b&gt;폼 제출&lt;/b&gt;로 처리합니다.&lt;br /&gt;7편 추가와 같은 Server Action 패턴입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동작 흐름&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수정&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;/memos/1/edit 에서 저장
    &amp;darr;
updateMemoAction
    &amp;darr;
updateMemo (배열에서 해당 항목 교체)
    &amp;darr;
revalidatePath(&quot;/memos&quot;, &quot;/memos/1&quot;)
    &amp;darr;
redirect(&quot;/memos/1&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삭제&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;/memos/1 에서 [삭제] 클릭
    &amp;darr;
deleteMemoAction
    &amp;darr;
deleteMemo (배열에서 splice)
    &amp;darr;
revalidatePath(&quot;/memos&quot;)
    &amp;darr;
redirect(&quot;/memos&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 삭제도 form + action인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클릭 핸들러로도 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;&quot;use client&quot;;
// onClick &amp;rarr; fetch('/api/...') ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 지금 앱은 &lt;b&gt;Server Action 중심&lt;/b&gt;입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 라우트를 따로 안 만들어도 됨&lt;/li&gt;
&lt;li&gt;create/update/delete가 &lt;b&gt;같은 파일&amp;middot;같은 패턴&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;서버에서만 데이터 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 &amp;ldquo;삭제 전 확인(confirm)&amp;rdquo;을 넣을 때 클라이언트 버튼이 필요해질 수 있습니다.&lt;br /&gt;입문+실전 1편에서는 &lt;b&gt;가장 단순한 form 삭제&lt;/b&gt;로 충분합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 하는 실수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 수정 폼에 &lt;code&gt;id&lt;/code&gt; hidden을 빼먹음&lt;/b&gt;&lt;br /&gt;&amp;rarr; Action에서 어떤 메모를 고칠지 모릅니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. &lt;code&gt;value&lt;/code&gt; 대신 controlled input을 서버에서 쓰려 함&lt;/b&gt;&lt;br /&gt;&amp;rarr; 서버 폼은 &lt;code&gt;defaultValue&lt;/code&gt;가 맞습니다 (&lt;code&gt;value&lt;/code&gt; + onChange는 Client)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. update 후 &lt;code&gt;revalidatePath('/memos/1')&lt;/code&gt;만 하고 목록은 안 함&lt;/b&gt;&lt;br /&gt;&amp;rarr; 목록 카드 제목이 옛날로 남을 수 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. &lt;code&gt;action={deleteMemoAction()}&lt;/code&gt; 처럼 호출&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;action={deleteMemoAction}&lt;/code&gt; (함수 참조)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 삭제 후 상세 URL에 그대로 머무름&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;redirect('/memos')&lt;/code&gt;로 보내는 게 자연스럽습니다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CRUD는 Create 패턴을 Update/Delete로 확장하면 된다&lt;/li&gt;
&lt;li&gt;수정&amp;middot;삭제도 Server Action + &lt;code&gt;FormData&lt;/code&gt; + hidden &lt;code&gt;id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;수정 페이지는 &lt;code&gt;/memos/[id]/edit&lt;/code&gt; + &lt;code&gt;defaultValue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;revalidatePath&lt;/code&gt;는 바뀌는 경로를 모두 호출&lt;/li&gt;
&lt;li&gt;지금은 메모리 배열, 나중에 DB로 함수만 교체&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Next.js + TS 실전] 2. Prisma + SQLite로 DB 붙이기&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 재시작해도 메모가 남게&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lib/memos.ts&lt;/code&gt;의 배열을 DB 쿼리로 교체&lt;/li&gt;
&lt;li&gt;CRUD를 &amp;ldquo;진짜 저장&amp;rdquo;으로 바꾸기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 목차 (실전)&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;메모 수정과 삭제 &amp;larr; 지금&lt;/li&gt;
&lt;li&gt;Prisma + SQLite로 DB 붙이기 (예정)&lt;/li&gt;
&lt;li&gt;Vercel 배포 (예정)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입문 시리즈와의 연결&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;입문&lt;/th&gt;
&lt;th&gt;실전 1편&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;addMemo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;updateMemo&lt;/code&gt;, &lt;code&gt;deleteMemo&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;createMemo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;updateMemoAction&lt;/code&gt;, &lt;code&gt;deleteMemoAction&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/memos/new&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/memos/[id]/edit&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추가 후 목록&lt;/td&gt;
&lt;td&gt;수정 후 상세, 삭제 후 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입문 7편을 이해했다면, 이번 편은 &lt;b&gt;복붙&amp;middot;수정 수준&lt;/b&gt;으로 따라올 수 있습니다.&lt;/p&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/788</guid>
      <comments>https://aamoos.tistory.com/788#entry788comment</comments>
      <pubDate>Thu, 30 Jul 2026 09:38:15 +0900</pubDate>
    </item>
    <item>
      <title># 면접 CS 정리 Day 2 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/787</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 2 (풀스택 / Java&amp;middot;Spring&amp;middot;React)&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q1. Spring Bean 스코프&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;Spring Bean 기본 스코프는 뭐고, &lt;code&gt;singleton&lt;/code&gt;이랑 &lt;code&gt;prototype&lt;/code&gt; 차이는?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 스코프는 &lt;b&gt;&lt;code&gt;singleton&lt;/code&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;singleton&lt;/code&gt;: 컨테이너당 Bean &lt;b&gt;1개&lt;/b&gt; 공유&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prototype&lt;/code&gt;: &lt;b&gt;가져올 때마다&lt;/b&gt; 새 객체 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;singleton Service에 요청별 상태(&lt;code&gt;currentUserId&lt;/code&gt; 필드 등)를 넣으면&lt;br /&gt;여러 요청이 같은 인스턴스를 공유해서 위험하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q2. Checked vs Unchecked Exception&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;&lt;code&gt;Checked Exception&lt;/code&gt;이랑 &lt;code&gt;Unchecked Exception&lt;/code&gt; 차이?&lt;br /&gt;&lt;code&gt;@Transactional&lt;/code&gt; 기본 롤백이랑 어떻게 연결되는지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Checked&lt;/b&gt;: &lt;code&gt;IOException&lt;/code&gt;, &lt;code&gt;SQLException&lt;/code&gt; 등 &amp;mdash; 컴파일러가 처리 강제&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Unchecked&lt;/b&gt;: &lt;code&gt;RuntimeException&lt;/code&gt; 계열 &amp;mdash; &lt;code&gt;IllegalArgumentException&lt;/code&gt;, &lt;code&gt;NPE&lt;/code&gt;, 보통의 &lt;code&gt;BusinessException&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Transactional&lt;/code&gt; 기본:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;unchecked&lt;/b&gt; &amp;rarr; 롤백&lt;/li&gt;
&lt;li&gt;&lt;b&gt;checked&lt;/b&gt; &amp;rarr; 롤백 안 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;checked도 롤백하려면&lt;br /&gt;&lt;code&gt;@Transactional(rollbackFor = Exception.class)&lt;/code&gt;&lt;br /&gt;(&lt;code&gt;@Rollback&lt;/code&gt; 아님)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q3. MyBatis &lt;code&gt;#{}&lt;/code&gt; vs &lt;code&gt;${}&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;MyBatis에서 &lt;code&gt;#{}&lt;/code&gt;이랑 &lt;code&gt;${}&lt;/code&gt; 차이? 왜 기본은 &lt;code&gt;#{}&lt;/code&gt;를 쓰는지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;#{}&lt;/code&gt;: PreparedStatement &lt;b&gt;파라미터 바인딩&lt;/b&gt; &amp;rarr; SQL Injection 방어에 유리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;${}&lt;/code&gt;: 문자열 &lt;b&gt;그대로 치환&lt;/b&gt; &amp;rarr; 위험&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본은 &lt;code&gt;#{}&lt;/code&gt;.&lt;br /&gt;&lt;code&gt;${}&lt;/code&gt;는 테이블명&amp;middot;컬럼명&amp;middot;ORDER BY처럼 바인딩이 안 될 때만,&lt;br /&gt;사용자 입력을 직접 넣지 말고 화이트리스트로 제한한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q4. INNER JOIN vs LEFT JOIN&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;&lt;code&gt;INNER JOIN&lt;/code&gt;이랑 &lt;code&gt;LEFT JOIN&lt;/code&gt; 차이? 매칭 안 되는 행 기준으로.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;INNER JOIN&lt;/code&gt;: 양쪽 테이블에서 &lt;b&gt;매칭되는 행만&lt;/b&gt; (교집합)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LEFT JOIN&lt;/code&gt;: &lt;b&gt;왼쪽 테이블 전부&lt;/b&gt; + 오른쪽이 없으면 &lt;code&gt;NULL&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q5. accessToken / refreshToken&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;JWT 로그인에서 accessToken이랑 refreshToken을 왜 나누는지?&lt;br /&gt;access는 짧게, refresh는 길게 두는 이유는?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;br /&gt;JWT는 탈취되면 만료 전까지 쓰일 수 있다.&lt;br /&gt;그래서 API에 쓰는 &lt;b&gt;accessToken은 짧게&lt;/b&gt; 두어 피해 구간을 줄이고,&lt;br /&gt;만료 시 &lt;b&gt;refreshToken으로 access를 재발급&lt;/b&gt;한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q6. useEffect 의존성 배열&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;React &lt;code&gt;useEffect&lt;/code&gt; 의존성 배열이 &lt;code&gt;[]&lt;/code&gt; / 없음 / &lt;code&gt;[id]&lt;/code&gt;일 때 각각 언제 실행되나?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;[]&lt;/code&gt; &amp;rarr; &lt;b&gt;마운트 시 1번&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;없음 &amp;rarr; &lt;b&gt;매 렌더마다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[id]&lt;/code&gt; &amp;rarr; 마운트 + &lt;b&gt;&lt;code&gt;id&lt;/code&gt; 변경 시&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q7. DB 중복 신청 방지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;같은 휴가/출퇴근이 동시에 두 번 신청되는 걸 막으려면&lt;br /&gt;앱 클릭 방지 말고 DB에서 쓸 수 있는 방법은?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;br /&gt;&lt;code&gt;(user_id, 날짜)&lt;/code&gt; 같은 컬럼 조합에 &lt;b&gt;UNIQUE 제약&lt;/b&gt;을 건다.&lt;br /&gt;동시 요청이 와도 하나는 중복 키 에러로 막힌다.&lt;br /&gt;(프론트 연속클릭 방지만으론 부족 &amp;mdash; DB가 최종 방어선)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Bean 기본 스코프 = &lt;code&gt;singleton&lt;/code&gt; / &lt;code&gt;prototype&lt;/code&gt; = 매번 새로&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Transactional&lt;/code&gt; 기본 롤백 = unchecked만&lt;/li&gt;
&lt;li&gt;MyBatis &lt;code&gt;#{}&lt;/code&gt; 바인딩 / &lt;code&gt;${}&lt;/code&gt; 치환&lt;/li&gt;
&lt;li&gt;access 짧게 + refresh로 재발급&lt;/li&gt;
&lt;li&gt;중복 방지 = UNIQUE 제약&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>기타/CS 질문</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/787</guid>
      <comments>https://aamoos.tistory.com/787#entry787comment</comments>
      <pubDate>Thu, 30 Jul 2026 09:25:50 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS] 8. 정리 + 다음 학습 로드맵</title>
      <link>https://aamoos.tistory.com/786</link>
      <description>&lt;h1&gt;[Next.js + TS] 8. 정리 + 다음 학습 로드맵&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1~7편에서 작은 &lt;b&gt;메모 앱&lt;/b&gt;을 만들며 Next.js + TypeScript의 핵심 흐름을 따라 왔습니다.&lt;br /&gt;마지막 편은 &lt;b&gt;복습&lt;/b&gt;, &lt;b&gt;지금 앱의 한계&lt;/b&gt;, &lt;b&gt;다음에 공부할 순서&lt;/b&gt;를 정리합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈를 마치면서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 React만으로도 화면은 만들 수 있다는 이야기에서 시작했습니다.&lt;br /&gt;끝날 즈음에는 이런 흐름을 직접 만들었을 겁니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;/              &amp;rarr; 소개
/memos         &amp;rarr; 목록 (async fetch + loading/error)
/memos/new     &amp;rarr; 작성 (Server Action)
/memos/[id]    &amp;rarr; 상세&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 경험이 있는 사람이 &lt;b&gt;Next.js App Router + TypeScript&lt;/b&gt;로 넘어올 때 필요한 것만 골라 다뤘습니다.&lt;br /&gt;고급 패턴 전부를 다룬 시리즈는 아닙니다.&lt;br /&gt;대신 &amp;ldquo;실제 앱을 한 바퀴 도는 감각&amp;rdquo;을 목표로 했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1~7편 한눈에 복습&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;편&lt;/th&gt;
&lt;th&gt;핵심 한 줄&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Next = React 위의 앱 프레임워크, TS = 타입 계약, App Router = 폴더가 URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;create-next-app&lt;/code&gt;, &lt;code&gt;app/page.tsx&lt;/code&gt;가 곧 홈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Memo&lt;/code&gt; 타입 + &lt;code&gt;MemoCard&lt;/code&gt; props&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/memos&lt;/code&gt;, &lt;code&gt;/memos/[id]&lt;/code&gt;, &lt;code&gt;layout&lt;/code&gt;에 공통 내비&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;기본은 Server, hooks/이벤트만 Client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;async&lt;/code&gt; 페이지, &lt;code&gt;loading.tsx&lt;/code&gt;, &lt;code&gt;error.tsx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Server Action + &lt;code&gt;FormData&lt;/code&gt; + &lt;code&gt;revalidatePath&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 편의 연결고리는 이겁니다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;타입(Memo)
  &amp;darr;
컴포넌트(MemoCard)
  &amp;darr;
라우팅(/memos, /memos/[id])
  &amp;darr;
서버/클라이언트 분리(LikeButton)
  &amp;darr;
데이터 가져오기(getMemos)
  &amp;darr;
데이터 쓰기(createMemo)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번에 외우려 하지 말고, &lt;b&gt;URL 하나가 어떤 파일&amp;middot;어떤 데이터&amp;middot;어떤 컴포넌트로 이어지는지&lt;/b&gt;만 다시 따라가 보세요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금 만든 앱 구조&lt;/h2&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;memo-app/
  app/
    layout.tsx          &amp;rarr; 공통 내비
    page.tsx            &amp;rarr; /
    memos/
      page.tsx          &amp;rarr; 목록
      loading.tsx
      error.tsx
      actions.ts        &amp;rarr; Server Action
      new/page.tsx      &amp;rarr; 작성
      [id]/page.tsx     &amp;rarr; 상세
  components/
    MemoCard.tsx
    LikeButton.tsx
  lib/
    memos.ts            &amp;rarr; 데이터 함수
  types/
    memo.ts&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조가 App Router 앱의 &lt;b&gt;작은 정석&lt;/b&gt;에 가깝습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;app/&lt;/code&gt; = 라우트 + 페이지&lt;/li&gt;
&lt;li&gt;&lt;code&gt;components/&lt;/code&gt; = UI 조각&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lib/&lt;/code&gt; = 데이터/비즈니스 로직&lt;/li&gt;
&lt;li&gt;&lt;code&gt;types/&lt;/code&gt; = 타입 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스스로 점검하는 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래를 설명할 수 있으면 1~7편은 제대로 따라온 겁니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Next.js / App Router&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;app/page.tsx&lt;/code&gt;와 URL &lt;code&gt;/&lt;/code&gt;의 관계를 설명할 수 있다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;[id]&lt;/code&gt; 폴더가 동적 라우트인 이유를 안다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;layout.tsx&lt;/code&gt;와 &lt;code&gt;page.tsx&lt;/code&gt; 차이를 안다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;Link&lt;/code&gt;를 쓰는 이유를 안다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TypeScript&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;Memo&lt;/code&gt; 타입이 왜 필요한지 안다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 컴포넌트 props에 타입을 붙일 수 있다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;Promise&amp;lt;Memo[]&amp;gt;&lt;/code&gt; 같은 async 반환 타입의 의미를 안다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Server / Client&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Server Component가 기본값인 이유를 안다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;'use client'&lt;/code&gt;가 필요한 경우를 2가지 이상 말할 수 있다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 페이지 전체가 아니라 버튼만 Client로 분리할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;useEffect&lt;/code&gt; fetch 대신 서버에서 &lt;code&gt;await&lt;/code&gt;하는 이유를 안다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;loading.tsx&lt;/code&gt;가 언제 보이는지 안다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;error.tsx&lt;/code&gt;가 왜 &lt;code&gt;&quot;use client&quot;&lt;/code&gt;인지 대략 안다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Server Action&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;&quot;use server&quot;&lt;/code&gt;의 역할을 안다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;FormData&lt;/code&gt;와 input &lt;code&gt;name&lt;/code&gt;의 관계를 안다&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; &lt;code&gt;revalidatePath&lt;/code&gt;와 &lt;code&gt;redirect&lt;/code&gt;가 각각 뭘 하는지 안다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나라도 막히면 해당 편만 다시 보면 됩니다.&lt;br /&gt;처음부터 다시 읽을 필요는 없습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금 앱에 아직 없는 것 (정상)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈 앱은 &lt;b&gt;학습용&lt;/b&gt;입니다.&lt;br /&gt;실서비스와 비교하면 아래가 비어 있습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;없는 것&lt;/th&gt;
&lt;th&gt;왜 아직 안 했나&lt;/th&gt;
&lt;th&gt;다음에&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DB&lt;/td&gt;
&lt;td&gt;메모리 배열로 흐름만 연습&lt;/td&gt;
&lt;td&gt;Prisma, Drizzle, Supabase 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증&lt;/td&gt;
&lt;td&gt;범위 밖&lt;/td&gt;
&lt;td&gt;NextAuth, Clerk, Supabase Auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;수정/삭제&lt;/td&gt;
&lt;td&gt;추가만 다룸&lt;/td&gt;
&lt;td&gt;Server Action 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;폼 에러 UI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;throw&lt;/code&gt;만 소개&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useActionState&lt;/code&gt;, zod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스타일링&lt;/td&gt;
&lt;td&gt;최소&lt;/td&gt;
&lt;td&gt;Tailwind, CSS Module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포&lt;/td&gt;
&lt;td&gt;로컬만&lt;/td&gt;
&lt;td&gt;Vercel 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;테스트&lt;/td&gt;
&lt;td&gt;범위 밖&lt;/td&gt;
&lt;td&gt;Vitest, Playwright&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;이게 없어서 실패&amp;rdquo;가 아니라, &lt;b&gt;의도적으로 빼 둔 것&lt;/b&gt;입니다.&lt;br /&gt;한 번에 다 넣으면 Next의 핵심이 묻힙니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React만 쓸 때와 비교 (최종 정리)&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;주제&lt;/th&gt;
&lt;th&gt;React만&lt;/th&gt;
&lt;th&gt;Next App Router&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;라우팅&lt;/td&gt;
&lt;td&gt;react-router 설정&lt;/td&gt;
&lt;td&gt;폴더 = URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;첫 데이터&lt;/td&gt;
&lt;td&gt;클라이언트 fetch 흔함&lt;/td&gt;
&lt;td&gt;서버 &lt;code&gt;await&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인터랙션&lt;/td&gt;
&lt;td&gt;전부 클라이언트&lt;/td&gt;
&lt;td&gt;필요한 조각만 Client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;폼 전송&lt;/td&gt;
&lt;td&gt;API 라우트 + fetch&lt;/td&gt;
&lt;td&gt;Server Action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;공통 UI&lt;/td&gt;
&lt;td&gt;Layout 컴포넌트 직접 구성&lt;/td&gt;
&lt;td&gt;&lt;code&gt;layout.tsx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 지식은 &lt;b&gt;버리는 게 아니라 그대로 씁니다.&lt;/b&gt;&lt;br /&gt;바뀐 건 &amp;ldquo;어디서 실행되고, 어떤 파일이 페이지인지&amp;rdquo; 규칙입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 학습 로드맵 (추천 순서)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: 지금 앱을 조금 더 완성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 가까운 다음 단계입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;메모 수정/삭제&lt;/b&gt; Server Action 추가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;폼 검증&lt;/b&gt; &amp;mdash; 빈 값 말고 필드별 에러 메시지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Tailwind&lt;/b&gt;로 목록&amp;middot;카드 UI 정리 (이미 템플릿에 있으면 활용)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &amp;ldquo;읽기 + 쓰기&amp;rdquo;가 있는 CRUD 입문&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: 데이터 영속화&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;4&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;SQLite + Prisma&lt;/b&gt; 또는 &lt;b&gt;Supabase&lt;/b&gt;로 DB 연결&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lib/memos.ts&lt;/code&gt;의 메모리 배열을 DB 쿼리로 교체&lt;/li&gt;
&lt;li&gt;서버 재시작해도 데이터가 남게 만들기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &amp;ldquo;장난감 앱&amp;rdquo;에서 &amp;ldquo;저장되는 앱&amp;rdquo;으로&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: 인증과 권한&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;7&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;로그인 붙이기&lt;/li&gt;
&lt;li&gt;&amp;ldquo;내 메모만 보기&amp;rdquo; 같은 간단한 권한&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 혼자 쓰는 앱 &amp;rarr; 사용자별 앱&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4단계: 배포와 운영&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;9&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Vercel&lt;/b&gt;에 배포&lt;/li&gt;
&lt;li&gt;환경 변수 (&lt;code&gt;.env&lt;/code&gt;) 다루기&lt;/li&gt;
&lt;li&gt;빌드/프리뷰/프로덕션 차이 이해&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 로컬에서 끝나는 프로젝트 &amp;rarr; 다른 사람도 접속 가능&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5단계: 깊게 파고들 주제 (필요할 때)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Middleware, Route Handlers (&lt;code&gt;app/api/...&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;캐싱 전략 (&lt;code&gt;revalidate&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt; 옵션)&lt;/li&gt;
&lt;li&gt;Parallel Routes, Intercepting Routes&lt;/li&gt;
&lt;li&gt;React Query와 Server Component 조합&lt;/li&gt;
&lt;li&gt;E2E 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한꺼번에 X. &lt;b&gt;2단계 DB 붙이기&lt;/b&gt;까지 가면 실무 감각이 확 올라갑니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 묻는 &amp;ldquo;다음에 뭐 보면 돼요?&amp;rdquo;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q. 공식 문서부터 볼까요?&lt;/b&gt;&lt;br /&gt;&amp;rarr; 네. &lt;a href=&quot;https://nextjs.org/docs&quot;&gt;Next.js Docs&lt;/a&gt;의 App Router 섹션을 이 시리즈와 병행하면 좋습니다.&lt;br /&gt;이미 만든 앱 파일 이름과 문서 용어가 1:1로 맞을 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q. TypeScript를 더 깊게?&lt;/b&gt;&lt;br /&gt;&amp;rarr; 지금은 &lt;code&gt;type&lt;/code&gt;, props, &lt;code&gt;Promise&amp;lt;T&amp;gt;&lt;/code&gt;면 충분합니다.&lt;br /&gt;필요해질 때 &lt;code&gt;Pick&lt;/code&gt;, &lt;code&gt;Omit&lt;/code&gt;, &lt;code&gt;zod&lt;/code&gt; 스키마를 보면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q. Pages Router도 배워야 해요?&lt;/b&gt;&lt;br /&gt;&amp;rarr; 새 프로젝트면 App Router만으로 충분합니다.&lt;br /&gt;레거시 코드를 읽을 일이 생기면 그때 비교하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q. 포트폴리오로 써도 돼요?&lt;/b&gt;&lt;br /&gt;&amp;rarr; DB + 배포 + 수정/삭제까지 붙이면 &amp;ldquo;Next 입문&amp;rdquo; 이상으로 보입니다.&lt;br /&gt;지금 상태는 &amp;ldquo;학습 완주&amp;rdquo; 증거로는 좋고, 포트폴리오는 1~2단계 더 가면 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 시리즈에서 가져갈 것 (마지막)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Next.js는 React를 대체하지 않는다. &lt;b&gt;앱 규칙&lt;/b&gt;을 더한다.&lt;/li&gt;
&lt;li&gt;TypeScript는 &lt;b&gt;데이터&amp;middot;props&amp;middot;함수&lt;/b&gt;의 계약이다.&lt;/li&gt;
&lt;li&gt;App Router는 &lt;b&gt;폴더&amp;middot;page&amp;middot;layout&lt;/b&gt;으로 URL과 화면을 만든다.&lt;/li&gt;
&lt;li&gt;Server Component가 기본이고, Client는 &lt;b&gt;꼭 필요할 때만&lt;/b&gt; 쓴다.&lt;/li&gt;
&lt;li&gt;데이터는 서버에서 &lt;code&gt;await&lt;/code&gt;하고, 폼은 Server Action으로 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;작은 앱 하나를 끝까지 밀어보는 게, 예제 조각 10개보다 낫다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 전체 목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왜 Next.js + TypeScript인가&lt;/li&gt;
&lt;li&gt;프로젝트 만들고 폴더 구조 이해하기&lt;/li&gt;
&lt;li&gt;TypeScript로 컴포넌트 타입 잡기&lt;/li&gt;
&lt;li&gt;라우팅과 레이아웃&lt;/li&gt;
&lt;li&gt;Server Component vs Client Component&lt;/li&gt;
&lt;li&gt;데이터 가져오기&lt;/li&gt;
&lt;li&gt;폼과 서버 액션 입문&lt;/li&gt;
&lt;li&gt;정리 + 다음 학습 로드맵 &amp;larr; 지금&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8편까지 오면 &amp;ldquo;Next.js가 뭔지 들어 본&amp;rdquo; 수준이 아니라,&lt;br /&gt;&lt;b&gt;직접 만든 앱 하나&lt;/b&gt;가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 새 글을 읽기보다,&lt;br /&gt;&lt;b&gt;메모 수정&amp;middot;DB&amp;middot;배포&lt;/b&gt; 중 하나를 골라 손으로 붙여 보는 게 가장 빠릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈가 도움이 됐다면 댓글로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 편이 제일 헷갈렸는지&lt;/li&gt;
&lt;li&gt;다음에 다뤄 줬으면 하는 주제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남겨 주세요.&lt;br /&gt;같은 메모 앱을 이어서 확장하는 후속 시리즈도 이어갈 수 있습니다.&lt;/p&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/786</guid>
      <comments>https://aamoos.tistory.com/786#entry786comment</comments>
      <pubDate>Wed, 29 Jul 2026 16:04:03 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS] 7. 폼과 서버 액션 입문</title>
      <link>https://aamoos.tistory.com/785</link>
      <description>&lt;h1&gt;[Next.js + TS] 7. 폼과 서버 액션 입문&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6편까지는 메모를 &lt;b&gt;읽기만&lt;/b&gt; 했습니다.&lt;br /&gt;이번 편은 폼으로 메모를 &lt;b&gt;추가&lt;/b&gt;합니다.&lt;br /&gt;목표: Server Action &amp;middot; &lt;code&gt;FormData&lt;/code&gt; &amp;middot; &lt;code&gt;revalidatePath&lt;/code&gt;로 목록 갱신.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Server Action이 뭔지 짧게 이해하기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createMemo&lt;/code&gt; 액션 만들기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memos/new&lt;/code&gt; 작성 폼 페이지 만들기&lt;/li&gt;
&lt;li&gt;저장 후 &lt;code&gt;/memos&lt;/code&gt; 목록이 갱신되는지 확인하기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB는 아직 없습니다.&lt;br /&gt;메모리 배열에 넣고, &lt;b&gt;새로고침 없이 목록이 바뀌는 흐름&lt;/b&gt;만 익힙니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 Server Action인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 흔했던 흐름:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트에서 &lt;code&gt;fetch('/api/memos', { method: 'POST' ... })&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;API 라우트에서 저장&lt;/li&gt;
&lt;li&gt;다시 목록 fetch&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App Router에서는 더 단순한 길이 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폼이 서버 함수를 &lt;b&gt;직접&lt;/b&gt; 호출한다 &amp;rarr; &lt;b&gt;Server Action&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도의 API 라우트를 꼭 안 만들어도 됨&lt;/li&gt;
&lt;li&gt;서버에서만 실행됨 (비밀키/DB 접근에 유리)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;form action={createMemo}&amp;gt;&lt;/code&gt; 로 연결 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전부 Action으로 대체하라는 뜻은 아닙니다.&lt;br /&gt;&lt;b&gt;폼 제출&amp;middot;간단한 mutation&lt;/b&gt;부터 배우기 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) 데이터 레이어에 &amp;ldquo;추가&amp;rdquo; 붙이기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;lib/memos.ts&lt;/code&gt;에 생성 함수를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import type { Memo } from &quot;@/types/memo&quot;;

const memos: Memo[] = [
  {
    id: &quot;1&quot;,
    title: &quot;Next.js 시작&quot;,
    content: &quot;App Router로 프로젝트 만들었다.&quot;,
    createdAt: &quot;2026-07-29&quot;,
  },
  {
    id: &quot;2&quot;,
    title: &quot;TypeScript props&quot;,
    content: &quot;MemoCard에 타입을 붙였다.&quot;,
    createdAt: &quot;2026-07-29&quot;,
  },
];

function delay(ms: number) {
  return new Promise((resolve) =&amp;gt; setTimeout(resolve, ms));
}

export async function getMemos(): Promise&amp;lt;Memo[]&amp;gt; {
  await delay(800);
  return memos;
}

export async function getMemoById(id: string): Promise&amp;lt;Memo | undefined&amp;gt; {
  await delay(500);
  return memos.find((memo) =&amp;gt; memo.id === id);
}

export async function addMemo(input: {
  title: string;
  content: string;
}): Promise&amp;lt;Memo&amp;gt; {
  await delay(300);

  const memo: Memo = {
    id: String(Date.now()),
    title: input.title,
    content: input.content,
    createdAt: new Date().toISOString().slice(0, 10),
  };

  memos.unshift(memo); // 맨 앞에 추가
  return memo;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;는 실습용으로 &lt;code&gt;Date.now()&lt;/code&gt; 사용&lt;/li&gt;
&lt;li&gt;서버를 재시작하면 배열은 초기값으로 돌아갑니다 (메모리라서 정상)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) Server Action 파일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로: &lt;code&gt;app/memos/actions.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;&quot;use server&quot;;

import { redirect } from &quot;next/navigation&quot;;
import { revalidatePath } from &quot;next/cache&quot;;
import { addMemo } from &quot;@/lib/memos&quot;;

export async function createMemo(formData: FormData) {
  const title = String(formData.get(&quot;title&quot;) ?? &quot;&quot;).trim();
  const content = String(formData.get(&quot;content&quot;) ?? &quot;&quot;).trim();

  if (!title || !content) {
    throw new Error(&quot;제목과 내용을 모두 입력하세요&quot;);
  }

  await addMemo({ title, content });

  revalidatePath(&quot;/memos&quot;);
  redirect(&quot;/memos&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어볼 것:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&quot;use server&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이 파일의 함수는 서버에서 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FormData&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;이 보낸 값 묶음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;formData.get(&quot;title&quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name=&quot;title&quot;&lt;/code&gt; 입력값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;revalidatePath(&quot;/memos&quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;목록 캐시/화면을 다시 준비&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redirect(&quot;/memos&quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;저장 후 목록으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;use server&quot;&lt;/code&gt;는 파일 맨 위에 둡니다.&lt;br /&gt;(함수 단위로 쓸 수도 있지만, 입문에서는 파일 단위가 단순합니다.)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) 작성 페이지: &lt;code&gt;/memos/new&lt;/code&gt;&lt;/h2&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;app/memos/new/page.tsx&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;import { createMemo } from &quot;@/app/memos/actions&quot;;

export default function NewMemoPage() {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;새 메모&amp;lt;/h1&amp;gt;

      &amp;lt;form action={createMemo}&amp;gt;
        &amp;lt;p&amp;gt;
          &amp;lt;label&amp;gt;
            제목
            &amp;lt;br /&amp;gt;
            &amp;lt;input type=&quot;text&quot; name=&quot;title&quot; required /&amp;gt;
          &amp;lt;/label&amp;gt;
        &amp;lt;/p&amp;gt;

        &amp;lt;p&amp;gt;
          &amp;lt;label&amp;gt;
            내용
            &amp;lt;br /&amp;gt;
            &amp;lt;textarea name=&quot;content&quot; rows={5} required /&amp;gt;
          &amp;lt;/label&amp;gt;
        &amp;lt;/p&amp;gt;

        &amp;lt;button type=&quot;submit&quot;&amp;gt;저장&amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;'use client'&lt;/code&gt; 없음 &amp;rarr; 서버 컴포넌트 페이지&lt;/li&gt;
&lt;li&gt;&lt;code&gt;action={createMemo}&lt;/code&gt; 로 서버 함수 연결&lt;/li&gt;
&lt;li&gt;input의 &lt;b&gt;&lt;code&gt;name&lt;/code&gt;&lt;/b&gt; 이 &lt;code&gt;FormData&lt;/code&gt; 키와 같아야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;http://localhost:3000/memos/new&lt;/code&gt; 에서 작성 &amp;rarr; 저장하면 &lt;code&gt;/memos&lt;/code&gt;로 이동하고 새 메모가 보여야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) 목록에서 &amp;ldquo;새 메모&amp;rdquo; 링크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/memos/page.tsx&lt;/code&gt;에 링크를 하나 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Link from &quot;next/link&quot;;
import MemoCard from &quot;@/components/MemoCard&quot;;
import { getMemos } from &quot;@/lib/memos&quot;;

export default async function MemosPage() {
  const memos = await getMemos();

  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;메모 목록&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;
        &amp;lt;Link href=&quot;/memos/new&quot;&amp;gt;새 메모 작성&amp;lt;/Link&amp;gt;
      &amp;lt;/p&amp;gt;

      &amp;lt;section&amp;gt;
        {memos.map((memo) =&amp;gt; (
          &amp;lt;MemoCard key={memo.id} memo={memo} /&amp;gt;
        ))}
      &amp;lt;/section&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하면 &lt;code&gt;layout&lt;/code&gt; 내비에도 &lt;code&gt;/memos/new&lt;/code&gt;를 넣어도 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동작 흐름 (한눈에)&lt;/h2&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;/memos/new 에서 제출
    &amp;darr;
createMemo (Server Action)
    &amp;darr;
addMemo (메모리 배열에 push)
    &amp;darr;
revalidatePath(&quot;/memos&quot;)
    &amp;darr;
redirect(&quot;/memos&quot;)
    &amp;darr;
목록이 새 데이터로 다시 렌더&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에서 &lt;code&gt;useState&lt;/code&gt;로 목록을 직접 고치지 않아도 됩니다.&lt;br /&gt;&lt;b&gt;서버가 데이터를 바꾸고, 해당 경로를 다시 그리게&lt;/b&gt; 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최소 검증만 넣은 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은:&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;if (!title || !content) {
  throw new Error(&quot;...&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도만 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실서비스에서는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필드별 에러 메시지 표시&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useActionState&lt;/code&gt; 같은 패턴&lt;/li&gt;
&lt;li&gt;zod 등 스키마 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 확장합니다.&lt;br /&gt;입문 편에서는 &lt;b&gt;폼 &amp;rarr; 서버 &amp;rarr; 목록 갱신&lt;/b&gt; 한 바퀴가 목표입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 하는 실수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. input에 &lt;code&gt;name&lt;/code&gt;을 안 줌&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;formData.get(&quot;title&quot;)&lt;/code&gt;이 비어 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. &lt;code&gt;&quot;use server&quot;&lt;/code&gt;를 클라이언트 컴포넌트 한가운데에 무작정 붙임&lt;/b&gt;&lt;br /&gt;&amp;rarr; Action은 서버 파일(또는 서버 함수)로 분리하세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. &lt;code&gt;revalidatePath&lt;/code&gt;를 빼먹음&lt;/b&gt;&lt;br /&gt;&amp;rarr; 저장은 됐는데 목록이 옛 화면일 수 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 서버 재시작 후 메모가 사라짐&lt;/b&gt;&lt;br /&gt;&amp;rarr; 메모리 배열이라 정상입니다. DB 붙이면 유지됩니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. &lt;code&gt;action={createMemo()}&lt;/code&gt; 처럼 호출해 버림&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;action={createMemo}&lt;/code&gt; (함수 참조)여야 합니다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Server Action은 폼이 호출하는 서버 함수다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&quot;use server&quot;&lt;/code&gt; + &lt;code&gt;FormData&lt;/code&gt; + &lt;code&gt;name&lt;/code&gt; 속성이 기본 세트다&lt;/li&gt;
&lt;li&gt;저장 후 &lt;code&gt;revalidatePath&lt;/code&gt;로 목록을 갱신한다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;redirect&lt;/code&gt;로 다음 화면으로 보낸다&lt;/li&gt;
&lt;li&gt;지금은 메모리, 나중엔 DB로 &lt;code&gt;addMemo&lt;/code&gt;만 바꾸면 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Next.js + TS] 8. 정리 + 다음 학습 로드맵&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1~7편 한눈에 복습&lt;/li&gt;
&lt;li&gt;지금 앱에서 부족한 점 (DB, 인증, 배포)&lt;/li&gt;
&lt;li&gt;다음에 공부하면 좋은 순서&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 마무리다운 &lt;b&gt;체크리스트 + 로드맵&lt;/b&gt;으로 끝냅니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왜 Next.js + TypeScript인가&lt;/li&gt;
&lt;li&gt;프로젝트 만들고 폴더 구조 이해하기&lt;/li&gt;
&lt;li&gt;TypeScript로 컴포넌트 타입 잡기&lt;/li&gt;
&lt;li&gt;라우팅과 레이아웃&lt;/li&gt;
&lt;li&gt;Server Component vs Client Component&lt;/li&gt;
&lt;li&gt;데이터 가져오기&lt;/li&gt;
&lt;li&gt;폼과 서버 액션 입문 &amp;larr; 지금&lt;/li&gt;
&lt;li&gt;정리 + 다음 학습 로드맵&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/785</guid>
      <comments>https://aamoos.tistory.com/785#entry785comment</comments>
      <pubDate>Wed, 29 Jul 2026 15:30:06 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS] 6. 데이터 가져오기</title>
      <link>https://aamoos.tistory.com/784</link>
      <description>&lt;h1&gt;[Next.js + TS] 6. 데이터 가져오기&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5편까지는 &lt;code&gt;lib/memos.ts&lt;/code&gt;의 배열을 &lt;b&gt;동기적으로&lt;/b&gt; 읽었습니다.&lt;br /&gt;이번 편은 서버 컴포넌트에서 데이터를 &lt;b&gt;기다리는(async)&lt;/b&gt; 방식으로 바꿉니다.&lt;br /&gt;목표: &lt;code&gt;async&lt;/code&gt; 페이지 &amp;middot; 로딩 UI &amp;middot; 에러 UI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;React의 &lt;code&gt;useEffect&lt;/code&gt; + fetch와 Next 서버 fetch의 차이 이해하기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getMemos&lt;/code&gt; / &lt;code&gt;getMemoById&lt;/code&gt;를 async로 바꾸기&lt;/li&gt;
&lt;li&gt;목록&amp;middot;상세 페이지를 &lt;code&gt;async&lt;/code&gt;로 고치기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loading.tsx&lt;/code&gt; / &lt;code&gt;error.tsx&lt;/code&gt; 맛보기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB나 외부 API를 당장 붙이지 않아도 됩니다.&lt;br /&gt;&lt;b&gt;&amp;ldquo;가져오는 동안 기다린다&amp;rdquo;&lt;/b&gt; 흐름만 잡으면 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 useEffect로 안 가져오나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite/CRA에서 흔했던 패턴:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;&quot;use client&quot;;

useEffect(() =&amp;gt; {
  fetch(&quot;/api/memos&quot;)
    .then((res) =&amp;gt; res.json())
    .then(setMemos);
}, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 초기 흐름이 이렇게 됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;빈 화면(또는 로딩) 먼저 그림&lt;/li&gt;
&lt;li&gt;JS 실행&lt;/li&gt;
&lt;li&gt;그제야 요청&lt;/li&gt;
&lt;li&gt;다시 그림&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App Router 서버 컴포넌트에서는:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export default async function MemosPage() {
  const memos = await getMemos();
  return ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버에서 데이터를 기다림&lt;/li&gt;
&lt;li&gt;완성된 HTML을 보냄&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록처럼 &lt;b&gt;첫 화면에 꼭 필요한 데이터&lt;/b&gt;는 서버에서 가져오는 쪽이 자연스럽습니다.&lt;br /&gt;클릭 이후 추가 로드, 검색어 디바운스 같은 건 클라이언트 fetch가 맞는 경우도 많습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) 데이터 함수를 async로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;lib/memos.ts&lt;/code&gt;를 아래처럼 바꿉니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import type { Memo } from &quot;@/types/memo&quot;;

const memos: Memo[] = [
  {
    id: &quot;1&quot;,
    title: &quot;Next.js 시작&quot;,
    content: &quot;App Router로 프로젝트 만들었다.&quot;,
    createdAt: &quot;2026-07-29&quot;,
  },
  {
    id: &quot;2&quot;,
    title: &quot;TypeScript props&quot;,
    content: &quot;MemoCard에 타입을 붙였다.&quot;,
    createdAt: &quot;2026-07-29&quot;,
  },
];

function delay(ms: number) {
  return new Promise((resolve) =&amp;gt; setTimeout(resolve, ms));
}

export async function getMemos(): Promise&amp;lt;Memo[]&amp;gt; {
  await delay(800); // 로딩 UI를 보기 위한 가짜 대기
  return memos;
}

export async function getMemoById(id: string): Promise&amp;lt;Memo | undefined&amp;gt; {
  await delay(500);
  return memos.find((memo) =&amp;gt; memo.id === id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;export const memos&lt;/code&gt; 직접 노출 대신 &lt;b&gt;함수로만&lt;/b&gt; 가져오게 바꿈&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Promise&amp;lt;Memo[]&amp;gt;&lt;/code&gt;로 반환 타입을 명시&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delay&lt;/code&gt;는 실습용입니다. 실제 DB/API 자리라고 생각하면 됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 컴포넌트에서는 자기 프로젝트의 &lt;code&gt;/api&lt;/code&gt;를 다시 fetch하기보다,&lt;br /&gt;이런 &lt;b&gt;데이터 함수를 직접 await&lt;/b&gt; 하는 방식을 먼저 쓰는 게 좋습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) 목록 페이지를 async로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/memos/page.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import MemoCard from &quot;@/components/MemoCard&quot;;
import { getMemos } from &quot;@/lib/memos&quot;;

export default async function MemosPage() {
  const memos = await getMemos();

  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;메모 목록&amp;lt;/h1&amp;gt;

      &amp;lt;section&amp;gt;
        {memos.map((memo) =&amp;gt; (
          &amp;lt;MemoCard key={memo.id} memo={memo} /&amp;gt;
        ))}
      &amp;lt;/section&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바뀐 점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;async function&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;await getMemos()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;더 이상 &lt;code&gt;import { memos }&lt;/code&gt;가 아님&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) 상세도 동일하게&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/memos/[id]/page.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { notFound } from &quot;next/navigation&quot;;
import { getMemoById } from &quot;@/lib/memos&quot;;

type MemoDetailPageProps = {
  params: Promise&amp;lt;{ id: string }&amp;gt;;
};

export default async function MemoDetailPage({ params }: MemoDetailPageProps) {
  const { id } = await params;
  const memo = await getMemoById(id);

  if (!memo) {
    notFound();
  }

  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;{memo.title}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{memo.content}&amp;lt;/p&amp;gt;
      &amp;lt;time dateTime={memo.createdAt}&amp;gt;{memo.createdAt}&amp;lt;/time&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;getMemoById&lt;/code&gt; 앞에 &lt;code&gt;await&lt;/code&gt;만 추가하면 됩니다.&lt;br /&gt;(이미 &lt;code&gt;async&lt;/code&gt; 페이지였다면 그대로 두면 됩니다.)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) 로딩 UI: &lt;code&gt;loading.tsx&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 기다리는 동안 보여줄 파일을 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로: &lt;code&gt;app/memos/loading.tsx&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function MemosLoading() {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;p&amp;gt;메모를 불러오는 중...&amp;lt;/p&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/memos&lt;/code&gt;로 이동하면 잠깐 이 문구가 보였다가 목록이 뜹니다.&lt;br /&gt;(&lt;code&gt;delay(800)&lt;/code&gt; 덕분에 눈에 보입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;loading.tsx&lt;/code&gt;는 같은 세그먼트(&lt;code&gt;memos&lt;/code&gt;)의 &lt;code&gt;page.tsx&lt;/code&gt;를 감싸는 &lt;b&gt;즉시 로딩 UI&lt;/b&gt;입니다.&lt;br /&gt;상세(&lt;code&gt;memos/[id]&lt;/code&gt;)에도 따로 두고 싶으면:&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;app/memos/[id]/loading.tsx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 추가하면 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5) 에러 UI: &lt;code&gt;error.tsx&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로: &lt;code&gt;app/memos/error.tsx&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 경계는 클라이언트 컴포넌트여야 합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;&quot;use client&quot;;

type MemosErrorProps = {
  error: Error;
  reset: () =&amp;gt; void;
};

export default function MemosError({ error, reset }: MemosErrorProps) {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;메모를 불러오지 못했습니다&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{error.message}&amp;lt;/p&amp;gt;
      &amp;lt;button type=&quot;button&quot; onClick={() =&amp;gt; reset()}&amp;gt;
        다시 시도
      &amp;lt;/button&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부러 에러를 내려면 &lt;code&gt;getMemos&lt;/code&gt; 안에 잠깐 넣어 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;export async function getMemos(): Promise&amp;lt;Memo[]&amp;gt; {
  await delay(800);
  throw new Error(&quot;샘플 에러입니다&quot;);
  // return memos;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 후 &lt;b&gt;꼭 &lt;code&gt;throw&lt;/code&gt;는 지우세요.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;외부 API를 fetch할 때는&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 HTTP API라면 서버 컴포넌트에서 이렇게 씁니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default async function MemosPage() {
  const res = await fetch(&quot;https://api.example.com/memos&quot;, {
    next: { revalidate: 60 }, // 60초 캐시 예시
  });

  if (!res.ok) {
    throw new Error(&quot;메모를 가져오지 못했습니다&quot;);
  }

  const memos: Memo[] = await res.json();
  // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 시리즈는 로컬 배열 + &lt;code&gt;delay&lt;/code&gt;로 &lt;b&gt;같은 async 흐름&lt;/b&gt;을 연습합니다.&lt;br /&gt;나중에 API URL만 바꿔 끼우면 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 하는 실수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 서버 페이지인데 &lt;code&gt;useEffect&lt;/code&gt;로 다시 fetch&lt;/b&gt;&lt;br /&gt;&amp;rarr; 초기 목록이면 &lt;code&gt;async&lt;/code&gt; + &lt;code&gt;await&lt;/code&gt;를 먼저&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. &lt;code&gt;await&lt;/code&gt; 없이 Promise를 map에 넣음&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;const memos = await getMemos()&lt;/code&gt; 필수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. &lt;code&gt;loading.tsx&lt;/code&gt;에 &lt;code&gt;'use client'&lt;/code&gt; + useState로 직접 로딩 관리&lt;/b&gt;&lt;br /&gt;&amp;rarr; 세그먼트 &lt;code&gt;loading.tsx&lt;/code&gt;만으로도 충분한 경우가 많음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. &lt;code&gt;error.tsx&lt;/code&gt;를 서버 컴포넌트로 작성&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;error.tsx&lt;/code&gt;는 &lt;code&gt;&quot;use client&quot;&lt;/code&gt; 필요&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 화면 데이터는 서버 컴포넌트에서 &lt;code&gt;await&lt;/code&gt;하는 편이 자연스럽다&lt;/li&gt;
&lt;li&gt;데이터 함수는 &lt;code&gt;async&lt;/code&gt; + &lt;code&gt;Promise&amp;lt;타입&amp;gt;&lt;/code&gt;으로 계약한다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loading.tsx&lt;/code&gt;는 기다리는 동안의 UI&lt;/li&gt;
&lt;li&gt;&lt;code&gt;error.tsx&lt;/code&gt;는 실패한 동안의 UI (클라이언트)&lt;/li&gt;
&lt;li&gt;지금은 가짜 delay, 나중엔 DB/API로 교체하면 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Next.js + TS] 7. 폼과 서버 액션 입문&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모 추가 폼 만들기&lt;/li&gt;
&lt;li&gt;Server Action으로 데이터 넣기&lt;/li&gt;
&lt;li&gt;목록이 갱신되는 흐름 맛보기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기만 하던 앱에 &lt;b&gt;쓰기&lt;/b&gt;를 붙입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왜 Next.js + TypeScript인가&lt;/li&gt;
&lt;li&gt;프로젝트 만들고 폴더 구조 이해하기&lt;/li&gt;
&lt;li&gt;TypeScript로 컴포넌트 타입 잡기&lt;/li&gt;
&lt;li&gt;라우팅과 레이아웃&lt;/li&gt;
&lt;li&gt;Server Component vs Client Component&lt;/li&gt;
&lt;li&gt;데이터 가져오기 &amp;larr; 지금&lt;/li&gt;
&lt;li&gt;폼과 서버 액션 입문&lt;/li&gt;
&lt;li&gt;정리 + 다음 학습 로드맵&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/784</guid>
      <comments>https://aamoos.tistory.com/784#entry784comment</comments>
      <pubDate>Wed, 29 Jul 2026 15:15:21 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS] 5. Server Component vs Client Component</title>
      <link>https://aamoos.tistory.com/783</link>
      <description>&lt;h1&gt;[Next.js + TS] 5. Server Component vs Client Component&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4편까지는 목록&amp;middot;상세&amp;middot;링크만 있었습니다. 전부 &lt;b&gt;읽기&lt;/b&gt;에 가깝습니다.&lt;br /&gt;이번 편은 App Router에서 제일 헷갈리는 부분,&lt;br /&gt;&lt;b&gt;서버 컴포넌트 / 클라이언트 컴포넌트&lt;/b&gt; 경계를 나눕니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;기본값이 서버 컴포넌트인 이유 이해하기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;'use client'&lt;/code&gt;가 필요한 순간 정리하기&lt;/li&gt;
&lt;li&gt;좋아요 버튼을 &lt;b&gt;작은 클라이언트 컴포넌트&lt;/b&gt;로 분리하기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MemoCard&lt;/code&gt;(서버)가 그 버튼을 조립하는 패턴 익히기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표는 &amp;ldquo;전부 &lt;code&gt;'use client'&lt;/code&gt;로 만들기&amp;rdquo;가 아닙니다.&lt;br /&gt;&lt;b&gt;꼭 필요한 곳만&lt;/b&gt; 클라이언트로 내리는 감각을 잡는 겁니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 줄 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;Server Component&lt;/th&gt;
&lt;th&gt;Client Component&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기본?&lt;/td&gt;
&lt;td&gt;App Router 기본값&lt;/td&gt;
&lt;td&gt;파일 맨 위에 &lt;code&gt;'use client'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 위치&lt;/td&gt;
&lt;td&gt;서버&lt;/td&gt;
&lt;td&gt;브라우저(+필요 시 서버에서 사전 렌더)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hooks (&lt;code&gt;useState&lt;/code&gt; 등)&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;클릭/입력 이벤트&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 fetch&lt;/td&gt;
&lt;td&gt;자연스러움&lt;/td&gt;
&lt;td&gt;가능하지만 목적이 다름&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React(Vite/CRA)에 익숙하면 전부 클라이언트처럼 느껴집니다.&lt;br /&gt;Next App Router는 &lt;b&gt;기본을 서버&lt;/b&gt;에 두고, 인터랙션만 브라우저로 보냅니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 기본이 서버인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 만든 &lt;code&gt;MemosPage&lt;/code&gt;, &lt;code&gt;MemoCard&lt;/code&gt;에는 아직:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;useState&lt;/code&gt; 없음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onClick&lt;/code&gt; 없음&lt;/li&gt;
&lt;li&gt;브라우저 API 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;서버 컴포넌트&lt;/b&gt;로 충분합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 HTML을 만들어 보내면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 화면을 빨리 그릴 수 있고&lt;/li&gt;
&lt;li&gt;불필요한 JS를 클라이언트로 덜 보내고&lt;/li&gt;
&lt;li&gt;(나중에) DB/시크릿에 가까운 작업을 서버에 두기 쉽습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;React 컴포넌트 = 무조건 브라우저&amp;rdquo;가 아니라는 점이 핵심입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;언제 &lt;code&gt;'use client'&lt;/code&gt;가 필요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래가 필요하면 클라이언트 컴포넌트입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useEffect&lt;/code&gt;, &lt;code&gt;useRef&lt;/code&gt; 같은 hooks&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onClick&lt;/code&gt;, &lt;code&gt;onChange&lt;/code&gt; 같은 이벤트&lt;/li&gt;
&lt;li&gt;&lt;code&gt;window&lt;/code&gt;, &lt;code&gt;localStorage&lt;/code&gt; 같은 브라우저 API&lt;/li&gt;
&lt;li&gt;일부 브라우저 전용 라이브러리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로, &lt;b&gt;데이터 보여주고 링크로 이동만&lt;/b&gt; 하면 서버로 남겨 두세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 습관:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;&quot;use client&quot;; // 습관적으로 파일 맨 위에 붙임

export default function MemosPage() {
  // 사실 hooks도 이벤트도 없음
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 전체를 클라이언트로 올리면,&lt;br /&gt;서버 컴포넌트의 이점을 스스로 버리는 셈입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 아이디어: 좋아요 버튼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모 카드에 &amp;ldquo;좋아요&amp;rdquo;를 눌러 숫자가 바뀌게 해 봅시다.&lt;br /&gt;이건 &lt;code&gt;useState&lt;/code&gt;가 필요하므로 &lt;b&gt;클라이언트&lt;/b&gt;가 맞습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;code&gt;MemoCard&lt;/code&gt; 전체를 &lt;code&gt;'use client'&lt;/code&gt;로 바꾸지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;MemoCard          &amp;larr; 서버 (제목, 내용, 링크)
└─ LikeButton     &amp;larr; 클라이언트 (클릭, state)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 덩어리는 서버, &lt;b&gt;인터랙션 조각만&lt;/b&gt; 클라이언트.&lt;br /&gt;이게 App Router에서 자주 쓰는 패턴입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) &lt;code&gt;LikeButton&lt;/code&gt; 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로: &lt;code&gt;components/LikeButton.tsx&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;&quot;use client&quot;;

import { useState } from &quot;react&quot;;

export default function LikeButton() {
  const [likes, setLikes] = useState(0);

  return (
    &amp;lt;button type=&quot;button&quot; onClick={() =&amp;gt; setLikes((prev) =&amp;gt; prev + 1)}&amp;gt;
      좋아요 {likes}
    &amp;lt;/button&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 파일 &lt;b&gt;맨 위&lt;/b&gt;에 &lt;code&gt;&quot;use client&quot;&lt;/code&gt; (또는 &lt;code&gt;'use client'&lt;/code&gt;)가 있어야 합니다.&lt;br /&gt;이 한 줄이 &amp;ldquo;이 파일은 클라이언트 컴포넌트 경계&amp;rdquo;라는 표시입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) 서버 &lt;code&gt;MemoCard&lt;/code&gt;에 조립&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;components/MemoCard.tsx&lt;/code&gt;는 &lt;code&gt;'use client'&lt;/code&gt;를 &lt;b&gt;붙이지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Link from &quot;next/link&quot;;
import type { Memo } from &quot;@/types/memo&quot;;
import LikeButton from &quot;@/components/LikeButton&quot;;

type MemoCardProps = {
  memo: Memo;
};

export default function MemoCard({ memo }: MemoCardProps) {
  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h2&amp;gt;
        &amp;lt;Link href={`/memos/${memo.id}`}&amp;gt;{memo.title}&amp;lt;/Link&amp;gt;
      &amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;{memo.content}&amp;lt;/p&amp;gt;
      &amp;lt;time dateTime={memo.createdAt}&amp;gt;{memo.createdAt}&amp;lt;/time&amp;gt;
      &amp;lt;div&amp;gt;
        &amp;lt;LikeButton /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/article&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 컴포넌트가 클라이언트 컴포넌트를 &lt;b&gt;자식으로 import&lt;/b&gt;하는 것은 OK입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로, 클라이언트 컴포넌트가 서버 컴포넌트를 같은 방식으로 막 import해서 서버 전용 이점을 유지하긴 어렵습니다.&lt;br /&gt;(필요하면 &lt;code&gt;children&lt;/code&gt;으로 서버 결과를 넘기는 패턴을 씁니다. 지금은 몰라도 됩니다.)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동작 확인&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;npm run dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memos&lt;/code&gt;에서 &lt;b&gt;좋아요&lt;/b&gt; 클릭&lt;/li&gt;
&lt;li&gt;숫자만 올라가면 성공&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지를 새로고침하면 숫자는 0으로 돌아갑니다.&lt;br /&gt;지금은 메모리 state만 쓰기 때문입니다. (저장은 7편 쪽에서 다룹니다.)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;경계를 가르는 기준 (실전용)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고민될 때 이 순서로 물어보세요.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;hooks / 이벤트가 필요한가?&lt;/b&gt;&lt;br /&gt;&amp;rarr; 없으면 서버&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 전체가 아니라 버튼/입력만 필요한가?&lt;/b&gt;&lt;br /&gt;&amp;rarr; 작은 클라이언트 컴포넌트로 분리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 읽기가 주목적인가?&lt;/b&gt;&lt;br /&gt;&amp;rarr; 페이지/카드는 서버에 두는 쪽을 먼저 생각&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;좋은 방향
─────────
page (Server)
  └─ MemoCard (Server)
       └─ LikeButton (Client)

피하고 싶은 방향
─────────
page (Client)  &amp;larr; 페이지 통째로
  └─ 모든 것&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 하는 실수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. &lt;code&gt;&quot;use client&quot;&lt;/code&gt;를 함수 안에 씀&lt;/b&gt;&lt;br /&gt;&amp;rarr; 반드시 파일 최상단 (import보다 위)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 서버 컴포넌트에서 &lt;code&gt;useState&lt;/code&gt; 사용&lt;/b&gt;&lt;br /&gt;&amp;rarr; 에러 납니다. state가 필요하면 그 부분만 클라이언트로 분리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. &lt;code&gt;Link&lt;/code&gt;가 있다고 무조건 클라이언트&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;next/link&lt;/code&gt;는 서버 컴포넌트에서도 사용 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 샘플이라서 전부 클라이언트로 통일&lt;/b&gt;&lt;br /&gt;&amp;rarr; 나중에 습관이 됩니다. 처음부터 경계를 나누는 연습을 하세요&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;App Router 기본은 Server Component다&lt;/li&gt;
&lt;li&gt;hooks/이벤트가 필요할 때만 Client Component다&lt;/li&gt;
&lt;li&gt;페이지 전체가 아니라 &lt;b&gt;인터랙션 조각&lt;/b&gt;에 &lt;code&gt;'use client'&lt;/code&gt;를 붙인다&lt;/li&gt;
&lt;li&gt;서버 카드 + 클라이언트 버튼 조합이 기본 패턴이다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Next.js + TS] 6. 데이터 가져오기&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 컴포넌트에서 &lt;code&gt;fetch&lt;/code&gt; 하기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loading.tsx&lt;/code&gt; / &lt;code&gt;error.tsx&lt;/code&gt; 맛보기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lib/memos.ts&lt;/code&gt; 하드코딩에서 한 걸음 나아가기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 로컬 배열을 읽고 있습니다.&lt;br /&gt;다음 편에서 &lt;b&gt;데이터를 가져오는 방식&lt;/b&gt;을 정리합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왜 Next.js + TypeScript인가&lt;/li&gt;
&lt;li&gt;프로젝트 만들고 폴더 구조 이해하기&lt;/li&gt;
&lt;li&gt;TypeScript로 컴포넌트 타입 잡기&lt;/li&gt;
&lt;li&gt;라우팅과 레이아웃&lt;/li&gt;
&lt;li&gt;Server Component vs Client Component &amp;larr; 지금&lt;/li&gt;
&lt;li&gt;데이터 가져오기&lt;/li&gt;
&lt;li&gt;폼과 서버 액션 입문&lt;/li&gt;
&lt;li&gt;정리 + 다음 학습 로드맵&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/783</guid>
      <comments>https://aamoos.tistory.com/783#entry783comment</comments>
      <pubDate>Wed, 29 Jul 2026 15:08:02 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS] 4. 라우팅과 레이아웃</title>
      <link>https://aamoos.tistory.com/782</link>
      <description>&lt;h1&gt;[Next.js + TS] 4. 라우팅과 레이아웃&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편에서 홈에 샘플 카드를 붙였습니다.&lt;br /&gt;이번 편은 &lt;b&gt;URL로 화면을 나누고&lt;/b&gt;, 공통 내비는 &lt;code&gt;layout&lt;/code&gt;에 둡니다.&lt;br /&gt;목표: &lt;code&gt;/memos&lt;/code&gt; 목록 &amp;middot; &lt;code&gt;/memos/[id]&lt;/code&gt; 상세 &amp;middot; 상단 링크.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;샘플 데이터를 공용 파일로 분리하기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memos&lt;/code&gt; 목록 페이지 만들기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memos/[id]&lt;/code&gt; 상세 페이지 만들기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MemoCard&lt;/code&gt;에 상세 링크 달기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;layout.tsx&lt;/code&gt;에 간단한 내비 넣기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App Router 핵심은 다시 이겁니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폴더 = URL&lt;br /&gt;&lt;code&gt;page.tsx&lt;/code&gt; = 화면&lt;br /&gt;&lt;code&gt;layout.tsx&lt;/code&gt; = 공통 틀&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 홈에만 두면 부족한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 &lt;code&gt;/&lt;/code&gt; 하나에 목록이 다 있습니다.&lt;br /&gt;실제 앱에서는 보통 이렇게 나눕니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;소개/홈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/memos&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;메모 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/memos/1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;id가 1인 메모 상세&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Router로 직접 구성하던 일을, Next에서는 &lt;b&gt;폴더를 만드는 것&lt;/b&gt;으로 해결합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) 샘플 데이터 공용화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록과 상세가 &lt;b&gt;같은 데이터&lt;/b&gt;를 봐야 합니다.&lt;br /&gt;&lt;code&gt;app/page.tsx&lt;/code&gt; 안에만 두면 상세에서 못 씁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로: &lt;code&gt;lib/memos.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import type { Memo } from &quot;@/types/memo&quot;;

export const memos: Memo[] = [
  {
    id: &quot;1&quot;,
    title: &quot;Next.js 시작&quot;,
    content: &quot;App Router로 프로젝트 만들었다.&quot;,
    createdAt: &quot;2026-07-29&quot;,
  },
  {
    id: &quot;2&quot;,
    title: &quot;TypeScript props&quot;,
    content: &quot;MemoCard에 타입을 붙였다.&quot;,
    createdAt: &quot;2026-07-29&quot;,
  },
];

export function getMemoById(id: string): Memo | undefined {
  return memos.find((memo) =&amp;gt; memo.id === id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;memos&lt;/code&gt;: 전체 목록&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getMemoById&lt;/code&gt;: id로 하나 찾기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 DB/API는 없습니다.&lt;br /&gt;&lt;b&gt;라우팅 연습용 가짜 데이터&lt;/b&gt;로 충분합니다. (6편에서 fetch로 바꿉니다.)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) 목록 페이지: &lt;code&gt;/memos&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폴더와 파일을 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;app/
  memos/
    page.tsx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/memos/page.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import MemoCard from &quot;@/components/MemoCard&quot;;
import { memos } from &quot;@/lib/memos&quot;;

export default function MemosPage() {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;메모 목록&amp;lt;/h1&amp;gt;

      &amp;lt;section&amp;gt;
        {memos.map((memo) =&amp;gt; (
          &amp;lt;MemoCard key={memo.id} memo={memo} /&amp;gt;
        ))}
      &amp;lt;/section&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 &lt;code&gt;http://localhost:3000/memos&lt;/code&gt; 를 열면 목록이 보여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;memos&lt;/code&gt;라는 &lt;b&gt;폴더 이름&lt;/b&gt;이 곧 URL 경로입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) 상세 페이지: &lt;code&gt;/memos/[id]&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 구간은 대괄호 폴더로 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;app/
  memos/
    page.tsx
    [id]/
      page.tsx&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/memos/1&lt;/code&gt; &amp;rarr; &lt;code&gt;params.id === &quot;1&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memos/2&lt;/code&gt; &amp;rarr; &lt;code&gt;params.id === &quot;2&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/memos/[id]/page.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { notFound } from &quot;next/navigation&quot;;
import { getMemoById } from &quot;@/lib/memos&quot;;

type MemoDetailPageProps = {
  params: Promise&amp;lt;{ id: string }&amp;gt;;
};

export default async function MemoDetailPage({ params }: MemoDetailPageProps) {
  const { id } = await params;
  const memo = getMemoById(id);

  if (!memo) {
    notFound();
  }

  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;{memo.title}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{memo.content}&amp;lt;/p&amp;gt;
      &amp;lt;time dateTime={memo.createdAt}&amp;gt;{memo.createdAt}&amp;lt;/time&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[id]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URL의 가변 구간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;params&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;그 구간 값이 들어옴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;await params&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;최근 Next에서는 params가 Promise인 경우가 많음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;notFound()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;없는 id면 404&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;http://localhost:3000/memos/1&lt;/code&gt; 이 열리고,&lt;br /&gt;&lt;code&gt;/memos/999&lt;/code&gt; 는 404가 나면 성공입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전마다 &lt;code&gt;params&lt;/code&gt;가 동기 객체인 템플릿도 있습니다.&lt;br /&gt;타입이 &lt;code&gt;Promise&lt;/code&gt;라고 나오면 &lt;code&gt;await&lt;/code&gt; 하면 되고, 아니면 &lt;code&gt;params.id&lt;/code&gt;를 그대로 쓰면 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) 카드에서 상세로 이동: &lt;code&gt;Link&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt;도 되지만, Next에서는 &lt;code&gt;Link&lt;/code&gt;를 씁니다.&lt;br /&gt;클라이언트 쪽 이동을 더 부드럽게 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;components/MemoCard.tsx&lt;/code&gt;를 수정합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Link from &quot;next/link&quot;;
import type { Memo } from &quot;@/types/memo&quot;;

type MemoCardProps = {
  memo: Memo;
};

export default function MemoCard({ memo }: MemoCardProps) {
  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h2&amp;gt;
        &amp;lt;Link href={`/memos/${memo.id}`}&amp;gt;{memo.title}&amp;lt;/Link&amp;gt;
      &amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;{memo.content}&amp;lt;/p&amp;gt;
      &amp;lt;time dateTime={memo.createdAt}&amp;gt;{memo.createdAt}&amp;lt;/time&amp;gt;
    &amp;lt;/article&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록에서 제목을 누르면 상세로 가야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5) 홈은 소개 페이지로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록은 &lt;code&gt;/memos&lt;/code&gt;로 옮겼으니, 홈은 가볍게 둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/page.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Link from &quot;next/link&quot;;

export default function HomePage() {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;Memo App&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;Next.js + TypeScript 시리즈 실습 프로젝트&amp;lt;/p&amp;gt;
      &amp;lt;p&amp;gt;
        &amp;lt;Link href=&quot;/memos&quot;&amp;gt;메모 목록 보기&amp;lt;/Link&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6) 공통 내비: &lt;code&gt;layout.tsx&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 페이지 위에 링크를 두고 싶다면 &lt;code&gt;app/layout.tsx&lt;/code&gt;에 넣습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;children&lt;/code&gt; 위에 내비만 추가하면 됩니다. (폰트 설정 등은 기존 것 유지)&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Link from &quot;next/link&quot;;
import type { Metadata } from &quot;next&quot;;
import &quot;./globals.css&quot;;

export const metadata: Metadata = {
  title: &quot;Memo App&quot;,
  description: &quot;Next.js + TypeScript 실습&quot;,
};

export default function RootLayout({
  children,
}: Readonly&amp;lt;{
  children: React.ReactNode;
}&amp;gt;) {
  return (
    &amp;lt;html lang=&quot;ko&quot;&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;header&amp;gt;
          &amp;lt;nav&amp;gt;
            &amp;lt;Link href=&quot;/&quot;&amp;gt;홈&amp;lt;/Link&amp;gt;
            {&quot; | &quot;}
            &amp;lt;Link href=&quot;/memos&quot;&amp;gt;메모&amp;lt;/Link&amp;gt;
          &amp;lt;/nav&amp;gt;
        &amp;lt;/header&amp;gt;
        {children}
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 Geist 폰트 등이 있다면 &lt;b&gt;내비만 추가&lt;/b&gt;해도 됩니다.&lt;br /&gt;중요한 건 위치입니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;layout
├─ header/nav   &amp;larr; 모든 페이지 공통
└─ children     &amp;larr; page.tsx 내용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/&lt;/code&gt;, &lt;code&gt;/memos&lt;/code&gt;, &lt;code&gt;/memos/1&lt;/code&gt; 어디로 가든 위 링크가 보여야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금 폴더 구조&lt;/h2&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;app/
  layout.tsx          &amp;rarr; 공통 내비
  page.tsx            &amp;rarr; /
  memos/
    page.tsx          &amp;rarr; /memos
    [id]/
      page.tsx        &amp;rarr; /memos/:id
components/
  MemoCard.tsx
lib/
  memos.ts
types/
  memo.ts&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 하는 실수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. &lt;code&gt;app/memos.tsx&lt;/code&gt; 파일만 만듦&lt;/b&gt;&lt;br /&gt;&amp;rarr; 목록은 &lt;code&gt;app/memos/page.tsx&lt;/code&gt; 여야 &lt;code&gt;/memos&lt;/code&gt;가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. &lt;code&gt;[id]&lt;/code&gt;를 일반 폴더명 &lt;code&gt;id&lt;/code&gt;로 만듦&lt;/b&gt;&lt;br /&gt;&amp;rarr; 대괄호가 있어야 동적 라우트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 상세에서 목록 데이터를 또 하드코딩&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;lib/memos.ts&lt;/code&gt;로 한곳에서 관리하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. &lt;code&gt;Link&lt;/code&gt;에 &lt;code&gt;href=&quot;#&quot;&lt;/code&gt;&lt;/b&gt;&lt;br /&gt;&amp;rarr; 반드시 &lt;code&gt;/memos/${memo.id}&lt;/code&gt;처럼 실제 경로를 넣습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;App Router는 폴더로 URL을 만든다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[id]&lt;/code&gt;는 동적 세그먼트다&lt;/li&gt;
&lt;li&gt;목록/상세가 같은 데이터를 쓰면 &lt;code&gt;lib&lt;/code&gt;로 분리한다&lt;/li&gt;
&lt;li&gt;페이지 이동은 &lt;code&gt;Link&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;모든 페이지 공통 UI는 &lt;code&gt;layout.tsx&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Next.js + TS] 5. Server Component vs Client Component&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본이 서버 컴포넌트인 이유&lt;/li&gt;
&lt;li&gt;언제 &lt;code&gt;'use client'&lt;/code&gt;가 필요한지&lt;/li&gt;
&lt;li&gt;클릭 같은 인터랙션은 어디에 두는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 &amp;ldquo;읽기 전용 페이지&amp;rdquo; 중심입니다.&lt;br /&gt;다음 편에서 &lt;b&gt;서버/클라이언트 경계&lt;/b&gt;를 나눕니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왜 Next.js + TypeScript인가&lt;/li&gt;
&lt;li&gt;프로젝트 만들고 폴더 구조 이해하기&lt;/li&gt;
&lt;li&gt;TypeScript로 컴포넌트 타입 잡기&lt;/li&gt;
&lt;li&gt;라우팅과 레이아웃 &amp;larr; 지금&lt;/li&gt;
&lt;li&gt;Server Component vs Client Component&lt;/li&gt;
&lt;li&gt;데이터 가져오기&lt;/li&gt;
&lt;li&gt;폼과 서버 액션 입문&lt;/li&gt;
&lt;li&gt;정리 + 다음 학습 로드맵&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/782</guid>
      <comments>https://aamoos.tistory.com/782#entry782comment</comments>
      <pubDate>Wed, 29 Jul 2026 14:52:47 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS] 3. TypeScript로 컴포넌트 타입 잡기</title>
      <link>https://aamoos.tistory.com/781</link>
      <description>&lt;h1&gt;[Next.js + TS] 3. TypeScript로 컴포넌트 타입 잡기&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2편에서 프로젝트를 띄웠다면, 이번 편은 &lt;b&gt;타입을 붙인 컴포넌트&lt;/b&gt;를 만듭니다.&lt;br /&gt;목표: &lt;code&gt;Memo&lt;/code&gt; 타입 정의 &amp;rarr; &lt;code&gt;MemoCard&lt;/code&gt; props 타입 &amp;rarr; 홈에 샘플 메모 보여 주기.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;메모 데이터 모델(&lt;code&gt;Memo&lt;/code&gt;) 타입 만들기&lt;/li&gt;
&lt;li&gt;props에 타입이 있는 &lt;code&gt;MemoCard&lt;/code&gt; 컴포넌트 만들기&lt;/li&gt;
&lt;li&gt;홈 페이지에 샘플 데이터로 카드 렌더하기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고급 타입(제네릭 마법, utility type 잔치)은 하지 않습니다.&lt;br /&gt;&lt;b&gt;props / 데이터 모델&lt;/b&gt;만 확실하게 잡습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 컴포넌트에 타입을 붙이나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React만 쓸 때도 props는 넘어갑니다.&lt;br /&gt;문제는 &amp;ldquo;이 컴포넌트가 &lt;b&gt;뭘 받는지&lt;/b&gt;&amp;rdquo;가 코드만 봐서는 흐릿하다는 점입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 타입 없이
function MemoCard({ title, content }) {
  return &amp;lt;article&amp;gt;...&amp;lt;/article&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt;이 string인지?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createdAt&lt;/code&gt;도 넘겨야 하는지?&lt;/li&gt;
&lt;li&gt;오타로 &lt;code&gt;titel&lt;/code&gt;을 넘기면?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript를 붙이면 &lt;b&gt;받는 쪽과 주는 쪽이 같은 계약&lt;/b&gt;을 공유합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) 데이터 모델부터: &lt;code&gt;Memo&lt;/code&gt; 타입&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 타입 전용 파일을 하나 둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로: &lt;code&gt;types/memo.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export type Memo = {
  id: string;
  title: string;
  content: string;
  createdAt: string;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;으로 객체의 모양을 고정한다&lt;/li&gt;
&lt;li&gt;나중에 API/DB에서 같은 형태를 쓰면 &lt;b&gt;한곳에서만&lt;/b&gt; 바꾸면 된다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;를 &lt;code&gt;number&lt;/code&gt;로 할지 &lt;code&gt;string&lt;/code&gt;으로 할지는 팀마다 다르지만, 이 시리즈는 &lt;b&gt;string&lt;/b&gt;으로 통일한다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) props 타입: &lt;code&gt;MemoCard&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 폴더를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로: &lt;code&gt;components/MemoCard.tsx&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import type { Memo } from &quot;@/types/memo&quot;;

type MemoCardProps = {
  memo: Memo;
};

export default function MemoCard({ memo }: MemoCardProps) {
  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h2&amp;gt;{memo.title}&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;{memo.content}&amp;lt;/p&amp;gt;
      &amp;lt;time dateTime={memo.createdAt}&amp;gt;{memo.createdAt}&amp;lt;/time&amp;gt;
    &amp;lt;/article&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어볼 것:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;import type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;타입만 가져옴 (런타임 코드 아님)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MemoCardProps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이 컴포넌트가 받는 props 계약&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{ memo }: MemoCardProps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;구조 분해 + 타입 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memo.title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Memo&lt;/code&gt;에 있는 필드만 자동완성됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실수로 &lt;code&gt;memo.titlle&lt;/code&gt;을 쓰면 에디터/타입이 바로 에러를 냅니다.&lt;br /&gt;1편에서 말했던 &amp;ldquo;안전벨트&amp;rdquo;가 여기서 체감됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) 홈에서 샘플 데이터로 연결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/page.tsx&lt;/code&gt;를 아래처럼 바꿉니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import MemoCard from &quot;@/components/MemoCard&quot;;
import type { Memo } from &quot;@/types/memo&quot;;

const sampleMemos: Memo[] = [
  {
    id: &quot;1&quot;,
    title: &quot;Next.js 시작&quot;,
    content: &quot;App Router로 프로젝트 만들었다.&quot;,
    createdAt: &quot;2026-07-29&quot;,
  },
  {
    id: &quot;2&quot;,
    title: &quot;TypeScript props&quot;,
    content: &quot;MemoCard에 타입을 붙였다.&quot;,
    createdAt: &quot;2026-07-29&quot;,
  },
];

export default function HomePage() {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;Memo App&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;샘플 메모 목록&amp;lt;/p&amp;gt;

      &amp;lt;section&amp;gt;
        {sampleMemos.map((memo) =&amp;gt; (
          &amp;lt;MemoCard key={memo.id} memo={memo} /&amp;gt;
        ))}
      &amp;lt;/section&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 포인트:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;sampleMemos: Memo[]&lt;/code&gt; &amp;rarr; 배열 원소가 전부 &lt;code&gt;Memo&lt;/code&gt;여야 함&lt;/li&gt;
&lt;li&gt;필드 하나 빠지거나 이름 틀리면 &lt;b&gt;페이지에서 바로 빨간줄&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;key={memo.id}&lt;/code&gt;는 React 리스트 렌더링 기본기 (타입과는 별개, 필수)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;npm run dev&lt;/code&gt;로 &lt;code&gt;http://localhost:3000&lt;/code&gt;을 보면 카드 두 개가 보여야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;props를 펼치는 다른 방식 (참고)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 &lt;code&gt;memo&lt;/code&gt; 객체 하나를 통째로 넘겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔은 필드를 하나씩 받기도 합니다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;type MemoCardProps = {
  title: string;
  content: string;
  createdAt: string;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 틀린 건 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈 추천:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 &lt;b&gt;하나의 모델&lt;/b&gt;이면 객체로 넘긴다 (&lt;code&gt;memo: Memo&lt;/code&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드가 늘어날 때마다 props를 하나씩 추가하는 것보다,&lt;br /&gt;모델만 고치면 되어서 유지보수가 쉽습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;@/&lt;/code&gt; 는 뭐지?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;create-next-app&lt;/code&gt;에서 import alias를 켰다면:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import type { Memo } from &quot;@/types/memo&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@/&lt;/code&gt;는 보통 프로젝트 루트를 가리킵니다.&lt;br /&gt;&lt;code&gt;tsconfig.json&lt;/code&gt;의 &lt;code&gt;paths&lt;/code&gt;에 정의되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대 경로 지옥(&lt;code&gt;../../../types/memo&lt;/code&gt;)을 피하려고 쓰는 설정입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 하는 실수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. &lt;code&gt;Memo&lt;/code&gt;를 값처럼 import&lt;/b&gt;&lt;br /&gt;&amp;rarr; 타입만 쓸 때는 &lt;code&gt;import type { Memo }&lt;/code&gt; 권장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. props 타입을 안 쓰고 매개변수만 적음&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;({ memo })&lt;/code&gt;만 쓰면 &lt;code&gt;memo&lt;/code&gt;가 &lt;code&gt;any&lt;/code&gt;에 가까워져 이득이 사라짐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. &lt;code&gt;key&lt;/code&gt;를 인덱스(&lt;code&gt;index&lt;/code&gt;)로만 씀&lt;/b&gt;&lt;br /&gt;&amp;rarr; 샘플에선 당장 안 터져도, 목록이 바뀌면 버그 나기 쉬움. &lt;code&gt;id&lt;/code&gt; 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. &lt;code&gt;types&lt;/code&gt; 폴더 위치 착각&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;app/types&lt;/code&gt;가 아니라 프로젝트 루트의 &lt;code&gt;types/&lt;/code&gt; (이 시리즈 기준)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 모양은 &lt;code&gt;type&lt;/code&gt;으로 먼저 고정한다&lt;/li&gt;
&lt;li&gt;컴포넌트 props에도 같은 타입을 연결한다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Memo[]&lt;/code&gt;로 샘플을 만들면 잘못된 데이터가 즉시 드러난다&lt;/li&gt;
&lt;li&gt;모델 하나면 props도 객체 하나로 넘기는 편이 단순하다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Next.js + TS] 4. 라우팅과 레이아웃&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/memos&lt;/code&gt; 목록 페이지 만들기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memos/[id]&lt;/code&gt; 상세 페이지&lt;/li&gt;
&lt;li&gt;&lt;code&gt;layout.tsx&lt;/code&gt;에 간단한 내비 넣기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 홈에 샘플이 붙어 있는 상태입니다.&lt;br /&gt;다음 편에서 &lt;b&gt;URL별로 화면을 나누기&lt;/b&gt; 시작합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왜 Next.js + TypeScript인가&lt;/li&gt;
&lt;li&gt;프로젝트 만들고 폴더 구조 이해하기&lt;/li&gt;
&lt;li&gt;TypeScript로 컴포넌트 타입 잡기 &amp;larr; 지금&lt;/li&gt;
&lt;li&gt;라우팅과 레이아웃&lt;/li&gt;
&lt;li&gt;Server Component vs Client Component&lt;/li&gt;
&lt;li&gt;데이터 가져오기&lt;/li&gt;
&lt;li&gt;폼과 서버 액션 입문&lt;/li&gt;
&lt;li&gt;정리 + 다음 학습 로드맵&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/781</guid>
      <comments>https://aamoos.tistory.com/781#entry781comment</comments>
      <pubDate>Wed, 29 Jul 2026 14:41:08 +0900</pubDate>
    </item>
    <item>
      <title># [Next.js + TS] 2. 프로젝트 만들고 폴더 구조 이해하기</title>
      <link>https://aamoos.tistory.com/780</link>
      <description>&lt;h1&gt;[Next.js + TS] 2. 프로젝트 만들고 폴더 구조 이해하기&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서 &amp;ldquo;왜 Next + TS인지&amp;rdquo;를 봤다면, 이번 편은 &lt;b&gt;실제로 프로젝트를 띄우고 폴더를 열어보는&lt;/b&gt; 시간입니다.&lt;br /&gt;목표: &lt;code&gt;create-next-app&lt;/code&gt; &amp;rarr; 실행 &amp;rarr; &lt;code&gt;app/page.tsx&lt;/code&gt; 문구 바꾸기까지.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 할 일&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Next.js + TypeScript 프로젝트 생성&lt;/li&gt;
&lt;li&gt;로컬에서 실행해 보기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app/&lt;/code&gt; 폴더 구조 읽기&lt;/li&gt;
&lt;li&gt;홈 화면 문구를 바꿔 &amp;ldquo;여기가 페이지다&amp;rdquo;를 몸으로 익히기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치만 하고 끝내면 금방 까먹습니다.&lt;br /&gt;&lt;b&gt;한 줄이라도 직접 고쳐 보는 것&lt;/b&gt;이 이번 편의 실습입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사전 준비&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래가 있으면 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Node.js (LTS 권장, 18 이상)&lt;/li&gt;
&lt;li&gt;터미널 (PowerShell, cmd, 또는 VS Code/Cursor 터미널)&lt;/li&gt;
&lt;li&gt;코드 에디터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전 확인:&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;node -v
npm -v&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자가 나오면 OK입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 원하는 폴더로 이동한 뒤:&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;npx create-next-app@latest memo-app&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문에 대략 이렇게 답하면 이 시리즈와 맞습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;질문&lt;/th&gt;
&lt;th&gt;추천 답&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESLint?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailwind CSS?&lt;/td&gt;
&lt;td&gt;Yes (있어도 되고, 없어도 됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/&lt;/code&gt; directory?&lt;/td&gt;
&lt;td&gt;No (처음엔 &lt;code&gt;app/&lt;/code&gt;이 루트에 있는 편이 단순)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App Router?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turbopack?&lt;/td&gt;
&lt;td&gt;Yes여도 무방&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;import alias (&lt;code&gt;@/*&lt;/code&gt;)?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성이 끝나면:&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;cd memo-app
npm run dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 &lt;code&gt;http://localhost:3000&lt;/code&gt; 을 엽니다.&lt;br /&gt;Next 기본 시작 화면이 보이면 성공입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 이 명령어인가&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;npx&lt;/code&gt;: 패키지를 전역 설치하지 않고 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;create-next-app&lt;/code&gt;: Next 공식 스캐폴딩 도구&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@latest&lt;/code&gt;: 최신 템플릿 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 &lt;code&gt;package.json&lt;/code&gt;을 손으로 조립해도 되지만,&lt;br /&gt;처음에는 &lt;b&gt;공식 템플릿으로 시작하는 게&lt;/b&gt; 삽질이 적습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;폴더 구조, 지금 볼 것만&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 프로젝트에서 &lt;b&gt;지금은 이것만&lt;/b&gt; 보면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;memo-app/
  app/
    layout.tsx
    page.tsx
    globals.css
  public/
  package.json
  tsconfig.json
  next.config.ts   (또는 next.config.mjs)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;app/&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App Router의 중심입니다.&lt;br /&gt;&lt;b&gt;페이지와 레이아웃이 여기 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;app/page.tsx&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/&lt;/code&gt; (홈) 화면입니다.&lt;br /&gt;이 파일의 return JSX가 브라우저에 보입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;app/layout.tsx&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 페이지를 감싸는 &lt;b&gt;공통 틀&lt;/b&gt;입니다.&lt;br /&gt;&lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;, 공통 폰트, 나중에 넣을 헤더 같은 것이 여기로 옵니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;app/globals.css&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 스타일입니다.&lt;br /&gt;이번 시리즈에서는 깊게 안 들어가도 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트와 의존성 목록입니다.&lt;br /&gt;&lt;code&gt;npm run dev&lt;/code&gt;가 여기서 정의됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;tsconfig.json&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript 설정입니다.&lt;br /&gt;지금은 &amp;ldquo;TS가 켜져 있다&amp;rdquo; 정도만 알면 충분합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;public/&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지, 파비콘처럼 &lt;b&gt;그대로 서빙할 정적 파일&lt;/b&gt; 자리입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 파일 열어보기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) &lt;code&gt;app/layout.tsx&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략 이런 형태입니다. (버전마다 세부 표현은 조금 다를 수 있음)&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function RootLayout({
  children,
}: Readonly&amp;lt;{
  children: React.ReactNode;
}&amp;gt;) {
  return (
    &amp;lt;html lang=&quot;en&quot;&amp;gt;
      &amp;lt;body&amp;gt;{children}&amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;children&lt;/code&gt; = 각 &lt;code&gt;page.tsx&lt;/code&gt;가 들어가는 자리&lt;/li&gt;
&lt;li&gt;홈이든 &lt;code&gt;/memos&lt;/code&gt;든, 결국 이 레이아웃 안에 렌더됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) &lt;code&gt;app/page.tsx&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 템플릿에는 데모 UI가 잔뜩 들어 있습니다.&lt;br /&gt;우리는 &lt;b&gt;메모 앱의 홈&lt;/b&gt;으로 바꿀 겁니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습: 홈 화면 문구 바꾸기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/page.tsx&lt;/code&gt;를 아래처럼 간단하게 교체합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function HomePage() {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;Memo App&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;Next.js + TypeScript 시리즈 실습 프로젝트&amp;lt;/p&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장 후 브라우저를 보면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제목이 &lt;b&gt;Memo App&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;아래 설명 문구&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 바뀌어 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 이번 편의 핵심 체감입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;app/page.tsx&lt;/code&gt;를 고치면 &lt;code&gt;/&lt;/code&gt;가 바뀐다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 컴포넌트를 수정하는 감각과 같습니다.&lt;br /&gt;차이점은 &lt;b&gt;이 파일이 곧 라우트&lt;/b&gt;라는 점입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;layout&lt;/code&gt;과 &lt;code&gt;page&lt;/code&gt; 관계 (그림으로)&lt;/h2&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;layout.tsx
└─ page.tsx   &amp;larr; 지금 고친 홈&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 &lt;code&gt;/memos&lt;/code&gt;를 만들면:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;layout.tsx
├─ page.tsx          &amp;rarr; /
└─ memos/page.tsx    &amp;rarr; /memos&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 헤더를 &lt;code&gt;layout.tsx&lt;/code&gt;에 넣으면 두 페이지에 다 보입니다.&lt;br /&gt;(헤더는 4편에서 본격적으로 다룹니다.)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 하는 실수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 서버가 안 떠 있는데 브라우저만 새로고침&lt;/b&gt;&lt;br /&gt;&amp;rarr; 터미널에서 &lt;code&gt;npm run dev&lt;/code&gt;가 켜져 있는지 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 다른 폴더의 파일을 수정&lt;/b&gt;&lt;br /&gt;&amp;rarr; 지금 실행 중인 &lt;code&gt;memo-app&lt;/code&gt; 안의 &lt;code&gt;app/page.tsx&lt;/code&gt;인지 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 저장을 안 함&lt;/b&gt;&lt;br /&gt;&amp;rarr; 에디터에 흰 점(미저장) 표시가 있으면 Ctrl+S&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 포트가 이미 사용 중&lt;/b&gt;&lt;br /&gt;&amp;rarr; &lt;code&gt;localhost:3000&lt;/code&gt; 대신 터미널에 나온 다른 포트(예: 3001)로 접속&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Next 프로젝트는 &lt;code&gt;create-next-app&lt;/code&gt;으로 시작하는 게 가장이다&lt;/li&gt;
&lt;li&gt;App Router의 중심은 &lt;code&gt;app/&lt;/code&gt;이다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;page.tsx&lt;/code&gt; = 그 경로의 화면&lt;/li&gt;
&lt;li&gt;&lt;code&gt;layout.tsx&lt;/code&gt; = 화면들을 감싸는 공통 틀&lt;/li&gt;
&lt;li&gt;홈을 바꾸려면 &lt;code&gt;app/page.tsx&lt;/code&gt;를 고치면 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Next.js + TS] 3. TypeScript로 컴포넌트 타입 잡기&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;props에 타입 붙이기&lt;/li&gt;
&lt;li&gt;메모 카드 같은 작은 컴포넌트 만들기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Memo&lt;/code&gt; 타입을 실제로 화면에 연결하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편부터 &amp;ldquo;폴더만 있는 상태&amp;rdquo;에서 &lt;b&gt;타입이 있는 UI 조각&lt;/b&gt;으로 넘어갑니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왜 Next.js + TypeScript인가&lt;/li&gt;
&lt;li&gt;프로젝트 만들고 폴더 구조 이해하기 &amp;larr; 지금&lt;/li&gt;
&lt;li&gt;TypeScript로 컴포넌트 타입 잡기&lt;/li&gt;
&lt;li&gt;라우팅과 레이아웃&lt;/li&gt;
&lt;li&gt;Server Component vs Client Component&lt;/li&gt;
&lt;li&gt;데이터 가져오기&lt;/li&gt;
&lt;li&gt;폼과 서버 액션 입문&lt;/li&gt;
&lt;li&gt;정리 + 다음 학습 로드맵&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/780</guid>
      <comments>https://aamoos.tistory.com/780#entry780comment</comments>
      <pubDate>Wed, 29 Jul 2026 14:33:30 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js + TS] 1. 왜 Next.js + TypeScript인가</title>
      <link>https://aamoos.tistory.com/779</link>
      <description>&lt;h1&gt;[Next.js + TS] 1. 왜 Next.js + TypeScript인가&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React는 써 봤고, 이제 Next.js와 TypeScript를 &lt;b&gt;제대로&lt;/b&gt; 잡고 싶은 사람을 위한 시리즈입니다.&lt;br /&gt;App Router 기준으로 작은 &lt;b&gt;메모 앱&lt;/b&gt;을 만들며 개념과 실습을 같이 갑니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 시리즈에서 하는 일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React만으로도 화면은 만들 수 있습니다.&lt;br /&gt;다만 실제 서비스에 가까워질수록 이런 질문이 생깁니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이지(라우팅)는 어떻게 나눌까?&lt;/li&gt;
&lt;li&gt;SEO / 초기 로딩은?&lt;/li&gt;
&lt;li&gt;서버에서 데이터를 가져오는 게 나을까, 클라이언트에서 할까?&lt;/li&gt;
&lt;li&gt;props랑 API 응답 타입이 자꾸 틀어지는데?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Next.js&lt;/b&gt;는 React 위에 &lt;b&gt;앱에 필요한 뼈대&lt;/b&gt;를 올려 준 프레임워크이고,&lt;br /&gt;&lt;b&gt;TypeScript&lt;/b&gt;는 그 뼈대와 데이터를 &lt;b&gt;타입이 있는 계약&lt;/b&gt;으로 붙잡아 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈는 이론만 늘어놓지 않습니다.&lt;br /&gt;매 편 &lt;b&gt;왜 쓰는지 &amp;rarr; 핵심 개념 &amp;rarr; 따라 치는 코드 &amp;rarr; 정리&lt;/b&gt; 순서로 갑니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React만 쓸 때 vs Next.js&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;React (CRA/Vite 등)&lt;/th&gt;
&lt;th&gt;Next.js (App Router)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;td&gt;UI 라이브러리&lt;/td&gt;
&lt;td&gt;React 기반 풀스택 프레임워크&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라우팅&lt;/td&gt;
&lt;td&gt;react-router 등을 직접 구성&lt;/td&gt;
&lt;td&gt;폴더 구조로 페이지 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;렌더링&lt;/td&gt;
&lt;td&gt;기본적으로 클라이언트 중심&lt;/td&gt;
&lt;td&gt;서버/클라이언트 컴포넌트를 나눔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터&lt;/td&gt;
&lt;td&gt;useEffect + fetch가 흔함&lt;/td&gt;
&lt;td&gt;서버에서 fetch, Server Actions 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포 관점&lt;/td&gt;
&lt;td&gt;SPA로 시작하는 경우가 많음&lt;/td&gt;
&lt;td&gt;페이지/레이아웃 단위로 최적화하기 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄로 말하면:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React = 부품&lt;br /&gt;Next.js = 그 부품으로 집을 짓는 설계도 + 공구&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React를 안다면 JSX, props, state, hooks는 그대로 씁니다.&lt;br /&gt;Next.js는 그 위에 &lt;b&gt;파일 기반 라우팅&lt;/b&gt;, &lt;b&gt;서버 컴포넌트&lt;/b&gt;, &lt;b&gt;데이터/폼 처리&lt;/b&gt; 같은 규칙을 더합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 TypeScript를 같이 쓰나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript는 &amp;ldquo;어려운 문법 모음&amp;rdquo;이 아니라, &lt;b&gt;실수 예방용 안전벨트&lt;/b&gt;에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 메모 객체:&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;type Memo = {
  id: string;
  title: string;
  content: string;
  createdAt: string;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 정해 두면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;memo.titlle&lt;/code&gt; 같은 오타를 에디터가 바로 잡아 줍니다&lt;/li&gt;
&lt;li&gt;컴포넌트 props가 뭐가 필요한지 읽지 않아도 보입니다&lt;/li&gt;
&lt;li&gt;API 응답 형태가 바뀌면 쓰는 쪽이 깨져서 빨리 알아챕니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React만 쓸 때도 TS는 유용하지만,&lt;br /&gt;Next.js처럼 &lt;b&gt;페이지 &amp;middot; 레이아웃 &amp;middot; 서버/클라이언트 경계&lt;/b&gt;가 늘어날수록 타입의 이득이 커집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈에서는 고급 타입 마법을 하지 않습니다.&lt;br /&gt;&lt;b&gt;props, 데이터 모델, 함수 인자/반환&lt;/b&gt; 정도만 확실하게 잡습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;App Router를 한눈에&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 13 이후 기본은 &lt;b&gt;App Router&lt;/b&gt;입니다.&lt;br /&gt;핵심은 단순합니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;app/
  layout.tsx   &amp;rarr; 공통 레이아웃 (헤더, 폰트 등)
  page.tsx     &amp;rarr; &quot;/&quot; 페이지
  memos/
    page.tsx   &amp;rarr; &quot;/memos&quot; 목록
    [id]/
      page.tsx &amp;rarr; &quot;/memos/123&quot; 상세&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;폴더 이름&lt;/b&gt;이 URL이 됩니다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;page.tsx&lt;/code&gt;가 있어야 그 경로가 페이지로 열립니다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;layout.tsx&lt;/code&gt;는 하위 페이지를 감싸는 공통 UI입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 다룰 &lt;b&gt;Server Component / Client Component&lt;/b&gt;도 이 구조 위에서 이해하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 이것만 기억하면 충분합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL = 폴더&lt;br /&gt;화면 = &lt;code&gt;page.tsx&lt;/code&gt;&lt;br /&gt;공통 틀 = &lt;code&gt;layout.tsx&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 예고: 메모 앱&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 만들 것은 거창한 SaaS가 아닙니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;메모 목록 보기&lt;/li&gt;
&lt;li&gt;메모 상세 보기&lt;/li&gt;
&lt;li&gt;메모 추가하기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작은 흐름 안에 라우팅, 타입, 서버/클라이언트, 데이터, 폼이 다 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;예제용 코드 조각&amp;rdquo;보다 &lt;b&gt;이어지는 앱 하나&lt;/b&gt;로 가면, 복습할 때 훨씬 편합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 편에서 가져갈 것&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Next.js는 React를 대체하는 게 아니라, React로 앱을 만들기 위한 프레임워크다&lt;/li&gt;
&lt;li&gt;TypeScript는 문법이 목적이 아니라, 데이터와 props의 계약을 고정한다&lt;/li&gt;
&lt;li&gt;App Router는 폴더 = URL, &lt;code&gt;page&lt;/code&gt; = 화면, &lt;code&gt;layout&lt;/code&gt; = 공통 틀이다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Next.js + TS] 2. 프로젝트 만들고 폴더 구조 이해하기&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;create-next-app&lt;/code&gt;으로 프로젝트 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app/&lt;/code&gt;, &lt;code&gt;page.tsx&lt;/code&gt;, &lt;code&gt;layout.tsx&lt;/code&gt; 직접 열어보기&lt;/li&gt;
&lt;li&gt;첫 화면 문구를 바꿔 보며 &amp;ldquo;여기가 홈이다&amp;rdquo;를 몸으로 익히기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왜 Next.js + TypeScript인가 &amp;larr; 지금&lt;/li&gt;
&lt;li&gt;프로젝트 만들고 폴더 구조 이해하기&lt;/li&gt;
&lt;li&gt;TypeScript로 컴포넌트 타입 잡기&lt;/li&gt;
&lt;li&gt;라우팅과 레이아웃&lt;/li&gt;
&lt;li&gt;Server Component vs Client Component&lt;/li&gt;
&lt;li&gt;데이터 가져오기&lt;/li&gt;
&lt;li&gt;폼과 서버 액션 입문&lt;/li&gt;
&lt;li&gt;정리 + 다음 학습 로드맵&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문이나 &amp;ldquo;이 부분 더 풀어서&amp;rdquo; 싶은 구간 있으면 댓글로 남겨 주세요.&lt;br /&gt;다음 편에서 실제로 프로젝트를 띄웁니다.&lt;/p&gt;</description>
      <category>frontend/NextJs</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/779</guid>
      <comments>https://aamoos.tistory.com/779#entry779comment</comments>
      <pubDate>Wed, 29 Jul 2026 14:26:11 +0900</pubDate>
    </item>
    <item>
      <title># 면접 CS 정리 Day 1 (풀스택 / Java&amp;middot;Spring&amp;middot;React)</title>
      <link>https://aamoos.tistory.com/778</link>
      <description>&lt;h1&gt;면접 CS 정리 Day 1 (풀스택 / Java&amp;middot;Spring&amp;middot;React)&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q1. PUT vs PATCH&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;HTTP &lt;code&gt;PUT&lt;/code&gt;과 &lt;code&gt;PATCH&lt;/code&gt;의 차이, 실무에서 각각 언제 쓰는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;PUT&lt;/code&gt;: 리소스 &lt;b&gt;전체 교체&lt;/b&gt;. 빠진 필드가 있으면 null로 덮일 수 있음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PATCH&lt;/code&gt;: &lt;b&gt;부분 수정&lt;/b&gt;. 요청으로 온 필드만 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성은 보통 &lt;code&gt;POST&lt;/code&gt;, 전체 수정은 &lt;code&gt;PUT&lt;/code&gt;, 일부 수정은 &lt;code&gt;PATCH&lt;/code&gt;.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q2. 생성자 주입을 선호하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;Spring에서 필드 주입(&lt;code&gt;@Autowired&lt;/code&gt; on field)보다 생성자 주입을 선호하는 이유는?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;테스트 시 mock 객체를 생성자로 넣기 쉽다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final&lt;/code&gt;로 필수 의존성을 명확히 하고 불변으로 유지할 수 있다&lt;/li&gt;
&lt;li&gt;의존성 누락 시 객체 생성이 실패해서 문제를 빨리 발견한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;생성자 주입이 순환참조를 &amp;ldquo;막아 주는&amp;rdquo; 것은 아니다.&lt;br /&gt;순환이면 기동 시 바로 실패해서 설계를 고치게 만든다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q3. 인덱스가 있는데도 느린 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;인덱스를 만들었는데도 쿼리가 느릴 때 의심할 원인 2가지는?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;조건 때문에 &lt;b&gt;인덱스를 못 탐&lt;/b&gt; (함수 감싸기, &lt;code&gt;LIKE '%keyword'&lt;/code&gt;, 타입 불일치)&lt;/li&gt;
&lt;li&gt;선택도가 낮거나, 가져와야 할 행이 너무 많음&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행계획(&lt;code&gt;EXPLAIN&lt;/code&gt;)에서 &lt;code&gt;type = ALL&lt;/code&gt;, &lt;code&gt;key = NULL&lt;/code&gt; 여부를 먼저 본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q4. GET으로 데이터를 바꾸면 안 되는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;왜 &lt;code&gt;GET&lt;/code&gt;으로 서버 상태를 변경하면 안 되는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;GET&lt;/code&gt;은 &lt;b&gt;safe&lt;/b&gt;해야 한다 &amp;rarr; 조회만, 상태 변경 X&lt;/li&gt;
&lt;li&gt;브라우저 프리페치, 크롤러, 캐시가 &lt;code&gt;GET&lt;/code&gt;을 자동 호출할 수 있음&lt;/li&gt;
&lt;li&gt;URL/로그/히스토리에 남아 의도치 않은 변경&amp;middot;노출이 생길 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경은 &lt;code&gt;POST&lt;/code&gt; / &lt;code&gt;PUT&lt;/code&gt; / &lt;code&gt;PATCH&lt;/code&gt; / &lt;code&gt;DELETE&lt;/code&gt;를 사용한다.&lt;br /&gt;(&lt;code&gt;POST&lt;/code&gt;가 무조건 더 보안에 강하다기보다, &lt;b&gt;메서드 의미&lt;/b&gt;가 다르다.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q5. React list의 key&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;리스트 렌더링에서 &lt;code&gt;key&lt;/code&gt;에 &lt;code&gt;id&lt;/code&gt;를 쓰고 &lt;code&gt;index&lt;/code&gt;를 잘 안 쓰는 이유는?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;br /&gt;&lt;code&gt;key&lt;/code&gt;는 React가 어떤 항목을 재사용/추가/삭제할지 구분하는 식별자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;index&lt;/code&gt;를 key로 쓰면 삽입&amp;middot;삭제&amp;middot;순서 변경 시 항목이 잘못 매칭되어&lt;br /&gt;input 값이 밀리거나 불필요한 리렌더가 발생할 수 있다.&lt;br /&gt;그래서 고유한 &lt;code&gt;id&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q6. 트랜잭션 전파 REQUIRED&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;&lt;code&gt;@Transactional&lt;/code&gt;의 기본 propagation &lt;code&gt;REQUIRED&lt;/code&gt;는&lt;br /&gt;트랜잭션이 있을 때 / 없을 때 각각 어떻게 동작하는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 &lt;b&gt;없음&lt;/b&gt; &amp;rarr; 새로 시작&lt;/li&gt;
&lt;li&gt;트랜잭션 &lt;b&gt;있음&lt;/b&gt; &amp;rarr; 기존 트랜잭션에 &lt;b&gt;참여&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄: &lt;b&gt;있으면 쓰고, 없으면 만든다.&lt;/b&gt;&lt;br /&gt;보너스: &lt;code&gt;REQUIRES_NEW&lt;/code&gt;는 기존 트랜잭션과 분리해 무조건 새 트랜잭션을 연다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q7. CORS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;br /&gt;CORS가 무엇인가? Spring에서 정석으로 어떻게 해결하는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답&lt;/b&gt;&lt;br /&gt;브라우저가 &lt;b&gt;다른 Origin&lt;/b&gt;(스킴 + 호스트 + 포트)으로 요청할 때&lt;br /&gt;보안상 제한하는 정책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 허용 Origin/메서드/헤더를 설정한다.&lt;br /&gt;(Spring: &lt;code&gt;CorsConfiguration&lt;/code&gt;, Security CORS 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 개발에서는 프론트 개발 서버 프록시로 우회하기도 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 한 줄 암기&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;PUT = 전체 교체 / PATCH = 부분 수정&lt;/li&gt;
&lt;li&gt;GET = 조회만 (safe)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REQUIRED&lt;/code&gt; = 있으면 참여, 없으면 생성&lt;/li&gt;
&lt;li&gt;React &lt;code&gt;key&lt;/code&gt; = 고유 id&lt;/li&gt;
&lt;li&gt;CORS = Origin이 다를 때 브라우저 제한&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>기타/CS 질문</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/778</guid>
      <comments>https://aamoos.tistory.com/778#entry778comment</comments>
      <pubDate>Wed, 29 Jul 2026 14:12:47 +0900</pubDate>
    </item>
    <item>
      <title>Claude Code / Cursor 개발 필수 스킬 4가지 설정법</title>
      <link>https://aamoos.tistory.com/777</link>
      <description>&lt;h1&gt;Claude Code / Cursor 개발 필수 스킬 4가지 설정법&lt;/h1&gt;
&lt;p&gt;AI 코딩 에이전트(Claude Code, Cursor)를 쓸 때 &amp;quot;그냥 시키는 대로 코드를 뱉는&amp;quot; 상태에서 벗어나게 해주는 스킬 4개를 정리했다. 각 스킬은 서로 다른 축을 커버하기 때문에 겹치지 않고 같이 쓸 수 있다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;스킬&lt;/th&gt;
&lt;th&gt;관여하는 영역&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Karpathy Guidelines&lt;/td&gt;
&lt;td&gt;사고방식 전반 (가정 명시, 최소 수정, 검증 기준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ponytail&lt;/td&gt;
&lt;td&gt;코드량 (과잉설계 방지, YAGNI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caveman&lt;/td&gt;
&lt;td&gt;말투/응답 토큰량 (불필요한 설명 압축)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Superpowers&lt;/td&gt;
&lt;td&gt;개발 프로세스 자체 (기획 → 계획 → TDD → 검증)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;1. Karpathy Guidelines&lt;/h2&gt;
&lt;p&gt;Andrej Karpathy가 지적한 &amp;quot;LLM 코딩의 흔한 실수 패턴&amp;quot;을 CLAUDE.md 파일 하나로 방지하는 원칙 모음이다. 별도 프로그램이 아니라 순수 텍스트 파일이라 이식성이 가장 좋다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;핵심 원칙&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;가정 드러내기 — 애매하면 짐작하지 않고 명시&lt;/li&gt;
&lt;li&gt;과잉설계 방지 — 요청하지 않은 기능·추상화 금지&lt;/li&gt;
&lt;li&gt;외과수술식 수정 — 필요한 부분만 건드리기&lt;/li&gt;
&lt;li&gt;검증 가능한 성공 기준 — 확인 가능한 완료 조건 먼저 정하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Claude Code 설정&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -o CLAUDE.md https://raw.githubusercontent.com/forrestchang/andrej-karpathy-skills/main/CLAUDE.md&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;프로젝트 루트에 두면 세션 시작 시 자동으로 읽힌다. 이미 CLAUDE.md가 있다면 덮어쓰지 말고 뒤에 이어붙이기(append) 하는 것을 권장한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cursor 설정&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p .cursor/rules
curl -o .cursor/rules/karpathy.mdc https://raw.githubusercontent.com/forrestchang/andrej-karpathy-skills/main/CLAUDE.md&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;프로젝트 전체가 아니라 모든 프로젝트에 적용하고 싶다면 홈 디렉토리의 &lt;code&gt;~/.cursor/rules/&lt;/code&gt;에 넣거나, Cursor Settings → Rules → User Rules에 내용을 붙여넣으면 된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;커스터마이징&lt;/strong&gt;&lt;br&gt;받은 파일 그대로 쓰지 말고 스택, 명령어, 컨벤션, 금지사항을 추가해서 120줄 이내로 다듬는 것이 좋다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. Ponytail&lt;/h2&gt;
&lt;p&gt;&amp;quot;이 코드 진짜 필요해?&amp;quot;를 강제로 묻는 YAGNI 결정 사다리 스킬이다. 과잉설계 함정(예: 날짜 피커 라이브러리 대신 &lt;code&gt;&amp;lt;input type=&amp;quot;date&amp;quot;&amp;gt;&lt;/code&gt;)을 잡아내는 데 특화되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude Code 설정&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/plugin marketplace add DietrichGebert/ponytail
/plugin add ponytail@ponytail&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;설치 후 첫 세션에서 hook 신뢰(trust) 여부를 확인하라는 프롬프트가 뜨면 승인한다. node가 PATH에 잡혀 있어야 정상 작동하며, 정상 작동 시 상태줄에 &lt;code&gt;[PONYTAIL]&lt;/code&gt; 뱃지가 표시된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cursor 설정&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p .cursor/rules
# 저장소의 .cursor/rules 안 규칙 파일을 복사&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Claude Code의 hook 기반 자동 활성화·상태줄 표시는 재현되지 않지만, 핵심 규칙 내용은 동일하게 적용된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;리뷰 기능&lt;/strong&gt;&lt;br&gt;코드 작성 후 &lt;code&gt;/ponytail-review&lt;/code&gt;를 입력하면 과잉설계된 부분만 짚어준다. 단, 버그·보안 이슈는 다루지 않으므로 일반 코드 리뷰와 병행해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Caveman&lt;/h2&gt;
&lt;p&gt;Claude의 응답 자체를 압축하는 스킬이다. 코드를 줄이는 게 아니라 서론·반복 설명·과한 안내문 같은 텍스트를 없애 출력 토큰을 줄인다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;설치 (Skills CLI, 여러 툴 공유 가능)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx skills add https://github.com/juliusbrussee/caveman --skill caveman -g&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;글로벌(&lt;code&gt;-g&lt;/code&gt;) 설치 시 여러 호환 툴이 공유하는 폴더에 들어가 Cursor 등에서도 인식될 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cursor 개별 설정&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p .cursor/rules
# 저장소 규칙 파일을 .cursor/rules/에 복사&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lite / full / ultra 강도를 선택해서 저장 가능&lt;/li&gt;
&lt;li&gt;위험한 작업(테이블 삭제 등)에서는 자동으로 정상 모드로 돌아옴&lt;/li&gt;
&lt;li&gt;팀원과 세션을 공유하거나 PR 코멘트로 남길 계획이면 lite 모드를 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4. Superpowers&lt;/h2&gt;
&lt;p&gt;코드 스타일이 아니라 &lt;strong&gt;개발 프로세스 전체&lt;/strong&gt;를 강제하는 프레임워크다. Jesse Vincent(obra)가 만들었으며 14개 스킬이 세트로 묶여 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;핵심 흐름&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brainstorming(요구사항 탐색) → writing-plans(계획 작성)
→ TDD(실패하는 테스트 먼저 작성) → 실행 → verification(완료 검증)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Claude Code 설정&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/plugin install superpowers@claude-plugins-official&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Cursor 설정&lt;/strong&gt;&lt;br&gt;Cursor 마켓플레이스에서 &amp;quot;Superpowers&amp;quot; 검색 후 설치하거나, 채팅창에 아래 명령 입력:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/add-plugin superpowers&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Cursor 팀이 공식 인증(Verified)한 몇 안 되는 플러그인이라 설치가 가장 간단하다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;설치 확인 방법&lt;/strong&gt;&lt;br&gt;새 기능을 요청했을 때 AI가 바로 코드를 쓰지 않고 요구사항을 먼저 되묻는다면 정상 작동 중인 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;스코프(적용 범위) 정리&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Cursor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;전역(모든 프로젝트)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.claude/skills/&lt;/code&gt; 또는 플러그인 설치&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.cursor/rules/&lt;/code&gt; 또는 Settings → User Rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프로젝트 단위&lt;/td&gt;
&lt;td&gt;프로젝트 루트 &lt;code&gt;CLAUDE.md&lt;/code&gt; 또는 &lt;code&gt;./.claude/skills/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;프로젝트 루트 &lt;code&gt;.cursor/rules/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;플러그인/스킬은 한 번 설정해두면 이후 세션마다 자동으로 로드되며, 매번 다시 설정할 필요는 없다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;추천 조합&lt;/h2&gt;
&lt;p&gt;넷 다 무조건 설치하기보다는 필요에 따라 조합하는 것이 좋다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;기본 3종&lt;/strong&gt;: Karpathy + Ponytail + (Claude Code 내장) &lt;code&gt;/code-review&lt;/code&gt; — 사고방식 → 코드 최소화 → 검증의 파이프라인 구성&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;프로세스까지 강제하고 싶다면&lt;/strong&gt;: Superpowers 추가 — 다만 Karpathy의 검증 기준 원칙과 일부 겹치므로 굳이 둘 다 넣을 필요는 없음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;토큰 비용이 부담될 때만&lt;/strong&gt;: Caveman 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;스킬 폴더는 dotfiles처럼 작고 계속 줄어드는 방향으로 관리하는 것이 이상적이다. 안 쓰는 스킬은 지우고, 부족한 부분은 직접 CLAUDE.md에 써넣는 것이 장기적으로 더 낫다.&lt;/p&gt;</description>
      <category>AI</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/777</guid>
      <comments>https://aamoos.tistory.com/777#entry777comment</comments>
      <pubDate>Wed, 22 Jul 2026 16:45:44 +0900</pubDate>
    </item>
    <item>
      <title>git 수정한 파일이 없는데 intelij에 staging에 올라가있는경우</title>
      <link>https://aamoos.tistory.com/776</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;intelij를 사용하다가 상단에 Contents have differences only in line separators 알림이 보이고, staging에 수정하지 않은 파일들이 올라갔을경우 아래 명령어를 터미널에 입력하여 해결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779860902243&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#1. Git core.autocrlf 설정 (권장)
git config --global core.autocrlf true

#2. 현재 스테이징된 찌꺼기 파일들 한 번에 정리하기
git add --renormalize .&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;429&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T46F4/dJMcacpKbha/rj0fZ08QwQAqTsg2ncEhk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T46F4/dJMcacpKbha/rj0fZ08QwQAqTsg2ncEhk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T46F4/dJMcacpKbha/rj0fZ08QwQAqTsg2ncEhk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT46F4%2FdJMcacpKbha%2Frj0fZ08QwQAqTsg2ncEhk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;710&quot; height=&quot;429&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;429&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>backend/오류모음</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/776</guid>
      <comments>https://aamoos.tistory.com/776#entry776comment</comments>
      <pubDate>Wed, 27 May 2026 14:49:45 +0900</pubDate>
    </item>
    <item>
      <title>커서 AI 세팅</title>
      <link>https://aamoos.tistory.com/775</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;모델선택&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;216&quot; data-origin-height=&quot;259&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cemPIY/dJMcacCW6hC/ysJehpnNDwT7DxgASVUBZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cemPIY/dJMcacCW6hC/ysJehpnNDwT7DxgASVUBZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cemPIY/dJMcacCW6hC/ysJehpnNDwT7DxgASVUBZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcemPIY%2FdJMcacCW6hC%2FysJehpnNDwT7DxgASVUBZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;216&quot; height=&quot;259&quot; data-origin-width=&quot;216&quot; data-origin-height=&quot;259&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,0,0&quot;&gt;Claude 3.5 Sonnet:&lt;/b&gt; 현재 코딩에서 가장 똑똑하다고 평가받는 모델입니다. 기본으로 설정하는 걸 추천합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0&quot;&gt;GPT-4o:&lt;/b&gt; 논리적인 설명이나 일반적인 질문에 강점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;필수 성능 세팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.cursorignore&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 프로젝트 루트에 해당 파일을 생성후 cursor가 인덱싱 하지 않을 파일을 설정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1777441093188&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 빌드 결과물 제외
dist/
build/

# 의존성 라이브러리 제외 (커서가 이미 구조를 알고 있으므로 인덱싱에서 제외하면 가벼워짐)
node_modules/

# 환경 변수 및 보안 파일 (가장 중요!)
.env
*.pem
keys/

# 로그 파일
*.log&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.cursorrules&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 프로젝트 루트에 해당 파일을 생성후 사용자가 프롬프트를 적어 규칙을 지정함&lt;/p&gt;
&lt;pre id=&quot;code_1777441314452&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- 모든 답변은 한국어로 친절하게 설명해줘.
- 코드 변경 전후의 핵심 차이점을 간단히 요약해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Thinking 끄기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FzJRN/dJMcabKPOcf/PCk21Pwt7DByTLrb7eo5KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FzJRN/dJMcabKPOcf/PCk21Pwt7DByTLrb7eo5KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FzJRN/dJMcabKPOcf/PCk21Pwt7DByTLrb7eo5KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFzJRN%2FdJMcabKPOcf%2FPCk21Pwt7DByTLrb7eo5KK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;433&quot; height=&quot;235&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;235&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thinking을 켜놓으면 그만큼 토큰이 많이 소모됨, 정말 풀리지않는 문제일경우만 Thinking 키기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Chat 10개정도 쌓이면 새로운 Chat 열어서 하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Chat에 내용이 너무 많이 쌓이면 ai가 이전 대화도 읽어서 실행하기 때문에 그만큼 토큰도 많이 소모됨&lt;/p&gt;</description>
      <category>AI</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/775</guid>
      <comments>https://aamoos.tistory.com/775#entry775comment</comments>
      <pubDate>Wed, 29 Apr 2026 14:42:09 +0900</pubDate>
    </item>
    <item>
      <title>커서 AI 외부 데이터 수집차단 설정 방법</title>
      <link>https://aamoos.tistory.com/774</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽 상단 톱니바퀴 버튼 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;217&quot; data-origin-height=&quot;33&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qbva5/dJMcaarA3q9/Wx4isXHk8ySKfsaKgoXoo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qbva5/dJMcaarA3q9/Wx4isXHk8ySKfsaKgoXoo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qbva5/dJMcaarA3q9/Wx4isXHk8ySKfsaKgoXoo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqbva5%2FdJMcaarA3q9%2FWx4isXHk8ySKfsaKgoXoo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;217&quot; height=&quot;33&quot; data-origin-width=&quot;217&quot; data-origin-height=&quot;33&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;General탭에서 맨하단에 privacy를 Privacy Mode로 변경&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;237&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zIBNG/dJMcaiC8hNQ/NDJOtHOJlNefTVHAldZkaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zIBNG/dJMcaiC8hNQ/NDJOtHOJlNefTVHAldZkaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zIBNG/dJMcaiC8hNQ/NDJOtHOJlNefTVHAldZkaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzIBNG%2FdJMcaiC8hNQ%2FNDJOtHOJlNefTVHAldZkaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1226&quot; height=&quot;237&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;237&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/774</guid>
      <comments>https://aamoos.tistory.com/774#entry774comment</comments>
      <pubDate>Wed, 29 Apr 2026 14:33:41 +0900</pubDate>
    </item>
    <item>
      <title>월배당 계산기 (자산 시뮬레이터)</title>
      <link>https://aamoos.tistory.com/773</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stock-ten-iota.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stock-ten-iota.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777010560153&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;월배당 자산 시뮬레이터&quot; data-og-description=&quot;월배당 ETF 배당 재투자와 장기 자산 성장을 계산해보세요. 일반계좌&amp;middot;ISA계좌별 세후 수익 시뮬레이션.&quot; data-og-host=&quot;stock-ten-iota.vercel.app&quot; data-og-source-url=&quot;https://stock-ten-iota.vercel.app/&quot; data-og-url=&quot;https://stock-ten-iota.vercel.app/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/1Qdyr/dJMb88e3eIA/aDMI9MA80jVZ7v4djh5ig1/img.png?width=1376&amp;amp;height=768&amp;amp;face=0_0_1376_768,https://scrap.kakaocdn.net/dn/p7Cdl/dJMb9aKHE6A/M5JnudcjgkaQctXas5JwE1/img.png?width=1376&amp;amp;height=768&amp;amp;face=0_0_1376_768&quot;&gt;&lt;a href=&quot;https://stock-ten-iota.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stock-ten-iota.vercel.app/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/1Qdyr/dJMb88e3eIA/aDMI9MA80jVZ7v4djh5ig1/img.png?width=1376&amp;amp;height=768&amp;amp;face=0_0_1376_768,https://scrap.kakaocdn.net/dn/p7Cdl/dJMb9aKHE6A/M5JnudcjgkaQctXas5JwE1/img.png?width=1376&amp;amp;height=768&amp;amp;face=0_0_1376_768');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;월배당 자산 시뮬레이터&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;월배당 ETF 배당 재투자와 장기 자산 성장을 계산해보세요. 일반계좌&amp;middot;ISA계좌별 세후 수익 시뮬레이션.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stock-ten-iota.vercel.app&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1559&quot; data-origin-height=&quot;980&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Acq87/dJMcafzCny8/kiyk8ACkvkDCp0dEVJGnKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Acq87/dJMcafzCny8/kiyk8ACkvkDCp0dEVJGnKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Acq87/dJMcafzCny8/kiyk8ACkvkDCp0dEVJGnKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAcq87%2FdJMcafzCny8%2Fkiyk8ACkvkDCp0dEVJGnKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1559&quot; height=&quot;980&quot; data-origin-width=&quot;1559&quot; data-origin-height=&quot;980&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배당주에 관심이있어서 월배당 자산 시뮬레이터를 React, Typescript 바이브 코딩으로 만들어봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 정보를 수집하는거 없이 사용할수 있게 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용법&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;165&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4nGH4/dJMcafl4Wyw/rhm2ZmjjldazpjXekzTfa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4nGH4/dJMcafl4Wyw/rhm2ZmjjldazpjXekzTfa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4nGH4/dJMcafl4Wyw/rhm2ZmjjldazpjXekzTfa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4nGH4%2FdJMcafl4Wyw%2Frhm2ZmjjldazpjXekzTfa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;336&quot; height=&quot;165&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;165&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배당 시뮬레이션을 실행하고자하는 기간을 설정, 시작년월, 환율을 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;326&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpSRka/dJMcaipzblU/sBXABwimkudbxjOC9ekGOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpSRka/dJMcaipzblU/sBXABwimkudbxjOC9ekGOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpSRka/dJMcaipzblU/sBXABwimkudbxjOC9ekGOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpSRka%2FdJMcaipzblU%2FsBXABwimkudbxjOC9ekGOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;326&quot; height=&quot;417&quot; data-origin-width=&quot;326&quot; data-origin-height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 버튼을 클릭해서 배당주의 종목명 티커 투자하고자하는 계좌, 가격 등을 입력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;차트로 표현&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRL4iA/dJMb99MS3es/8xkRP4yEFbkNX3efRDTwZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRL4iA/dJMb99MS3es/8xkRP4yEFbkNX3efRDTwZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRL4iA/dJMb99MS3es/8xkRP4yEFbkNX3efRDTwZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRL4iA%2FdJMb99MS3es%2F8xkRP4yEFbkNX3efRDTwZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1159&quot; height=&quot;1036&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;569&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sdmmV/dJMcac3U90z/3jxCkWoTHm5kVYz4FRlkCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sdmmV/dJMcac3U90z/3jxCkWoTHm5kVYz4FRlkCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sdmmV/dJMcac3U90z/3jxCkWoTHm5kVYz4FRlkCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsdmmV%2FdJMcac3U90z%2F3jxCkWoTHm5kVYz4FRlkCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1148&quot; height=&quot;569&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;569&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;월별 예상 적립금, 배당금, 평가금, 누적원금등을 시뮬레이션 하게 개발하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시뮬레이터이기 때문에 100프로 정확하지는 않습니다. 주식 가격이 계속 변동되고 배당금도 변하기때문에&lt;/p&gt;</description>
      <category>기타</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/773</guid>
      <comments>https://aamoos.tistory.com/773#entry773comment</comments>
      <pubDate>Fri, 24 Apr 2026 15:06:14 +0900</pubDate>
    </item>
    <item>
      <title>Intelij Smart Tomcat 설정 방법</title>
      <link>https://aamoos.tistory.com/772</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;간혹 프로젝트를 하다보면 Spring boot가 아닌 레거시 프로젝트를 진행할때가 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프로젝트를 실행하기 위해 smart tomcat을 설치하고 설정하는법을 포스팅 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;File - Settings&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYXEvy/dJMcaf7j6gk/KlRCrJfxpIQtup0wRtMiI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYXEvy/dJMcaf7j6gk/KlRCrJfxpIQtup0wRtMiI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYXEvy/dJMcaf7j6gk/KlRCrJfxpIQtup0wRtMiI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYXEvy%2FdJMcaf7j6gk%2FKlRCrJfxpIQtup0wRtMiI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;986&quot; height=&quot;728&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- SmartTomcat 플러그인 installed&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;351&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I6biV/dJMcacv2bEd/6lzZUkvRSTBe4xN5Fe5kdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I6biV/dJMcacv2bEd/6lzZUkvRSTBe4xN5Fe5kdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I6biV/dJMcacv2bEd/6lzZUkvRSTBe4xN5Fe5kdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI6biV%2FdJMcacv2bEd%2F6lzZUkvRSTBe4xN5Fe5kdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;351&quot; height=&quot;124&quot; data-origin-width=&quot;351&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽 상단에 점 클릭후 Edit ... 클릭&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;282&quot; data-origin-height=&quot;567&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DcCI1/dJMcabcLWV9/COYxEMeoUGNiYTl190t8Vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DcCI1/dJMcabcLWV9/COYxEMeoUGNiYTl190t8Vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DcCI1/dJMcabcLWV9/COYxEMeoUGNiYTl190t8Vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDcCI1%2FdJMcabcLWV9%2FCOYxEMeoUGNiYTl190t8Vk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;282&quot; height=&quot;567&quot; data-origin-width=&quot;282&quot; data-origin-height=&quot;567&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 플러스 버튼 클릭후 Smart Tomcat 클릭&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;553&quot; data-origin-height=&quot;583&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sQ8hN/dJMcajoh3Ux/uBPuDaYWoqKoUaSAxiUHHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sQ8hN/dJMcajoh3Ux/uBPuDaYWoqKoUaSAxiUHHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sQ8hN/dJMcajoh3Ux/uBPuDaYWoqKoUaSAxiUHHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsQ8hN%2FdJMcajoh3Ux%2FuBPuDaYWoqKoUaSAxiUHHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;553&quot; height=&quot;583&quot; data-origin-width=&quot;553&quot; data-origin-height=&quot;583&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Tomcat Server: 오른쪽에 폴더모양 클릭후 pc에 설치한 Apache Tomcat을 선택해서 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;527&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI3uVB/dJMcadBByPj/Kammi92wAzqZbLZxwS09zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI3uVB/dJMcadBByPj/Kammi92wAzqZbLZxwS09zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI3uVB/dJMcadBByPj/Kammi92wAzqZbLZxwS09zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI3uVB%2FdJMcadBByPj%2FKammi92wAzqZbLZxwS09zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;940&quot; height=&quot;527&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;527&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 apache tomcat이 없을경우&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 링크 접속후 download에서 받고자하는 버전의 톰캣을 다운로드합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tomcat.apache.org/download-90.cgi&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tomcat.apache.org/download-90.cgi&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;191&quot; data-origin-height=&quot;217&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cv1rzN/dJMcagkRuzN/KgL0RhAAlDEmH8kXc8pCD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cv1rzN/dJMcagkRuzN/KgL0RhAAlDEmH8kXc8pCD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cv1rzN/dJMcagkRuzN/KgL0RhAAlDEmH8kXc8pCD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcv1rzN%2FdJMcagkRuzN%2FKgL0RhAAlDEmH8kXc8pCD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;191&quot; height=&quot;217&quot; data-origin-width=&quot;191&quot; data-origin-height=&quot;217&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;587&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/StcgI/dJMb990n93U/kL5UvNbztjFKnLpPb975m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/StcgI/dJMb990n93U/kL5UvNbztjFKnLpPb975m1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/StcgI/dJMb990n93U/kL5UvNbztjFKnLpPb975m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FStcgI%2FdJMb990n93U%2FkL5UvNbztjFKnLpPb975m1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;558&quot; height=&quot;587&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;587&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Context Path는 톰캣을 실행했을때 맨뒤에 처음에 붙는 root context 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;nbsp; Server Port는 8080포트로 띄우겠다 이런 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VM options는 톰캣에 환경변수를 설정할수 있는데, 저가 설정한거는 로컬에서 톰캣을 실행했을때, log4j2-dev.xml 개발 로그 설정 파일을 바라보겠다 이런 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;118&quot; data-origin-height=&quot;27&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpZVNM/dJMcaffe7nU/dzUQMF8Kp8NlqaLeNYYOP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpZVNM/dJMcaffe7nU/dzUQMF8Kp8NlqaLeNYYOP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpZVNM/dJMcaffe7nU/dzUQMF8Kp8NlqaLeNYYOP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpZVNM%2FdJMcaffe7nU%2FdzUQMF8Kp8NlqaLeNYYOP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;118&quot; height=&quot;27&quot; data-origin-width=&quot;118&quot; data-origin-height=&quot;27&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 플레이 버튼 누르면 정상적으로 실행이 됩니다. 만약 안되면 build가 안되서 그럴경우가 대부분이므로, rebuild를 다시한후 실행하면 됩니다.&lt;/p&gt;</description>
      <category>backend</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/772</guid>
      <comments>https://aamoos.tistory.com/772#entry772comment</comments>
      <pubDate>Thu, 16 Apr 2026 15:48:32 +0900</pubDate>
    </item>
    <item>
      <title>Intelij 단축키 설정</title>
      <link>https://aamoos.tistory.com/771</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 검색 및 이동&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;shift 2번&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 파일설정 클래스등 모든 것을 검색&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/smf9B/dJMcagFcSz5/MTx74eFYqZ9Jabnhzlmi9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/smf9B/dJMcagFcSz5/MTx74eFYqZ9Jabnhzlmi9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/smf9B/dJMcagFcSz5/MTx74eFYqZ9Jabnhzlmi9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsmf9B%2FdJMcagFcSz5%2FMTx74eFYqZ9Jabnhzlmi9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;673&quot; height=&quot;718&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ctrl + N&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Java 클래스 파일을 이름으로 검색&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;709&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bORvVw/dJMcafTODvV/tZtSs2GaN8nkMGjzIwOLl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bORvVw/dJMcafTODvV/tZtSs2GaN8nkMGjzIwOLl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bORvVw/dJMcafTODvV/tZtSs2GaN8nkMGjzIwOLl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbORvVw%2FdJMcafTODvV%2FtZtSs2GaN8nkMGjzIwOLl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;709&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;709&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ctrl + Shift + N&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span data-path-to-node=&quot;6,3,2,0&quot;&gt;모든 형식의 파일을 검색&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;716&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TfIxA/dJMcaf7j4ZO/I07KUfh3Ta5fTzWCNai4Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TfIxA/dJMcaf7j4ZO/I07KUfh3Ta5fTzWCNai4Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TfIxA/dJMcaf7j4ZO/I07KUfh3Ta5fTzWCNai4Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTfIxA%2FdJMcaf7j4ZO%2FI07KUfh3Ta5fTzWCNai4Wk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;667&quot; height=&quot;716&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;716&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ctrl + E&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-path-to-node=&quot;6,4,2,0&quot;&gt;- 최근에 열었던 파일 리스트 확인&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;495&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NH9aN/dJMcabqmg6E/NTVLvCJcRFEYbSE73V12bK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NH9aN/dJMcabqmg6E/NTVLvCJcRFEYbSE73V12bK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NH9aN/dJMcabqmg6E/NTVLvCJcRFEYbSE73V12bK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNH9aN%2FdJMcabqmg6E%2FNTVLvCJcRFEYbSE73V12bK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;495&quot; height=&quot;434&quot; data-origin-width=&quot;495&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ctrl + B&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 메서드나 변수가 정의된 곳으로 이동 (Go to Declaration)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- test(); 에 커서를 두고 ctrl+B를 누르면 아래 test 정의된곳으로 커서가 감&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bH8lAD/dJMcaf7j43R/MhKeX2Zv591Cf7VQrrw1oK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bH8lAD/dJMcaf7j43R/MhKeX2Zv591Cf7VQrrw1oK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bH8lAD/dJMcaf7j43R/MhKeX2Zv591Cf7VQrrw1oK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbH8lAD%2FdJMcaf7j43R%2FMhKeX2Zv591Cf7VQrrw1oK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;487&quot; height=&quot;412&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 코드 편집&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ctrl + Space bar&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span data-path-to-node=&quot;10,1,2,0&quot;&gt;가장 기본적인 자동 완성&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;387&quot; data-origin-height=&quot;89&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNnuAf/dJMcah5ay3t/K4iuOmfI3m4vAzkHNvZq30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNnuAf/dJMcah5ay3t/K4iuOmfI3m4vAzkHNvZq30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNnuAf/dJMcah5ay3t/K4iuOmfI3m4vAzkHNvZq30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNnuAf%2FdJMcah5ay3t%2FK4iuOmfI3m4vAzkHNvZq30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;387&quot; height=&quot;89&quot; data-origin-width=&quot;387&quot; data-origin-height=&quot;89&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ctrl + Shift + Enter&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span data-path-to-node=&quot;10,2,2,0&quot;&gt;세미콜론(;)이나 중괄호({})를 자동으로 채우고 줄바꿈&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ctrl + Alt + L&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-path-to-node=&quot;10,3,2,0&quot;&gt;- 설정된 포맷에 맞춰 코드를 깨끗하게 정리 (Reformat Code)&lt;/span&gt; &lt;span data-path-to-node=&quot;10,3,2,0&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span data-path-to-node=&quot;10,3,2,0&quot;&gt;Ctrl + D&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-path-to-node=&quot;10,3,2,0&quot;&gt;- 현재 커서가 있는 줄을 바로 아래에 복사 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ctrl + Y&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span data-path-to-node=&quot;10,5,2,0&quot;&gt;현재 커서가 있는 줄을 삭제&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ctrl + / (주석)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-path-to-node=&quot;10,6,2,0&quot;&gt;// 주석 처리 및 해제&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Alt + Enter&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span data-path-to-node=&quot;10,7,1,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;10,7,2,0&quot;&gt;에러 수정 제안이나 Quick Fix 실행 (가장 중요!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span data-path-to-node=&quot;10,7,2,0&quot;&gt;3. 리팩토링&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Shift + F6&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/34Sir/dJMcafM4qzI/eA4YK45B1OFMzmGhxk3rZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/34Sir/dJMcafM4qzI/eA4YK45B1OFMzmGhxk3rZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/34Sir/dJMcafM4qzI/eA4YK45B1OFMzmGhxk3rZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F34Sir%2FdJMcafM4qzI%2FeA4YK45B1OFMzmGhxk3rZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;403&quot; height=&quot;314&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 변수, 메서드, 클래스 이름을 전체적으로 안전하게 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ctrl + Alt + V&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;127&quot; data-origin-height=&quot;39&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ApZDs/dJMcacpftho/4CxlSFQTYtMIo5e0RDvAaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ApZDs/dJMcacpftho/4CxlSFQTYtMIo5e0RDvAaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ApZDs/dJMcacpftho/4CxlSFQTYtMIo5e0RDvAaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FApZDs%2FdJMcacpftho%2F4CxlSFQTYtMIo5e0RDvAaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;127&quot; height=&quot;39&quot; data-origin-width=&quot;127&quot; data-origin-height=&quot;39&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을 먼저 입력하고 ctrl + Alt + V를 누르면 자동완성&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;277&quot; data-origin-height=&quot;43&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWqs7n/dJMcahKRVUp/AWO7nQO20NdEFnuSWaSx80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWqs7n/dJMcahKRVUp/AWO7nQO20NdEFnuSWaSx80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWqs7n/dJMcahKRVUp/AWO7nQO20NdEFnuSWaSx80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWqs7n%2FdJMcahKRVUp%2FAWO7nQO20NdEFnuSWaSx80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;277&quot; height=&quot;43&quot; data-origin-width=&quot;277&quot; data-origin-height=&quot;43&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &amp;nbsp;&lt;span data-path-to-node=&quot;14,2,2,0&quot;&gt;리턴값이나 식을 변수로 추출 (Introduce Variable)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ctrl + Alt + M&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;281&quot; data-origin-height=&quot;23&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7CiOF/dJMcaaLJST5/JCbwFhmqQGkFILHejUVKu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7CiOF/dJMcaaLJST5/JCbwFhmqQGkFILHejUVKu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7CiOF/dJMcaaLJST5/JCbwFhmqQGkFILHejUVKu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7CiOF%2FdJMcaaLJST5%2FJCbwFhmqQGkFILHejUVKu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;281&quot; height=&quot;23&quot; data-origin-width=&quot;281&quot; data-origin-height=&quot;23&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;162&quot; data-origin-height=&quot;32&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx3OhP/dJMcagyn7gM/Gd1M56Vg2HXrlycGF6SmEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx3OhP/dJMcagyn7gM/Gd1M56Vg2HXrlycGF6SmEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx3OhP/dJMcagyn7gM/Gd1M56Vg2HXrlycGF6SmEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx3OhP%2FdJMcagyn7gM%2FGd1M56Vg2HXrlycGF6SmEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;162&quot; height=&quot;32&quot; data-origin-width=&quot;162&quot; data-origin-height=&quot;32&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;416&quot; data-origin-height=&quot;72&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d724Ef/dJMb99MMWGP/9zf2fiiqvO74pn2Th1UK40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d724Ef/dJMb99MMWGP/9zf2fiiqvO74pn2Th1UK40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d724Ef/dJMb99MMWGP/9zf2fiiqvO74pn2Th1UK40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd724Ef%2FdJMb99MMWGP%2F9zf2fiiqvO74pn2Th1UK40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;416&quot; height=&quot;72&quot; data-origin-width=&quot;416&quot; data-origin-height=&quot;72&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span data-path-to-node=&quot;14,3,2,0&quot;&gt;특정 코드 블록을 별도 메서드로 분리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span data-path-to-node=&quot;14,2,2,0&quot;&gt;Ctrl + Alt + P&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-path-to-node=&quot;14,2,2,0&quot;&gt;- 메서드 내부의 값을 파라미터로 변경 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span data-path-to-node=&quot;14,2,2,0&quot;&gt;4. 디버깅 및 실행&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Shift + F10&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 프로젝트 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Shift + F9&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 디버깅 모드 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ctrl + F8&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 브레이크 포인트(Breakpoint) 설정/해제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;F8 / F7&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Step Over (다음 줄) / Step Into (메서드 안으로)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 보너스&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Multi-Cursor&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Alt + Click 또는 Alt + Shift + Insert를 이용해 여러 줄을 동시에 수정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;489&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lbiFb/dJMcaaSv69f/hgmFlOy3Yrs88ZPmstg8D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lbiFb/dJMcaaSv69f/hgmFlOy3Yrs88ZPmstg8D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lbiFb/dJMcaaSv69f/hgmFlOy3Yrs88ZPmstg8D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlbiFb%2FdJMcaaSv69f%2FhgmFlOy3Yrs88ZPmstg8D0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;489&quot; height=&quot;282&quot; data-origin-width=&quot;489&quot; data-origin-height=&quot;282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Key Promoter X 플러그인&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;979&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TiUSq/dJMcaiXfEhe/6lP4irEfsvRs5ftmRqpCq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TiUSq/dJMcaiXfEhe/6lP4irEfsvRs5ftmRqpCq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TiUSq/dJMcaiXfEhe/6lP4irEfsvRs5ftmRqpCq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTiUSq%2FdJMcaiXfEhe%2F6lP4irEfsvRs5ftmRqpCq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;979&quot; height=&quot;718&quot; data-origin-width=&quot;979&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 해당 플러그인 install을 하면 마우스 클릭시 오른쪽 하단 팝업으로 단축키를 알려줘서 자연스럽게 익힐수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 예를들면 ctrl + 마우스왼쪽 클릭으로 메서드를 클릭해서 들어갔을경우 Ctrl + B로 들어갈수 있다고 알려주네요&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;319&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwJSOY/dJMcabYa9z4/EQE4MyymTwVx8jlTlvpSG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwJSOY/dJMcabYa9z4/EQE4MyymTwVx8jlTlvpSG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwJSOY/dJMcabYa9z4/EQE4MyymTwVx8jlTlvpSG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwJSOY%2FdJMcabYa9z4%2FEQE4MyymTwVx8jlTlvpSG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;319&quot; height=&quot;84&quot; data-origin-width=&quot;319&quot; data-origin-height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>backend</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/771</guid>
      <comments>https://aamoos.tistory.com/771#entry771comment</comments>
      <pubDate>Thu, 16 Apr 2026 15:32:09 +0900</pubDate>
    </item>
    <item>
      <title>Error occurred during initialization of VMjava.lang.Error: Properties init: Could not determine current working directory.at jdk.internal.util.SystemProps$Raw.platformProperties(java.base@17.0.18/Native Method)at jdk.internal.util.SystemProps$Raw.&amp;lt;init&amp;gt;(j</title>
      <link>https://aamoos.tistory.com/770</link>
      <description>&lt;pre id=&quot;code_1771835112468&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Error occurred during initialization of VM
java.lang.Error: Properties init: Could not determine current working directory.
at jdk.internal.util.SystemProps$Raw.platformProperties(java.base@17.0.18/Native Method)
at jdk.internal.util.SystemProps$Raw.&amp;lt;init&amp;gt;(java.base@17.0.18/SystemProps.java:234)
at jdk.internal.util.SystemProps.initProperties(java.base@17.0.18/SystemProps.java:54)
at java.lang.System.initPhase1(java.base@17.0.18/System.java:2106)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ssh에서 java -version을 입력했을때 위와 같은 에러가 발생하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이경우 java 자체의 결함이기보단 현재 터미널에 위치한 디렉터리가 갑자기 삭제되었거나 그럴때 터미널이 위치한 폴더를 시스템이 찾지 못했을때 생기는 에러입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771835205045&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd /
java -version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상위 폴더나 루트 디렉터리로 이동후 다시 입력을 해보면 정상적으로 java version이 표기가 됩니다.&lt;/p&gt;</description>
      <category>DevOps</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/770</guid>
      <comments>https://aamoos.tistory.com/770#entry770comment</comments>
      <pubDate>Mon, 23 Feb 2026 17:27:07 +0900</pubDate>
    </item>
    <item>
      <title>Vue3 기본문법</title>
      <link>https://aamoos.tistory.com/768</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. App.vue&lt;/h3&gt;
&lt;pre id=&quot;code_1768374292842&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;h1&amp;gt;Hello Vue 3&amp;lt;/h1&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;style scoped&amp;gt;
&amp;lt;/style&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- script setup -&amp;gt; Vue3 권장 문법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- template -&amp;gt; 화면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- style scoped -&amp;gt; 이 컴포넌트에만 스타일 적용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 반응형 데이터 (ref)&lt;/h3&gt;
&lt;pre id=&quot;code_1768374430892&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from &quot;vue&quot;;

const count = ref(0);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;h1&amp;gt;Count: {{ count }}&amp;lt;/h1&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ref(값) -&amp;gt; 반응형&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- template에서는 .value 안씀&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 이벤트 바인딩 (click)&lt;/h3&gt;
&lt;pre id=&quot;code_1768374513020&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from &quot;vue&quot;;

const count = ref(0);

function increment(){
  count.value++;
}

&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;h1&amp;gt;Count: {{ count }}&amp;lt;/h1&amp;gt;
  &amp;lt;button @click=&quot;increment&quot; &amp;gt;+&amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- @click = v-on:click&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- script에서는 .value로 값을 뽑음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 버튼 클릭시 count를 1씩 증가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Wkffz/dJMcaacXqBP/HpvMU17tkLk1GRkxk27Fp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Wkffz/dJMcaacXqBP/HpvMU17tkLk1GRkxk27Fp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Wkffz/dJMcaacXqBP/HpvMU17tkLk1GRkxk27Fp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWkffz%2FdJMcaacXqBP%2FHpvMU17tkLk1GRkxk27Fp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;332&quot; height=&quot;216&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. v-model (양방향 바인딩)&lt;/h3&gt;
&lt;pre id=&quot;code_1768374663424&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from &quot;vue&quot;;

const name = ref(&quot;&quot;);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;input v-model=&quot;name&quot; placeholder=&quot;이름 입력&quot; /&amp;gt;
  &amp;lt;p&amp;gt;안녕하세요, {{ name }}&amp;lt;/p&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- v-model = input &amp;lt;-&amp;gt; state 자동 연결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- form에서 필수 문법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c82i0M/dJMcaiPxTOU/8lwbC3Q7vdzYKKDfHsUsS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c82i0M/dJMcaiPxTOU/8lwbC3Q7vdzYKKDfHsUsS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c82i0M/dJMcaiPxTOU/8lwbC3Q7vdzYKKDfHsUsS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc82i0M%2FdJMcaiPxTOU%2F8lwbC3Q7vdzYKKDfHsUsS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;270&quot; height=&quot;90&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;90&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 조건 렌더링 (v-if)&lt;/h3&gt;
&lt;pre id=&quot;code_1768374803883&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from &quot;vue&quot;;

const isLogin = ref(false);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button @click=&quot;isLogin = !isLogin&quot;&amp;gt;
    로그인 토글
  &amp;lt;/button&amp;gt;

  &amp;lt;p v-if=&quot;isLogin&quot;&amp;gt;로그인 상태&amp;lt;/p&amp;gt;
  &amp;lt;p v-else&amp;gt;로그아웃 상태&amp;lt;/p&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 기본값 false이므로 처음 화면은 로그아웃 상태, 버튼 클릭시 로그인 상태로 변경됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 조간이 자주 바뀌면 v-show 고려,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. reactive vs ref&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ref : 원시값 (number, string, boolean)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- reactive : 객체, 폼데이터&lt;/p&gt;
&lt;pre id=&quot;code_1768375150201&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { reactive, ref } from &quot;vue&quot;;

const count = ref(0);

const user = reactive({
  name: &quot;&quot;,
  age: 0,
})
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;p&amp;gt;count: {{ count }}&amp;lt;/p&amp;gt;

  &amp;lt;input v-model=&quot;user.name&quot; placeholder=&quot;이름&quot; /&amp;gt;
  &amp;lt;input v-model=&quot;user.age&quot; type=&quot;number&quot; placeholder=&quot;이름&quot; /&amp;gt;

  &amp;lt;p&amp;gt;{{ user.name }} // {{ user.age }}&amp;lt;/p&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- reactive는 .value 못씀&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 객체는 왠만하면 reactive&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. computed&lt;/h3&gt;
&lt;pre id=&quot;code_1768375296099&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { computed, ref } from &quot;vue&quot;;

const count = ref(0);

const doubleCount = computed(() =&amp;gt; {
  return count.value * 2;
})

&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button @click=&quot;count++&quot;&amp;gt;&amp;lt;/button&amp;gt;
  &amp;lt;p&amp;gt;count: {{ count }}&amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;double: {{ doubleCount }}&amp;lt;/p&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 값 가공은 computed&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- computed는 캐싱효과가 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-1. computed 캐싱&lt;/h3&gt;
&lt;pre id=&quot;code_1768375476911&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { computed, ref } from &quot;vue&quot;;

const count = ref(0);

const doubleCount = computed(() =&amp;gt; {
  console.log(&quot;computed&quot;)
  return count.value * 2;
})

const test = () =&amp;gt; {
  console.log(&quot;method&quot;);
  return count.value * 2;
}

&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button @click=&quot;count++&quot;&amp;gt;&amp;lt;/button&amp;gt;
  &amp;lt;p&amp;gt;count: {{ count }}&amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;double: {{ doubleCount }}&amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;double: {{ doubleCount }}&amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;double: {{ test() }}&amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;double: {{ test() }}&amp;lt;/p&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 위에 코드를 실행후 network console로 확인을하면 computed로 선언한 함수는 2번 호출했는데 1번이 console로 찍히고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수를 2번 호출시는 2번 console이 찍힘&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;206&quot; data-origin-height=&quot;65&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oiI3X/dJMcacPnsap/iM4iJaYKa4BkKDbzS4FQmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oiI3X/dJMcacPnsap/iM4iJaYKa4BkKDbzS4FQmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oiI3X/dJMcacPnsap/iM4iJaYKa4BkKDbzS4FQmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoiI3X%2FdJMcacPnsap%2FiM4iJaYKa4BkKDbzS4FQmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;206&quot; height=&quot;65&quot; data-origin-width=&quot;206&quot; data-origin-height=&quot;65&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. Watch&lt;/h3&gt;
&lt;pre id=&quot;code_1768375711243&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref, watch } from &quot;vue&quot;;

const count = ref(0);

watch(count, (newVal, oldVal) =&amp;gt; {
  console.log(&quot;변경 : &quot;, oldVal, &quot;-&amp;gt;&quot;, newVal);
})

&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button @click=&quot;count++&quot;&amp;gt;&amp;lt;/button&amp;gt;
  &amp;lt;p&amp;gt;{{ count }}&amp;lt;/p&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- count 값이 변경될때 watch가 동작&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. v-for&lt;/h3&gt;
&lt;pre id=&quot;code_1768375877869&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from &quot;vue&quot;;

const todos = ref([
  { id: 1, text: &quot;Vue 공부&quot; },
  { id: 2, text: &quot;TypeScript 공부&quot; },
]);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;ul&amp;gt;
    &amp;lt;li v-for=&quot;todo in todos&quot; :key=&quot;todo.id&quot;&amp;gt;
      {{ todo.text }}
    &amp;lt;/li&amp;gt;
  &amp;lt;/ul&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- key 필수, id 없으면 에러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9-1. computed + v-for&lt;/h3&gt;
&lt;pre id=&quot;code_1768375915327&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref, computed } from &quot;vue&quot;;

const todos = ref([
  { id: 1, text: &quot;Vue&quot;, done: true },
  { id: 2, text: &quot;TS&quot;, done: false },
]);

const doneTodos = computed(() =&amp;gt;
  todos.value.filter(t =&amp;gt; t.done)
);
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;h3&amp;gt;완료&amp;lt;/h3&amp;gt;
  &amp;lt;ul&amp;gt;
    &amp;lt;li v-for=&quot;t in doneTodos&quot; :key=&quot;t.id&quot;&amp;gt;
      {{ t.text }}
    &amp;lt;/li&amp;gt;
  &amp;lt;/ul&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- todo 리스트에서 done이 true인 항목만 필터링해서 화면에 뿌림&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. props &amp;amp; emit&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Parent.vue&lt;/h4&gt;
&lt;pre id=&quot;code_1768453312470&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
import { ref } from 'vue'
import Child from './Child.vue' // 자식 컴포넌트 불러오기

const myTitle = ref(&quot;부모가 주는 선물&quot;)
const myCount = ref(0)

// 자식이 보낸 신호를 받았을 때 실행할 함수
const handleIncrease = () =&amp;gt; {
  myCount.value++
  console.log(&quot;자식의 신호를 받았습니다!&quot;)
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div style=&quot;border: 2px solid #35495e; padding: 20px;&quot;&amp;gt;
    &amp;lt;h1&amp;gt; &amp;zwj; &amp;zwj;  여기는 부모 컴포넌트&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;현재 부모의 숫자: {{ myCount }}&amp;lt;/p&amp;gt;

    &amp;lt;Child 
      :childTitle=&quot;myTitle&quot;
      :count=&quot;myCount&quot;
      @increase-count=&quot;handleIncrease&quot; 
    /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Child.vue&lt;/h4&gt;
&lt;pre id=&quot;code_1768453343809&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
// 1. Props 정의: 부모에게 받을 데이터 이름표를 만듭니다.
const props = defineProps({
  childTitle: String, // 타입 지정 (String, Number 등)
  count: Number
})

// 2. Emits 정의: 부모에게 보낼 신호 이름을 정합니다.
const emit = defineEmits(['increase-count'])

// 버튼 클릭 시 실행할 함수
const sendSignal = () =&amp;gt; {
  // 'increase-count'라는 신호를 부모에게 발사! (필요하면 두 번째 인자로 데이터 전달 가능)
  emit('increase-count')
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div style=&quot;border: 2px solid #42b883; padding: 10px; margin: 10px;&quot;&amp;gt;
    &amp;lt;h3&amp;gt;  여기는 자식 컴포넌트&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;받은 제목: {{ childTitle }}&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;받은 숫자: {{ count }}&amp;lt;/p&amp;gt;
    
    &amp;lt;button @click=&quot;sendSignal&quot;&amp;gt;숫자 올려달라고 조르기 (Emit)&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>frontend/Vue</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/768</guid>
      <comments>https://aamoos.tistory.com/768#entry768comment</comments>
      <pubDate>Thu, 15 Jan 2026 13:56:13 +0900</pubDate>
    </item>
    <item>
      <title>TypeScript 기본문법 정리</title>
      <link>https://aamoos.tistory.com/767</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript = 자바스크립트에 타입을 붙인 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰는 이유?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 컴파일 전에 오류를 잡아줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 자동완성 + 리팩토링이 미친 듯이 좋아짐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 협업 / 대규모 프로젝트 필수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- TS는 &quot;문법&quot;이 아니라 &quot; 안전장치&quot;라고 생각하면됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 타입 붙이기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Javascript&lt;/h3&gt;
&lt;pre id=&quot;code_1768371959747&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let count = 0;
count = &quot;hello&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 자바스크립트는 런타임 실행 전까지 에러인지 모름&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Typescript&lt;/h3&gt;
&lt;pre id=&quot;code_1768372010563&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let count: number = 0;
count = &quot;hello&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 타입스크립트는 컴파일시 에러가 발생하여 바로 확인이 가능함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 쓰는 기본 타입&lt;/h3&gt;
&lt;pre id=&quot;code_1768372125237&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let userName: string = &quot;kim&quot;
let age: number = 30;
let isLogin: boolean = true;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Typescript 타입 추론&lt;/h3&gt;
&lt;pre id=&quot;code_1768372160975&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let price = 100;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- pice 뒤에 : number를 선언하지 않아도 값을 보고 자동으로 타입을 추론함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 배열 &amp;amp; 객체&lt;/h2&gt;
&lt;pre id=&quot;code_1768372213023&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let numbers: number[] = [1, 2, 3];
let users: string[] = [&quot;kim&quot;, &quot;lee&quot;];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. interface / type&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;interface&lt;/h3&gt;
&lt;pre id=&quot;code_1768372251862&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface User {
  id: number;
  email: string;
  isAdmin: boolean;
}

const user: User = {
  id: 1,
  email: &quot;test@test.com&quot;,
  isAdmin: false,
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1 interface를 써야하는 상황&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DTO / API 응답 구조&lt;/h4&gt;
&lt;pre id=&quot;code_1768373197190&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface UserDto {
  id: number;
  email: string;
  name: string;
}

interface ApiResponse&amp;lt;T&amp;gt; {
  success: boolean;
  data: T;
  message?: string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Vue props 정의&lt;/h4&gt;
&lt;pre id=&quot;code_1768373256694&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface User {
  id: number;
  email: string;
}

const props = defineProps&amp;lt;{
  user: User;
  isAdmin: boolean;
}&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;implements&lt;/h3&gt;
&lt;pre id=&quot;code_1768373297015&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface LoginService {
  login(email: string, password: string): Promise&amp;lt;void&amp;gt;;
}

class AuthService implements LoginService {
  async login(email: string, password: string) {
    // ...
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;확장 (extends)&lt;/h4&gt;
&lt;pre id=&quot;code_1768373332121&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface BaseEntity {
  id: number;
  createdAt: string;
}

interface User extends BaseEntity {
  email: string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;type&lt;/h3&gt;
&lt;pre id=&quot;code_1768372306091&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type User = {
  id: number;
  email: string;
  isAdmin: boolean;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 구조 정의 -&amp;gt; interface&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;union, 함수 타입 -&amp;gt; type&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. type를 써야하는 상황&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Union 타입&lt;/h4&gt;
&lt;pre id=&quot;code_1768373401288&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type Status = &quot;idle&quot; | &quot;loading&quot; | &quot;error&quot;;
let status: Status = &quot;loading&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;함수 타입 정의&lt;/h4&gt;
&lt;pre id=&quot;code_1768373442379&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type LoginFn = (email: string, password: string) =&amp;gt; Promise&amp;lt;void&amp;gt;;
const login: LoginFn = async (email, password) =&amp;gt; {
  // ...
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;조합 타입 (intersection)&lt;/h4&gt;
&lt;pre id=&quot;code_1768373472115&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type WithToken = {
  accessToken: string;
};

type User = {
  id: number;
  email: string;
};

type AuthedUser = User &amp;amp; WithToken;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;제네릭 + 조건 타입&lt;/h4&gt;
&lt;pre id=&quot;code_1768373494794&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type ApiResult&amp;lt;T&amp;gt; = 
  T extends string ? { value: string } : { value: number };&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 함수 타입&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 기본 함수&lt;/h3&gt;
&lt;pre id=&quot;code_1768372413935&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function add(a: number, b: number): number {
  return a + b;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4-2. 화살표 함수&lt;/h3&gt;
&lt;pre id=&quot;code_1768372443722&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const add = (a: number, b: number): number =&amp;gt; {
  return a + b;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. return 없는 함수&lt;/h3&gt;
&lt;pre id=&quot;code_1768372461088&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function log(msg: string): void {
  console.log(msg);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Optional / Nullable&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. Optional&lt;/h3&gt;
&lt;pre id=&quot;code_1768372496988&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface User {
  id: number;
  name?: string; // 있어도 되고 없어도 됨
}

const u1: User = { id: 1 };
const u2: User = { id: 2, name: &quot;kim&quot; };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- name뒤에 ?를 붙이면 값이 있을수도 있고 없을수도 있다라는 의미임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-2. null / undefined&lt;/h3&gt;
&lt;pre id=&quot;code_1768372543113&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let token: string | null = null;

token = &quot;abc&quot;;
token = null; // OK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- token 값의 타입이 string이거나 null일수 있다 라는 의미&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Union 타입&lt;/h2&gt;
&lt;pre id=&quot;code_1768372593148&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let status: &quot;idle&quot; | &quot;loading&quot; | &quot;success&quot; | &quot;error&quot;;
status = &quot;loading&quot;;
status = &quot;done&quot;;   // 컴파일 에러&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 상태값에 done을 선언하지 않아서 컴파일 에러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. any&lt;/h2&gt;
&lt;pre id=&quot;code_1768372720272&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let data: any = 123;
data.toUpperCase(); // 런타임 에러&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- any는 어떤타입이든 들어올수 있다라고 설정하는거여서, 사용하면 typescript 쓰는 의미가 없음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 123을 대문자로 바꾸려니 런타임 에러 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Unknown&lt;/h2&gt;
&lt;pre id=&quot;code_1768372815291&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let data: unknown;

if (typeof data === &quot;string&quot;) {
  data.toUpperCase();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- unknown은 타입을 아직 모른다는걸 typescript에게 솔직하게 말하는 타입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. any vs unknown&lt;/h2&gt;
&lt;pre id=&quot;code_1768372935045&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let data: any;

data = 123;
data = &quot;hello&quot;;

data.toUpperCase();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- any로 선언을하면 toUpperCase 컴파일은 통과하지만, 런타임에서 에러가 발생함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1768372970313&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let data: unknown;

data = 123;
data = &quot;hello&quot;;

data.toUpperCase();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- unknown으로 선언하면 toUpperCase 컴파일에서 에러가 발생함, 즉 unknown은 확인하고 쓰라는걸 강제함&lt;/p&gt;</description>
      <category>frontend/Typescript</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/767</guid>
      <comments>https://aamoos.tistory.com/767#entry767comment</comments>
      <pubDate>Wed, 14 Jan 2026 15:43:37 +0900</pubDate>
    </item>
    <item>
      <title>ubuntu 서버 최초 세팅</title>
      <link>https://aamoos.tistory.com/766</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 서버 접속&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(기본 계정 : ubuntu)&lt;/p&gt;
&lt;pre id=&quot;code_1768356567387&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh ubuntu@server_ip&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 시스템업데이트&lt;/h3&gt;
&lt;pre id=&quot;code_1768356613192&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update
sudo apt upgrade -y&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 타임존 설정(선택이지만 추천)&lt;/h3&gt;
&lt;pre id=&quot;code_1768356638032&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo timedatectl set-timezone Asia/Seoul
timedatectl&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. sudo 권한 일반 사용자 계정 생성&lt;/h3&gt;
&lt;pre id=&quot;code_1768356809768&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo adduser username
sudo usermod -aG sudo username
groups username&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-1. sudo 계정 생성후 정상확인&lt;/h4&gt;
&lt;pre id=&quot;code_1768356854351&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;su - username
sudo ls&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 비밀번호 묻고 정상실행돠면 성공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. ROOT 계정 SSH 로그인 비활성화&lt;/h3&gt;
&lt;pre id=&quot;code_1768356904335&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;vim /etc/ssh/sshd_config

PermitRootLogin no # 해당 부분 no로 설정 yes면 root로 바로 로그인 가능한 상태임&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(선택) 로그인 유지시간 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1768357315607&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;vim /etc/ssh/sshd_config

ClientAliveInterval 300
ClientAliveCountMax 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClientAliveInterval 300 : **300초(5분)**마다 클라이언트 응답 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClientAliveCountMax 0 : 응답 없으면 &lt;b&gt;즉시 세션 종료&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과: &lt;b&gt;5분 동안 아무 작업 없으면 자동 로그아웃&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(선택) 패스워드 오류 허용 횟수 (로그인 실패 제한)&lt;/h3&gt;
&lt;pre id=&quot;code_1768357383307&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;MaxAuthTries 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패스워드 3번 틀리면 즉시 접속 종료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(선택) 로그인 시도 제한&lt;/h3&gt;
&lt;pre id=&quot;code_1768357558926&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LoginGraceTime 30&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접속 후 &lt;b&gt;30초 안에 인증 못 하면 강제 종료&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영서버에서 가장 많이 쓰는 조합&lt;/h3&gt;
&lt;pre id=&quot;code_1768357622052&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 로그인 유지시간
ClientAliveInterval 300
ClientAliveCountMax 0

# 인증 보안
MaxAuthTries 3
LoginGraceTime 30

# root 로그인 차단 (이미 설정했을 가능성 높음)
PermitRootLogin no&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 만약 추가적으로 ssh 파일로만 로그인하고싶으면 아래 옵션을 설정하면 된다. id password 로그인 x&lt;/p&gt;
&lt;pre id=&quot;code_1768357726662&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 패스워드 로그인 허용 여부 (권장: 키 로그인만)
PasswordAuthentication no&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1 ssh 설정파일 정상적으로 변경되었는지 확인후 restart&lt;/h3&gt;
&lt;pre id=&quot;code_1768357105780&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo sshd -t
sudo systemctl restart ssh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sudo sshd -t : sshd_config에 문법에 오류가 없으면 아무것도 안뜸&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sudo systemctl restart ssh : ssh 재시작&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 방화벽(UFW) 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1768363497259&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt install ufw -y
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp   # 또는 변경 포트
sudo ufw enable
sudo ufw status verbose&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-1. 웹서버 방화벽 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1768363646947&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo ufw allow 80/tcp
sudo ufw allow 443/tcp&lt;/code&gt;&lt;/pre&gt;</description>
      <category>DevOps</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/766</guid>
      <comments>https://aamoos.tistory.com/766#entry766comment</comments>
      <pubDate>Wed, 14 Jan 2026 13:07:52 +0900</pubDate>
    </item>
    <item>
      <title>npm : 'npm' 용어가 cmdlet, 함수, 스크립트 파일 또는 실행할 수 있는 프로그램 이름으로 인식되지 않습니다. 이름이 정확한지 확인하고 경로가 포함된 경우 경로가 올바른지 검증한 다음 다시 시도하십시오.</title>
      <link>https://aamoos.tistory.com/765</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTKvJC/dJMcahXrtfZ/ckrDAVIsbktAzwO75s0ko0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTKvJC/dJMcahXrtfZ/ckrDAVIsbktAzwO75s0ko0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTKvJC/dJMcahXrtfZ/ckrDAVIsbktAzwO75s0ko0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTKvJC%2FdJMcahXrtfZ%2FckrDAVIsbktAzwO75s0ko0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1244&quot; height=&quot;125&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터를 포맷한후 npm명령어가 먹지않아서 해결방법 포스팅합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm 명령어를 쓰다가 해당 에러가 발생할경우, cmd창에 아래명령어를 쳐서 확인합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1767948979703&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;node -v&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;576&quot; data-origin-height=&quot;59&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D45rt/dJMcacV6Fjo/sQKGghE2WgodMZ8unotRz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D45rt/dJMcacV6Fjo/sQKGghE2WgodMZ8unotRz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D45rt/dJMcacV6Fjo/sQKGghE2WgodMZ8unotRz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD45rt%2FdJMcacV6Fjo%2FsQKGghE2WgodMZ8unotRz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;576&quot; height=&quot;59&quot; data-origin-width=&quot;576&quot; data-origin-height=&quot;59&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; node js가 설치가 안되어있으면 node js를 다운로드합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nodejs.org/ko/download&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nodejs.org/ko/download&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1768356213787&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Node.js &amp;mdash; Node.js&amp;reg; 다운로드&quot; data-og-description=&quot;Node.js&amp;reg; is a free, open-source, cross-platform JavaScript runtime environment that lets developers create servers, web apps, command line tools and scripts.&quot; data-og-host=&quot;nodejs.org&quot; data-og-source-url=&quot;https://nodejs.org/ko/download&quot; data-og-url=&quot;https://nodejs.org/ko/download&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hgJfM/dJMb8SXq8iM/9UeODXgBtGGfwabNkAkng0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bjrH6c/dJMb9hCUDkO/b9Dj5lVNzHybMN6RRfFQqK/img.png?width=224&amp;amp;height=256&amp;amp;face=0_0_224_256&quot;&gt;&lt;a href=&quot;https://nodejs.org/ko/download&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nodejs.org/ko/download&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hgJfM/dJMb8SXq8iM/9UeODXgBtGGfwabNkAkng0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bjrH6c/dJMb9hCUDkO/b9Dj5lVNzHybMN6RRfFQqK/img.png?width=224&amp;amp;height=256&amp;amp;face=0_0_224_256');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Node.js &amp;mdash; Node.js&amp;reg; 다운로드&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Node.js&amp;reg; is a free, open-source, cross-platform JavaScript runtime environment that lets developers create servers, web apps, command line tools and scripts.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nodejs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;powershell에서 관리자 권한으로 실행&lt;/p&gt;
&lt;pre id=&quot;code_1767949232089&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Get-ExecutionPolicy
Set-ExecutionPolicy RemoteSigned
npm -v&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;363&quot; data-origin-height=&quot;39&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvHT7f/dJMcaajG35k/pR3SKIGPkXwimzsTDWbOm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvHT7f/dJMcaajG35k/pR3SKIGPkXwimzsTDWbOm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvHT7f/dJMcaajG35k/pR3SKIGPkXwimzsTDWbOm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvHT7f%2FdJMcaajG35k%2FpR3SKIGPkXwimzsTDWbOm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;363&quot; height=&quot;39&quot; data-origin-width=&quot;363&quot; data-origin-height=&quot;39&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OfoZv/dJMcaiWipWt/QhYz0qcUG2IPXk2Gz1WQhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OfoZv/dJMcaiWipWt/QhYz0qcUG2IPXk2Gz1WQhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OfoZv/dJMcaiWipWt/QhYz0qcUG2IPXk2Gz1WQhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOfoZv%2FdJMcaiWipWt%2FQhYz0qcUG2IPXk2Gz1WQhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;932&quot; height=&quot;131&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 npm명령어가 정상적으로 동작하는것을 볼수있습니다.&lt;/p&gt;</description>
      <category>frontend</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/765</guid>
      <comments>https://aamoos.tistory.com/765#entry765comment</comments>
      <pubDate>Wed, 14 Jan 2026 11:04:16 +0900</pubDate>
    </item>
    <item>
      <title>reactJs 확장프로그램 devtool 설치</title>
      <link>https://aamoos.tistory.com/764</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765796958149&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;React Developer Tools - Chrome 웹 스토어&quot; data-og-description=&quot;Adds React debugging tools to the Chrome Developer Tools. Created from revision 3cde211b0c on 10/20/2025.&quot; data-og-host=&quot;chromewebstore.google.com&quot; data-og-source-url=&quot;https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi&quot; data-og-url=&quot;https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/KDXvJ/hyZPriI5bx/TbGRW5yg8caez4o0H6x4h0/img.jpg?width=128&amp;amp;height=128&amp;amp;face=0_0_128_128&quot;&gt;&lt;a href=&quot;https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/KDXvJ/hyZPriI5bx/TbGRW5yg8caez4o0H6x4h0/img.jpg?width=128&amp;amp;height=128&amp;amp;face=0_0_128_128');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;React Developer Tools - Chrome 웹 스토어&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Adds React debugging tools to the Chrome Developer Tools. Created from revision 3cde211b0c on 10/20/2025.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chromewebstore.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 사이트에서 devtools 확장프로그램 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App.jsx&lt;/p&gt;
&lt;pre id=&quot;code_1765797165663&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Counter from &quot;@/components/Counter&quot;;
import UserCard from &quot;@/components/UserCard&quot;;

function App() {
  const user = {
    name: &quot;Jaesung&quot;,
    age: 30,
    role: &quot;Frontend Dev&quot;,
  };

  return (
    &amp;lt;div style={{ padding: 20 }}&amp;gt;
      &amp;lt;h1&amp;gt;React DevTools Test&amp;lt;/h1&amp;gt;

      &amp;lt;Counter initial={5} /&amp;gt;
      &amp;lt;Counter /&amp;gt;

      &amp;lt;UserCard user={user} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;components/Counter.jsx&lt;/p&gt;
&lt;pre id=&quot;code_1765797180846&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;;

function Counter({ initial = 0 }) {
    const [count, setCount] = useState(initial);

    return (
        &amp;lt;div style={{ border: &quot;1px solid #ccc&quot;, padding: 12, marginBottom: 12 }}&amp;gt;
            &amp;lt;h3&amp;gt;Counter&amp;lt;/h3&amp;gt;
            &amp;lt;p&amp;gt;count: {count}&amp;lt;/p&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; setCount((c) =&amp;gt; c + 1)}&amp;gt;+&amp;lt;/button&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; setCount((c) =&amp;gt; c - 1)}&amp;gt;-&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    );
}

export default Counter;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;components/UserCard.jsx&lt;/p&gt;
&lt;pre id=&quot;code_1765797196315&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function UserCard({ user }) {
    return (
        &amp;lt;div style={{ border: &quot;1px solid #aaa&quot;, padding: 12 }}&amp;gt;
            &amp;lt;h3&amp;gt;UserCard&amp;lt;/h3&amp;gt;
            &amp;lt;p&amp;gt;name: {user.name}&amp;lt;/p&amp;gt;
            &amp;lt;p&amp;gt;age: {user.age}&amp;lt;/p&amp;gt;
            &amp;lt;p&amp;gt;role: {user.role}&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    );
}

export default UserCard;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;655&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GB8La/dJMcab3OKJ9/YtZzDT50koDvER9bIZKrEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GB8La/dJMcab3OKJ9/YtZzDT50koDvER9bIZKrEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GB8La/dJMcab3OKJ9/YtZzDT50koDvER9bIZKrEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGB8La%2FdJMcab3OKJ9%2FYtZzDT50koDvER9bIZKrEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;741&quot; height=&quot;655&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;655&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;94&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WedgJ/dJMcaajxey5/KWFhSOjckIpTMqhRFCxARk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WedgJ/dJMcaajxey5/KWFhSOjckIpTMqhRFCxARk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WedgJ/dJMcaajxey5/KWFhSOjckIpTMqhRFCxARk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWedgJ%2FdJMcaajxey5%2FKWFhSOjckIpTMqhRFCxARk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;568&quot; height=&quot;94&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;94&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;537&quot; data-origin-height=&quot;307&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tXJib/dJMcadAvN3Z/lXr7fPryh3jRuwghFg5Wzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tXJib/dJMcadAvN3Z/lXr7fPryh3jRuwghFg5Wzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tXJib/dJMcadAvN3Z/lXr7fPryh3jRuwghFg5Wzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtXJib%2FdJMcadAvN3Z%2FlXr7fPryh3jRuwghFg5Wzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;537&quot; height=&quot;307&quot; data-origin-width=&quot;537&quot; data-origin-height=&quot;307&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;531&quot; data-origin-height=&quot;275&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dADjbH/dJMcaf6dDg3/3BfT3cyryrV40VzekKI7pK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dADjbH/dJMcaf6dDg3/3BfT3cyryrV40VzekKI7pK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dADjbH/dJMcaf6dDg3/3BfT3cyryrV40VzekKI7pK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdADjbH%2FdJMcaf6dDg3%2F3BfT3cyryrV40VzekKI7pK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;531&quot; height=&quot;275&quot; data-origin-width=&quot;531&quot; data-origin-height=&quot;275&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;535&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b720pC/dJMcafLUQvJ/CItbhQWnS2IYEVzPuK1g80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b720pC/dJMcafLUQvJ/CItbhQWnS2IYEVzPuK1g80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b720pC/dJMcafLUQvJ/CItbhQWnS2IYEVzPuK1g80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb720pC%2FdJMcafLUQvJ%2FCItbhQWnS2IYEVzPuK1g80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;535&quot; height=&quot;255&quot; data-origin-width=&quot;535&quot; data-origin-height=&quot;255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자도구 - Components를 클릭해보면 각 컴포넌트의 props hooks등을 확인할수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>frontend/React</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/764</guid>
      <comments>https://aamoos.tistory.com/764#entry764comment</comments>
      <pubDate>Mon, 15 Dec 2025 20:16:10 +0900</pubDate>
    </item>
    <item>
      <title>reactJs vsCode eslint 플러그인 설정</title>
      <link>https://aamoos.tistory.com/763</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인 선택후 eslint 설치&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;346&quot; data-origin-height=&quot;229&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lVIoo/dJMcah31Y50/TH3QxCp8wVEiTvhIdktnE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lVIoo/dJMcah31Y50/TH3QxCp8wVEiTvhIdktnE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lVIoo/dJMcah31Y50/TH3QxCp8wVEiTvhIdktnE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlVIoo%2FdJMcah31Y50%2FTH3QxCp8wVEiTvhIdktnE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;346&quot; height=&quot;229&quot; data-origin-width=&quot;346&quot; data-origin-height=&quot;229&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eslint.config.js&lt;/p&gt;
&lt;pre id=&quot;code_1765796771669&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{js,jsx}'],
    extends: [
      js.configs.recommended,
      reactHooks.configs.flat.recommended,
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
      parserOptions: {
        ecmaVersion: 'latest',
        ecmaFeatures: { jsx: true },
        sourceType: 'module',
      },
    },
    rules: {
      'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
      'react/prop-types': 'off',
    },
  },
])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- rule안에 react/prop-types off로 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>frontend/React</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/763</guid>
      <comments>https://aamoos.tistory.com/763#entry763comment</comments>
      <pubDate>Mon, 15 Dec 2025 20:07:42 +0900</pubDate>
    </item>
    <item>
      <title>reactJs 프로젝트 경로 src '@' 설정</title>
      <link>https://aamoos.tistory.com/762</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;vite.config.js&lt;/p&gt;
&lt;pre id=&quot;code_1765796102247&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { defineConfig } from &quot;vite&quot;;
import react from &quot;@vitejs/plugin-react&quot;;
import { resolve } from &quot;path&quot;;

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      &quot;@&quot;: resolve(&quot;src&quot;),
    },
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트디렉터리에 jsconfig.json 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- vite.config.js만 생성하도 컴파일 에러가 나지않지만, Eslint 설정이 빡세게 되어있거나, idel에서 에러가 날경우가 있어서 아래 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;jsconfig.json&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1765796130880&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;compilerOptions&quot;: {
        &quot;baseUrl&quot;: &quot;.&quot;,
        &quot;paths&quot;: {
            &quot;@/*&quot;: [
                &quot;src/*&quot;
            ]
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;304&quot; data-origin-height=&quot;273&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSTbyE/dJMcachlNC9/XbgI0pL58aM5UIJQl7lsyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSTbyE/dJMcachlNC9/XbgI0pL58aM5UIJQl7lsyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSTbyE/dJMcachlNC9/XbgI0pL58aM5UIJQl7lsyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSTbyE%2FdJMcachlNC9%2FXbgI0pL58aM5UIJQl7lsyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;304&quot; height=&quot;273&quot; data-origin-width=&quot;304&quot; data-origin-height=&quot;273&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;components/Header.jsx&lt;/p&gt;
&lt;pre id=&quot;code_1765796247611&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function Header() {
    return (
        &amp;lt;header&amp;gt;
            &amp;lt;h1&amp;gt;My App Header&amp;lt;/h1&amp;gt;
        &amp;lt;/header&amp;gt;
    );
}

export default Header;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App.jsx&lt;/p&gt;
&lt;pre id=&quot;code_1765796261734&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Header from &quot;@/components/Header&quot;;

function App() {
  return &amp;lt;Header /&amp;gt;;
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index.css, app.css는 삭제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 시작&lt;/p&gt;
&lt;pre id=&quot;code_1765796305686&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm run dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;328&quot; data-origin-height=&quot;165&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cH3rfw/dJMcahXhxEu/PkEakbdZj60PICYZqHanvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cH3rfw/dJMcahXhxEu/PkEakbdZj60PICYZqHanvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cH3rfw/dJMcahXhxEu/PkEakbdZj60PICYZqHanvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcH3rfw%2FdJMcahXhxEu%2FPkEakbdZj60PICYZqHanvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;328&quot; height=&quot;165&quot; data-origin-width=&quot;328&quot; data-origin-height=&quot;165&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>frontend/React</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/762</guid>
      <comments>https://aamoos.tistory.com/762#entry762comment</comments>
      <pubDate>Mon, 15 Dec 2025 19:58:53 +0900</pubDate>
    </item>
    <item>
      <title>react 프로젝트 세팅</title>
      <link>https://aamoos.tistory.com/761</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;노드 설치 및 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vsCode를 실행한후 아래 명령어를 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1765792597252&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;node -v
npm -v&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;328&quot; data-origin-height=&quot;73&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nEgn1/dJMcagcZTiq/0X94KiDr6RmUbJ5sBBTphK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nEgn1/dJMcagcZTiq/0X94KiDr6RmUbJ5sBBTphK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nEgn1/dJMcagcZTiq/0X94KiDr6RmUbJ5sBBTphK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnEgn1%2FdJMcagcZTiq%2F0X94KiDr6RmUbJ5sBBTphK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;328&quot; height=&quot;73&quot; data-origin-width=&quot;328&quot; data-origin-height=&quot;73&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 설치가 되어있지않으면 node js 설치를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nodejs.org/en/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nodejs.org/en/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765792691503&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Node.js &amp;mdash; Run JavaScript Everywhere&quot; data-og-description=&quot;Node.js&amp;reg; is a free, open-source, cross-platform JavaScript runtime environment that lets developers create servers, web apps, command line tools and scripts.&quot; data-og-host=&quot;nodejs.org&quot; data-og-source-url=&quot;https://nodejs.org/en/&quot; data-og-url=&quot;https://nodejs.org/en/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/7u0Pi/hyZPs9JzTm/7vMDQ52kk5etgd7CnDqGXk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bm7Pak/hyZPusXdhH/dWrN0Z4pSWEtAjT7Atr3bK/img.png?width=224&amp;amp;height=256&amp;amp;face=0_0_224_256&quot;&gt;&lt;a href=&quot;https://nodejs.org/en/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nodejs.org/en/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/7u0Pi/hyZPs9JzTm/7vMDQ52kk5etgd7CnDqGXk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bm7Pak/hyZPusXdhH/dWrN0Z4pSWEtAjT7Atr3bK/img.png?width=224&amp;amp;height=256&amp;amp;face=0_0_224_256');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Node.js &amp;mdash; Run JavaScript Everywhere&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Node.js&amp;reg; is a free, open-source, cross-platform JavaScript runtime environment that lets developers create servers, web apps, command line tools and scripts.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nodejs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vsCode 터미널에서 프로젝트를 생성할 폴더로 이동후 아래 명령어를 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1765792776477&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm create vite@latest 프로젝트명&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;182&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bK25hb/dJMcajgqyuj/d4AWODHtqbcAcNQNcEgrRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK25hb/dJMcajgqyuj/d4AWODHtqbcAcNQNcEgrRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bK25hb/dJMcajgqyuj/d4AWODHtqbcAcNQNcEgrRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK25hb%2FdJMcajgqyuj%2Fd4AWODHtqbcAcNQNcEgrRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;182&quot; height=&quot;225&quot; data-origin-width=&quot;182&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- React 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;257&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bO2zcw/dJMcacPbHxm/ku6gWBd2ee8vEkYMsGorZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bO2zcw/dJMcacPbHxm/ku6gWBd2ee8vEkYMsGorZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bO2zcw/dJMcacPbHxm/ku6gWBd2ee8vEkYMsGorZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbO2zcw%2FdJMcacPbHxm%2Fku6gWBd2ee8vEkYMsGorZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;257&quot; height=&quot;232&quot; data-origin-width=&quot;257&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Typescript나 JavaScript 사용하고자 하는 언어 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;285&quot; data-origin-height=&quot;75&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd0xIn/dJMcadAvMRQ/6j2hy4bdothvf4CBQIkfI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd0xIn/dJMcadAvMRQ/6j2hy4bdothvf4CBQIkfI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd0xIn/dJMcadAvMRQ/6j2hy4bdothvf4CBQIkfI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd0xIn%2FdJMcadAvMRQ%2F6j2hy4bdothvf4CBQIkfI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;285&quot; height=&quot;75&quot; data-origin-width=&quot;285&quot; data-origin-height=&quot;75&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- no 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; rolldown-vite는 Rust로 만든 차세대 번들러임 , rollup을 대체하려는 목적&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징은 훨씬 빠른 빌드속도를 가지고 있지만, 아직 실험단계임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;247&quot; data-origin-height=&quot;47&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRcUQ4/dJMcad1z4fV/PY2eYb7o0QXesRVN1OWADK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRcUQ4/dJMcad1z4fV/PY2eYb7o0QXesRVN1OWADK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRcUQ4/dJMcad1z4fV/PY2eYb7o0QXesRVN1OWADK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRcUQ4%2FdJMcad1z4fV%2FPY2eYb7o0QXesRVN1OWADK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;247&quot; height=&quot;47&quot; data-origin-width=&quot;247&quot; data-origin-height=&quot;47&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- yes 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;219&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs3y5g/dJMcaaqi1Wa/uRlOjgvyuMRIllqFZkX7ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs3y5g/dJMcaaqi1Wa/uRlOjgvyuMRIllqFZkX7ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs3y5g/dJMcaaqi1Wa/uRlOjgvyuMRIllqFZkX7ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs3y5g%2FdJMcaaqi1Wa%2FuRlOjgvyuMRIllqFZkX7ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;280&quot; height=&quot;219&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;219&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 프로젝트안에 접속후 코드를 보고 싶으면 해당 경로로 이동후 아래 명령어를 입력한다.&lt;/p&gt;
&lt;pre id=&quot;code_1765793130116&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd 프로젝트경로
code .&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;209&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPmrF6/dJMcabW26JG/rh78rJpGN5U4u6qWwkUfpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPmrF6/dJMcabW26JG/rh78rJpGN5U4u6qWwkUfpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPmrF6/dJMcabW26JG/rh78rJpGN5U4u6qWwkUfpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPmrF6%2FdJMcabW26JG%2Frh78rJpGN5U4u6qWwkUfpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;209&quot; height=&quot;251&quot; data-origin-width=&quot;209&quot; data-origin-height=&quot;251&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>frontend/React</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/761</guid>
      <comments>https://aamoos.tistory.com/761#entry761comment</comments>
      <pubDate>Mon, 15 Dec 2025 19:06:48 +0900</pubDate>
    </item>
    <item>
      <title>[코프링] 13. dotenv 적용해서 spring 프로퍼티 중요정보 .env 파일로 분리하기</title>
      <link>https://aamoos.tistory.com/757</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;spring 프로퍼티를 설정할때 중요정보를 보여주지 않고 따로 환경설정 파일로 분리하고 싶을때가 있을것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 dotenv를 적용해서 env파일로 분리하는 방법을 아래 내용에 작성하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle.kts&lt;/p&gt;
&lt;pre id=&quot;code_1739853946425&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//dotenv
implementation (&quot;io.github.cdimascio:java-dotenv:5.2.2&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;427&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lBdkh/btsMljNtTk2/Pe3p2Fa67Oc6FvQpxWQiyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lBdkh/btsMljNtTk2/Pe3p2Fa67Oc6FvQpxWQiyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lBdkh/btsMljNtTk2/Pe3p2Fa67Oc6FvQpxWQiyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlBdkh%2FbtsMljNtTk2%2FPe3p2Fa67Oc6FvQpxWQiyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;403&quot; height=&quot;427&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;427&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 디렉터리 경로에 .env 파일 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.env&lt;/p&gt;
&lt;pre id=&quot;code_1739854033740&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SPRING_PROFILES_ACTIVE=local
SECRET=jwt시크릿키&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml&lt;/p&gt;
&lt;pre id=&quot;code_1739854054083&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  config:
    import: optional:file:.env[.properties]
  profiles:
    active: ${SPRING_PROFILES_ACTIVE}  # 기본적으로 local 프로파일 사용 (운영에서는 prod로 변경)

jwt:
  expiration_time: 86400000 # 1일
  secret: ${secret}

logging:
  level:
    org.hibernate.SQL: DEBUG  # Hibernate 쿼리 로그 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;config 아래 import 부분을 추가, .env에 설정한 key를 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.kt&lt;/p&gt;
&lt;pre id=&quot;code_1739854110080&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.contact.management

import io.github.cdimascio.dotenv.Dotenv
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.data.jpa.repository.config.EnableJpaAuditing


@SpringBootApplication
@EnableJpaAuditing
class ManagementApplication

fun main(args: Array&amp;lt;String&amp;gt;) {
	runApplication&amp;lt;ManagementApplication&amp;gt;(*args)
	val dotenv = Dotenv.load()
	System.out.println(&quot;SPRING_PROFILES_ACTIVE: &quot; + dotenv.get(&quot;SPRING_PROFILES_ACTIVE&quot;));
	System.out.println(&quot;SECRET: &quot; + dotenv.get(&quot;SECRET&quot;));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test 코드를 만들어도 되지만 간단하게 테스트 하기위해서 applicaiton.kt에 해당 코드 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;43&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDsFt1/btsMnolYx0K/oN04MkcdCl7ruzsSshTYs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDsFt1/btsMnolYx0K/oN04MkcdCl7ruzsSshTYs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDsFt1/btsMnolYx0K/oN04MkcdCl7ruzsSshTYs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDsFt1%2FbtsMnolYx0K%2FoN04MkcdCl7ruzsSshTYs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;596&quot; height=&quot;43&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;43&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.gitignore&lt;/p&gt;
&lt;pre id=&quot;code_1739854203334&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.env*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 깃에 커밋시 해당 환경설정 파일이 commit되면 안되므로 맨 아래 라인에 .env*을 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git ignore에 .env*을 포함해도 .env파일이 git staging에 보일경우 안보이게 하는법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. .env 파일을 백업하고 프로젝트에 있는 .env 파일을 삭제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 아래 명령어 intelij 터미널에 입력&lt;/p&gt;
&lt;pre id=&quot;code_1739854818234&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git rm .env --cached
git add .
git commit -m &quot;remove .env file from git&quot;
git push&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 이후 다시 루트 디렉터리에 백업한 .env파일을 생성한후 git - commit 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;401&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1Nf4j/btsMlFJuqVi/Y14CFhMiJTxJ5y0sEKUFkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1Nf4j/btsMlFJuqVi/Y14CFhMiJTxJ5y0sEKUFkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1Nf4j/btsMlFJuqVi/Y14CFhMiJTxJ5y0sEKUFkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1Nf4j%2FbtsMlFJuqVi%2FY14CFhMiJTxJ5y0sEKUFkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;401&quot; height=&quot;183&quot; data-origin-width=&quot;401&quot; data-origin-height=&quot;183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;env 파일을 변경해도 커밋 changes에 안보이는것을 확인할수 있음&lt;/p&gt;</description>
      <category>backend/코프링</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/757</guid>
      <comments>https://aamoos.tistory.com/757#entry757comment</comments>
      <pubDate>Tue, 18 Feb 2025 14:01:42 +0900</pubDate>
    </item>
    <item>
      <title>[코프링] 12. Render 무료 호스팅서버에 Spring boot + Kotlin 배포하기</title>
      <link>https://aamoos.tistory.com/756</link>
      <description>&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 무료서버가 몇가지있지만 그중 Render라는 무료 서버 호스팅 사이트에 배포하는 방법 관련 내용입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이전 포스팅한 간단한 프로젝트를 Render에 배포해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본적으로 docker hub 회원가입 및 도커가 설치 되어있다는 기준으로 진행하겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;intelij 프로젝트에서 gradle build 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVo9pI/btsMjCL1HOK/kiKiYuMjtAKIP6MWnKLErk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVo9pI/btsMjCL1HOK/kiKiYuMjtAKIP6MWnKLErk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVo9pI/btsMjCL1HOK/kiKiYuMjtAKIP6MWnKLErk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVo9pI%2FbtsMjCL1HOK%2FkiKiYuMjtAKIP6MWnKLErk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;843&quot; height=&quot;170&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DockerFile&lt;/p&gt;
&lt;pre id=&quot;code_1739515448666&quot; class=&quot;dockerfile&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;FROM azul/zulu-openjdk:17-latest
VOLUME /tmp
COPY build/libs/*.jar app.jar
ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;/app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIlCIz/btsMjBfg9vq/K0riv7ykEXEe3aCPjU7Ll0/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIlCIz/btsMjBfg9vq/K0riv7ykEXEe3aCPjU7Ll0/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIlCIz/btsMjBfg9vq/K0riv7ykEXEe3aCPjU7Ll0/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIlCIz%2FbtsMjBfg9vq%2FK0riv7ykEXEe3aCPjU7Ll0%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;404&quot; height=&quot;264&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;intelij 터미널에 아래 명령어 순서대로 입력&lt;/p&gt;
&lt;pre id=&quot;code_1739515467447&quot; class=&quot;properties&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;docker build -t 프로젝트명 .
docker images
docker tag 프로젝트명:latest         도커아이디명/프로젝트명
docker push 도커아이디명/프로젝트명&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker hub 사이트 접속&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hub.docker.com/&quot;&gt;https://hub.docker.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739515481701&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Docker Hub Container Image Library | App Containerization&quot; data-og-description=&quot;Increase your reach and adoption on Docker Hub With a Docker Verified Publisher subscription, you'll increase trust, boost discoverability, get exclusive data insights, and much more.&quot; data-og-host=&quot;hub.docker.com&quot; data-og-source-url=&quot;https://hub.docker.com/&quot; data-og-url=&quot;https://hub.docker.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kqSh0/hyYfI0mPnA/59Knn0B7Vkao5jkXHr0T8k/img.png?width=2560&amp;amp;height=1030&amp;amp;face=0_0_2560_1030,https://scrap.kakaocdn.net/dn/bBLTUz/hyYf5VzF5X/bYkuobQ9uCcqEOmTutvVv0/img.png?width=1000&amp;amp;height=1000&amp;amp;face=0_0_1000_1000&quot;&gt;&lt;a href=&quot;https://hub.docker.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hub.docker.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kqSh0/hyYfI0mPnA/59Knn0B7Vkao5jkXHr0T8k/img.png?width=2560&amp;amp;height=1030&amp;amp;face=0_0_2560_1030,https://scrap.kakaocdn.net/dn/bBLTUz/hyYf5VzF5X/bYkuobQ9uCcqEOmTutvVv0/img.png?width=1000&amp;amp;height=1000&amp;amp;face=0_0_1000_1000');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Docker Hub Container Image Library | App Containerization&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Increase your reach and adoption on Docker Hub With a Docker Verified Publisher subscription, you'll increase trust, boost discoverability, get exclusive data insights, and much more.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hub.docker.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1441&quot; data-origin-height=&quot;263&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UtFoc/btsMiIZ4Iqo/itsJwNGcHcSg1UoohBvrh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UtFoc/btsMiIZ4Iqo/itsJwNGcHcSg1UoohBvrh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UtFoc/btsMiIZ4Iqo/itsJwNGcHcSg1UoohBvrh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUtFoc%2FbtsMiIZ4Iqo%2FitsJwNGcHcSg1UoohBvrh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1441&quot; height=&quot;263&quot; data-origin-width=&quot;1441&quot; data-origin-height=&quot;263&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- docker hub사이트에 접속해보면 이미지가 push가 잘된것을 볼수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;render 사이트에 접속을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dashboard.render.com/&quot;&gt;https://dashboard.render.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739515547662&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Cloud Application Hosting for Developers | Render&quot; data-og-description=&quot;Render is a unified cloud to build and run all your apps and websites with free SSL, global CDN, private networks and automatic deploys from Git.&quot; data-og-host=&quot;dashboard.render.com&quot; data-og-source-url=&quot;https://dashboard.render.com/&quot; data-og-url=&quot;https://dashboard.render.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/byOOnY/hyYfLiutUl/Xi3H7qyaC0ZQMRJdqZ1MD1/img.png?width=3840&amp;amp;height=2146&amp;amp;face=0_0_3840_2146,https://scrap.kakaocdn.net/dn/b36ZzX/hyYfDdGV9d/ktokRUf2A2NcH5FeKKKqyk/img.png?width=3840&amp;amp;height=2146&amp;amp;face=0_0_3840_2146&quot;&gt;&lt;a href=&quot;https://dashboard.render.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dashboard.render.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/byOOnY/hyYfLiutUl/Xi3H7qyaC0ZQMRJdqZ1MD1/img.png?width=3840&amp;amp;height=2146&amp;amp;face=0_0_3840_2146,https://scrap.kakaocdn.net/dn/b36ZzX/hyYfDdGV9d/ktokRUf2A2NcH5FeKKKqyk/img.png?width=3840&amp;amp;height=2146&amp;amp;face=0_0_3840_2146');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Cloud Application Hosting for Developers | Render&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Render is a unified cloud to build and run all your apps and websites with free SSL, global CDN, private networks and automatic deploys from Git.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dashboard.render.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZCf1U/btsMjhVw3Xs/SeijrMaSOHDLlFDc3Q9ISk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZCf1U/btsMjhVw3Xs/SeijrMaSOHDLlFDc3Q9ISk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZCf1U/btsMjhVw3Xs/SeijrMaSOHDLlFDc3Q9ISk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZCf1U%2FbtsMjhVw3Xs%2FSeijrMaSOHDLlFDc3Q9ISk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1486&quot; height=&quot;478&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Overview - Create New Service&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;543&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FsBXp/btsMhSJgowD/az8ujn2PPyQUrerCFuutRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FsBXp/btsMhSJgowD/az8ujn2PPyQUrerCFuutRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FsBXp/btsMhSJgowD/az8ujn2PPyQUrerCFuutRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFsBXp%2FbtsMhSJgowD%2Faz8ujn2PPyQUrerCFuutRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1426&quot; height=&quot;543&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;543&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Web Service 클릭&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;433&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPm9xL/btsMjJqs5uf/PdDH585xmmdBCIcmIwV0B0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPm9xL/btsMjJqs5uf/PdDH585xmmdBCIcmIwV0B0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPm9xL/btsMjJqs5uf/PdDH585xmmdBCIcmIwV0B0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPm9xL%2FbtsMjJqs5uf%2FPdDH585xmmdBCIcmIwV0B0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1444&quot; height=&quot;433&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;433&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker hub에 push한 image url을 입력후 Connect&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;727&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br8OF7/btsMh3DWvRT/KudEVDIgPcQY1KUU3brgbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br8OF7/btsMh3DWvRT/KudEVDIgPcQY1KUU3brgbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br8OF7/btsMh3DWvRT/KudEVDIgPcQY1KUU3brgbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr8OF7%2FbtsMh3DWvRT%2FKudEVDIgPcQY1KUU3brgbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1410&quot; height=&quot;727&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;727&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Region은 가장 가까운 Singapore로 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Instance Type는 Free로 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1385&quot; data-origin-height=&quot;277&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNMXXz/btsMka8VPfB/uUI0riKrfHtd9vjIMemDX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNMXXz/btsMka8VPfB/uUI0riKrfHtd9vjIMemDX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNMXXz/btsMka8VPfB/uUI0riKrfHtd9vjIMemDX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNMXXz%2FbtsMka8VPfB%2FuUI0riKrfHtd9vjIMemDX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1385&quot; height=&quot;277&quot; data-origin-width=&quot;1385&quot; data-origin-height=&quot;277&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPRING_PROFILES_ACTIVE -&amp;gt; prod로 설정이후 Deploy Web Service&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 완료화면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;653&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NmFjs/btsMjDjO4dQ/QPC9zG9c8fFiZVKRILiyf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NmFjs/btsMjDjO4dQ/QPC9zG9c8fFiZVKRILiyf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NmFjs/btsMjDjO4dQ/QPC9zG9c8fFiZVKRILiyf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNmFjs%2FbtsMjDjO4dQ%2FQPC9zG9c8fFiZVKRILiyf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1350&quot; height=&quot;653&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;653&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 로그를 보면 application.yml prod를 바라보게 설정이 된것을 볼수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1065&quot; data-origin-height=&quot;822&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TW1pC/btsMiJSofyD/ooD7lpSAX29iKIKjcVc3x1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TW1pC/btsMiJSofyD/ooD7lpSAX29iKIKjcVc3x1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TW1pC/btsMiJSofyD/ooD7lpSAX29iKIKjcVc3x1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTW1pC%2FbtsMiJSofyD%2FooD7lpSAX29iKIKjcVc3x1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1065&quot; height=&quot;822&quot; data-origin-width=&quot;1065&quot; data-origin-height=&quot;822&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포한 서버의 사용자 조회 api 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;525&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lmdr5/btsMjJqzh1k/PgZQO3prtY29tJb2Ju2WH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lmdr5/btsMjJqzh1k/PgZQO3prtY29tJb2Ju2WH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lmdr5/btsMjJqzh1k/PgZQO3prtY29tJb2Ju2WH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flmdr5%2FbtsMjJqzh1k%2FPgZQO3prtY29tJb2Ju2WH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;728&quot; height=&quot;525&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;525&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포한 서버의 신규 사용자 등록 api 호출&lt;/p&gt;</description>
      <category>backend/코프링</category>
      <author>JaeSung Kim</author>
      <guid isPermaLink="true">https://aamoos.tistory.com/756</guid>
      <comments>https://aamoos.tistory.com/756#entry756comment</comments>
      <pubDate>Fri, 14 Feb 2025 16:26:12 +0900</pubDate>
    </item>
  </channel>
</rss>