<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>vellalog</title>
    <link>https://tavi.tistory.com/</link>
    <description>사용자 경험과 코드 품질을 함께 고민하는 프론트엔드 개발자 &amp;middot; 풀스택  &amp;middot;  I &amp;hearts; UX/UI</description>
    <language>ko</language>
    <pubDate>Sun, 27 Sep 2026 15:11:03 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>vellahw</managingEditor>
    <image>
      <title>vellalog</title>
      <url>https://tistory1.daumcdn.net/tistory/5523820/attach/93ce8c60fd5c47638c42cc40ff81a44b</url>
      <link>https://tavi.tistory.com</link>
    </image>
    <item>
      <title>  MySQL과 Tailscale로 안전한 홈서버 만들기(외부 접속)</title>
      <link>https://tavi.tistory.com/194</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 오라클로 홈서버를 만들었었다. &lt;a title=&quot;요기&quot; href=&quot;https://tavi.tistory.com/190&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;요기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS로 환경을 변경하면서 MySQL로 마이그레이션을 하게 돼서 MySQL로 홈서버를 구축했다.&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;처음에는 오라클 환경처럼 공유기 포트포워딩으로 MySQL의 3306 포트를 외부에 공개하려 했지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 Tailscale이란 신세계를 접하게 되어 Tailscale을 사용해 구축했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt; &lt;br /&gt;Tailscale을 이용하면 인터넷 상에 포트를 공개하지 않고 팀원만 안전하게 접속할 수 있다.&lt;br /&gt;팀원은 내 컴퓨터의 공인 IP나 공유기 내부 IP로 접속하지 않고 Tailscale이 발급한 100.x.x.x 형태의 전용 IP로 MySQL에 접속한다.&lt;br /&gt;&lt;br /&gt;Tailscale을 사용하면 공인 IP가 변경되어도 영향을 받지 않기 때문에 IP가 변경될 걱정을 하지 않아도 된다!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;b&gt;MySQL 외부 접속 허용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원이 접속하려면 MySQL이 외부 네트워크 인터페이스에서도 연결을 받도록 설정해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;C:\ProgramData\MySQL\MySQL Server 8.4\my.ini
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 설정 파일은 일반적으로 위와 같은 위치에 있고 ProgramData는 숨김 폴더다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;my.ini를 관리자 권한으로 열고 [mysqld] 부분의 bind-address를 0.0.0.0으로 수정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1789194844211&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[mysqld]
bind-address=0.0.0.0
port=3306&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bind-address가 127.0.0.1으로 되어있다면 다른 PC에서는 접속할 수 없고 로컬 연결만 허용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 수정했다면 PowerShell에 아래 명령어를 입력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;Restart-Service MySQL84
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. MySQL 서비스를 재시작한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1789194879459&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Get-NetTCPConnection -LocalPort 3306 -State Listen&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 정상적으로 포트를 열고있는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;218&quot; data-origin-height=&quot;77&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHR2Ao/dJMcaix41RJ/QxkWTDYoTaMQeAZdkjEut0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHR2Ao/dJMcaix41RJ/QxkWTDYoTaMQeAZdkjEut0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHR2Ao/dJMcaix41RJ/QxkWTDYoTaMQeAZdkjEut0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHR2Ao%2FdJMcaix41RJ%2FQxkWTDYoTaMQeAZdkjEut0%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;218&quot; height=&quot;77&quot; data-origin-width=&quot;218&quot; data-origin-height=&quot;77&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;127.0.0.1:3306만 나온다면 my.ini 설정을 다시 확인해야 한다.&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프로젝트 데이터베이스 생성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 MySQL 환경변수를 등록하지 않았다면 등록해준다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로: &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;C:\Program&amp;nbsp;Files\MySQL\MySQL&amp;nbsp;Server&amp;nbsp;8.4\bin&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;623&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uY8Ya/dJMcaf2g82R/QXSIxmTiKpmsf8pYdagQLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uY8Ya/dJMcaf2g82R/QXSIxmTiKpmsf8pYdagQLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uY8Ya/dJMcaf2g82R/QXSIxmTiKpmsf8pYdagQLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuY8Ya%2FdJMcaf2g82R%2FQXSIxmTiKpmsf8pYdagQLK%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;1434&quot; height=&quot;623&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;623&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&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;b&gt;MySQL 관리자 계정으로 접속&lt;/b&gt;한다.&lt;/h4&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;mysql -u root -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호를 입력할 때 PowerShell 화면에는 아무것도 표시되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력이 안 되는 것처럼 보여도 실제로는 입력되고 있으므로 비밀번호를 입력하고 Enter를 누르면 된다.&lt;/p&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;b&gt;이번 프로젝트에서 사용할 데이터베이스를 생성&lt;/b&gt;한다.&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;CREATE DATABASE 프로젝트명
    CHARACTER SET utf8mb4
    COLLATE utf8mb4_0900_ai_ci;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 데이터베이스를 확인한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SHOW DATABASES;
&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;외부 접속용 계정 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원이 접속할 때 root 계정을 사용하면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;root는 서버 전체를 관리할 수 있기 때문에 문제가 생겼을 때 피해 범위가 너무 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 팀원별로 별도의 계정을 만드는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1789193869945&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE USER 'gildong'@'100.90.30.40'
IDENTIFIED BY '길고-복잡한-개별비밀번호';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gildong 에는 팀원 이름이나 닉네임을, @뒤 부분엔 팀원의 IP를 입력하면 된다.&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;아직 팀원의 Tailscale IP를 모르는 상태라면 다음처럼 계정을 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE USER 'team_user'@'%'
IDENTIFIED BY '길고-복잡한-비밀번호';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'team_user'@'%'는 모든 IP에서 해당 계정으로 로그인할 수 있다는 의미다.&lt;/p&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;b&gt;필요한 권한을 부여&lt;/b&gt;한다.&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;GRANT SELECT, INSERT, UPDATE, DELETE,
      CREATE, ALTER, INDEX, REFERENCES
ON 데이터베이스명.*
TO 'gildong'@'100.90.30.40';

FLUSH PRIVILEGES;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 중 테이블 삭제가 필요하다면 DROP 권한을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;GRANT DROP
ON 데이터베이스명.*
TO 'gildong'@'100.90.30.40';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어를 입력해 마무리.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;FLUSH PRIVILEGES;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록된 사용자와 접속 허용 주소는 다음 쿼리로 확인한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT User, Host
FROM mysql.user;
&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Tailscale 설치하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tailscale은 여러 컴퓨터를 하나의 사설 네트워크처럼 연결해주는 VPN 서비스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tailscale.com/download/windows&quot;&gt;Tailscale 다운로드&lt;/a&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;MySQL 홈서버로 사용할 내 컴퓨터와 팀원 컴퓨터에 모두 Tailscale을 설치해 각자 계정을 만들어 로그인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 홈서버의 Tailscale IP는 다음 명령어로 확인한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;tailscale ip -4
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;명령어를 입력하면 나오는&lt;span&gt; &lt;/span&gt;&lt;/span&gt;주소가 팀원이 MySQL에 접속할 때 사용할 호스트 주소다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;팀원에게 홈서버 장치 공유하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Tailscale 관리자 페이지로 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://login.tailscale.com/admin/machines&quot;&gt;Tailscale Machines 관리 페이지&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1550&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wPkQ2/dJMcai5J75a/hVKMIn0IbgKu5s98ulsvzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wPkQ2/dJMcai5J75a/hVKMIn0IbgKu5s98ulsvzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wPkQ2/dJMcai5J75a/hVKMIn0IbgKu5s98ulsvzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwPkQ2%2FdJMcai5J75a%2FhVKMIn0IbgKu5s98ulsvzk%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;1550&quot; height=&quot;417&quot; data-origin-width=&quot;1550&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 순서로 공유한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. MySQL이 설치된 홈서버 컴퓨터를 찾는다.&lt;br /&gt;2. 오른쪽의 ⋯ 버튼을 hover하면 노출되는 Share를 선택&lt;br /&gt;3. 팀원의 이메일을 입력하고 초대를 전송한다.&lt;br /&gt;4. 팀원이 이메일로 받은 초대를 수락한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장치 공유 방식을 사용하면 팀원에게 내 Tailscale 네트워크 전체가 노출되지 않고 공유한 홈서버 한 대에만 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원이 여러 명이라면 이메일로 각각 초대하는 것이 안전하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Windows 방화벽에서 MySQL 허용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tailscale 연결이 되어 있어도 Windows 방화벽이 3306 포트를 막고 있으면 팀원은 MySQL에 접속할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원 컴퓨터에서 다음 명령어로 Tailscale IP를 확인한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;tailscale ip -4
&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;팀원 IP가 100.x.x.x 이라면 내 홈서버의 관리자 PowerShell에서 해당 IP만 허용한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;New-NetFirewallRule `
  -DisplayName &quot;MySQL - 팀원 홍길동&quot; `
  -Direction Inbound `
  -Protocol TCP `
  -LocalPort 3306 `
  -RemoteAddress 100.x.x.x `
  -Action Allow&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록된 MySQL 방화벽 규칙은 다음 명령어로 확인한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Get-NetFirewallRule |
  Where-Object DisplayName -Like &quot;*MySQL*&quot; |
  Format-Table DisplayName, Enabled, Direction, Action
&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;팀원 컴퓨터에서 연결 테스트하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원이 DBeaver를 바로 실행하기 전에 Tailscale 연결부터 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원 컴퓨터에서 다음 명령어를 실행한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;tailscale status
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 Tailscale 전용 ping을 실행한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;tailscale ping 100.x.x.x&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기서 입력하는 IP는 반드시 내 홈서버의 Tailscale IP&lt;/b&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;마지막으로 MySQL 3306 포트에 연결할 수 있는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;Test-NetConnection 100.x.x.x -Port 3306&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상이라면 다음과 같이 표시된다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;TcpTestSucceeded : True
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;b&gt;DBeaver에서 홈서버 MySQL 연결하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원은 DBeaver에서 MySQL 연결을 추가하고 다음과 같이 설정한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;DB 종류: MySQL
Host: 내 홈서버의 Tailscale IP
Port: 3306
Database: 데이터베이스명
Username: 팀원 전용 계정
Password: 팀원 전용 비밀번호&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Test Connection을 눌러 연결을 확인한다.&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;Spring Boot에서는 다음과 같이 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://100.x.x.x:3306/데이터베이스명?serverTimezone=Asia/Seoul&amp;amp;characterEncoding=UTF-8
    username: gildong
    password: ${DB_PASSWORD}
    driver-class-name: com.mysql.cj.jdbc.Driver&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;⚠️&lt;/b&gt;&lt;i&gt;&lt;b&gt; TcpTestSucceeded : False가 발생했을 때&lt;/b&gt;&lt;/i&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원이 다음 명령어를 실행했는데 실패할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;Test-NetConnection 100.x.x.x -Port 3306&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;TcpTestSucceeded : False
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 오류는 아직 MySQL 계정이나 비밀번호를 확인하는 단계까지 도달하지 못했다는 뜻이다. 보통 다음 중 하나가 원인이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잘못된 IP를 입력했다.&lt;/li&gt;
&lt;li&gt;Tailscale 연결이 끊겨 있다.&lt;/li&gt;
&lt;li&gt;장치 공유 초대를 수락하지 않았다.&lt;/li&gt;
&lt;li&gt;MySQL 서비스가 중지되어 있다.&lt;/li&gt;
&lt;li&gt;MySQL이 127.0.0.1에서만 연결을 받고 있다.&lt;/li&gt;
&lt;li&gt;Windows 방화벽이 3306 포트를 차단하고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 팀원 컴퓨터에서 확인한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;tailscale status
tailscale ping 내_홈서버_Tailscale_IP
Test-NetConnection 내_홈서버_Tailscale_IP -Port 3306
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;판단 방법은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;tailscale ping 실패
&amp;rarr; Tailscale 로그인 또는 장치 공유 문제다.

tailscale ping 성공 + 3306 실패
&amp;rarr; MySQL 리스닝 또는 Windows 방화벽 문제다.

3306 성공 + DBeaver 실패
&amp;rarr; MySQL 계정, Host, 권한 또는 비밀번호 문제다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 홈서버에서는 다음 항목을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;tailscale ip -4
Get-Service -Name &quot;MySQL*&quot;
Get-NetTCPConnection -LocalPort 3306 -State Listen
Test-NetConnection 내_Tailscale_IP -Port 3306
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LocalAddress가 127.0.0.1이라면 my.ini의 bind-address 설정을 다시 확인해야 한다.&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;⚠️&lt;/b&gt;&lt;i&gt;&lt;b&gt;Tailscale에서 로그아웃 또는 Expired가 뜰 때&lt;/b&gt;&lt;/i&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원이 Tailscale에서 Logged out, Expired 또는 재인증 관련 메시지를 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tailscale은 보안을 위해 각 장치의 인증 키를 주기적으로 만료시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 키가 만료되면 해당 컴퓨터는 Tailscale 네트워크에서 연결이 끊기고 MySQL에도 접속할 수 없다.&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;이 경우 팀원은 Tailscale 앱을 열고 다시 로그인하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PowerShell에서는 다음 명령어로 강제 재인증할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;tailscale up --force-reauth
&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;재인증 후 다음 명령어로 상태를 확인한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;tailscale status
tailscale ip -4
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 다시 테스트한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;tailscale ping 내_홈서버_Tailscale_IP
Test-NetConnection 내_홈서버_Tailscale_IP -Port 3306
&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;재로그인 후 팀원의 Tailscale IP가 변경됐다면 Windows 방화벽과 MySQL 사용자에 등록한 기존 IP도 수정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 홈서버는 항상 접속 가능해야 하므로 인증 키 만료를 비활성화할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. Tailscale Machines 관리 페이지에 접속한다.&lt;br /&gt;2. 홈서버 오른쪽의 ⋯ 버튼을 누른다.&lt;br /&gt;3. Disable Key Expiry를 선택한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원 개인 컴퓨터는 보안을 위해 인증 만료를 유지하고, 만료됐을 때 다시 로그인하는 편이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✅ 보안을 위해 꼭 지켜야 할 것&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홈서버는 편리하지만 설정을 잘못하면 데이터베이스가 외부 공격에 노출될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소한 다음 원칙은 지키는 것이 좋다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 공유기에서 3306 포트를 인터넷에 포트포워딩하지 않는다.&lt;br /&gt;- 외부 접속에 root 계정을 사용하지 않는다.&lt;br /&gt;- 팀원별로 MySQL 계정을 따로 만든다.&lt;br /&gt;- 계정에는 프로젝트 DB에 필요한 권한만 부여한다.&lt;br /&gt;- 팀원마다 서로 다른 비밀번호를 사용한다.&lt;br /&gt;- Windows 방화벽 전체를 끄지 않는다.&lt;br /&gt;- Tailscale IP 또는 Tailscale 대역에서만 3306을 허용한다.&lt;br /&gt;- 프로젝트에서 빠진 팀원의 Tailscale 공유와 MySQL 계정을 제거한다.&lt;br /&gt;- DB 비밀번호를 Git 저장소에 커밋하지 않는다.&lt;br /&gt;- 중요한 데이터는 정기적으로 백업한다.&lt;/blockquote&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;h2 data-ke-size=&quot;size26&quot;&gt;  마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 MySQL 홈서버를 만들려면 공인 IP, 포트포워딩, 공유기 설정 등 복잡한 과정이 필요할 것이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Tailscale을 사용하니 공유기 설정 없이 내 컴퓨터와 팀원 컴퓨터를 하나의 사설 네트워크처럼 연결할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오라클에 비해 너무나 간단하고 신기해서 신세계였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 3306 포트를 인터넷 전체에 공개하지 않아도 된다는 점도 마음에 들었다.&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;비용 부담 없이 사이드 프로젝트용 공용 데이터베이스가 필요하다면 MySQL과 Tailscale을 조합한 홈서버도 충분히 괜찮은 선택이다. &lt;span style=&quot;color: #dddddd;&quot;&gt;우리는 배포를 위해 결국 AWS로 다시 이관했지만...&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrEQX%2FbtrK4CAyKrB%2FutM4awBF5CvfGVQ7UvveyK%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;175&quot; height=&quot;86&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>BE/Database</category>
      <category>db</category>
      <category>MySQL</category>
      <category>데이터베이스</category>
      <category>홈서버</category>
      <author>vellahw</author>
      <guid isPermaLink="true">https://tavi.tistory.com/194</guid>
      <comments>https://tavi.tistory.com/194#entry194comment</comments>
      <pubDate>Fri, 31 Jul 2026 19:50:23 +0900</pubDate>
    </item>
    <item>
      <title> ️ TanStack Query useMutation 사용법 정리 - 서버 데이터 변경부터 캐시 갱신까지</title>
      <link>https://tavi.tistory.com/193</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 API를 다루다 보면 데이터를 조회하는 것만큼 데이터를 생성하거나 수정하고 삭제하는 작업도 자주 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TanStack Query에서는 데이터를 조회할 때 useQuery를 사용하고 &lt;b&gt;서버의 데이터를 변경할 때 useMutation을 사용한다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 useMutation을 접했을 때는 단순히 POST나 PUT 요청을 보내는 함수 정도로 생각했는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 사용해 보니 요청 상태 관리부터 성공/실패 처리, 기존 Query 캐시 갱신까지 함께 처리할 수 있어 꽤 편리하다고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 useMutation의 기본적인 사용법과 실제 프로젝트에서 자주 사용하는 패턴을 정리해보려고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;useMutation이란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useQuery와 useMutation의 차이를 간단하게 정리하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  useQuery &amp;rarr; 데이터를 조회할 때 사용&lt;br /&gt;  useMutation &amp;rarr; 데이터를 생성, 수정, 삭제할 때 사용&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 게시글 목록을 조회하는 API가 있다면 useQuery를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 아래와 같은 작업에는 useMutation을 사용할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입/로그인 요청&lt;/li&gt;
&lt;li&gt;게시글 등록/수정/삭제&lt;/li&gt;
&lt;li&gt;댓글 등록&lt;/li&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기본적인 useMutation 사용법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 게시글을 수정하는 API가 있다고 가정해보면&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const updatePost = async ({
  id,
  title,
  content,
}: {
  id: number;
  title: string;
  content: string;
}) =&amp;gt; {
  const response = await fetch(`/posts/${id}`, {
    method: &quot;PUT&quot;,
    headers: {
      &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
    body: JSON.stringify({
      title,
      content,
    }),
  });

  if (!response.ok) {
    throw new Error(&quot;게시글 수정 실패&quot;);
  }

  return response.json();
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 API 함수를 useMutation과 연결하면 다음과 같이 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;capnproto&quot;&gt;&lt;code&gt;import { useMutation } from &quot;@tanstack/react-query&quot;;

const mutation = useMutation({
  mutationFn: updatePost,
});
&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;mutationFn&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;mutationFn: updatePost
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mutationFn에는 실제 서버 요청을 수행하는 함수를 전달한다.&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;b&gt;mutation을 실행하는 mutate&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useMutation을 선언했다고 해서 API 요청이 바로 실행되는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useQuery는 조건에 따라 자동으로 데이터를 가져오는 경우가 많지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mutation은 일반적으로 사용자의 행동에 따라 실행해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 사용하는 것이 mutate다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;mutation.mutate({
  id: 1,
  title: &quot;수정된 제목&quot;,
  content: &quot;수정된 내용&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;pre class=&quot;dts&quot;&gt;&lt;code&gt;&amp;lt;button
  onClick={() =&amp;gt;
    mutation.mutate({
      id: 1,
      title: &quot;수정된 제목&quot;,
      content: &quot;수정된 내용&quot;,
    })
  }
&amp;gt;
  수정하기
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mutate()에 전달한 객체는 그대로 mutationFn의 인자로 전달된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;mutation.mutate({
  id: 1,
  title: &quot;제목&quot;,
  content: &quot;내용&quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 위 함수를 실행하면 내부적으로 다음 함수가 호출되는 구조다.  &lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;updatePost({
  id: 1,
  title: &quot;제목&quot;,
  content: &quot;내용&quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;성공과 실패 처리하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 요청을 보냈다면 성공했을 때와 실패했을 때 각각 처리해야 하는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 onSuccess, onError를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;const mutation = useMutation({
  mutationFn: updatePost,

  onSuccess: (data) =&amp;gt; {
    console.log(&quot;수정 성공&quot;, data);
  },

  onError: (error) =&amp;gt; {
    console.error(&quot;수정 실패&quot;, error);
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 게시글 수정에 성공한 뒤 사용자에게 알림을 보여주거나 페이지를 이동할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const mutation = useMutation({
  mutationFn: updatePost,

  onSuccess: () =&amp;gt; {
    alert(&quot;게시글이 수정되었어요.&quot;);
    navigate(&quot;/posts&quot;);
  },

  onError: () =&amp;gt; {
    alert(&quot;게시글 수정에 실패했어요.&quot;);
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 요청과 성공/실패에 따른 처리를 하나의 mutation 안에서 관리할 수 있다는 점이 편리하다.&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;b&gt;mutate와 mutateAsync의 차이&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mutation을 실행하는 방법은 크게 두 가지가 있다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;mutation.mutate();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;await mutation.mutateAsync();
&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;mutate는 콜백 기반으로 처리할 때 사용&lt;/b&gt;하기 좋다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;mutation.mutate(
  {
    id: 1,
    title: &quot;제목&quot;,
    content: &quot;내용&quot;,
  },
  {
    onSuccess: () =&amp;gt; {
      console.log(&quot;성공&quot;);
    },
    onError: () =&amp;gt; {
      console.log(&quot;실패&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;b&gt;mutateAsync는 Promise를 반환하기 때문에 async/await을 사용할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;try {
  const result = await mutation.mutateAsync({
    id: 1,
    title: &quot;제목&quot;,
    content: &quot;내용&quot;,
  });

  console.log(result);
  navigate(&quot;/posts&quot;);
} catch (error) {
  console.error(error);
}
&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;요청 이후 여러 작업을 순차적으로 처리해야 한다면 mutateAsync가 읽기 편한 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 mutateAsync가 항상 더 좋은 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 성공/실패 콜백만 필요한 경우라면 mutate와 onSuccess, onError 조합으로도 충분하다.&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;b&gt;mutation의 상태값 활용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useMutation은 API 요청뿐만 아니라 현재 요청의 상태도 관리해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 다음과 같은 값을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;mutation.isPending;
mutation.isSuccess;
mutation.isError;
mutation.data;
mutation.error;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 요청 중에는 버튼을 비활성화할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;button
  onClick={handleUpdate}
  disabled={mutation.isPending}
&amp;gt;
  {mutation.isPending ? &quot;수정 중...&quot; : &quot;수정하기&quot;}
&amp;lt;/button&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;별도로 useState를 만들어서 로딩 상태를 관리하지 않고 mutation이 제공하는 상태를 활용할 수 있다는 장점이 있다.&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;b&gt;❓ mutation 성공 후 화면의 데이터가 안 바뀌는 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useMutation을 처음 사용할 때 가장 헷갈릴 수 있는 부분인데,&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;b&gt;서버의 데이터와 TanStack Query가 가지고 있는 캐시는 별개이기 때문이다.&lt;/b&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;예를 들어 게시글을 다음 Query로 조회하고 있다고 가정한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;useQuery({
  queryKey: [&quot;post&quot;, id],
  queryFn: () =&amp;gt; getPost(id),
});
&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;그리고 mutation을 통해 게시글을 수정한다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;mutation.mutate({
  id,
  title,
  content,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버의 게시글은 정상적으로 변경됐지만 기존 Query 캐시에는 수정 전 데이터가 남아 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 mutation 성공 후 해당 Query가 오래된 데이터라는 것을 TanStack Query에 알려줘야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;invalidateQueries로 캐시 무효화하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 사용하는 것이 &lt;b&gt;invalidateQueries&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;useQueryClient&lt;/b&gt;를 가져오고,&lt;/p&gt;
&lt;pre class=&quot;capnproto&quot;&gt;&lt;code&gt;import {
  useMutation,
  useQueryClient,
} from &quot;@tanstack/react-query&quot;;

const queryClient = useQueryClient();
&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;mutation 성공 후 Query를 무효화한다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;const mutation = useMutation({
  mutationFn: updatePost,

  onSuccess: (_, variables) =&amp;gt; {
    queryClient.invalidateQueries({
      queryKey: [&quot;post&quot;, variables.id],
    });
  },
});
&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;게시글 목록도 함께 변경될 가능성이 있다면 목록 Query도 무효화할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;onSuccess: (_, variables) =&amp;gt; {
  queryClient.invalidateQueries({
    queryKey: [&quot;post&quot;, variables.id],
  });

  queryClient.invalidateQueries({
    queryKey: [&quot;posts&quot;],
  });
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 TanStack Query가 해당 캐시를 오래된 데이터로 판단하고 필요한 시점에 다시 조회할 수 있다.&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;blockquote data-ke-style=&quot;style3&quot;&gt;사용자가 수정 버튼을 클릭&lt;br /&gt;&amp;darr;&lt;br /&gt;mutation.mutate()를 실행&lt;br /&gt;&amp;darr; &lt;br /&gt;서버에 PUT 요청&lt;br /&gt;&amp;darr;&lt;br /&gt;서버 데이터가 변경됨&lt;br /&gt;&amp;darr;&lt;br /&gt;onSuccess가 실행됨&lt;br /&gt;&amp;darr;&lt;br /&gt;invalidateQueries()를 실행&lt;br /&gt;&amp;darr;&lt;br /&gt;관련 Query가 갱신&lt;br /&gt;&amp;darr;&lt;br /&gt;화면에 최신 데이터가 표시됨&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름을 이해하는 것이 useMutation을 사용할 때 가장 중요하다.&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;b&gt;  Query Key를 정확하게 맞춰야 한다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;invalidateQueries를 사용할 때는 Query Key도 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회할 때 다음과 같이 작성했다면,&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;useQuery({
  queryKey: [&quot;post&quot;, id],
  queryFn: () =&amp;gt; getPost(id),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mutation에서도 동일한 구조의 Query Key를 사용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;queryClient.invalidateQueries({
  queryKey: [&quot;post&quot;, id],
});
&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;Query Key가 다르면 내가 원하는 Query가 제대로 갱신되지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 프로젝트가 커지면 Query Key를 별도로 관리하는 방식도 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;export const postKeys = {
  all: [&quot;posts&quot;] as const,

  detail: (id: number) =&amp;gt;
    [&quot;post&quot;, id] as const,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용할 때는 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;queryClient.invalidateQueries({
  queryKey: postKeys.detail(id),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Query 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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;⚠️ &lt;i&gt;API 에러는 반드시 throw 해야 한다&lt;/i&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetch를 사용할 때 특히 주의해야 하는 부분이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetch는 HTTP 응답이 400이나 500이라고 해서 자동으로 Promise를 reject하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const response = await fetch(&quot;/posts&quot;, {
  method: &quot;POST&quot;,
});

if (!response.ok) {
  throw new Error(&quot;요청 실패&quot;);
}

return response.json();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;따라서&lt;/span&gt; 에러를 throw해야 TanStack Query가 해당 요청을 실패한 mutation으로 인식하고 onError를 실행할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;onError: (error) =&amp;gt; {
  console.error(error);
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axios처럼 HTTP 상태 코드에 따라 자동으로 reject되는 라이브러리를 사용하고 있었다면 이 차이 때문에 처음에는 헷갈릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 useMutation을 단순히 POST나 PUT 요청을 보내기 위한 Hook으로 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로는 서버 데이터 변경 요청뿐만 아니라 요청 상태와 성공/실패 처리, 기존 Query 캐시와의 연결까지 담당하는 기능이다.&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&gt;서버의 데이터를 변경한 뒤 기존 Query 캐시를 어떻게 최신 상태와 동기화할 것인가&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 CRUD에서는 mutationFn, onSuccess, invalidateQueries 정도만 제대로 이해해도 충분히 활용할 수 있지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 조금 더 복잡한 UI를 구현하게 된다면 setQueryData를 이용한 직접적인 캐시 업데이트나 Optimistic Update까지 확장해서 사용할 수 있을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrEQX%2FbtrK4CAyKrB%2FutM4awBF5CvfGVQ7UvveyK%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;175&quot; height=&quot;86&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>FE/React</category>
      <category>FE</category>
      <category>TanstackQuery</category>
      <category>usemutation</category>
      <category>프론트엔드</category>
      <author>vellahw</author>
      <guid isPermaLink="true">https://tavi.tistory.com/193</guid>
      <comments>https://tavi.tistory.com/193#entry193comment</comments>
      <pubDate>Sun, 24 May 2026 18:26:21 +0900</pubDate>
    </item>
    <item>
      <title>  React 로그인 인증 흐름 정리 (ProtectedRoute / PublicRoute 설계)</title>
      <link>https://tavi.tistory.com/191</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 프로젝트를 하다 보면 반드시 마주치는 문제가 있는데&lt;br /&gt;바로 &lt;b&gt;로그인 상태에 따른 라우팅 처리&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나도 이번에 로그인 플로우를 구현하면서 로그인 인증이 성공 상태(200 코드)인데도 메인 페이지로 이동이 안 되는 상황을 겪었고 그 과정에서 로그인/비로그인 라우팅 구조를 한 번 제대로 정리하게 됐다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 이런 구조였다.&lt;/p&gt;
&lt;pre id=&quot;code_1789196608800&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
  * fileName : useCheckAuth
  * description : 로그인 상태 확인 후 필요 시 토큰 갱신 및 페이지 이동을 결정하는 커스텀 훅
  */
export const useCheckAuth = () =&amp;gt; {
  const { data, isLoading, isError, refetch } = useAuthQuery(); // 로그인 상태 확인
  
  ...
  
  if (data) {
    const code = data.code; // 응답 코드
  
    if (code === 200) return { isLoading: false, navigateTo: &quot;/home&quot;, showChildren: true };
  
    ...
  } 
  
  return { isLoading: false, navigateTo: &quot;/login&quot;, showChildren: false }; 
  
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;/**
  * fileName : ProtectRoute
  * description : 인증이 필요한 라우트를 보호하는 컴포넌트. 로그인 인증된 사용자만 페이지 접근 허용
  */
export default function ProtectedRoute({ children }: { children: React.ReactNode; }) { 
  const { isLoading, navigateTo, showChildren } = useCheckAuth();
    
  ...
  
  if (navigateTo) return &amp;lt;Navigate to={navigateTo} replace /&amp;gt;;
  
  if (showChildren) return children;
}&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;br /&gt;하지만 실제로는 &lt;b&gt;구조적으로 충돌이 발생한다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;뭐가 문제였나?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은&amp;nbsp;  &lt;b&gt;navigateTo와 showChildren을 동시에 쓰고 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ProtectedRoute에서 /home으로 보내는 로직 자체가 구조적으로 어색한 상태&lt;/b&gt;인거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 인증 성공 시 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;이동도 하려고 하고 &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;렌더링도 하려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;커스텀 훅에서 &lt;/span&gt;code === 200이면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;509&quot; data-start=&quot;463&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;486&quot; data-start=&quot;463&quot; data-section-id=&quot;1rhkv3i&quot;&gt;navigateTo: &quot;/home&quot;&lt;/li&gt;
&lt;li data-end=&quot;509&quot; data-start=&quot;487&quot; data-section-id=&quot;1p7lhsk&quot;&gt;showChildren: true&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;525&quot; data-start=&quot;511&quot; data-ke-size=&quot;size16&quot;&gt;둘 다 true인 상태이다.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;navigateTo 먼저 실행됨&lt;/li&gt;
&lt;li&gt;showChildren은 아예 렌더링 안 됨&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게다가 /home에서도 ProtectedRoute를 쓰고 있으면 &lt;b&gt;자기 자신으로 계속 redirect를 시도한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;이동이 안 되는 것처럼 보이거나 &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;무한 루프가 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  해결 방향&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;라우트 가드는 이동시키는 역할이 아니다&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-end=&quot;1137&quot; data-start=&quot;1116&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1137&quot; data-start=&quot;1116&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사실 이 로직은 설계가 잘못된거다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1172&quot; data-start=&quot;1139&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;인증에 성공했는데 왜 /home으로 이동시키나?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1193&quot; data-start=&quot;1174&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ProtectedRoute는 원래 리다이렉트 담당이 아니라 그 값만 보고&amp;nbsp;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt;접근 허용이나 차단만&lt;/b&gt; 해야 하는 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;역할 분리&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 84px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;인증 상태 판단&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;useCheckAuth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;접근 허용/차단&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;ProtectedRoute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;로그인 유저 접근 차단&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;PublicRoute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;페이지 이동&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;로그인 성공 시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ProtectedRoute는 UI를 담당하고&lt;br /&gt;useCheckAuth는 인증 상태를 판단한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;올바른 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;114&quot; data-start=&quot;100&quot; data-ke-size=&quot;size16&quot;&gt;라우팅은 2개로 나눈다.&lt;/p&gt;
&lt;p data-end=&quot;114&quot; data-start=&quot;100&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;143&quot; data-start=&quot;116&quot; data-section-id=&quot;1ixnqwm&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;인증 필요 라우트 (Private)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;181&quot; data-start=&quot;144&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;157&quot; data-start=&quot;144&quot; data-section-id=&quot;104pshg&quot;&gt;로그인해야 접근 가능&lt;/li&gt;
&lt;li data-end=&quot;181&quot; data-start=&quot;158&quot; data-section-id=&quot;m87cf2&quot;&gt;예: /home, /mypage&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;210&quot; data-start=&quot;183&quot; data-section-id=&quot;10wjz0g&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;비인증 전용 라우트 (Public)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;254&quot; data-start=&quot;211&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;229&quot; data-start=&quot;211&quot; data-section-id=&quot;1wr7pj0&quot;&gt;로그인 상태면 접근하면 안 됨&lt;/li&gt;
&lt;li data-end=&quot;254&quot; data-start=&quot;230&quot; data-section-id=&quot;1f39ig3&quot;&gt;예: /login, /signup&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 인증 상태 커스텀 훅&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;export const useCheckAuth = () =&amp;gt; {
  const { data, isLoading, isError } = useAuthQuery();

  if (isLoading) {
    return { isLoading: true, isAuthenticated: false };
  }

  if (isError) {
    return { isLoading: false, isAuthenticated: false };
  }

  // 이동시키지 말고 그냥 통과
  if (data?.code === 200) {
    return { isLoading: false, isAuthenticated: true };
  }

  return { isLoading: false, isAuthenticated: false };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은&lt;b&gt; isAuthenticated 하나로 단순화&lt;/b&gt;하여 &lt;b&gt;단일 책임 + 단순 로직으로 버그를 감소&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;b&gt;2. ProtectedRoute (로그인 필요 페이지 보호)&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export default function ProtectedRoute({ children }) {
  const { isLoading, isAuthenticated } = useCheckAuth();

  if (isLoading) return &amp;lt;Loading /&amp;gt;;

  if (!isAuthenticated) {
    return &amp;lt;Navigate to=&quot;/login&quot; replace /&amp;gt;;
  }

  return children;
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.&lt;/b&gt; &lt;b&gt;PublicRoute (로그인한 사람 막기)&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export default function PublicRoute({ children }) {
  const { isLoading, isAuthenticated } = useCheckAuth();

  if (isLoading) return &amp;lt;Loading /&amp;gt;;

  if (isAuthenticated) {
    return &amp;lt;Navigate to=&quot;/home&quot; replace /&amp;gt;;
  }

  return children;
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. Router 구성&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;&amp;lt;Routes&amp;gt;
  {/* 로그인 안 된 사람만 */}
  &amp;lt;Route
    path=&quot;/login&quot;
    element={
      &amp;lt;PublicRoute&amp;gt;
        &amp;lt;Login /&amp;gt;
      &amp;lt;/PublicRoute&amp;gt;
    }
  /&amp;gt;

  {/* 로그인 된 사람만 */}
  &amp;lt;Route
    path=&quot;/home&quot;
    element={
      &amp;lt;ProtectedRoute&amp;gt;
        &amp;lt;Home /&amp;gt;
      &amp;lt;/ProtectedRoute&amp;gt;
    }
  /&amp;gt;
&amp;lt;/Routes&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 로그인 성공 시 이동 처리&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const handleLogin = async () =&amp;gt; {
  const res = await loginApi();

  if (res.success) {
    navigate(&quot;/home&quot;);
  }
};
&lt;/code&gt;&lt;/pre&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;&amp;nbsp;&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;b&gt;내가 했던 실수&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;❌ ProtectedRoute에서 /home으로 보내기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 절대 하면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;❌ navigateTo + showChildren 같이 쓰기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 상태 충돌이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;❌ 인증 성공 시 redirect를 가드에서 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우는 책임 분리가 깨진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&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;인증 훅 &amp;rarr; 상태만 반환한다&lt;/li&gt;
&lt;li&gt;라우트 가드 &amp;rarr; 막기만 한다&lt;/li&gt;
&lt;li&gt;페이지 이동 &amp;rarr; 이벤트에서 처리한다&lt;/li&gt;
&lt;/ul&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;p&gt;&lt;figure class=&quot;imageblock alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrEQX%2FbtrK4CAyKrB%2FutM4awBF5CvfGVQ7UvveyK%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;175&quot; height=&quot;86&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>FE/React</category>
      <category>라우트</category>
      <category>리액트</category>
      <author>vellahw</author>
      <guid isPermaLink="true">https://tavi.tistory.com/191</guid>
      <comments>https://tavi.tistory.com/191#entry191comment</comments>
      <pubDate>Tue, 31 Mar 2026 11:08:49 +0900</pubDate>
    </item>
    <item>
      <title>  Oracle XE 홈서버 만들기(외부 접속)부터 SQL Developer ORA-17410 에러 해결까지</title>
      <link>https://tavi.tistory.com/190</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 플젝을 오라클 홈서버를 구축해 공용DB로 이용하고자 도전해봤다.&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;a href=&quot;https://developlsb2dwb.tistory.com/5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 글&lt;/a&gt;을 바탕으로 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 게이트웨이 주소가 접속 안 되는걸 시작으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 구축에 성공했는데 ORA-17410 에러가 뜨면서 데이터베이스 접촉 자체가 안 됐었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법을 순차적으로 정리해본다...&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. &lt;/b&gt;&lt;b&gt;포트포워딩&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Lz9vx/dJMcaiWU4lp/EQK8SxqjSrRKCkkDsuorF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Lz9vx/dJMcaiWU4lp/EQK8SxqjSrRKCkkDsuorF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Lz9vx/dJMcaiWU4lp/EQK8SxqjSrRKCkkDsuorF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLz9vx%2FdJMcaiWU4lp%2FEQK8SxqjSrRKCkkDsuorF1%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;588&quot; height=&quot;257&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;257&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;공유기 관리자 페이지에 접속해 &lt;/span&gt;포트포워딩을 해줘야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트포워딩을 해주기 위해선&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cmd에 ipconfg 를 입력해 내 IPv4 주소와 기본 게이트웨이 주소가 필요하다.&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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;근데 나는 IPv4 주소가 보통 사람들과 달랐다!!!&lt;br /&gt;게이트웨이 주소로 접속도 안 됨&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리집은 KT 공유기를 사용하고 있는데 인터넷에 검색해보면 대부분 172로 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 나는 200?인가 다른 번호로 시작을 하고 기본 게이트웨이 주소로 접속도 안 됐었다&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;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;집에 모뎀 있으신가요? 거기에 꽂혀있는 컴퓨터 랜선을 공유기로 옮겨보세요&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옮기고 다시 ipconfig 를 입력해보면 172로 시작하는 주소가 나온다.&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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRtcvM/dJMcahwZWns/FCeW8FmZtxsLAd7QliJKq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRtcvM/dJMcahwZWns/FCeW8FmZtxsLAd7QliJKq0/img.png&quot; data-alt=&quot;관리자 페이지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRtcvM/dJMcahwZWns/FCeW8FmZtxsLAd7QliJKq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRtcvM%2FdJMcahwZWns%2FFCeW8FmZtxsLAd7QliJKq0%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;1045&quot; height=&quot;559&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;관리자 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이디, 비밀번호 모르면 로그인 실패시 계정 재설정 alret이 뜨니 거기서 수정해주면 된다.&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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;445&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfd0uZ/dJMcaivQ8CN/k4TrInYhgopFq1s9FKmtgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfd0uZ/dJMcaivQ8CN/k4TrInYhgopFq1s9FKmtgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfd0uZ/dJMcaivQ8CN/k4TrInYhgopFq1s9FKmtgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfd0uZ%2FdJMcaivQ8CN%2Fk4TrInYhgopFq1s9FKmtgK%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;1024&quot; height=&quot;445&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;445&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kt 기준으로 포트포워딩은 장치설정-트래픽 관리 메뉴에서 설정해주면 된다.&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;외부/내부 포트번호 = 1521&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내부 IP 주소 = IPv4 주소&lt;/b&gt;&lt;/li&gt;
&lt;/ul&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 방화벽 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;329&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MdLWR/dJMcahqd1pN/ujmnmII31Dmzc6WUtNu3D1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MdLWR/dJMcahqd1pN/ujmnmII31Dmzc6WUtNu3D1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MdLWR/dJMcahqd1pN/ujmnmII31Dmzc6WUtNu3D1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMdLWR%2FdJMcahqd1pN%2FujmnmII31Dmzc6WUtNu3D1%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;516&quot; height=&quot;329&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;329&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제어판\시스템 및 보안\Windows Defender 방화벽 이동 후 고급설정&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;410&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rhNgf/dJMcahDJbrN/d54p5V3PGKG4yYMbIXeni0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rhNgf/dJMcahDJbrN/d54p5V3PGKG4yYMbIXeni0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rhNgf/dJMcahDJbrN/d54p5V3PGKG4yYMbIXeni0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrhNgf%2FdJMcahDJbrN%2Fd54p5V3PGKG4yYMbIXeni0%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;410&quot; height=&quot;196&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;196&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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;951&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu6BF6/dJMcacoUttY/HnyVTvOswzAts9lYgIBukK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu6BF6/dJMcacoUttY/HnyVTvOswzAts9lYgIBukK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu6BF6/dJMcacoUttY/HnyVTvOswzAts9lYgIBukK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu6BF6%2FdJMcacoUttY%2FHnyVTvOswzAts9lYgIBukK%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;951&quot; height=&quot;529&quot; data-origin-width=&quot;951&quot; data-origin-height=&quot;529&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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rF1sx/dJMcaaYUQVG/HTRm3LZMGWGdjJyH5tJkr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rF1sx/dJMcaaYUQVG/HTRm3LZMGWGdjJyH5tJkr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rF1sx/dJMcaaYUQVG/HTRm3LZMGWGdjJyH5tJkr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrF1sx%2FdJMcaaYUQVG%2FHTRm3LZMGWGdjJyH5tJkr0%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;949&quot; height=&quot;530&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkRKPV/dJMcabXP9zZ/W8d1GKE4Id88ohjkAW0Vi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkRKPV/dJMcabXP9zZ/W8d1GKE4Id88ohjkAW0Vi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkRKPV/dJMcabXP9zZ/W8d1GKE4Id88ohjkAW0Vi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkRKPV%2FdJMcabXP9zZ%2FW8d1GKE4Id88ohjkAW0Vi1%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;950&quot; height=&quot;532&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;533&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o9c33/dJMcafsn4Xw/1GqvoiR3PiSRzKUzTklaC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o9c33/dJMcafsn4Xw/1GqvoiR3PiSRzKUzTklaC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o9c33/dJMcafsn4Xw/1GqvoiR3PiSRzKUzTklaC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo9c33%2FdJMcafsn4Xw%2F1GqvoiR3PiSRzKUzTklaC1%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;953&quot; height=&quot;533&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;533&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blwRlT/dJMcajasiiJ/FBsleJ3azmSdRWv343DQZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blwRlT/dJMcajasiiJ/FBsleJ3azmSdRWv343DQZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blwRlT/dJMcajasiiJ/FBsleJ3azmSdRWv343DQZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblwRlT%2FdJMcajasiiJ%2FFBsleJ3azmSdRWv343DQZk%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;956&quot; height=&quot;532&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;532&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;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;141&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bnnv5/dJMcabwKG9H/F3PnTaJ2HsgfaSFduTLK50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bnnv5/dJMcabwKG9H/F3PnTaJ2HsgfaSFduTLK50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bnnv5/dJMcabwKG9H/F3PnTaJ2HsgfaSFduTLK50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBnnv5%2FdJMcabwKG9H%2FF3PnTaJ2HsgfaSFduTLK50%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;382&quot; height=&quot;141&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;141&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. listener.ora 파일 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 글에서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;listener.ora와 tnsnames.ora에서 HOST 주소를 수정하라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;근데 쳇지피티는 &lt;span&gt;SQL Developer에서 직접 입력하면 되기 때문에 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;tnsnames.ora&lt;span&gt;는&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;굳이 수정 안 해도 된다고 한당 ㅇㅅㅇ&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;결과적으로 나는 listener.ora만 수정해서 성공하긴 했다.&lt;/span&gt;&lt;/span&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;215&quot; data-origin-height=&quot;201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bT04xK/dJMcajhdE2C/CK8APvdUq6bxvlgc4Ti3O0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bT04xK/dJMcajhdE2C/CK8APvdUq6bxvlgc4Ti3O0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bT04xK/dJMcajhdE2C/CK8APvdUq6bxvlgc4Ti3O0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbT04xK%2FdJMcajhdE2C%2FCK8APvdUq6bxvlgc4Ti3O0%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;215&quot; height=&quot;201&quot; data-origin-width=&quot;215&quot; data-origin-height=&quot;201&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;HOST 주소에는 네이버에서 내 ip를 검색해 조회된 주소를 넣어준다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://search.naver.com/search.naver?where=nexearch&amp;amp;sm=top_hty&amp;amp;fbm=0&amp;amp;ie=utf8&amp;amp;query=%EB%82%B4+ip&amp;amp;ackey=sdckq9po&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;내IP조회&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&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;597&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmliaL/dJMcafzbqTV/2KAO1CDkvIHpOXaTr1CyG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmliaL/dJMcafzbqTV/2KAO1CDkvIHpOXaTr1CyG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmliaL/dJMcafzbqTV/2KAO1CDkvIHpOXaTr1CyG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmliaL%2FdJMcafzbqTV%2F2KAO1CDkvIHpOXaTr1CyG0%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;597&quot; height=&quot;181&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;181&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;이 HOST 주소에 IPv4 주소를 입력하라던가, 컴퓨터 장치 이름을 입력하라고 하는데 나는 다 안 되고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 검색결과로 나온 IP 주소로는 접속이 됐다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; .ora 파일 찾을 때 개큰 꿀팁&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;b&gt;everything&lt;/b&gt; 이란걸 설치해서 파일을 찾았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;443&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0deYs/dJMcajnXqBH/skDtvBRqT7gIVBIdDSqqXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0deYs/dJMcajnXqBH/skDtvBRqT7gIVBIdDSqqXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0deYs/dJMcajnXqBH/skDtvBRqT7gIVBIdDSqqXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0deYs%2FdJMcajnXqBH%2FskDtvBRqT7gIVBIdDSqqXk%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;861&quot; height=&quot;443&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;443&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://www.voidtools.com/ko-kr/downloads/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.voidtools.com/ko-kr/downloads/&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;64비트 설치파일 다운 받아 설치했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;149&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rCFO0/dJMcahqd1y6/wUeO0eIq160QLl5S5O7CA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rCFO0/dJMcahqd1y6/wUeO0eIq160QLl5S5O7CA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rCFO0/dJMcahqd1y6/wUeO0eIq160QLl5S5O7CA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrCFO0%2FdJMcahqd1y6%2FwUeO0eIq160QLl5S5O7CA0%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;567&quot; height=&quot;149&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;149&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;윈도우 파일탐색기 검색을 이용하면 하루 온종일 걸리는데 1초만에 파일을 찾아준다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 접속 해보기&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;543&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOfTJr/dJMb99Z25w4/zCpXeluHncuIFOFdzGq5PK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOfTJr/dJMb99Z25w4/zCpXeluHncuIFOFdzGq5PK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOfTJr/dJMb99Z25w4/zCpXeluHncuIFOFdzGq5PK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOfTJr%2FdJMb99Z25w4%2FzCpXeluHncuIFOFdzGq5PK%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;543&quot; height=&quot;210&quot; data-origin-width=&quot;543&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 환경의 DB는 호스트 이름 localhost로 만들어주고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 접속할 때의 호스트 이름에는 listener.ora의 HOST에 입력한 주소와 같은 내IP주소를 입력해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 접속 성공!!  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정은 홈서버 띄우기 최종 성공 기록이고&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;이제는 ORA-17410를 해결한 방법!&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 원격 DB 접속이 되는걸 확인을 했는데&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; &lt;/span&gt;&lt;/span&gt;SQL Developer에서 데이터베이스에 접속이 안 되면서 ORA-17410 &lt;span&gt;에러가 떴다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-19 235520.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;175&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZOF9X/dJMcadH4A65/vg9f0twZZL0E5VQ5kCzfkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZOF9X/dJMcadH4A65/vg9f0twZZL0E5VQ5kCzfkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZOF9X/dJMcadH4A65/vg9f0twZZL0E5VQ5kCzfkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZOF9X%2FdJMcadH4A65%2Fvg9f0twZZL0E5VQ5kCzfkk%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;400&quot; height=&quot;175&quot; data-filename=&quot;스크린샷 2026-03-19 235520.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;175&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;마지막으로 백엔드 로직을 만지고 있었어서 처음에 쿼리 문제인가 생각했는데&lt;/span&gt;&lt;br /&gt;&lt;span&gt;결론은 &lt;/span&gt;&lt;span&gt;DB 연결 자체 문제&lt;/span&gt;&lt;span&gt;였다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;나는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span&gt;localhost / 1521 / XE&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;로 접속 중&lt;/b&gt;에 이 문제가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;서비스로 확인했을 때 OracleServiceXE,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;OracleXETNSListener도 정상 실행되고 있었다.&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&gt;이 에러는 간단하게 말해서&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;DB랑 연결이 끊겼다&lt;/span&gt;&lt;span&gt;는 뜻인데&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;주요 원인은 &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;DB 인스턴스 꺼짐, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Listener 문제, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;네트워크 문제, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;잘못된 접속 정보 등이 있다고..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;1. Listener 상태 확인&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;먼저 Listener 상태부터 확인해준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;lsnrctl status&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;터미널을 관리자 권한으로 실행&lt;/b&gt;하고 위 명령어를 입력하면&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Service &quot;XE&quot;
Instance &quot;XE&quot;, status UNKNOWN&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 결과가 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;UNKNOWN !!!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 상태는 &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Listener는 살아있는데 &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;DB(Oracle XE 인스턴스)가 떠있긴 한데 정상이 아닌 상태&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, &lt;/span&gt;&lt;span&gt;DB가 제대로 OPEN 상태가 아니거나 등록이 안 된 상태이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;2. DB 재시작으로 1차 해결&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;SQL*Plus로 접속해서 DB를 재시작한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sqlplus / as sysdba
startup;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 입력하니&lt;/p&gt;
&lt;pre id=&quot;code_1774025210291&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ORA-01081: cannot start already-running ORACLE - shut it down first SQL&amp;gt; Disconnected from Oracle Database 11g Express Edition Release 11.2.0.2.0 - 64bit Production&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;이 상황은 DB는 켜져 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;정상적으로 OPEN 상태가 아님&amp;rdquo; (= mount까지만 됐거나, 뭔가 꼬인 상태) 상태인 것!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 꼬였으니 우리는 재기동을 해주어야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774025285969&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sqlplus / as sysdba&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1774025291225&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;shutdown immediate;
startup;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데도 결과는 UNKNOWN...&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 진짜 원인: local_listener 비어있음 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 &lt;span&gt;&lt;b&gt;동적 등록(dynamic registration)이 안 된 상태&lt;/b&gt;였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;261&quot; data-start=&quot;259&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;DB는 켜져 있고 &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Listener도 켜져 있지만 &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt;서로 연결(등록)이 안 된 것&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;910&quot; data-start=&quot;850&quot; data-ke-size=&quot;size16&quot;&gt;Oracle은 Listener에 자동 등록할 때 local_listener 값을 기준으로 연결하는데&lt;/p&gt;
&lt;p data-end=&quot;928&quot; data-start=&quot;912&quot; data-ke-size=&quot;size16&quot;&gt;이 값이 없거나 틀리면 DB는 살아있는데 Listener는 &quot;누군지 몰라서&amp;rdquo; UNKNOWN이 뜬다고 한다..&lt;/p&gt;
&lt;pre id=&quot;code_1774025545004&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;show parameter local_listener;&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;400&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djC6xb/dJMcahRg6cx/j8L3NQvgJS7pmwGjul1g80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djC6xb/dJMcahRg6cx/j8L3NQvgJS7pmwGjul1g80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djC6xb/dJMcahRg6cx/j8L3NQvgJS7pmwGjul1g80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjC6xb%2FdJMcahRg6cx%2Fj8L3NQvgJS7pmwGjul1g80%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;400&quot; height=&quot;177&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;177&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;확실히 value가 비어있었다.&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;b&gt;&lt;span&gt;4. 해결: local_listener 설정&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;alter system set local_listener='(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))' scope=both;
alter system register;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;위 명령어를 차례로 입력해줬고&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;lsnrctl status&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 상태를 확인해줬다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;Instance &quot;XE&quot;, status READY&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;  드디어 UNKNOWN의 늪에서 벗어나게 됨..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrEQX%2FbtrK4CAyKrB%2FutM4awBF5CvfGVQ7UvveyK%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;175&quot; height=&quot;86&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>BE/Database</category>
      <category>db</category>
      <category>oracle</category>
      <author>vellahw</author>
      <guid isPermaLink="true">https://tavi.tistory.com/190</guid>
      <comments>https://tavi.tistory.com/190#entry190comment</comments>
      <pubDate>Sat, 21 Mar 2026 02:02:44 +0900</pubDate>
    </item>
    <item>
      <title>  Zustand vs TanStack Query, 같이 쓰자!</title>
      <link>https://tavi.tistory.com/189</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt; &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 프론트엔드 프로젝트에서 &lt;b&gt;Zustand + TanStack Query&lt;/b&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;b&gt;1. Zustand는 무엇인가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zustand는 &lt;b&gt;전역 상태 관리 라이브러리&lt;/b&gt;다.&lt;br /&gt;Redux보다 훨씬 간단하고, 보일러플레이트도 거의 없다.&lt;/p&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;span style=&quot;letter-spacing: 0px;&quot;&gt;로그인 모달 열림 여부, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;다크모드 상태, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;사이드바 열림/닫힘, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;사용자가 선택한 탭 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 것들을 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;사용예&lt;/h4&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { create } from 'zustand'

export const useStore = create((set) =&amp;gt; ({
  isOpen: false,
  toggle: () =&amp;gt; set((state) =&amp;gt; ({ isOpen: !state.isOpen })),
}))&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. TanStack Query는 무엇인가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TanStack Query &lt;b&gt;서버 데이터 관리 엔진&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;API 호출을 중심으로 돌아가는 라이브러리&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 fetch만 하는 게 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;캐싱, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;자동 재요청, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;로딩 상태 관리, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;에러 처리, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;데이터 동기화 등&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런걸 전부 알아서 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 로그인 상태를 확인하는 API가 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useQuery } from '@tanstack/react-query'
import axios from 'axios'

export const useAuthQuery = () =&amp;gt; {
  return useQuery({
    queryKey: ['auth'],
    queryFn: async () =&amp;gt; {
      const res = await axios.get('/api/auth/me', {
        withCredentials: true,
      })
      return res.data
    },
    retry: false,
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 한 줄로 &amp;ldquo;로그인 상태 + 캐싱 + 로딩 + 에러 처리&amp;rdquo;가 다 해결된다.&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;b&gt;3. 둘의 차이 한 번에 정리&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 122px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; text-align: right;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;&lt;b&gt; Zustand &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;&lt;b&gt; TanStack Query &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: right;&quot;&gt;역할&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;클라이언트 상태&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;서버 상태&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: right;&quot;&gt;데이터 위치&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;브라우저&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;서버&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: right;&quot;&gt;주요 기능&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;상태 저장&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;API 관리&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: right;&quot;&gt;로딩/에러&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;직접 구현&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;자동 제공&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은&amp;nbsp;&lt;b&gt;&amp;ldquo;데이터가 어디에서 왔느냐&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  서버에서 왔다 &amp;rarr; TanStack Query&lt;br /&gt; &amp;nbsp;프론트에서 만든 상태다 &amp;rarr; Zustand&lt;/blockquote&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 로그인 구현에서의 실제 차이&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오 로그인 기반으로 구현하면서 이 차이가 더 명확해졌다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 로그인 성공 &amp;rarr; accessToken, refreshToken 쿠키 저장&lt;br /&gt;2. 프론트는 토큰을 읽을 수 없음 (httpOnly 쿠키)&lt;br /&gt;3. API로 로그인 여부 확인&lt;/blockquote&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;서버에 물어봐야 한다&lt;/b&gt;는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이건 Zustand가 아니라 TanStack Query 영역이다.&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;b&gt;  올바른 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 Zustand에 유저 상태를 저장하려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 하면 문제는 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;페이지 이동마다 API를 호출 해야하고, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;동기화가 깨지며 &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;상태가 꼬인다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;u&gt;TanStack Query에는 서버 상태를 저장하고, Zustand에는 UI 상태를 저장&lt;/u&gt;하는 구조로 바꾸는 게 맞다!&lt;/p&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;b&gt;1. TanStack Query &amp;rarr; 서버 상태&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const { data, isLoading, isError } = useAuthQuery()&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  로그인 여부 판단&lt;br /&gt;  유저 정보 가져오기&lt;br /&gt;  자동 캐싱&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Zustand &amp;rarr; UI 상태&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;isLoginModalOpen: false
toggleLoginModal: () =&amp;gt; {}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  모달 열림 여부&lt;br /&gt;  UI 상태 관리&lt;/blockquote&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;b&gt;5. 실제 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적인 로그인 흐름:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;브라우저 접속 (/)&lt;br /&gt;&amp;rarr; 로그인 검증 API 요청&lt;br /&gt;&amp;rarr; 서버가 로그인 여부 판단&lt;br /&gt;&amp;rarr; TanStack Query가 결과 관리&lt;br /&gt;&amp;rarr; 라우팅 분기 (/home 또는 /login)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Zustand는 등장하지 않는다.&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 둘 다 상태 관리처럼 보여서 헷갈리지만&lt;br /&gt;실제로 써보면 완전히 다른 도구라는 걸 알게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;서버 데이터는 TanStack Query로 관리&lt;br /&gt;UI 상태는 Zustand로 관리&lt;/blockquote&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;br /&gt;특히 로그인 같은 인증 로직이 안정적으로 돌아간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrEQX%2FbtrK4CAyKrB%2FutM4awBF5CvfGVQ7UvveyK%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;175&quot; height=&quot;86&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&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>FE/React</category>
      <category>react</category>
      <category>tanstack query</category>
      <category>zustand</category>
      <category>상태관리</category>
      <author>vellahw</author>
      <guid isPermaLink="true">https://tavi.tistory.com/189</guid>
      <comments>https://tavi.tistory.com/189#entry189comment</comments>
      <pubDate>Sun, 15 Mar 2026 18:16:05 +0900</pubDate>
    </item>
    <item>
      <title>  기존 Vite + React 프로젝트에 TypeScript 추가하기 (마이그레이션 정리)</title>
      <link>https://tavi.tistory.com/188</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt; &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 프로젝트를 새로 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 세팅은 되었던 프로젝트라, &lt;b&gt;Vite + React(JavaScript)&lt;/b&gt;&amp;nbsp;&lt;b&gt;프로젝트에 TypeScript를 추가&lt;/b&gt;하면서 겪었던 과정과 필요한 설정을 정리해보았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. TypeScript 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 프로젝트에 TypeScript와 React 타입을 설치한다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;npm install -D typescript @types/react @types/react-dom
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 완료되면 TypeScript 설정 파일을 생성한다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;npx tsc --init
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 프로젝트 루트에 tsconfig.json 파일이 생성된다.&lt;/p&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. tsconfig.json 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 생성된 tsconfig.json을 Vite 환경에 맞게 일부 수정했다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;ESNext&quot;,
    &quot;lib&quot;: [&quot;DOM&quot;, &quot;DOM.Iterable&quot;, &quot;ESNext&quot;],

    &quot;module&quot;: &quot;ESNext&quot;,
    &quot;moduleResolution&quot;: &quot;Bundler&quot;,

    &quot;jsx&quot;: &quot;react-jsx&quot;,

    &quot;strict&quot;: true,
    &quot;skipLibCheck&quot;: true,

    &quot;resolveJsonModule&quot;: true,
    &quot;isolatedModules&quot;: true,
    &quot;noEmit&quot;: true,

    &quot;allowJs&quot;: true
  },
  &quot;include&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;✅주요 옵션&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- strict&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;TypeScript의 타입 검사를 엄격하게 한다.&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;&lt;b&gt;- allowJs&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;기존 JavaScript 파일을 그대로 사용할 수 있게 해준다.&lt;br /&gt;&amp;nbsp;기존 프로젝트에 TypeScript를 도입할 때 유용함. 파일이 많을 때는 중요!&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;&lt;b&gt;- moduleResolution: Bundler&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Vite 환경에 맞는 모듈 해석 방식&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. vite-env.d.ts 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite 프로젝트에서는 환경 변수 타입을 위해&lt;br /&gt;&lt;b&gt;vite-env.d.ts&lt;/b&gt; 파일이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;src 폴더 안에 아래 파일을 생성하고 내용은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;/// &amp;lt;reference types=&quot;vite/client&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일이 있어야 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;import.meta.env.VITE_API_URL&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;4. vite.config&lt;span style=&quot;color: #f89009;&quot;&gt;.js&lt;/span&gt; &amp;rarr; vite.config&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;.ts&lt;/b&gt;&lt;/span&gt; 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite 설정 파일도 TypeScript로 변경할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;vite.config.js &amp;rarr; vite.config.ts
&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;h2 data-ke-size=&quot;size26&quot;&gt;5. main 파일 먼저 TypeScript로 변환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;기존 프로젝트를 TypeScript로 마이그레이션할 때는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;entry 파일부터 변환하는 것이 좋다고 한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;import React from &quot;react&quot;;
import ReactDOM from &quot;react-dom/client&quot;;
import App from &quot;./App&quot;;

ReactDOM.createRoot(document.getElementById(&quot;root&quot;)!).render(
  &amp;lt;React.StrictMode&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/React.StrictMode&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;여기서 &lt;b&gt;!&lt;/b&gt;는 &lt;b&gt;Non-null assertion&lt;/b&gt;으로&lt;br /&gt;root 요소가 null이 아니라는 것을 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;main 이후로는 App 컴포넌트, 다른 컴포넌트들을 하나씩 변환해주면 된다.&lt;/p&gt;
&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  타입 정의&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Props 타입 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트를 TypeScript로 변환할 때는 Props 타입을 정의해준다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;type ButtonProps = {
  children: React.ReactNode;
  onClick?: () =&amp;gt; void;
};

export function Button({ children, onClick }: ButtonProps) {
  return &amp;lt;button onClick={onClick}&amp;gt;{children}&amp;lt;/button&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 컴포넌트를 사용할 때 타입 체크가 가능해진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. API 타입 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 데이터를 사용할 때도 타입을 정의해준다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;interface User {
  id: number;
  name: string;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;axios 요청에서도 타입을 지정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;axios.get&amp;lt;User[]&amp;gt;(&quot;/api/users&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;pre class=&quot;elm&quot;&gt;&lt;code&gt;src
 ├ components
 ├ hooks
 ├ api
 ├ types
 ├ utils
 └ pages
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;src/types/user.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export interface User {
  id: number;
  name: string;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 &lt;b&gt;Vite + React 프로젝트에 TypeScript를 적용할 때 필요한 과정&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;TypeScript 설치&lt;/li&gt;
&lt;li&gt;tsconfig.json 생성 및 설정&lt;/li&gt;
&lt;li&gt;vite-env.d.ts 생성&lt;/li&gt;
&lt;li&gt;vite.config.ts 변환&lt;/li&gt;
&lt;li&gt;main.tsx부터 변환&lt;/li&gt;
&lt;li&gt;컴포넌트를 하나씩 TypeScript로 변환&lt;/li&gt;
&lt;li&gt;Props와 API 타입 정의&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 번거롭게 느껴질 수 있지만&lt;br /&gt;프로젝트 규모가 커질수록 TypeScript의 장점이 크게 느껴진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로는 새 프로젝트를 만들 때&lt;br /&gt;처음부터 &lt;b&gt;React + TypeScript&lt;/b&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 alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrEQX%2FbtrK4CAyKrB%2FutM4awBF5CvfGVQ7UvveyK%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;175&quot; height=&quot;86&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&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>FE/React</category>
      <category>react</category>
      <category>TypeScript</category>
      <category>ViTE</category>
      <category>리액트</category>
      <category>타입스크립트</category>
      <author>vellahw</author>
      <guid isPermaLink="true">https://tavi.tistory.com/188</guid>
      <comments>https://tavi.tistory.com/188#entry188comment</comments>
      <pubDate>Thu, 12 Mar 2026 16:58:25 +0900</pubDate>
    </item>
    <item>
      <title>  Git 커밋 이력 변경하기 (작성자, 날짜 수정하기)</title>
      <link>https://tavi.tistory.com/187</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Git을 사용하다 보면 &lt;/span&gt;&lt;span&gt;커밋 메시지를 잘못 작성했거나&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;민감한 정보가 커밋에 포함된 경우&lt;/span&gt;&lt;span&gt;처럼 커밋 이력을 수정해야 하는 상황이 생긴다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;나는 내 개인 레포지토리에 회사 계정으로 커밋을 해버렸다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Contributors에도 회사 계정이!..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;회사PC를 사용하면서 유저를 변경하지 않고 신나게 커밋해버린 실수..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 글에서는 &lt;b&gt;여러개&lt;/b&gt;의 커밋 이력을 변경하는 방법을 정리했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span&gt;⚠️ 주의: 나는 나 혼자 쓰는 개인 레포였지만, &lt;/span&gt;&lt;span&gt;공유 브랜치&lt;/span&gt;&lt;span&gt;에서 작업할 경우 팀원들에게 큰 영향을 줄 수 있으므로 반드시 주의할 것!&lt;/span&gt;&lt;/b&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;369&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckM383/dJMcahXi1BP/ibx6pQmK5gw2q1JLaDF3Mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckM383/dJMcahXi1BP/ibx6pQmK5gw2q1JLaDF3Mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckM383/dJMcahXi1BP/ibx6pQmK5gw2q1JLaDF3Mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckM383%2FdJMcahXi1BP%2Fibx6pQmK5gw2q1JLaDF3Mk%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;314&quot; height=&quot;196&quot; data-origin-width=&quot;369&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;참고) VSCode 기준 Git Bash 터미널에서 진행했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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 style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;1. 유저 변경&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre id=&quot;code_1766112964312&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git config --global user.name 유저이름 
git config --global user.email 유저이메일&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;(--global 옵션은 전역 설정이기 때문에 해당 레포지토리 한정으로 유저를 변경할 거라면 --global 생략 가능)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766113101370&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git config user.name
git config user.email&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;2. 로그 확인 후 리베이스&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #333333;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;&amp;nbsp;git log&lt;/span&gt;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&lt;span&gt; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;명령어로 히스토리를 확인해주고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766113549355&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git rebase -i --root&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;위 명령어로 리베이스를 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;나는 &lt;b&gt;첫번째 커밋부터 전부 다 변경&lt;/b&gt;해야 되는 상황이었어서 &lt;b&gt;--root&lt;/b&gt; &lt;b&gt;옵션&lt;/b&gt;을 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;--root 자리에&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #333333; color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;HEAD~n&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ffffff; text-align: start;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이런식으로 작성하면 가장 최근 커밋부터 n개의 커밋을 선택할 수 있다.&lt;/span&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;353&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1NVyt/dJMcahQwfmB/sTr6fEa7yS37cm0xdoAOkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1NVyt/dJMcahQwfmB/sTr6fEa7yS37cm0xdoAOkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1NVyt/dJMcahQwfmB/sTr6fEa7yS37cm0xdoAOkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1NVyt%2FdJMcahQwfmB%2FsTr6fEa7yS37cm0xdoAOkk%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;353&quot; height=&quot;186&quot; data-origin-width=&quot;353&quot; data-origin-height=&quot;186&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;&lt;b&gt;e 라고 써진 부분은 원래 pick 이 써져있다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;i 를 입력하여 작성 모드로 들어간 후,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정이 필요한 커밋 내역들을 사진처럼&lt;b&gt; &lt;/b&gt;&lt;u&gt;&lt;b&gt;pick 을 e 혹은 edit 으로 수정&lt;/b&gt;&lt;/u&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&gt;esc &amp;rarr; :wq 를 입력하여 모드를 빠져나온다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bISooi/dJMcacIskxc/36zU191kYL4RBaU8tPl8P1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bISooi/dJMcacIskxc/36zU191kYL4RBaU8tPl8P1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bISooi/dJMcacIskxc/36zU191kYL4RBaU8tPl8P1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbISooi%2FdJMcacIskxc%2F36zU191kYL4RBaU8tPl8P1%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;440&quot; height=&quot;207&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;207&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 style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그럼 사진처럼 commit 을 할건지 rebase를 할건지 선택하라고 하는데&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;우리는 작성자(+날짜)를 변경해줘야 하기 때문에&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1766120733700&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git rebase --continue&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;span&gt;명령어를 입력해 다시 리베이스를 실행한다.&lt;/span&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;516&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/co48Kd/dJMb9951URT/U5UF9rqqQpCG7OlaeAVkr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/co48Kd/dJMb9951URT/U5UF9rqqQpCG7OlaeAVkr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/co48Kd/dJMb9951URT/U5UF9rqqQpCG7OlaeAVkr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fco48Kd%2FdJMb9951URT%2FU5UF9rqqQpCG7OlaeAVkr0%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;516&quot; height=&quot;240&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그럼 사진과 같이 &lt;b&gt;한 개&lt;/b&gt;의 커밋 이력이 보일 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 작성자 변경하기&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre id=&quot;code_1766111697367&quot; class=&quot;jboss-cli&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; git commit --amend --author=&quot;Author Name&amp;lt;author@email&amp;gt;&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;span&gt;위 명령어를 입력해 작성자를 수정해준다.&lt;/span&gt;&lt;span&gt;위 명령어를 입력하면 아까 pick을 e로 바꿔주었던 창이 또 뜨는데, 거기서 커밋메세지를 수정할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;수정할게없으면 --amend 뒤에 &lt;b&gt;--no-edit&lt;/b&gt; 옵션을 붙이면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766121480199&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; git commit --amend --no-edit --author=&quot;Author Name&amp;lt;author@email&amp;gt;&quot;&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;b&gt;3-1. 날짜 변경하기&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성자만 변경하면 된다면 4번으로 넘어가 push 해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 나는 &lt;span style=&quot;color: #dddddd;&quot;&gt;커밋 수정을 한 번 잘못 하는 바람에&lt;/span&gt;&amp;nbsp; 날짜 수정을 추가로 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766121832979&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GIT_COMMITTER_DATE=&quot;Tue Dec 9 11:09:48 2025 +0900&quot; git commit --amend --no-edit --date &quot;Tue Dec 9 11:09:48 2025 +0900&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;GIT_COMMITTER_DATE 변수를 만들어 날짜를 수정해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쌍따옴표 안에 요일 월 등 Git 형식에 맞춰 날짜 값을 입력해주면 된다.&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;작성자 수정할 때와 마찬가지로 --no-edit 옵션 유무는 선택하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  커밋 반영&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;481&quot; data-origin-height=&quot;57&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B9Mdq/dJMcaacM8pY/j7KZ6QPOgyQZKYy97TzW7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B9Mdq/dJMcaacM8pY/j7KZ6QPOgyQZKYy97TzW7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B9Mdq/dJMcaacM8pY/j7KZ6QPOgyQZKYy97TzW7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB9Mdq%2FdJMcaacM8pY%2Fj7KZ6QPOgyQZKYy97TzW7K%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;481&quot; height=&quot;57&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;57&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;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;위&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;git rabase 부터 --amend 수정 과정을 반복&lt;/b&gt;&lt;/u&gt;하면 사진처럼 Successfully~ 문구가 나온다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;그럼 이제 모든 커밋의 수정이 완료된 것! push 만 해주면 끝난다.&lt;/span&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;pre id=&quot;code_1766121651826&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git push --force&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;&lt;span&gt;⚠️ 주의: 개인 레포가 아닌 &lt;/span&gt;&lt;span&gt;공유 브랜치&lt;/span&gt;&lt;span&gt;에서 force push 하지 않도록 주의해야한다&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;a href=&quot;https://hirlawldo.tistory.com/156&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://hirlawldo.tistory.com/156&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://sangminlog.tistory.com/entry/modify-author&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://sangminlog.tistory.com/entry/modify-author&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrEQX%2FbtrK4CAyKrB%2FutM4awBF5CvfGVQ7UvveyK%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;175&quot; height=&quot;86&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&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>etc/+@</category>
      <category>GIT</category>
      <category>Github</category>
      <category>깃허브</category>
      <author>vellahw</author>
      <guid isPermaLink="true">https://tavi.tistory.com/187</guid>
      <comments>https://tavi.tistory.com/187#entry187comment</comments>
      <pubDate>Fri, 19 Dec 2025 14:32:57 +0900</pubDate>
    </item>
    <item>
      <title>  모바일 UI 디자인 시스템과 프론트엔드에서 디자인 시스템 적용하기</title>
      <link>https://tavi.tistory.com/186</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #323232; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;서론&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 휴대폰의 화면비율은 정말 다양하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미니 사이즈의 선호도가 줄고 휴대폰의 평균 사이즈가 커졌어도 작은 기종을&amp;nbsp;고려하지 않을 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 기기는 세로 길이가 667px 정도로 작고, 어떤 기기는 882px 이상으로 꽤 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 디자인 시안을 바탕으로 프론트엔드 개발을 하다 보면 이런 고민이 생긴다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 요소 크기는 작은 화면 기준으로 해야 할까? 큰 화면 기준으로 해야 할까?&lt;br /&gt;작은 사이즈에서는 페이지 디자인이 &quot;예뻐&quot;보이는데 큰 사이즈에서는 적절하지 않아보인다면.. 어떻게 해야할까?&lt;/p&gt;
&lt;/blockquote&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모바일 UI를 &lt;b&gt;디자이너 관점에서 어떻게 설계하는지&lt;/b&gt;&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;를 가볍게 정리해보았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;b&gt;1. 모바일 UI는 어떤 화면 기준으로 디자인할까?&lt;/b&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;b&gt;보통 작은 화면 기준으로 설계&lt;/b&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;작은 화면에서 UI가 깨지면 &lt;b&gt;복구가 거의 불가능&lt;/b&gt;하고&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✅ 고정되는 UI&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;버튼 높이, 터치 영역, 아이콘 사이즈, 텍스트 크기&lt;/u&gt; 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;사용성에 직접적인 영향을 주는 요소는 고정&lt;/b&gt;&lt;/u&gt;된다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;button height = 48px
icon = 24px
body text = 16px
&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;b&gt;✅ 유동적으로 늘어나는 것&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;섹션 간격, hero 영역, 카드 간격, 여백&lt;/u&gt; 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;레이아웃 요소들은 보통 유동적으로 확장&lt;/b&gt;&lt;/u&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;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// 작은 화면
section spacing = 24px
hero height = 220px

// 큰 화면
section spacing = 40px
hero height = 320px&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;콘텐츠는 유지하고 여백을 확장한다는 것&lt;/b&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 모바일 디자인에서 많이 쓰는 기본 규칙&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✅ 8pt Spacing System&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 UI에서는 보통 &lt;b&gt;8pt 시스템&lt;/b&gt;을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;4
8
16
24
32
40
48
64
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;icon &amp;harr; text = 8
card padding = 16
section gap = 24
&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;터치 영역 규칙&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터치 영역 규칙은 모바일 UX에서 중요한 규칙이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;최소 터치 영역 = 44px
버튼 높이 = 48~56px
아이콘 = 24px
&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 프론트엔드에서 디자인 시스템 적용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 CSS 기준에서 디자인 시스템은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;디자인 토큰 &amp;rarr; 공통 스타일 &amp;rarr; 컴포넌트 스타일&lt;/b&gt; 구조로 정의하는 것이 일반적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 목적은 &lt;b&gt;일관성 있는 UI 스타일을 유지하고 재사용성을 높이는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;137&quot; data-start=&quot;123&quot; data-section-id=&quot;1s9gttk&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1) 디자인 토큰 정의&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;225&quot; data-start=&quot;139&quot; data-ke-size=&quot;size16&quot;&gt;디자인 시스템의 가장 기초 단계는 디자인 토큰이다.&lt;br /&gt;색상, 간격, 폰트 같은 &lt;b&gt;기본 스타일 값을 변수로 관리&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-end=&quot;278&quot; data-start=&quot;227&quot; data-ke-size=&quot;size16&quot;&gt;CSS에서는 보통 CSS Custom Properties (CSS 변수)로 정의한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1773716782669&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;:root {
  /* color */
  --color-primary: #111111;
  --color-secondary: #666666;
  --color-background: #ffffff;

  /* spacing */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;

  /* radius */
  --radius-sm: 4px;
  --radius-md: 8px;

  /* font */
  --font-body: 16px;
  --font-title: 24px;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;649&quot; data-start=&quot;613&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 정의하면 프로젝트 전체에서 동일한 값을 재사용할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;649&quot; data-start=&quot;613&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;676&quot; data-start=&quot;656&quot; data-section-id=&quot;1mfgufr&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2) 리셋 및 글로벌 스타일 정의&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;713&quot; data-start=&quot;678&quot; data-ke-size=&quot;size16&quot;&gt;브라우저 기본 스타일을 제거하고 기본 레이아웃 규칙을 설정한다.&lt;/p&gt;
&lt;p data-end=&quot;713&quot; data-start=&quot;678&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;모든 UI 컴포넌트의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;기본 스타일 환경을 통일시킨다.&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1773716908562&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
}

body {
  font-family: system-ui, sans-serif;
  font-size: var(--font-body);
  color: var(--color-primary);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;978&quot; data-start=&quot;933&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1003&quot; data-start=&quot;985&quot; data-section-id=&quot;6nyjqu&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3) 타이포그래피 시스템 정의&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1773716956021&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.text-body {
  font-size: 16px;
  line-height: 1.5;
}

.text-title {
  font-size: 24px;
  font-weight: 600;
}

.text-caption {
  font-size: 12px;
  color: var(--color-secondary);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1265&quot; data-start=&quot;1222&quot; data-ke-size=&quot;size16&quot;&gt;본문, 제목, 설명글 등&lt;/p&gt;
&lt;p data-end=&quot;1265&quot; data-start=&quot;1222&quot; data-ke-size=&quot;size16&quot;&gt;텍스트 스타일을 공통 클래스로 정의한다.&lt;/p&gt;
&lt;p data-end=&quot;1265&quot; data-start=&quot;1222&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1288&quot; data-start=&quot;1272&quot; data-section-id=&quot;caboy6&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4) 레이아웃 시스템 정의&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1349&quot; data-start=&quot;1290&quot; data-ke-size=&quot;size16&quot;&gt;페이지의 공통 레이아웃 규칙을 만든다. 대표적인 예가 &lt;b&gt;container와 spacing 시스템&lt;/b&gt;이다.&lt;/p&gt;
&lt;h4 data-end=&quot;1364&quot; data-start=&quot;1351&quot; data-section-id=&quot;108vk8x&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✅ container&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1773717039801&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.container {
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 var(--space-md);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;container는 &lt;/span&gt;레이아웃에서 자주 등장하는 패턴이&lt;/span&gt;&lt;span&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;container는 페이지 콘텐츠의 가로 폭과 좌우 여백을 통제한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 화면이 너무 넓으면 엔터 없이 작성된 글처럼 &lt;/span&gt;&lt;span&gt;가독성이 떨어진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 max-width가 지정된 container를 사용하는 것!&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1504&quot; data-start=&quot;1477&quot; data-section-id=&quot;z44ym9&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-section-id=&quot;108vk8x&quot; data-start=&quot;1351&quot; data-end=&quot;1364&quot;&gt;&lt;b&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;stack (vertical layout)&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1773717401908&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.stack &amp;gt; * + * {
  margin-top: var(--space-md);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1608&quot; data-start=&quot;1568&quot; data-ke-size=&quot;size16&quot;&gt;이런 패턴은 &lt;b&gt;컴포넌트 사이 간격을 일정하게 유지&lt;/b&gt;하는 데 사용한다.&lt;/p&gt;
&lt;p data-end=&quot;1608&quot; data-start=&quot;1568&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1608&quot; data-start=&quot;1568&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1631&quot; data-start=&quot;1615&quot; data-section-id=&quot;1qzo4v8&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5) 컴포넌트 스타일 정의&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1658&quot; data-start=&quot;1633&quot; data-ke-size=&quot;size16&quot;&gt;UI 요소를 재사용 가능한 클래스로 정의한다.&lt;/p&gt;
&lt;h4 data-end=&quot;1669&quot; data-start=&quot;1660&quot; data-section-id=&quot;1qfga16&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✅ 버튼 예시&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1773717438557&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;

  height: 40px;
  padding: 0 var(--space-md);

  border-radius: var(--radius-md);
  border: none;
  cursor: pointer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1915&quot; data-start=&quot;1887&quot; data-ke-size=&quot;size16&quot;&gt;variant는 modifier 클래스로 정의한다.&lt;/p&gt;
&lt;pre id=&quot;code_1773717466712&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.button--primary {
  background: var(--color-primary);
  color: white;
}

.button--secondary {
  background: transparent;
  border: 1px solid var(--color-primary);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2099&quot; data-start=&quot;2095&quot; data-ke-size=&quot;size16&quot;&gt;사용 예&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1773717489577&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;button class=&quot;button button--primary&quot;&amp;gt;
  확인
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2196&quot; data-start=&quot;2169&quot; data-ke-size=&quot;size16&quot;&gt;이 구조는 보통 &lt;b&gt;BEM 방식&lt;/b&gt;이라고 부른다.&lt;/p&gt;
&lt;p data-end=&quot;2217&quot; data-start=&quot;2203&quot; data-section-id=&quot;1ohnpdi&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2217&quot; data-start=&quot;2203&quot; data-section-id=&quot;1ohnpdi&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6) 상태 스타일 정의&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1773717577624&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.button:hover {
  opacity: 0.9;
}

.button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2383&quot; data-start=&quot;2343&quot; data-ke-size=&quot;size16&quot;&gt;상태 스타일은 pseudo class(의사/가상 클래스)로 관리하는 것이 일반적&lt;/p&gt;
&lt;p data-end=&quot;2404&quot; data-start=&quot;2390&quot; data-section-id=&quot;exp7t7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2404&quot; data-start=&quot;2390&quot; data-section-id=&quot;exp7t7&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7) 반응형 규칙 정의&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2440&quot; data-start=&quot;2406&quot; data-ke-size=&quot;size16&quot;&gt;반응형 스타일은 breakpoint 기준으로 관리한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1773717596255&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.container {
  padding: 0 var(--space-sm);
}

@media (min-width: 768px) {
  .container {
    padding: 0 var(--space-lg);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&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;b&gt;모바일(max-width: 767px)이 기본값 &amp;rarr; min-width로 확장&lt;/b&gt;하는 패턴을 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2584&quot; data-start=&quot;2574&quot; data-section-id=&quot;11h9x24&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8) 파일 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2620&quot; data-start=&quot;2586&quot; data-ke-size=&quot;size16&quot;&gt;위 내용을 바탕으로 디자인 시스템을 정의하면 이러한 파일 구조가 완성된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1773717904114&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;styles
 ├ reset.css
 ├ tokens.css
 ├ typography.css
 ├ layout.css
 └ components
  ├ button.css
  ├ card.css
  └ input.css&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.62em; letter-spacing: -1px;&quot;&gt;  정리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;디자이너가 만든 규칙을 &lt;/span&gt;&lt;b&gt;토큰으로 강제&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;하면&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;UI 일관성이 유지되고&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;유지보수가 쉬워지고&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;팀 협업이 훨씬 편해진다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;프론트엔드에서 디자인 시스템을 잘 만들어 두면 &lt;/span&gt;&lt;span&gt;UI 개발 속도가 정말 많이 빨라질 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;3025&quot; data-start=&quot;2971&quot; data-ke-size=&quot;size16&quot;&gt;위처럼 정리한 구조를 따르면 디자인 일관성을 유지하면서 확장 가능한 CSS 아키텍처를 만들 수 있을 것이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #263238; color: #eeffff;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrEQX%2FbtrK4CAyKrB%2FutM4awBF5CvfGVQ7UvveyK%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;175&quot; height=&quot;86&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>FE/Styling</category>
      <category>CSS</category>
      <category>디자인시스템</category>
      <author>vellahw</author>
      <guid isPermaLink="true">https://tavi.tistory.com/186</guid>
      <comments>https://tavi.tistory.com/186#entry186comment</comments>
      <pubDate>Sat, 20 Jan 2024 18:56:39 +0900</pubDate>
    </item>
    <item>
      <title>  CSS position: sticky 제대로 이해하기 (왜 고정이 안 될까?)</title>
      <link>https://tavi.tistory.com/184</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #323232; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;서론&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여태 브라우저의 헤더나 하단 바를 만들 때는 보통 &lt;/span&gt;&lt;span&gt;position: fixed&lt;/span&gt;&lt;span&gt;를 사용해왔다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 무신사나 지그재그 웹 브라우저처럼 &lt;/span&gt;&lt;b&gt;&lt;span&gt;레이아웃이 모바일 사이즈(&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;max-width&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;)로 고정된 구조&lt;/span&gt;&lt;/b&gt;&lt;span&gt;에서는&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fixed를 사용하면 요소가 max-width로 제한된 레이아웃을 벗어나 &lt;b&gt;좌우 바깥으로 튀어나오는 현상&lt;/b&gt;이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;fixed는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;뷰포트 기준으로 고정되기 때문&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;)&lt;/span&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;772&quot; data-origin-height=&quot;591&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmdbG8/dJMcacbhcf4/nZpmb6KIuYIMD4ZFgZ1O2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmdbG8/dJMcacbhcf4/nZpmb6KIuYIMD4ZFgZ1O2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmdbG8/dJMcacbhcf4/nZpmb6KIuYIMD4ZFgZ1O2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmdbG8%2FdJMcacbhcf4%2FnZpmb6KIuYIMD4ZFgZ1O2k%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;772&quot; height=&quot;591&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;591&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지그재그 웹을 살펴보니 헤더에 position: sticky가 사용되고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 같은 방식으로 sticky를 적용해봤지만 고정이 안 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 sticky 속성을 사용했는데 왜 고정이 안 될까?&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;작은 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;결론)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;position: sticky는&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; ✔ top, left 등 기준값 필수&lt;/b&gt;&lt;br /&gt;&lt;b&gt;✔ 부모 요소에 overflow 사용 X&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. position: sticky란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;position: sticky는 &lt;b&gt;relative와 fixed의 중간 개념&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;일반 문서 흐름(relative)처럼 동작&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스크롤이 특정 위치에 도달하면&lt;/b&gt; &lt;b&gt;고정(fixed)처럼 동작&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기본 사용 예시&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.header {
  position: sticky;
  top: 0;
}
&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;&lt;b&gt;스크롤해서 요소가 top: 0 위치에 도달하면&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;해당 위치에 &lt;b&gt;붙어서 고정&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;  따라서 top, left, right 등의 기준값을 함께 작성해야함!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. sticky와 fixed의 차이&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성특징&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;position: fixed&lt;/td&gt;
&lt;td&gt;화면(Viewport) 기준으로 항상 고정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;position: sticky&lt;/td&gt;
&lt;td&gt;&lt;b&gt;부모 영역 내부에서만 고정&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사이드바에 sticky를 사용하면&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. position: sticky가 동작하지 않는 대표적인 이유&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;1️⃣&lt;b&gt; top, left 등의 기준 값이 없음&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sticky는 &lt;b&gt;기준 위치가 반드시 필요하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;top, bottom, left, right 중 하나는 반드시 지정해야 한다.&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 style=&quot;background-color: #ffc1c8;&quot;&gt;2️⃣ &lt;b&gt;부모 요소에 overflow 속성이 있음&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 부모 요소의 overflow 때문에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-size: 1.44em; background-color: #ffc1c8;&quot;&gt; 3️⃣ &lt;/span&gt;&lt;span style=&quot;font-size: 1.44em; background-color: #ffc1c8;&quot;&gt;Flex / Grid 레이아웃 환경 문제&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flex 컨테이너 안에서 sticky가 제대로 작동하지 않는 경우도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 다음 속성을 추가해 해결한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.sticky-element {
  align-self: flex-start;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrEQX%2FbtrK4CAyKrB%2FutM4awBF5CvfGVQ7UvveyK%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;175&quot; height=&quot;86&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>FE/Styling</category>
      <category>CSS</category>
      <author>vellahw</author>
      <guid isPermaLink="true">https://tavi.tistory.com/184</guid>
      <comments>https://tavi.tistory.com/184#entry184comment</comments>
      <pubDate>Tue, 12 Dec 2023 01:29:55 +0900</pubDate>
    </item>
    <item>
      <title>  &amp;quot;리액트는 가상 DOM을 사용하여 성능을 최적화합니다.&amp;quot; 가 무슨 말이야?</title>
      <link>https://tavi.tistory.com/178</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #323232; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;서론&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;프론트엔드를 공부하다 보면 한 번쯤은 이런 문장을 보게 된다.&lt;/span&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;span&gt;&lt;br /&gt;&amp;ldquo;리액트는 가상 DOM(Virtual DOM)을 사용하여 성능을 최적화합니다.&amp;rdquo;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&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&gt;그런데 처음 보면 이게 무슨 말인지 꽤 추상적으로 느껴진다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;이 문장이 실제로 무엇을 의미하는지, 그리고 왜 성능이 좋아지는지 정리해보았다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;1. DOM이란 무엇인가?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;먼저 &lt;b&gt;DOM(Document Object Model)&lt;/b&gt;부터 이해해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹페이지는 내부적으로 DOM 구조로 관리되는데, 쉽게 말하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span&gt;&lt;b&gt;브라우저는 우리가 작성한 HTML을&lt;/b&gt; 그대로 사용하는 것이 아니라&lt;/span&gt;&lt;/u&gt;&lt;br /&gt;&lt;u&gt;&lt;b&gt; 트리 구조의 객체 형태(DOM)로 변환해서 관리&lt;/b&gt;한다.&lt;/u&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&gt;예를 들어&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;h1&amp;gt;제목&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;내용&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 코드는 브라우저 내부에서 다음과 같은 구조로 관리된다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;body&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;h1&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;p&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이러한 구조를 DOM이라고 한다.&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;b&gt;&lt;span&gt;2. 문제: DOM을 직접 수정하면 느리다&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;웹에서 버튼 클릭, 데이터 변경, 입력값 변경 등으로 상태가 바뀌면 화면도 바뀌어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이때 가장 직관적인 방식은&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span&gt;DOM을 직접 수정하는 것&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 문제는&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;DOM 수정은 비용이 크고&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;화면을 다시 계산해야 하고 (reflow)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;다시 그려야 한다 (repaint)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, &lt;/span&gt;&lt;b&gt;&lt;span&gt;DOM을 자주 건드릴수록 성능이 떨어진다.&lt;/span&gt;&lt;/b&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;b&gt;&lt;span&gt;3. 해결 방법: 가상 DOM (Virtual DOM)&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;리액트는 이 문제를 해결하기 위해 가상 DOM(Virtual DOM)이라는 개념을 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;가상 DOM이란&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;b&gt;&lt;span&gt;실제 DOM을 흉내 낸 &amp;ldquo;자바스크립트 객체&amp;rdquo;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&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;사용자가 웹 애플리케이션을 열면 리액트는 초기 UI를 생성한다.&lt;br /&gt;실제 DOM을 만드는 대신 가상 DOM 트리를 만드는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 DOM은 메모리에 존재하며 실제 화면에 표시되지 않는다.&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&gt;즉, 리액트는 브라우저의 DOM을 직접 건드리지 않고&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;메모리 안에서 가짜 DOM을 하나 더 만들어서 작업한다.&lt;/span&gt;&lt;/b&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;b&gt;&lt;span&gt;4. 리액트의 동작 방식  &lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기존 방식은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;변경이 발생할 때마다 DOM을 직접 수정하기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;비효율적이고 느리다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 리액트 방식은&lt;/span&gt;&lt;span&gt;&lt;/span&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;&lt;b&gt;&lt;span&gt;①&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자가&amp;nbsp;어떤&amp;nbsp;상태&amp;nbsp;변경을&amp;nbsp;트리거(예:&amp;nbsp;버튼&amp;nbsp;클릭)하면,&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;리액트는 해당 변경을 감지하고 새로운 가상 DOM 트리를 생성한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 트리에는 변경된 상태를 반영한 UI 요소가 포함된다.&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;&lt;b&gt;②&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 리액트는 이전 가상 DOM 트리와 새로운 가상 DOM 트리를 비교한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(이 과정을 &quot;재조정&quot; 또는 &quot;린더링&quot;이라고 한다.)&lt;br /&gt;리액트는 두 가상 DOM 트리를 비교하면서 어떤 부분이 변경되었는지를 효율적으로 찾는다.&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;&lt;b&gt;③&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;변경된 부분만을 찾아내면, 리액트는 이 부분만을 실제 DOM에 반영한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이전과 새로운 가상 DOM 사이의 변경된 부분만을 조작하므로&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&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;정리하자면&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;상태(state)가 변경됨&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;새로운 가상 DOM 생성&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;이전 가상 DOM과 비교&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;변경된 부분만 실제 DOM에 반영&lt;/span&gt;&lt;/b&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;이것은 브라우저가 실제 DOM 조작에 비해 빠르게 처리할 수 있도록 도와주는 것이다.&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;b&gt;5. 가상 DOM이 성능이 좋아지는 이유&lt;/b&gt;&lt;b&gt;&lt;/b&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;b&gt;리액트는 고장난 부분만 찾아서 고치는 느낌&lt;/b&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;가상 DOM이 성능이 좋아지는 이유의 핵심은 딱 하나다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;br /&gt;실제 DOM 접근을 최소화하기 때문&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&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;리액트는 Virtual DOM을 이용해 변경 사항을 비교한 뒤,&lt;/b&gt;&lt;/p&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;(필요한 부분만 실제 DOM에 반영함으로써 성능을 최적화한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;이러한 과정을 통해 리액트는 렌더링 성능을 최적화한다.&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;사용자에게 빠른 응답성을 제공하면서도 불필요한 계산과 DOM 조작을 최소화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가상 DOM은 단순한 개념처럼 보이지만 &lt;/span&gt;&lt;span&gt;리액트의 전체를 이해하는 핵심이라고 볼 수 있는 것 같다.&lt;/span&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 alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrEQX/btrK4CAyKrB/utM4awBF5CvfGVQ7UvveyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrEQX%2FbtrK4CAyKrB%2FutM4awBF5CvfGVQ7UvveyK%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;175&quot; height=&quot;86&quot; data-filename=&quot;썸네일.png&quot; data-origin-width=&quot;175&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>FE/React</category>
      <category>VirtualDom</category>
      <category>가상DOM</category>
      <category>리액트</category>
      <author>vellahw</author>
      <guid isPermaLink="true">https://tavi.tistory.com/178</guid>
      <comments>https://tavi.tistory.com/178#entry178comment</comments>
      <pubDate>Wed, 13 Sep 2023 14:49:04 +0900</pubDate>
    </item>
  </channel>
</rss>