<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>r0_0r.log</title>
        <link>https://velog.io/</link>
        <description>즐거워지고 싶다.</description>
        <lastBuildDate>Wed, 21 May 2025 05:10:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>r0_0r.log</title>
            <url>https://velog.velcdn.com/images/r0_0r/profile/d02efd8a-555c-4840-b431-9c91623328a8/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. r0_0r.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/r0_0r" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Strategy]]></title>
            <link>https://velog.io/@r0_0r/Strategy</link>
            <guid>https://velog.io/@r0_0r/Strategy</guid>
            <pubDate>Wed, 21 May 2025 05:10:33 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-typescript">interface Pay {
    pay(price: number): number;
}

class NaverPay implements Pay {

    pay(price: number) {
        console.log(&#39;naver...&#39;);
        return price;
    }
}

class KaKaoPay implements Pay {

    pay(price: number) {
        console.log(&#39;kakao...&#39;);
        return price;
    }
}

class Payment {
    constructor(
        private readonly payment: Pay,
    ) {}

    pay(price: number) {
        return this.payment.pay(price);
    }
}

const nPay = new Payment(new NaverPay());

nPay.pay(1000);

const kPay = new Payment(new KaKaoPay());

kPay.pay(2000);</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UI/UX] 3D 애니메이션과 팝업 알림창]]></title>
            <link>https://velog.io/@r0_0r/UIUX-3D-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98%EA%B3%BC-%ED%8C%9D%EC%97%85-%EC%95%8C%EB%A6%BC%EC%B0%BD</link>
            <guid>https://velog.io/@r0_0r/UIUX-3D-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98%EA%B3%BC-%ED%8C%9D%EC%97%85-%EC%95%8C%EB%A6%BC%EC%B0%BD</guid>
            <pubDate>Sat, 22 Feb 2025 11:54:32 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/r0_0r/post/21b1a0d2-314d-40bb-9749-81e939401e69/image.png" alt=""></p>
<p>먼저 우리 <a href="https://velog.io/@designer_julie/%ED%86%A0%EC%8A%A4-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-%EB%B8%94%EB%A0%8C%EB%8D%94Blender%EB%A1%9C-3D-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-UI-%EB%A7%8C%EB%93%A4%EA%B8%B0">디자이너</a>가 만든 귀여운 하트 편지지부터 보고 가요.</p>
<hr />

<p>요즘 앱들을 사용해 보면 3D 애니메이션이 정말 많이 보인다.
그래서 우리 앱에도 적용시켜 보면 어떨까라는 의견이 나와서 해보기로 했다.</p>
<h2 id="블렌더에서-추출된-파일-받아서-적용시키기">블렌더에서 추출된 파일 받아서 적용시키기</h2>
<p>디자이너가 열심히 만든 <code>.glb</code> <code>.gltf</code> 확장자로 된 받아서 플러터에 적용시킬 수 있는데
여러 방법 중에 가장 간단하게 라이브러리를 사용해서 화면에 표시해 보기로 했다.</p>
<p>바로 <a href="https://pub.dev/packages/flutter_3d_controller">flutter_3d_controller</a>라는 라이브러리이다.</p>
<p>애니메이션을 시작, 정지, 처음 상태로 되돌리기나 카메라 조절 기능도 제공해준다.</p>
<pre><code class="language-dart">Flutter3DController controller = Flutter3DController();
// ...
Flutter3DViewer(
    onLoad: (String modleAddress) {
        // 파일이 다 불러와졌을 경우
        controller.playAnimation();
    },
    controller: controller,
    src: &#39;assets/letter.glb&#39;,
);</code></pre>
<p>먼저 컨트롤러를 선언한 다음
<code>Flutter3DViewer</code> 속성에 컨트롤러를 등록해 준다.
<code>src</code> 속성에 파일 경로를 작성해 준 다음
<code>onLoad</code>에서 컨트롤러를 통해 애니메이션을 재생시켜주면 된다.
<br />
<br />
<br /></p>
<p><img src="https://velog.velcdn.com/images/designer_julie/post/f2650b18-d8d7-43b7-a40d-8db2cb00b059/image.gif" alt=""></p>
<br />
<br />
<br />
그러면 이렇게 다양한 각도에서 애니메이션을 볼 수 있다.

<p>아직 라이브러리를 제대로 이해한 게 아니라서 <code>.glb</code> 파일로 표시하는 애니메이션의 경우
최적화나 사이즈 조절 등... 하는 방법을 찾아봐야 한다.</p>
<hr />

<h2 id="json-파일로-애니메이션-표시하기">json 파일로 애니메이션 표시하기</h2>
<p>위의 라이브러리를 써보면서 살짝 렉이 있는 듯한 느낌이 들어서, 우리 앱에 바로 적용시키기에는 힘들어 보였다.
그래서 우리 앱에서 이미 적용시킨 <code>json (lottie)</code> 방식을 사용하기로 결정했다.</p>
<p>이것도 역시 <a href="https://pub.dev/packages/lottie">lottie</a>라는 라이브러리를 쓰면 간단하게 화면에 애니메이션을 표시할 수 있다.</p>
<pre><code class="language-dart">late final Future&lt;LottieComposition&gt; letter;

  @override
  void initState() {
    super.initState();
    letter = AssetLottie(&#39;assets/lottie/letter.json&#39;).load();
    checkPopup();
  }</code></pre>
<p>먼저 <code>json</code> 파일을 불러와야 한다.</p>
<p>그 다음에는 <code>FutureBuilder</code>를 활용해서 이 파일이 다 불러와졌을 때
화면에 나타나도록 하면 된다.</p>
<blockquote>
<p>FutureBuilder
비동기 작업이 완료될 때까지 기다렸다가 결과를 UI에 반영하는 데에 주로 사용됨.
<code>future</code> 값에 비동기 함수를 전달하면 해당 상태에 따라 UI를 다르게 표시할 수 있다.
<code>snapshot.connectionState</code>는 <code>ConnecitonState</code>로 비교하면 된다.</p>
<blockquote>
<table>
<thead>
<tr>
<th>상태</th>
<th>설명</th>
<th>예제</th>
</tr>
</thead>
<tbody><tr>
<td>waiting</td>
<td>future 작업 대기 중</td>
<td>로딩 UI</td>
</tr>
<tr>
<td>done</td>
<td>완료</td>
<td>데이터 표시</td>
</tr>
<tr>
<td>active</td>
<td>수신 중</td>
<td>주로 StreamBuilder 사용</td>
</tr>
<tr>
<td>none</td>
<td>Future가 null</td>
<td>기본 UI 표시</td>
</tr>
</tbody></table>
</blockquote>
</blockquote>
<pre><code class="language-dart">//...
FutureBuilder&lt;LottieComposition&gt;(
    future: letter,
    builder: (context, snapshot) {
        var lottie = snapshot.data;
        if (lottie != null) {
            // null이 아니라면, 다 불러와진 것.
            return Lottie(
                // 사이즈 등 옵션 주기
                composition: lottie,
            );
        } else {
            return Container();
        }
    }
)</code></pre>
<p>위와 같이 작성해 보면</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/21e02317-54ff-472a-934a-986f518989de/image.png" alt=""></p>
<p>이렇게 귀여운 하트 편지지가 화면에 나타나게 된다.</p>
<hr />

<h2 id="팝업-애니메이션-적용시키기">팝업 애니메이션 적용시키기</h2>
<p>아무래도 <code>json 파일의 로딩이 완료된 후 나타나기</code> 때문에 화면에는 갑자기 나타나기 때문에
사용자 입장에서는 레이아웃들이 밀려서 잘못 터치할 수 있겠다, 라는 생각이 들었다.</p>
<p>그래서 팝업이 나타나는 애니메이션을 줘서 미리 대비(?)할 수 있게 하는 게 어떨까 싶었다.</p>
<h3 id="애니메이션-위젯의-크기를-알아내기">애니메이션 위젯의 크기를 알아내기</h3>
<p>먼저 애니메이션이 포함된 위젯의 크기가 얼마인지 알아내야했다.</p>
<blockquote>
<p>우리 앱에서는 <code>height</code>에 값을 주는 건 최소한으로 하고 <code>padding</code> 활용하여 UI를 구성하고 있다.
기기마다 해상도나 DPI가 다르기 때문에 고정된 값을 사용하면 특정 기기에서 레이아웃이 깨질 가능성이 있기 때문이다.
기기에 <code>height</code> 크기를 렌더링 하는 것을 맡기고 우리는 <code>padding</code> 같이 여백만 주면
다양한 기기에서도 일관된 UI를 유지할 수가 있다.</p>
</blockquote>
<p><code>GlobalKey</code>를 애니메이션 위젯에 적용한 다음,
<code>json</code> 로딩이 끝났을 경우가 우리의 원하는 크기이므로 <code>FutureBuilder</code>를 활용해서
로딩이 끝났을 때의 크기를 가져오면 된다.</p>
<pre><code class="language-dart">//...
if (lottie != null) {
    final RenderBox? containerRenderBox =
        containerKey.currentContext?.findRenderObject() as RenderBox?;

    if (containerRenderBox != null) {
        if (!isClose) {
            containerSize = containerRenderBox.size.height;
        }

    }
}</code></pre>
<h3 id="슬라이드-애니메이션-적용시키기">슬라이드 애니메이션 적용시키기</h3>
<p><code>AnimatedContainer</code>를 사용해서 <code>height</code> 값에 변화를 주면
아래로 밀리는 느낌의 애니메이션을 줄 수가 있다.</p>
<pre><code class="language-dart">AnimatedContainer(
    height: containerSize,
    duration: const Duration(milliseconds: 300),
    onEnd: () {
        // 밀리는 애니메이션이 끝나면 팝업 표시
        if (isClose) return;
        setState(() {
            isLoading = false;
        });
        doPopup();
    },
),</code></pre>
<h3 id="팝업-애니메이션-적용시키기-1">팝업 애니메이션 적용시키기</h3>
<p>저번에 활용했던 <code>mixin</code> 클래스를 써서 애니메이션을 분리시킨 뒤
애니메이션 먼저 만들어줬다.</p>
<pre><code class="language-dart">mixin PopupAnimationMixin&lt;T extends StatefulWidget&gt;
    on State&lt;T&gt;, SingleTickerProviderStateMixin&lt;T&gt; {
  late AnimationController popupController;
  late Animation&lt;double&gt; popupAnimation;

  @override
  void initState() {
    super.initState();

    popupController = AnimationController(
      vsync: this,
      duration: const Duration(
        milliseconds: 300,
      ),
    );

    popupAnimation = Tween(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: popupController,
        curve: Curves.fastLinearToSlowEaseIn,
      ),
    );
  }

  @override
  void dispose() {
    popupController.dispose();
    super.dispose();
  }

  void doPopup() async {
    popupController.forward();
  }
}</code></pre>
<p>또, 저번에 활용했던 <code>AnimatedBuilder</code>를 사용하여
<code>Transform.scale</code>과 <code>Opacitiy</code> 위젯을 쓰면 페이드인이 되면서 점점 커지는 애니메이션을 구현할 수 있다.</p>
<pre><code class="language-dart">AnimatedBuilder(
    animation: popUpAnimation,
    builder: (context, child) {
        return isLoading
            ? Container()
            : Opacitiy(
                opacity: popUpanimation.value,
                child: Transform.scale(
                    scale: popupAnimation.value,
                    child: child,
                ),
            );
       child: // ...
    }
)</code></pre>
<h4 id="🤔-isloading을-쓴-이유">🤔 isLoading을 쓴 이유?</h4>
<p><code>Transform.scale</code> 같은 경우에는 <code>value</code> 값이 <code>(0,0)</code>이더라도 크기를 이미 차지하고 있기 때문에
위에서 적용시켰던 슬라이드 애니메이션이 보이지 않게 된다.
<code>이미 공간을 차지하고 있기 때문에 애니메이션이 진행되고 있는 게 가려져서 보이지 않는 것.</code></p>
<p>그래서 <code>isLoading</code>으로 로딩 중일 때는 빈 컨테이너를 보여주고
로딩이 끝나면 팝업 위젯을 보여주는 식으로 했다.</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/0c01d309-baf8-4dfe-869a-5b9765414168/image.gif" alt=""></p>
<hr />

]]></description>
        </item>
        <item>
            <title><![CDATA[[UI/UX] 햅틱을 적용해 보기.]]></title>
            <link>https://velog.io/@r0_0r/UIUX-%ED%96%85%ED%8B%B1%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%B4-%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@r0_0r/UIUX-%ED%96%85%ED%8B%B1%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%B4-%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 15 Feb 2025 11:47:10 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Haptic
모바일 사용 환경에서 진동을 통해 사용자에게 반응을 전달하는 것.
키보드 입력 시, 미세한 진동으로 누르는 느낌을 주는 것 등이 있다.</p>
</blockquote>
<p>이번에는 우리 앱에서 회원가입이나 로그인 할 때 이메일 형식에 맞지 않았을 경우
이메일 형식이 아니라는 문구와 함께 <code>햅틱 피드백</code> 그리고 <code>흔들리는 효과</code>를 적용해 보기로 했다.</p>
<h2 id="✨-디자인-요구-사항">✨ 디자인 요구 사항</h2>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/48e541cd-cb6f-462a-a8cf-4324aa36b230/image.gif" alt=""></p>
<p>이번에는 <code>Protopie</code>라는 디자인 프로그램으로 받았는데, 피그마와 연동을 해서
실제 기기에서 진동까지도 확인할 수 있었다.</p>
<p>실제로 얼마 만큼 흔들려야 하는지 (좌로 몇 xp 이동해야되는지) 값을 알고 싶었지만
유료 결제를 해야지만 볼 수 있었기에 따로 스크린샷을 받아 값을 이동 값을 확인했다.</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/371b9501-0bff-4652-8022-392d00903ae3/image.png" alt="">
<img src="https://velog.velcdn.com/images/r0_0r/post/a207f026-9f66-4a73-94b7-3fd4dd08a6b7/image.png" alt="">
오른쪽으로 100ms 동안 5px 이동 후 원래 위치로 돌아온 뒤
<img src="https://velog.velcdn.com/images/r0_0r/post/66474ecc-b431-4802-8dc9-c703d0c48c5d/image.png" alt="">
<img src="https://velog.velcdn.com/images/r0_0r/post/560e61c3-9add-40a5-8628-5df6c6aafa14/image.png" alt="">
다시 오른쪽으로 100ms 동안 3px 이동 후 원래 위치로 돌아오고
<img src="https://velog.velcdn.com/images/r0_0r/post/2f200f05-bb7e-44cf-8bf7-4f85ad196cc1/image.png" alt="">
<img src="https://velog.velcdn.com/images/r0_0r/post/8854fe72-48e7-4360-b846-a772b157d2b9/image.png" alt="">
또 오른쪽으로 100ms 동안 1px 이동 후 원래 위치로 돌아오는 애니메이션이다.</p>
<h2 id="✨-생각하기">✨ 생각하기</h2>
<h3 id="🤔-어떻게-해볼까-1">🤔 어떻게 해볼까? (1)</h3>
<p>플러터에서 위젯을 이동시키려면 뭘 써야 할까?
가장 처음에 떠오른 것은 <code>Positioned</code>였다.</p>
<p><code>Positioned</code>는 <code>Stack</code> 위젯 내부에서만 쓸 수 있는 위젯인데 <code>top</code> <code>left</code> <code>bottom</code> <code>right</code> 값을 줘서
<code>Stack</code> 내부에서 원하는 위치에 위젯을 배치시킬 수 있다.</p>
<p>마치 <code>CSS</code>에서는 <code>display: absolute;</code> 안에 있는 요소들처럼 말이다.</p>
<h4 id="❌-하지만-쓸-수-없었다">❌ 하지만 쓸 수 없었다...</h4>
<p>우리 앱에서 회원가입 화면에 그대로 적용시키기에는 여러 문제가 있었다.</p>
<pre><code class="language-dart">// ...
Expanded(
    child: SingleChildScrollView(
        child: Column(
            children: [
                Padding( // 이메일 입력 칸
                    child: ...
                )
            ]
        )
    )
)</code></pre>
<p>이런 구조로 되어 있었기 때문에 부모를 <code>Stack</code>으로 감싸고 <code>Positioned</code>에 <code>top</code> 등 위치 값을 주면
플러터에서는 <code>Stack</code>의 크기를 정확히 모른다는 에러가 발생했다.</p>
<p>수정하기 위해서는 아마도, 이 화면을 다시 만들어야겠다는 생각이 들었고
그렇기에는 시간이 너무 지체될 것 같아서 다른 방법을 떠올려 보려 했다.</p>
<h3 id="🤔-어떻게-해볼까-2">🤔 어떻게 해볼까? (2)</h3>
<p><code>Positioned</code>를 쓸 수 있었다면 <code>AnimatedPositioned</code>를 활용해서
<code>top</code> 등, 위치 값에 변화를 줘서 애니메이션을 구현하려고 했었다.</p>
<h5 id="다른-방법으로-애니메이션을-만드는-방법이-있을까-👀">다른 방법으로 애니메이션을 만드는 방법이 있을까? 👀</h5>
<p><code>AnimatedBuilder</code>를 써보면 되지 않을까? 🧐
이 위젯 내부에서 값을 변화를 줘서 이동시켜보자는 생각이 들었다.</p>
<p>그렇다면 위치를 이동시켜주는 위젯으로 뭘 쓰면 좋을까?</p>
<p><code>Transform</code>이라는 위젯을 쓰면 우리가 원하는 애니메이션을 만들 수 있을 것 같았다.</p>
<blockquote>
<h5 id="transform">Transform</h5>
<p>자식 위젯을 회전, 크기 조정, 이동, 기울이기 등의 변형을 해주는 위젯</p>
</blockquote>
<ul>
<li><code>rotate</code> → <code>Transform.rotate(angle: pi / 4)</code> (45도 회전)</li>
<li><code>translate</code> → <code>Transform.translate(offset: Offset(5, 0))</code> (x로 5만큼 이동)</li>
<li><code>scale</code> → <code>Transform.scale(scale: 1.5)</code> (1.5배 확대)</li>
<li><code>skew</code> → <code>Transform.skewX(0.3)</code> (x축으로 기울이기)</li>
</ul>
<p>애니메이션으로 <code>Offset</code> 값만 변경시켜주면 될 것 같았다.</p>
<h4 id="⭕-정답입니다">⭕ 정답입니다~</h4>
<p>먼저 실제로 x 축으로 이동시킬 수 있는지 확인할 필요가 있었다.</p>
<pre><code class="language-dart">AnimatedBuilder(
    animation: // ...animation,
    builder: (context, child) {
        return Transform.translate(
            offset: const Offset(20, 0),
            child: child,
        );
    },
    child: Column() // 이메일 입력 폼
)</code></pre>
<p>위 코드와 같이 적용시켜 보니</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/6f7fe6e9-c24e-422a-a47f-6bc0103e459f/image.png" alt=""></p>
<p>내가 원하는 대로  x 축으로 20만큼 이동했다.</p>
<p>이제 <code>Offset</code>의 값을 애니메이션으로 위의 요구사항 대로 변경시켜주면 된다!</p>
<h2 id="📱-구현하기">📱 구현하기</h2>
<p>위젯도 이동시켰겠다, 어떻게 애니메이션을 만들어 보자.</p>
<p>먼저 든 생각은 아무리 간단한 애니메이션이라도 코드가 길어질 테니
분리시킬 필요가 있을 거라고 생각했다.</p>
<p>그래서 dart 언어의 <code>mixin</code> 클래스를 활용하기로 했다.
<code>mixin</code> 클래스에 애니메이션 로직을 만들고 사용한다면
실제 이메일 폼을 구현하는 코드에서는 간결해져서 보기 편해질 것이다.</p>
<blockquote>
<p>mixin
다중 상속 없이 여러 클래스에서 재사용할 수 있는 코드 블록
클래스를 <code>상속받지 않고</code>도 기능을 공유하여 사용할 수 있다.</p>
</blockquote>
<pre><code class="language-dart">mixin Bread {
    void buy() =&gt; print(&#39;우울해서 빵 샀어...&#39;);
}

class People with Bread {} // with 키워드로 Bread 적용

void main() {
    People().buy(); // 우울해서 빵 샀어...
}
</code></pre>
<h4 id="⌨️-애니메이션-작성">⌨️ 애니메이션 작성</h4>
<pre><code class="language-dart">mixin JitterAnimation&lt;T extends StatefulWidget&gt; on State&lt;T&gt;, TickerProviderStateMixin&lt;T&gt; {
    // ...
}</code></pre>
<p>먼저 위와 같이 <code>mixin</code>를 정의해 준다.</p>
<blockquote>
<p>애니메이션 개수에 따라서 다른 프로바이더를 써야한다.</p>
</blockquote>
<ul>
<li><code>SingleTickerProviderMixin</code> → 애니메이션이 한 개일 경우 사용한다.</li>
<li><code>TickerProviderStateMixin</code> → 애니메이션이 두 개 이상일 경우 사용한다.</li>
</ul>
<p>그런 다음 <code>AnimationController</code>와 <code>Animation</code>을 정의해 주면 된다.</p>
<pre><code class="language-dart">// ...
  late AnimationController jitterAnimationController1;
  late AnimationController jitterAnimationController2;
  late AnimationController jitterAnimationController3;
  late AnimationController jitterAnimationController4;
  late AnimationController jitterAnimationController5;

  late Animation&lt;double&gt; jitterAnimation1;
  late Animation&lt;double&gt; jitterAnimation2;
  late Animation&lt;double&gt; jitterAnimation3;
  late Animation&lt;double&gt; jitterAnimation4;
  late Animation&lt;double&gt; jitterAnimation5;

  @override
  void iniState() {
      super.initState();

    jitterAnimationController1 = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 100),
    );

    jitterAnimationController2 = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 100),
    );

    jitterAnimationController3 = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 100),
    );

    jitterAnimationController4 = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 100),
    );

    jitterAnimationController5 = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 100),
    );

    jitterAnimation1 = Tween&lt;double&gt;(begin: 0, end: 5).animate(
      CurvedAnimation(
        parent: jitterAnimationController1,
        curve: const Cubic(0.65, 0, 0.35, 1),
      ),
    );

    jitterAnimation2 = Tween&lt;double&gt;(begin: 0, end: -5).animate(
      CurvedAnimation(
        parent: jitterAnimationController2,
        curve: const Cubic(0.65, 0, 0.35, 1),
      ),
    );

    jitterAnimation3 = Tween&lt;double&gt;(begin: 0, end: 3).animate(
      CurvedAnimation(
        parent: jitterAnimationController3,
        curve: const Cubic(0.65, 0, 0.35, 1),
      ),
    );

    jitterAnimation4 = Tween&lt;double&gt;(begin: 0, end: -3).animate(
      CurvedAnimation(
        parent: jitterAnimationController4,
        curve: const Cubic(0.65, 0, 0.35, 1),
      ),
    );

    jitterAnimation5 = Tween&lt;double&gt;(begin: 0, end: 1).animate(
      CurvedAnimation(
        parent: jitterAnimationController5,
        curve: const Cubic(0.65, 0, 0.35, 1),
      ),
    );
  }

  // 컨트롤러를 해제시켜서, 메모리 관리.
  @override
  void dispose() {
    jitterAnimationController1.dispose();
    jitterAnimationController2.dispose();
    jitterAnimationController3.dispose();
    jitterAnimationController4.dispose();
    jitterAnimationController5.dispose();
    super.dispose();
  }</code></pre>
<p>애니메이션을 차례대로 진행시키기 위해서 저번에도 활용했었던
컨트롤러에 리스너를 추가해서 차례대로 실행시키기로 했다.</p>
<pre><code class="language-dart">jitterAnimationController1.addStatusListener((status) async {
      if (status == AnimationStatus.completed) {
        jitterAnimationController2.forward();
      }
});

// ... 4번까지 같음

    jitterAnimationController5.addStatusListener((status) async {
      if (status == AnimationStatus.completed) {
        jitterAnimationController5.reverse(); // 1에서 0으로 되돌아가기
        jitterAnimationController1.reset(); // 초기 상태로 되돌리기
        jitterAnimationController2.reset();
        jitterAnimationController3.reset();
        jitterAnimationController4.reset();
      }
    });
  }
</code></pre>
<p>마지막으로 이 애니메이션을 실행시켜줄 트리거를 만들어준다.</p>
<pre><code class="language-dart">void doJitter() {
    jitterAnimationConroller1.forward();
}</code></pre>
<h4 id="⌨️-위젯에-적용시키기">⌨️ 위젯에 적용시키기</h4>
<p><code>mixin</code>으로 정의된 것을 쓰고 싶다면 <code>with</code> 키워드로 상속받듯이 작성해주면 된다. </p>
<pre><code class="language-dart">// ...
class _JoinEmailScreenState extends ConsumerState&lt;JoinEmailScreen&gt;
    with TickerProviderStateMixin, JitterAnimation {
// ...
AnimatedBuilder(
    animation: Listenable.merge([ // 여러 Listenable 객체를 하나로 합쳐서 한 번게 감지할 수 있게 하는 기능
        jitterAnimation1,
        jitterAnimation2,
        jitterAnimation3,
        jitterAnimation4,
        jitterAnimation5,
    ]),
    builder: (context, child) {
    // 모든 애니메이션의 value 값을 더함
    final offsetX = jitterAnimation1.value +
                    jitterAnimation2.value +
                    jitterAnimation3.value +
                    jitterAnimation4.value +
                    jitterAnimation5.value;
        return Transform.translate(
            // 더해진 값들을 offset으로 전달
            offset: const Offset(offsetX, 0),
            child: child,
        );
    },
    child: Column() // 이메일 입력 폼
)</code></pre>
<p>이런 다음 이메일 형식에 맞지 않을 때마다 이 애니메이션을 실행시켜주면 된다.</p>
<pre><code class="language-dart">  void nextButtonClick() {
    if (isAllFieldsValid()) {
      context.pushNamed(JoinCheckScreen.routeName);
      } else {
         doJitter();
      }
    }
  }</code></pre>
<h3 id="🥳-결과">🥳 결과</h3>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/8041fc8b-af15-4724-91bd-7046cd09fe47/image.gif" alt=""></p>
<h3 id="🫨-햅틱-효과도-넣자">🫨 햅틱 효과도 넣자</h3>
<p>플러터에서 기본적으로 제공하는 <code>HapticFeedBack</code>이라는 클래스가 있어서
이걸 쓴다면 간단하게 햅틱 효과를 적용시킬 수 있다.</p>
<pre><code class="language-dart">void doJitter() {
    // ...
    HapticFeedBack.HeavyImpact();
}</code></pre>
<h4 id="hapticfeedback-주요-메서드">HapticFeedback 주요 메서드</h4>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>lightImpact()</td>
<td>가벼운 진동 (짧고 부드러움)</td>
</tr>
<tr>
<td>mediumImpact()</td>
<td>중간 강도의 진동 (조금 더 강함)</td>
</tr>
<tr>
<td>heavyImpact()</td>
<td>강한 진동 (뚜렷한 느낌)</td>
</tr>
<tr>
<td>selectionClick()</td>
<td>짧고 가벼운 클릭 느낌 (UI 요소 선택 시 사용)</td>
</tr>
<tr>
<td>vibrate()</td>
<td>OS 기본 진동</td>
</tr>
</tbody></table>
<p>자세한 진동 요구 사항은 없었기에 기본 제공하는 걸 썼지만
더 길게 진동이 되어야한다든지 등 다른 요구 사항이 있다면
어떻게 구현해야할지는... <del>그때 가서 생각해 보기로</del></p>
<h2 id="🔨-살짝-개선시키기">🔨 살짝 개선시키기</h2>
<p>코드를 다시 살펴보니,</p>
<h5 id="어차피-같은-시간으로-x로-이동하고-되돌아오는데-각각-컨트롤러-애니메이션을-만들어야-할까-🤔">어차피 같은 시간으로 x로 이동하고 되돌아오는데 각각 컨트롤러, 애니메이션을 만들어야 할까? 🤔</h5>
<p>이런 생각이 들었고 줄여도 되겠다, 싶었다.</p>
<p>그래서 1번 애니메이션을 진행시킨 다음 <code>reverse()</code>로 초기 상태로 돌아가고
돌아가는 시간을 기다리기 위해서 <code>Future.delay()</code>를 썼다.</p>
<p>또 애니메이션이 진행 중일 때 버튼을 여러 번 누르면 애니메이션이 중첩되어서 실행될 것 같아서
<code>isAnimation</code>이라는 플래그를 추가해서 애니메이션이 진행 중일 때는
중첩되지 않도록 추가했다.</p>
<pre><code class="language-dart">  late AnimationController jitterAnimationController1;
  late AnimationController jitterAnimationController2;
  late AnimationController jitterAnimationController3;

  late Animation&lt;double&gt; jitterAnimation1;
  late Animation&lt;double&gt; jitterAnimation2;
  late Animation&lt;double&gt; jitterAnimation3;

  bool isAnimation = false; // 애니메이션 진행 중인지 확인하기 위해</code></pre>
<pre><code class="language-dart">    jitterAnimationController1 = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 100),
    );

    jitterAnimationController2 = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 100),
    );

    jitterAnimationController3 = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 100),
    );

    jitterAnimation1 = Tween&lt;double&gt;(begin: 0, end: 5).animate(
      CurvedAnimation(
        parent: jitterAnimationController1,
        curve: const Cubic(0.65, 0, 0.35, 1),
      ),
    );

    jitterAnimation2 = Tween&lt;double&gt;(begin: 0, end: 3).animate(
      CurvedAnimation(
        parent: jitterAnimationController2,
        curve: const Cubic(0.65, 0, 0.35, 1),
      ),
    );

    jitterAnimation3 = Tween&lt;double&gt;(begin: 0, end: 1).animate(
      CurvedAnimation(
        parent: jitterAnimationController3,
        curve: const Cubic(0.65, 0, 0.35, 1),
      ),
    );</code></pre>
<pre><code class="language-dart">jitterAnimationController1.addStatusListener((status) async {
      if (status == AnimationStatus.completed) {
          jitterAnimationController1.reverse();
        await Future.delay(const Duration(milleSecond: 100));
        jitterAnimationController2.forward();
      }
});

// ... 2번까지 같음

    jitterAnimationController3.addStatusListener((status) async {
      if (status == AnimationStatus.completed) {
        jitterAnimationController3.reverse(); // 1에서 0으로 되돌아가기
        // reverse()로 초기 상태로 돌아갔기 때문에 reset()은 쓰지 않아도 됨.
        isAnimation = false;
      }
    });
  }</code></pre>
<pre><code class="language-dart">void doJitter() {
    if(isAnimation) return;
    isAnimation = true;
    jitterAnimationController1.forward();
}</code></pre>
<hr />
<br />

<p>with <a href="https://velog.io/@designer_julie/%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%82%AC%EC%9A%A9%EC%84%B1-%EA%B0%9C%EC%84%A0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2.-%ED%94%84%EB%A1%9C%ED%86%A0%ED%8C%8C%EC%9D%B4Protopie%EB%A1%9C-%EC%97%90%EB%9F%AC%EB%A9%94%EC%84%B8%EC%A7%80-%EC%B4%89%EA%B0%81%ED%9A%A8%EA%B3%BC-%ED%96%85%ED%8B%B1haptic-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">디자이너의 벨로그</a> 그리고 <a href="https://velog.io/@gyeore/Flutter-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-Haptic-Feedback%ED%96%85%ED%8B%B1%EC%A2%8C%EC%9A%B0-%EC%9D%B4%EB%8F%99">팀원의 벨로그</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter 애니메이션 따라하기 01]]></title>
            <link>https://velog.io/@r0_0r/Flutter-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-01</link>
            <guid>https://velog.io/@r0_0r/Flutter-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%94%B0%EB%9D%BC%ED%95%98%EA%B8%B0-01</guid>
            <pubDate>Mon, 03 Feb 2025 12:37:51 GMT</pubDate>
            <description><![CDATA[<p>나와 팀원 그리고 디자이너는 올해, 더 성장하기 위해서 플러터로 구현할 수 있는
애니메이션을 연습해 보고, 우리 앱에 적용시켜 보기로 했다.</p>
<p>쉬운 것부터 하나씩, 서로 구현해 보고 싶은 애니메이션을 하기로 했다.</p>
<p>그 첫 번째로 아래와 같은 애니메이션이다.
<br />
<br />
<br /></p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/f90853a3-8b5b-4a53-a4db-aa8581a9e825/image.gif" alt=""></p>
<br />
<br />
<br />

<p>먼저, 디자이너가 피그마로 이 애니메이션을 만들어 주었다.
나는 피그마를 참고해서 Curve 값이나, 속도 등을 그대로 적용시키기로 했다.
<br />
<br />
<br /></p>
<h3 id="linear인-줄-알았는데">Linear인 줄 알았는데!</h3>
<p>처음 만들 때, 값을 제대로 확인하지 않고 &#39;이거, 같은 속도로 커지고 있다&#39;고 생각해서
플러터에서 기본적으로 제공해 주는 <code>Curves.linear</code>를 적용시켜보았다.
하지만...</p>
<br />
<br />
<br />

<p><img src="https://velog.velcdn.com/images/r0_0r/post/3b334e61-84f7-4399-924b-7bdc6cbc4ca1/image.gif" alt=""></p>
<br />
<br />
<br />



<p>커지는 느낌이 너무 많이 달랐다.
그래서 값을 제대로 봐야겠다 싶어 피그마 값을 확인했다.</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/19344acb-b256-4e55-bb3a-17129cb3653d/image.png" alt=""></p>
<p>커지는 애니메이션에는 가속도가 있었다...
그렇다면, 내가 원하는 대로 커브 값을 줄 수 있을까?</p>
<p>물론 가능하다.</p>
<p>Curves의 구조를 보면 <code>Cubic</code>으로 직접 값을 줘서 구현한 것을 알 수 있는데
그렇다면 우리도 직접 값을 주면 점점 가속되거나 감속시킬 수 있지 않을까?</p>
<pre><code class="language-dart">const Cubic(a,b,c,d);
// a와 b: 곡선의 시작 제어점(x1, y1)
// c와 d: 곡선의 끝 제어점(x2, y2)
// 0 &lt;= x &lt;= 1 범위의 값만 줘야함.</code></pre>
<p><code>Stiffness</code> <code>Damping</code> <code>Mass</code> 값을 어떤 식으로 계산해서 넣어야 하는지는
<del>어려워서</del> 채찍피티와 함께 했다.</p>
<p>그래서 구해준 값은...</p>
<pre><code class="language-dart">const Cubic(0.23, 0.86, 0.29, 1)</code></pre>
<p><a href="https://velog.velcdn.com/images/r0_0r/post/1cfde96a-a661-42cc-9179-52d46e31dce1/image.gif"></a></p>
<p>이 값을 적용시켜 주니
<br />
<br />
<br /></p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/1b97ffce-3f23-4492-8209-282cb9fe2e8d/image.gif" alt=""></p>
<br />
<br />
<br />


<h4 id="비슷해졌다">비슷해졌다!</h4>
<p>그 다음은 작아지면서 약간 통통 튀는 듯한 애니메이션을 줘야 한다.
즉, 애니메이션이 두 개가 연속으로 보여진다는 것이다.</p>
<p>어떻게 애니메이션을 연속으로 보여줄까 생각하다가
<code>AnimationController</code>에는 리스너를 추가할 수 있었고
<code>AnimaitionStatus</code>라는 enum도 있었다.</p>
<p>1번 애니메이션이 끝난다면, 2번 애니메이션을 진행시키자! 라는 생각이 들었다.</p>
<pre><code class="language-dart">    extendController.addStatusListener((status) async {
      if (status == AnimationStatus.completed) {
        await Future.delayed(const Duration(milliseconds: 200));
        bounceController.forward();
      }
    });</code></pre>
<p>위와 같이 작성했다.
200ms 딜레이를 넣은 이유는 첫 번째 애니메이션과 두 번째 애니메이션에 딜레이가 있다고 느꼈기 때문.</p>
<p>그리고, 애니메이션을 한 번만 재생시킬 게 아니기 때문에
두 번째 애니메이션이 끝나면 초기 상태로 되돌려 놓아야 했다.</p>
<pre><code class="language-dart">   bounceController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        extendController.reset();
        bounceController.reset();
      }
    });</code></pre>
<p>이런 식으로.</p>
<h3 id="bounce-느낌이-암튼-다름">bounce 느낌이 암튼 다름...</h3>
<p>flutter에서는 여러 Curves를 제공해 주는데 그 중에서 당연히 튀기는 듯한 느낌은 <code>bounceOut</code>이 있다.
바로 적용시켜 보았는데...
<br />
<br />
<br /></p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/e41d2ded-3d30-4e5d-a2e0-d5630bd1b3e5/image.gif" alt=""></p>
<br />
<br />
<br />

<p>음... 뭔가 강도가 약한 듯한 느낌이 들었다.
속도를 줄이면 너무 빨리 끝나서 튀기는 듯한 느낌이 들지 않았고
크기를 키우거나 줄이기에는 피그마의 애니메이션과 느낌이 매우 달랐다.</p>
<p>그렇다면 이것은 Curve를 커스텀해야 되겠다고 생각했다...</p>
<pre><code class="language-dart">class CustomBounceCurve extends Curve {
  @override
  double transform(double t) {
    const double amplitude = 0.5;
    const double period = 0.4;

    if (t == 0.0) return 0.0;
    if (t == 1.0) return 1.0;

    return 1.0 -
        amplitude *
            pow(2.0, -10.0 * t) *
            sin((t - period / 4.0) * (2 * pi) / period);
  }
}</code></pre>
<p>위 코드인데, 어떻게 구현해야 될지 몰라서
이것도 <del>채찍피티</del>와 함께 했다.</p>
<p>나중에는 함께 하지 않고도 구현할 수 있으면 좋겠다...</p>
<p>아무튼 이 Custon Curve를 적용시키고 보니
<br />
<br />
<br /></p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/531f32d9-7dfc-45bb-8e90-a06754028d30/image.gif" alt=""></p>
<br />
<br />
<br />

<h4 id="비슷해졌다-1">비슷해졌다!</h4>
<br />
<br />
<br />

<p>정말 간단한 애니메이션인데도 많이 복잡했다.
그래도 이것저것 찾아보면서 재미는 있었다.</p>
<p>아직 복잡한 애니메이션을 구현할 실력은 없지만,
계속 하다 보면 언젠가 구현하게 될 수 있지 않을까...
<br />
<br />
<br />
<del>GG</del></p>
<br />
<br />
<br />

<p>이것저것 찾아보다가 좋은 라이브러리를 발견했다.
바로 <code>Sprung</code>이라는 라이브러리이다.</p>
<p><code>Spring animation</code>을 구현해주는 라이브러리인데
피그마에서 mass, stiffness, damping 값을 넘겨주면 알아서 적절한 커브를 만들어준다...!</p>
<pre><code class="language-dart">    extendAnimation = Tween&lt;double&gt;(begin: start, end: end).animate(
      CurvedAnimation(
        parent: extendController,
        curve: Sprung.custom(
          mass: 1,
          stiffness: 250,
          damping: 31.6,
        ),
      ),
    );</code></pre>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/61e9414a-7c28-4795-b712-f12262b09485/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UI/UX] 디자인 QA와 앱 사용성 개선을 위해.]]></title>
            <link>https://velog.io/@r0_0r/UIUX-%EB%94%94%EC%9E%90%EC%9D%B8-QA%EC%99%80-%EC%95%B1-%EC%82%AC%EC%9A%A9%EC%84%B1-%EA%B0%9C%EC%84%A0%EC%9D%84-%EC%9C%84%ED%95%B4</link>
            <guid>https://velog.io/@r0_0r/UIUX-%EB%94%94%EC%9E%90%EC%9D%B8-QA%EC%99%80-%EC%95%B1-%EC%82%AC%EC%9A%A9%EC%84%B1-%EA%B0%9C%EC%84%A0%EC%9D%84-%EC%9C%84%ED%95%B4</guid>
            <pubDate>Mon, 03 Feb 2025 11:41:00 GMT</pubDate>
            <description><![CDATA[<p>입사하고 나서 첫 앱을 출시했다. v^-^/</p>
<p>시작하기 전에는 과연 끝낼 수 있을까 싶었는데...
<del>어떻게든 되는 법이더라</del></p>
<p>하지만, 첫 배포 후에 보완해야 될 점이 정말 산더미처럼 많았다.</p>
<p>나와 팀원의 첫 프로젝트이고 출시까지 주어진 시간이 많이 짧았지만
무엇보다 앱 개발에 대한 지식이 거의 전무했던 점이 컸다.</p>
<p>그래서 우리 팀은 디자이너와 함께 디자인 QA를 진행하며
앱을 개선시키기로 했다.</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/9a4ba089-a1b1-47b9-a00b-e18986069de2/image.png" alt="">
그중에 첫 번째로 터치 영역인데,
사용하는 사람마다 &#39;잘 눌리지 않는다&#39;라는 말이 나왔다.</p>
<p>보통 아이콘 크기는 24px이 많이 쓰이는데, 터치 영역을 아이콘 만큼만 지정했기 때문에
&#39;정확히 눌러야만&#39; 터치했을 때 원하는 동작이 실행된다.</p>
<p>모바일에서는 &#39;한 손으로&#39; &#39;엄지 손가락&#39;으로 앱을 사용하는 경우가 대부분이기에
매번 정확히 누르기란 힘들 것이다.</p>
<p>그렇기에 터치 영역을 아이콘보다는 넓게 잡아서 앱 사용성을 좋게 만들어야 한다.</p>
<p>터치 영역 관련 수정은 내가 아닌 팀원이 정말 많이 고생해줬다...
<del>나의 잘못이 여기저기에...</del></p>
<br />
<br />
<br />

<p>그 다음은, &#39;슬라이드&#39; 인터렉션에서 문제가 있었다.
<br />
<br />
<br />
<br /></p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/e82da83f-bf1c-4012-8f1e-a1deea3df5d3/image.gif" alt=""></p>
<p>위 화면을 자세히 보면 이전 문항에서 다음 문항으로 이동될 때
이전 문항과 다음 문항이 셔플되고(겹치고) 있기 때문에 사용자가 보기엔
어지럽고, 복잡해 보인다.</p>
<p>위 화면은 단순히 이전에서 다음 문항으로 이동하는 것만 있어서 덜 복잡하게 느낄 수도 있겠지만
다음 문항에서 이전 문항으로 이동될 때는 빙빙 돌 만큼 어지럽다.</p>
<p>그렇기에 셔플(겹치지)하지 않고 한 방향으로 흐를 수 있도록 수정할 필요가 있었다.</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/dd446da4-33a4-465a-8286-7d8a4b3450b4/image.gif" alt=""></p>
<h4 id="편안해졌다">편안해졌다!</h4>
<p>잘 만들어진 앱을 사용해 보면 정말 편안하다는 느낌을 받는다.</p>
<p>그 편안하다는 느낌이 들게 하기 위해, 얼마나 많은 시행착오 그리고 사용자의 시선을 고려했을까.
대단하다는 생각이 든다.</p>
<p>앞으로 이런 식으로 우리 팀은 디자이너와 꾸준히 소통하면서 앱을 개선해 나갈 생각이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트의 전역 상태 관리]]></title>
            <link>https://velog.io/@r0_0r/%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%9D%98-%EC%A0%84%EC%97%AD-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@r0_0r/%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%9D%98-%EC%A0%84%EC%97%AD-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Wed, 04 Dec 2024 07:05:41 GMT</pubDate>
            <description><![CDATA[<h1 id="context-api">Context API</h1>
<p><code>Context API</code>는 리액트 v16.3에 추가됐다.
이걸 활용하면 전역 상태 관리를 쉽게 사용할 수 있다.</p>
<h2 id="문제점">문제점</h2>
<p>하지만 이걸 전역 상태 관리로 쓰기에는 적합하지 않는 경우가 있다.</p>
<h3 id="성능-문제">성능 문제</h3>
<p>Context API 값을 구독한 모든 컴포넌트가 리렌더링되는 문제점이 있다.
만약 Context API로 상태를 전달했다면, 불필요한 컴포넌트까지 리렌더링이 돼서 성능이 하락할 수도 있다.</p>
<h3 id="한-가지-상태만-관리-가능">한 가지 상태만 관리 가능</h3>
<p>앱이 커지면 관리해야될 상태가 많아지게 되는데
그러면 Context API를 사용하는 개수가 늘어나게 돼서 관리하기 어렵게 된다.</p>
<pre><code class="language-typescript">&lt;AuthContext.Provider&gt;
  &lt;ThemeContext.Provider&gt;
      &lt;DataContext.Provider&gt;
          // ...
      &lt;/DataContext.Provider&gt;
  &lt;/ThemeContext.Provider&gt;
&lt;/AuthContext.Provider&gt;</code></pre>
<h3 id="다른-훅과-같이-써야-업데이트-가능">다른 훅과 같이 써야 업데이트 가능</h3>
<p>Context API는 단순히 상태를 전달하는 역할만 해서
<code>useReducer</code>나 <code>setState</code>를 같이 전달해야 상태를 업데이트할 수 있다.</p>
<h1 id="전역-상태-관리-라이브러리를-쓰는-이유">전역 상태 관리 라이브러리를 쓰는 이유</h1>
<p><code>Props Drilling</code> 문제를 해결할 수 있다.</p>
<p>상태 관리 라이브러리들은 리렌더링 최적화 기능을 제공하는 경우가 있어서 불필요한 렌더링을 방지해준다.</p>
<p>상태의 변화를 추적하고 디버깅하기가 수월해진다.</p>
<h1 id="redux">Redux</h1>
<p><code>Redux</code>는 <code>Flux</code> 패턴을 따르는 자바스크립트에서 대표적인 상태 관리 라이브러리이다.</p>
<p><code>Flux</code> 패턴은 단방향으로 데이터가 흐르는 패턴이다.
단방향으로 데이터가 이동하므로, 데이터 예측과 디버깅을 쉽게 할 수 있다.</p>
<h2 id="flux">Flux</h2>
<h3 id="action">Action</h3>
<p><code>&quot;무슨 일이 일어나고 있는지?&quot;</code>
액션은 어떤 일이 있는지 정의한다.</p>
<pre><code class="language-typescript">{
  type: &quot;ADD&quot;, // 어떤 일인지
  payload: {id: 1, count: 1} // 데이터
}</code></pre>
<h3 id="dispatch">Dispatch</h3>
<p>디스패치는 액션을 스토어에 전달한다.
앱에서 어떤 일이 생겨야하는지 스토어에 알려주는 역할이다.</p>
<pre><code class="language-typescript">dispatch({type: &quot;ADD&quot;, payload:{id: 1, count: 1}});</code></pre>
<h3 id="스토어">스토어</h3>
<p>스토어는 앱의 전체 상태를 저장 및 관리하는 곳이다.
디스패치를 통해 액션을 받으면, 업데이트하고 새로운 상태를 만든다.</p>
<h3 id="리듀서">리듀서</h3>
<p>리듀서는 스토어가 상태를 업데이트할 때 사용하는 함수다.</p>
<pre><code class="language-typescript">function addCount(state = [], action) {
  switch(action.type) {
    case &quot;ADD&quot;:
      // 로직...
      return [...state, action.payload];
    default:
      return state;
  }
}</code></pre>
<h3 id="subscribe">Subscribe</h3>
<p>스토어는 상태가 바뀌면 모든 구독자들에게 알려준다.
React에서는 이를 이용해서 리렌더링을 하게 된다.</p>
<h2 id="장점">장점</h2>
<p>스토어 한 곳에서 상태를 괸리하게 되므로 편리하고 복잡한 상태 관리가 쉬워진다.
정해진 규칙에 따라 상태를 변경하므로, 변화를 예측하기 쉽다.
리덕스에서 제공하는 개발 도구를 사용해서 디버깅이 수월해진다.
여러 미들웨어 기능이 있어, 비동기 작업이나 로깅 등 기능 추가가 쉽다.
<code>redux-toolkit</code>를 사용하면 리덕스를 쉽게 사용할 수 있다.</p>
<h2 id="단점">단점</h2>
<p>&#39;액션&#39;, &#39;리듀서&#39;, &#39;스토어&#39; 같은 개념을 알아둬야한다.</p>
<p>아주 작은 기능이라고 해도, 리덕스로 구현하려면
액션, 리듀서 등, 미리 작성해야 하는 코드량이 많다.</p>
<p>추가 도구 없이(미들웨어 등) 기본 리덕스만으로는 한계가 있다.
<code>redux-thunk</code>: 비동기 작업 처리
<code>redux-saga</code>: 복잡한 비동기 흐림 관리
<code>redux-logger</code>: 상태 변경 로그</p>
<p>리덕스와 다른 미들웨어까지 학습해야 하므로 러닝커브가 높아진다.</p>
<h2 id="redux-toolkit-사용해보기">redux-toolkit 사용해보기</h2>
<p><code>redux-toolkit</code>를 쓰면 그냥 리덕스를 썼을 때보다
더 쉽게 상태 관리를 할 수 있다.</p>
<p><code>npm install redux redux-react @reduxjs/toolkit</code>
먼저 위와 같이 총 3개를 설치해줘야 한다.</p>
<h3 id="store-작성">store 작성</h3>
<p>스토어는 앱의 전체 상태를 저장 및 관리하는 곳이다.</p>
<pre><code class="language-typescript">// redux/store.ts
import { configureStore } from &quot;@reduxjs/toolkit&quot;;

// 스토어 생성
const store = configureStore({
    reducer: {
      // reducer를 작성하는 곳.
    },
});

export default store;</code></pre>
<p>그 다음 앱을 프로바이더로 감싸서 어느 곳에서든 스토어를 사용할 수 있게 해줘야 한다.</p>
<pre><code class="language-tsx">// main.tsx

import { Provider } from &#39;react-redux&#39;
import store from &#39;./redux/store.ts&#39;

createRoot(document.getElementById(&#39;root&#39;)!).render(
    &lt;Provider store={store}&gt;
      &lt;App /&gt;
    &lt;/Provider&gt;
);
</code></pre>
<h3 id="slice-작성">slice 작성</h3>
<p><code>slice</code>는 <code>redux-toolkit</code>에서 상태 관리의 기본 단위이다.
<code>state</code>와 <code>reducer</code> <code>action</code>을 한 곳에 모아둔 곳이다.</p>
<pre><code class="language-tsx">// redux/countSlice.tsx
const initialState = {
  count: 0,
}</code></pre>
<p>먼저 상태의 초기 값을 선언해 준다.
그 다음 <code>createSlice</code>로 slice를 생성하면 된다.</p>
<pre><code class="language-tsx">// ...
import { createSlice } from &quot;@reduxjs/toolkit&quot;;

export const countSlice = createSlice({
  name: &#39;count&#39;, // slice의 이름을 지정.
  initialState, // 상태의 초기 값
  reducers: {
    // 리듀서를 작성
    add: (state, action) =&gt; {
      // action에는 type과 payload가 들어있다.
      return state + action.payload;
    },
    sub: (state, action) =&gt; {
      return state - action.payload;
    },
  },
});</code></pre>
<p>작성한 다음 이것을 다른 곳에서 쓸 수 있게 export 해주면 된다.</p>
<pre><code class="language-tsx">// ...
const reducer = countSlice.reducer;
export const countActions = countSlice.actions;
export default reducer;</code></pre>
<p>작성이 끝난다면 이 리듀서를 스토어에 등록해줘야 한다.</p>
<pre><code class="language-tsx">// redux/store.ts

import { configureStore } from &quot;@reduxjs/toolkit&quot;;
import countReducer from &#39;./countSlice&#39;;

const store = configureStore({
    reducer: {
        counter: countReducer, // 추가된 부분
    },
})</code></pre>
<h3 id="사용하기">사용하기</h3>
<p>실제 사용할 컴포넌트에서 사용려면 <code>useDispatch</code> <code>useSelector</code>를 사용하면 된다.</p>
<pre><code class="language-tsx">// counter.tsx
import { useDispatch, useSelector } from &#39;react-redux&#39;;
import { countActions } from &#39;@/redux/countSlice&#39;;

const Counter = () =&gt; {
      const { add, sub } = countActions;
    const dispatch = useDispatch();
      const count = useSelector((state) =&gt; state.count);

    const handleAdd = () =&gt; {
        dispatch(add(10));
    }
    const handleSub = () =&gt; {
        dispatch(sub(10));
    }
    return (
        &lt;div&gt;
            &lt;h2&gt;{counter}&lt;/h2&gt;
            &lt;div&gt;
                &lt;button onClick={handleAdd}&gt;++&lt;/button&gt;
                &lt;button onClick={handleSub}&gt;--&lt;/button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    )
};

export default Counter;
</code></pre>
<h2 id="jotai">Jotai</h2>
<p><code>Recoil</code>이라는 상태 관리 라이브러리에 영감을 받아 만들었다고 한다.
<code>Atomic</code> 방식으로 상태 관리를 한다고 한다.</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/7feb6b10-40e0-4929-9139-fb5d7106d3ec/image.jpg" alt=""></p>
<p>작은 원자들을 하나씩 쌓아서 큰 형태로 만들어 나가는 느낌(bottom-up)으로 앱이 만들어진다고 한다.</p>
<p>리액트의 <code>useState</code>와 비슷하게 사용할 수 있어서 러닝 커브가 낮다.</p>
<h3 id="설치">설치</h3>
<p><code>npm install jotai</code></p>
<h3 id="상태-만들기">상태 만들기</h3>
<pre><code class="language-tsx">import { atom, useAtom } from &quot;jotai&quot;;

const counter = atom&lt;number&gt;(0);

const App = () =&gt; {
  const [count, setCount] = useAtom(counter);

  return(
    &lt;div&gt;
      {count}&lt;br /&gt;
      &lt;button onClick={()=&gt;setCount(prev =&gt; prev+1)}&gt;&lt;/button&gt;
    &lt;/div&gt;
  )
}

export default App;</code></pre>
<p>마치 <code>useState</code>를 쓰듯이 사용하면 된다.</p>
<h3 id="읽기-전용-쓰기-전용">읽기 전용, 쓰기 전용</h3>
<p><code>jotai</code>는 읽기 전용으로도 쓰기 전용으로도 쓸 수 있다.</p>
<pre><code class="language-tsx">// ...
const count = useAtomValue(counter); // 읽기
const setCount = useSetAtom(counter); // 쓰기
// ...</code></pre>
<p>좀 더 복잡한 작업을 해서 상태를 변경시킬 수도 있다.</p>
<pre><code class="language-tsx">const counterAction = atom((get) =&gt; get(counter), (get, set) =&gt; {
  const value = get(counter);
  const newValue = value + 1;
  set(counter, newValue);
});</code></pre>
<p>이 action도 읽기, 쓰기만 할 수 있다.</p>
<pre><code class="language-tsx">// ...
const counterWithString = atom((get) =&gt; get(counter) + &#39; 번&#39;);
const onlyCounterWrite = atom(null, (get, set, newValue) =&gt; {
  set(counter, newValue);
});
// ...</code></pre>
<p>비동기 atom도 지원해주는데
redux였다면 <code>redux-thunk</code> 같은 라이브러리를 설치해서 사용하는 경우가 많지만, <code>jotai</code>는 내부적으로 지원해준다.</p>
<pre><code class="language-tsx">// ...
export const asyncAtom = atom(async (get) =&gt; {
    await new Promise((resolve) =&gt; setTimeout(resolve, 5000));
    return get(counter);
});
// ...</code></pre>
<p>이렇게 <code>async</code>로 atom을 만든 다음 사용할 때는
jotai에서 자체적으로 제공하는 <code>loadable</code>를 사용하면 된다.</p>
<pre><code class="language-tsx">// ...
  const loadableAtom = loadable(asyncAtom);
  const [value] = useAtom(loadableAtom);
// ...
&lt;div&gt;
  {
      value.state === &#39;hasError&#39; &amp;&amp; &lt;div&gt;ERROR&lt;/div&gt;
  }
  {
      value.state === &#39;loading&#39; &amp;&amp; &lt;div&gt;LOADING&lt;/div&gt;
  }
  {
      value.state === &#39;hasData&#39; &amp;&amp; &lt;div&gt;{value.data}&lt;/div&gt;
  }
&lt;/div&gt;</code></pre>
<h3 id="provider">Provider</h3>
<p>jotai는 기본적으로 전역으로 상태 관리를 해주지만
따로 분리시켜서 상태를 관리할 수 있다.</p>
<p>동일한 atom을 사용하더라도, Provider로 감싸면 내부의 상태는 독립적으로 작동한다.</p>
<pre><code class="language-tsx">import React from &#39;react&#39;;
import { atom, useAtom, Provider } from &#39;jotai&#39;;

const countAtom = atom(0);

const Counter = () =&gt; {
  const [count, setCount] = useAtom(countAtom);
  return (
    &lt;div&gt;
      &lt;p&gt;Count: {count}&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount((c) =&gt; c + 1)}&gt;Increment&lt;/button&gt;
    &lt;/div&gt;
  );
};

const App = () =&gt; (
  &lt;div&gt;
    &lt;Provider&gt;
      &lt;h1&gt;Counter 1&lt;/h1&gt;
      &lt;Counter /&gt;
    &lt;/Provider&gt;
    &lt;Provider&gt;
      &lt;h1&gt;Counter 2&lt;/h1&gt;
      &lt;Counter /&gt;
    &lt;/Provider&gt;
  &lt;/div&gt;
);

export default App;</code></pre>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/b0262865-50b7-4e1a-a33d-128251e658ba/image.PNG" alt=""></p>
<h2 id="zustand">Zustand</h2>
<p><code>redux</code>와 비슷하게 스토어를 두고 상태 관리를 한다.</p>
<h3 id="설치-1">설치</h3>
<p><code>npm i zustand</code></p>
<h3 id="상태-만들기-1">상태 만들기</h3>
<p>스토어를 만든 다음, 초기값과 변경시킬 액션을 선언해 주면 된다.
보통 이름은 <code>use(이름)Store</code>으로 많이 한다.</p>
<pre><code class="language-tsx">// store/count.ts
import { create } from &#39;zustand&#39;;

export const useCountStore = create((get, set) =&gt; {
    count: 0,
      add: () =&gt; {
        const { count } = get();
        set({ count: count + 1 });
      },
});</code></pre>
<p><code>get</code>은 상태에서 값을 가져올 수 있는 함수이고
<code>set</code>은 상태의 값을 변경시킬 수 있는 함수이다.</p>
<p><code>get</code>을 사용하지 않고 상태를 변경시킬 수도 있다.</p>
<pre><code class="language-tsx">// ...
    sub: () =&gt; {
      set(state =&gt; ({ count: state.count - 1 }));
    }
// ...</code></pre>
<h3 id="상태-사용하기">상태 사용하기</h3>
<p>사용하고 싶은 컴포넌트에서 스토어 훅을 호출하면 상태와 액션을 얻을 수 있다.
상태가 변경될 경우, 컴포넌트는 다시 렌더링이 된다.</p>
<pre><code class="language-tsx">// App.tsx
import { useCountStore } from &#39;./store/count&#39;;

function App() {
  const count = useCountStore(state =&gt; state.count);
  const add = useCountStore(state =&gt; state.add);
  const sub = useCountStore(state =&gt; state.sub);
  return (
    &lt;div&gt;
      &lt;h2&gt;{count}&lt;/h2&gt;
      &lt;button onClick={add}&gt;++&lt;/button&gt;
      &lt;button onClick={sub}&gt;--&lt;/button&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p>콜백 없이도 스토어 훅에서 상태나 액션 등, 스토어 객체를 얻을 수 있지만
컴포넌트에서 사용하지 않는 상태가 변경되어도 렌더링되기 때문에 권장하진 않는다.</p>
<pre><code class="language-tsx">// ...
// count외에 min이나 max 등
// 이 컴포넌트에서 사용하지 않아도 min이 변경되면 다시 렌더링 된다.
const { count, add, sub } = useCountStore();

// ...</code></pre>
<p>보통 액션은 많이 작성되기 때문에 이를 분리시켜서 스토어를 생성할 수도 있다.</p>
<pre><code class="language-tsx">export const useCountStoreAction = create&lt;{
    count: number,
    actions: {
        add: () =&gt; void,
        sub: () =&gt; void,
    }
}&gt;(set =&gt; ({
    count: 0,
    actions: {
        add: () =&gt; set(state =&gt; ({ count: state.count + 1 })),
        sub: () =&gt; set(state =&gt; ({ count: state.count - 1 })),
    },
}));

// --- cut ---
  const count = useCountStoreAction(state =&gt; state.count);
  const { add, sub } = useCountStoreAction(state =&gt; state.actions);</code></pre>
<h3 id="미들웨어-사용">미들웨어 사용</h3>
<p><code>zustand</code>는 미들웨어를 사용할 수 있게 제공하는데
그 중 하나인 <code>immer</code>를 쓰면 중첩된 객체를 쉽게 변경할 수 있다.</p>
<p>먼저 <code>immer</code>를 설치한다.
<code>npm i immer</code></p>
<p>그 다음 아래와 같이 액션을 작성하면 된다.</p>
<pre><code class="language-tsx">import { immer } from &#39;zustand/middleware/immer&#39;;

export const useUserStore = create(
        immer&lt;UserState &amp; UserActions&gt;(set =&gt; ({
            ...userInit,
            actions: {
                signIn: () =&gt; {
                    set({
                        user: {
                            email: &#39;abc@acb.net&#39;,
                            displayName: &#39;abc&#39;,
                            isValid: true,
                        }
                    })
                },
                setDisplayName: name =&gt; {
                    // immer 사용으로 짧아짐
                    set(state =&gt; {
                        if (state.user) {
                            state.user.displayName = name;
                        }
                    })
                }
            }
        }))
)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 스타일링]]></title>
            <link>https://velog.io/@r0_0r/React-%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81</link>
            <guid>https://velog.io/@r0_0r/React-%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81</guid>
            <pubDate>Tue, 19 Nov 2024 02:22:40 GMT</pubDate>
            <description><![CDATA[<p>리액트에서 컴포넌트를 스타일링하는 방법은 여러 가지가 있다.
그 중에서 요구하는 스펙이나 자신에게 맞는 방법을 사용하면 된다.</p>
<h2 id="일반-css">일반 CSS</h2>
<p>가장 흔하고 일반적인 방식이다.
css 파일을 따로 만들고 클래스나 아이디, 태그에 따라 스타일링하는 방법이다.</p>
<h3 id="이름-짓는-규칙">이름 짓는 규칙</h3>
<p>프로젝트에 따라 이름 짓는 규칙은 다를 수도 있지만
가장 많이 쓰이는 방법은 <code>BEM 네이밍</code>이라는 것이 있다.</p>
<h4 id="bem">BEM</h4>
<blockquote>
<p><code>Block</code> <code>Element</code> <code>Modifier</code>의 약자이다.</p>
</blockquote>
<p>작명규칙</p>
<ul>
<li>태그, id를 사용하지 않고 class만 사용한다.</li>
<li>개발, 디버깅, 유지 보수를 위하여 CSS 선택자의 이름을 가능한 한 명확하게 만든다.</li>
<li>소문자, 숫자만을 이용해서 작명한다.</li>
<li>여러 단어의 조합은 <code>-</code>로 연결하여 작명한다.</li>
<li>외형 묘사가 아닌 구조적, 의미적인 이름을 짓는다.
class=&quot;red&quot; [<span style="color: red">X</span>] class=&quot;success&quot; [<span style="color: blue">O</span>]</li>
</ul>
<h5 id="block">Block</h5>
<p>서로 중첩될 수 있으며, 몇 겹으로 중첩되는 것도 허용된다.
상태가 아닌 용도를 나타내야 한다.</p>
<pre><code>&lt;!-- 용도: 에러인 것을 나타내고 있다. --&gt;
&lt;div class=&quot;error&quot;&gt;&lt;/div&gt;

&lt;!-- 잘못된 사용: 상태를 나타내고 있다. --&gt;
&lt;div class=&quot;violet&quot;&gt;&lt;/div&gt;</code></pre><p>외부의 환경에 의존적이지 않기 위해서
block 자체에 외부 여백(margin)이나, 위치(position)를 설정하지 않아야 한다.</p>
<h5 id="element">Element</h5>
<p>Block은 독립적이지만 Element는 블록에 의존적이다.
자식이 속한 불럭 내에서만 사용하며, 다른 곳에는 사용해서는 안 된다.</p>
<pre><code>&lt;!-- tab은 블록이고, __item은 엘리먼트이다. --&gt;
&lt;ul class=&quot;tab&quot;&gt;
    &lt;li class=&quot;tab__item&quot;&gt;1&lt;/li&gt;
    &lt;li class=&quot;tab__item&quot;&gt;2&lt;/li&gt;
    &lt;li class=&quot;tab__item&quot;&gt;3&lt;/li&gt;
&lt;/ul&gt;</code></pre><p>엘리먼트는 상태가 아닌 용도를 나타낸다.</p>
<pre><code>여떤 용도인가? [O]
item, text, button</code></pre><pre><code>어떻게 생겼는가? [X]
big, small</code></pre><h5 id="modifier">Modifier</h5>
<p>블럭이나, 엘리먼트의 속성이다.
상태 또는 동작을 정의한다.</p>
<pre><code>&lt;ul class=&quot;tab&quot;&gt;
  &lt;li class=&quot;tab__item tab__item--active&quot;&gt;1&lt;/li&gt;
  &lt;li class=&quot;tab__item&quot;&gt;2&lt;/li&gt;
  &lt;li class=&quot;tab__item&quot;&gt;3&lt;/li&gt;
&lt;/ul&gt;</code></pre><p>모양, 상태, 동작을 나타낸다.</p>
<ul>
<li>사이즈, 테마: size_small, theme_black</li>
<li>상태: disabled, focused, active, selected</li>
<li>동작: move_left-top</li>
</ul>
<p>Modifier는 블록, 엘리먼트의 모양, 상태, 동작을 변경하는 것이기 때문에
블록, 엘리먼트에 추가하여 사용한다.</p>
<h3 id="css-selector">CSS Selector</h3>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/85be9672-abfd-478f-a55b-7d55f018395f/image.png" alt="">
이것은 css의 기본적인 구조인데, <code>h1</code> 부분을 Selector(선택자)라고 한다.</p>
<h4 id="전체-셀렉터">전체 셀렉터</h4>
<pre><code class="language-css">&lt;!-- css reset --&gt;
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}</code></pre>
<p><code>*</code>은 HTML 문서 내의 모든 요소를 선택한다.
한꺼번에 적용해야 하는 스타일할 때 적용할 때 주로 사용한다.</p>
<h4 id="태그-셀렉터">태그 셀렉터</h4>
<pre><code class="language-css">p {
    font-size: 1.6rem;
}</code></pre>
<p>지정한 태그에만 스타일을 적용시킬 때 사용한다.</p>
<blockquote>
<p><code>rem</code>과 <code>em</code>은 상대적인 단위를 나타낸다. 둘의 차이는 참조 기준에 따라 결정된다.</p>
</blockquote>
<h5 id="rem-root-em">rem (Root em)</h5>
<p>HTML의 루트 요소의 글꼴 크기를 기준으로 한다.</p>
<pre><code class="language-css">html {
    font-size: 16px;
}
p {
    font-size: 1rem; /* 16px */
}
h1 {
    font-size: 2rem; /* 32px */
}</code></pre>
<h5 id="em-element-em">em (Element em)</h5>
<p>현재 요소의 글꼴 크기의 기준으로 한다.</p>
<pre><code class="language-css">html {
    font-size: 16px;
}
div {
    font-size: 1.5em; /* 24px */
}
div p {
    font-size: 1em; /* 24px */
}</code></pre>
<h4 id="id-셀렉터">ID 셀렉터</h4>
<pre><code class="language-css">#nav {
    position: absolute;
}</code></pre>
<p>선택한 id에 스타일을 적용시킬 때 사용한다.</p>
<h4 id="클래스-셀렉터">클래스 셀렉터</h4>
<pre><code class="language-css">.tab__item tab__item--active {
    color: blueviolet;
}</code></pre>
<p>선택한 class에 스타일을 적용시킬 때 사용한다.</p>
<h4 id="태그-id-클래스-복합-셀렉터">태그, ID, 클래스 복합 셀렉터</h4>
<p><code>태그#id</code> <code>태그.class</code> <code>id.class</code>
요소를 구체적으로 지정하여 스타일을 적용시킬 때 사용한다.</p>
<pre><code class="language-css">/* p태그이면서 selected라는 클래스를 가진 요소 */
p.selected {
    color: blueviolet;
}</code></pre>
<h4 id="어트리뷰트-셀렉터">어트리뷰트 셀렉터</h4>
<p>지정된 속성 값을 가지는 모든 요소를 선택하여 스타일을 적용시킬 때 사용한다.</p>
<pre><code class="language-css">a[href] {
    color: blueviolet;
}</code></pre>
<h4 id="후손-셀렉터">후손 셀렉터</h4>
<p>상위 요소의 하위에 속하는 모든 요소를 지정하여 스타일을 적용시킬 때 사용한다.</p>
<pre><code class="language-css">/* div 요소에 속해 있는 모든 p */
div p {
    font-color: white;</code></pre>
<h4 id="자식-셀렉터">자식 셀렉터</h4>
<p>상위 요소에 속해 있는 요소 중 바로 밑에 있는 요소를 지정하여 스타일을 적용시킬 때 사용한다.</p>
<pre><code class="language-css">div &gt; p {
    color: blueviolet;
}</code></pre>
<h4 id="인접-형제-셀렉터">인접 형제 셀렉터</h4>
<p>셀렉터 A의 형제 요소 중 셀렉터 A 바로 뒤에 위치하는 요소를 선택하여 스타일을 적용시킬 때 사용한다.
사이에 다른 요소가 존재하면 선택되지 않는다.</p>
<pre><code class="language-html">&lt;style&gt;
  p + ul {
      color: blueviolet;
  }
&lt;/style&gt;
&lt;p&gt;Text&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Text 01&lt;/li&gt;
&lt;/ul&gt;</code></pre>
<h4 id="일반-형제-셀렉터">일반 형제 셀렉터</h4>
<p>셀렉터 A의 형제 요소 중, 셀렉터 A 뒤에 위치하는 셀렉터 B 요소를 모두 선택하여 스타일을 적용시킬 때 사용한다.</p>
<pre><code class="language-html">&lt;style&gt;
p ~ ul {
    color: blueviolet;
}
&lt;/style&gt;
&lt;div&gt;
      &lt;!-- 적용되지 않는다 --&gt;
      &lt;ul&gt;
      &lt;li&gt;00&lt;/li&gt;
      &lt;/ul&gt;
    &lt;p&gt;Text&lt;/p&gt;
    &lt;!-- 적용된다 --&gt;
    &lt;ul&gt;
        &lt;li&gt;01&lt;/li&gt;
    &lt;/ul&gt;
    &lt;p&gt;Text&lt;/p&gt;
    &lt;ul&gt;
        &lt;li&gt;01&lt;/li&gt;
    &lt;/ul&gt;
&lt;/div&gt;</code></pre>
<h4 id="가상-클래스-셀렉터">가상 클래스 셀렉터</h4>
<p>마우스가 들어왔을 때(hover)의 스타일을 적용시킬 때 사용한다.</p>
<pre><code class="language-css">div:hover {
    color: blueviolet;
}</code></pre>
<h5 id="구조-가상-클래스-셀렉터">구조 가상 클래스 셀렉터</h5>
<p><code>:first-child</code>: 셀렉터에 해당하는 모든 요소 중 첫번째 자식인 요소를 선택
<code>:last-child</code>: 셀렉터에 해당하는 모든 요소 중 마지막 자식인 요소를 선택
<code>:nth-child(n)</code>: 셀렉터에 해당하는 모든 요소 중 앞에서 n번째 자식인 요소를 선택
<code>:nth-last-child(n)</code>: 셀렉터에 해당하는 모든 요소 중 뒤에서 n번째 자식인 요소를 선택
<code>:nth-child(odd)</code>: 셀렉터에 해당하는 모든 요소 중 홀수번째 자식인 요소 선택
<code>nth-child(even)</code>: 셀렉터에 해당하는 모든 요소 중 짝수번째 자식인 요소 선택</p>
<h5 id="부정-셀렉터">부정 셀렉터</h5>
<p><code>:not</code>: 셀렉터에 해다되지 않는 모든 요소를 선택.</p>
<pre><code class="language-css">input:not([type=password]) {
    color: blueviolet;
}</code></pre>
<h5 id="가상-요소-셀렉터">가상 요소 셀렉터</h5>
<p><code>::first-letter</code>: 콘텐츠의 첫글자를 선택
<code>::first-line</code>: 콘텐츠의 첫 줄을 선택. 블록 요소에만 적용할 수 있다
<code>::after</code>: 콘텐츠의 뒤에 위치하는 공간을 선택. 일반적으로 content 어트리뷰트와 함께 사용된다
<code>::before</code>: 콘텐츠의 앞에 위치하는 공간을 선택. 일반적으로 content 어트리뷰트와 함께 사용된다
<code>::selection</code>: 드래그한 콘텐츠를 선택. iOS Safari 등 일부 브라우저에서 동작하지 않을 수도 있다.</p>
<h5 id="css에서-변수-사용하기">css에서 변수 사용하기</h5>
<pre><code class="language-css">:root {
    --primary-color: #6200EE;
}
div {
    color: var(--primary-color);
}</code></pre>
<h3 id="sass">Sass</h3>
<p>Syntactically Awesome Style Sheets의 약자로 CSS 전처리기이다.
CSS를 더 효율적으로 작성할 수 있게 도와준다.</p>
<p>Sass는 <code>Scss</code>와 <code>Sass</code> 두 가지 구문을 지원하며
<code>Scss</code>는 css와 비슷한 문법을 사용하지만 <code>Sass</code>는 중괄호와 세미콜론을 사용하지 않아도 된다.</p>
<p><strong>SASS</strong></p>
<pre><code class="language-css">$primary-color: #0f0
$secondary-color: #f00

=button-styles
  background-color: $primary-color
  color: #fff
  padding: 10px 20px

button
  +button-styles

a
  color: $secondary-color</code></pre>
<p><strong>SCSS</strong></p>
<pre><code class="language-css">$primary-color: #0f0;
$secondary-color: #f00;

@mixin button-styles {
  background-color: $primary-color;
  color: #fff;
  padding: 10px 20px;
}

button {
  @include button-styles;
}

a {
  color: $secondary-color;
}</code></pre>
<p><code>Sass</code>는 css 전처리기이기 때문에 브라우저가 이해하지 못하므로 
<code>Sass</code>로 작성된 파일을 css로 컴파일 할 필요가 있다.
그래서 이를 변환해 주는 라이브러리를 설치해야 된다.
<del>npm install node-sass</del>
<code>node-sass</code>는 더 이상 지원하지 않는다고 해서 아래의 라이브러리를 설치했다.
(vite 환경)</p>
<pre><code>npm install -D sass-embedded</code></pre><h4 id="변수">변수</h4>
<p><code>$</code> 기호를 사용해서 변수를 정의할 수 있다.</p>
<pre><code class="language-css">/* 변수 정의 */
$primary-color: #6200EE
$secondary-color: #9654F4

/* 변수 사용 */
h1 {
    color: $primary-color;
}</code></pre>
<h4 id="중첩">중첩</h4>
<p>선택자를 중첩하여 작성할 수 있다. 이를 통해 가독성을 높일 수 있다.</p>
<pre><code class="language-css">nav {
    ul {
        margin: 0;
        padding: 0;
        list-style: none;
    }
    li {
        display: inline-block;
        margin-right: 10px;
        a {
            color: #fff;
            text-decoration: none;
            &amp;:hover {
                text-decoration: underline;
            }
        }
    }
}</code></pre>
<h4 id="mixin">Mixin</h4>
<p><code>@mixin</code>을 사용해서 스타일 블록을 정의하고
<code>@include</code>를 사용해서 해당 스타일 블록을 적용할 수 있다.
스타일을 재사용하고 더욱 간결하게 작성할 수 있다.</p>
<pre><code class="language-css">/* 믹스인 정의 */
@mixin text-style($font-size, $line-hiehgt, $text-color) {
    font-size: $font-size;
    line-hiehgt: $line-height;
    color: $text-color;
}

/* 믹스인 사용 */
h1 {
    @include text-style(24px, 1.2, #eee);
}
p {
    @include text-style(16px, 1.5, #666);
}</code></pre>
<h4 id="상속">상속</h4>
<p><code>@extend</code>를 사용해서 스타일을 상속할 수 있다.
중복되는 스타일을 줄일 수 있다.</p>
<pre><code class="language-css">/* 스타일 정의 */
.btn {
    display: iline-block;
    padding: 8px 16px;
    font-size: 14px;
    color: #fff;
    text-align: center;
    background-color: #EFE6FD;
    border-radius: 4px;
}

/* 스타일 상속 */
.btn-primary {
    @extend .btn;
    background-color: #5cb85c;
}</code></pre>
<h4 id="use">use</h4>
<p><code>sass</code>는 <code>@mixin</code>이나 변수를 다른 파일에 분리시켜서 가져올 수 있는데
예전에는 <code>@import</code>로 가져와서 썼지만 더 이상 사용할 수 없고
대신에 <code>@use</code>를 사용해서 가져와서 사용할 수 있다.</p>
<p><code>@use &#39;가져올 파일명&#39;</code>로 가져온 다음
사용할 때는 <code>가져온 파일명.변수명</code>으로 사용하면 된다.</p>
<pre><code class="language-css">@use &#39;utils&#39;
.SassComponent {
    display: flex;
    .box {
        background: red;
        cursor: pointer;
        transition: all 0.3s ease-in;
        &amp;.red {
            background: utils.$red;
            @include utils.square(1)
        }
        &amp;.orange {
            background: utils.$orange;
            @include utils.square(2)
        }
        &amp;.yellow {
            background: utils.$yellow;
            @include utils.square(3)
        }
        &amp;.green {
            background: utils.$green;
            @include utils.square(4)
        }
        &amp;.blue {
            background: utils.$blue;
            @include utils.square(5)
        }
        &amp;.indigo {
            background: utils.$indigo;
            @include utils.square(6)
        }
        &amp;.violet {
            background: utils.$violet;
            @include utils.square(7)
        }
        &amp;:hover {
            background: black;
        }
    }
}</code></pre>
<p>scss 라이브러리를 설치하고 가져와서 사용하고 싶다면 아래와 같이 작성하면 된다.</p>
<pre><code class="language-css">/* as를 사용해서 대신할 이름을 지정할 수 있다 */
@use &#39;include-media&#39; as im;
@use &#39;utils&#39;;

.SassComponent {
    display: flex;
    /* 가로 크기가 768px 이하면 적용된다 */
    @include im.media(&#39;&lt;768px&#39;) {
        background: blueviolet;
    }
    /* ... */
}</code></pre>
<h4 id="path-alias-사용하기">path alias 사용하기</h4>
<p>프로젝트를 진행하다보면 여러 파일을 import 하는 경우가 많다.
그럴 때마다 상대 경로로 가져오게 될 경우
<code>../../../../style.css</code>와 같이 알아보기 힘들고 길어지는 경우가 있다.</p>
<p>그럴 때 path alias를 사용해서 가독성을 높여줄 수 있다.</p>
<p>지금 <code>vite</code>를 사용하고 있기 때문에 <code>vite.config.ts</code>에서
이것을 설정해 줄 수 있다.</p>
<pre><code class="language-ts">// vite.config.ts
import { defineConfig } from &#39;vite&#39;
import react from &#39;@vitejs/plugin-react&#39;
import path from &#39;path&#39;;


// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: [
      { find: &quot;@/*&quot;, replacement: path.resolve(__dirname, &quot;src&quot;) },
      {
        find: &quot;@styles&quot;,
        replacement: path.resolve(__dirname, &quot;src/styles&quot;),
      }
    ]
  }
})</code></pre>
<pre><code class="language-ts">// tsconfig.app.json
// ...
    &quot;baseUrl&quot;: &quot;.&quot;,
    &quot;paths&quot;: {
      &quot;@/*&quot;: [&quot;src/*&quot;],
      &quot;@styles/*&quot;: [&quot;src/styles/*&quot;],
    },
// ...</code></pre>
<p>위와 같이 파일을 변경한 다음, 실제 사용하는 곳에서는</p>
<pre><code class="language-tsx">import &#39;@styles/SassComponent.scss&#39;

const SassComponent = () =&gt; {
    return (
        &lt;div className=&quot;SassComponent&quot;&gt;
            &lt;div className=&quot;box red&quot; /&gt;
            &lt;div className=&quot;box orange&quot; /&gt;
            &lt;div className=&quot;box yellow&quot; /&gt;
            &lt;div className=&quot;box green&quot; /&gt;
            &lt;div className=&quot;box blue&quot; /&gt;
            &lt;div className=&quot;box indigo&quot; /&gt;
            &lt;div className=&quot;box violet&quot; /&gt;
        &lt;/div&gt;
    );
}</code></pre>
<p>이런 식으로 사용해서 깔끔하게 작성할 수 있다.</p>
<h3 id="css-module">CSS Module</h3>
<p>CSS를 불러와서 사용할 때, 클래스 이름을 고유한 값으로 자동으로 만들어서
클래스 이름이 중첩되는 현상을 방지해주는 기술이다.</p>
<p>파일명을 이름.module.css로 작성해 주면 된다.</p>
<pre><code class="language-css">/* 클래스 이름을 자동으로 바꿔주므로 겹쳐도 된다 */
.wrapper {
    background: black;
    padding: 1rem;
    color: white;
    font-size: 2rem;
}

/* 전역으로 사용하고 싶다면 :global을 추가하자 */
:global .something {
    font-weight: 800;
    color: aqua;
}</code></pre>
<pre><code class="language-tsx">import styles from &#39;@styles/CSSModule.module.css&#39;;

const CSSModule = () =&gt; {
    return (
        &lt;div className={styles.wrapper}&gt;
            안녕하세요, 저는 &lt;span className=&quot;something&quot;&gt;CSS Module!&lt;/span&gt;
        &lt;/div&gt;
    );
}</code></pre>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/1523cc6d-6016-4bb2-9689-2c7d976a2849/image.PNG" alt="">
클래스 이름이 <code>_클래스이름_랜덤문자열</code>으로 자동으로 생성됐다.</p>
<h3 id="styled-component">styled-component</h3>
<p><code>CSS in JS</code> 방식으로 CSS를 작성할 수 있게 해주는 라이브러리이다.
가장 대표적인 것이 <code>styled-component</code>이다.</p>
<blockquote>
<p>CSS in JS
자바스크립트 파일 안에서 css를 작성하는 방식이다.
컴포넌트 단위로 스타일링을 할 수 있으며
자바스크립트의 변수와 로직을 사용해서 스타일을 동적으로 적용시킬 수 있다.</p>
</blockquote>
<p>먼저 라이브러리를 설치해야 된다.
<code>npm install styled-component -D @types/styled-component</code></p>
<pre><code class="language-tsx">import styled, { css } from &#39;styled-components&#39;;

const Box = styled.div`
    /* props로 넣어 준 값을 직접 전달해 줄 수 있다 */
    background: ${props =&gt; props.color || &#39;blue&#39;};
    padding: 1rem;
    display: flex;
`;

const Button = styled.button&lt;{inverted?: boolean}&gt;`
    background: white;
    color: black;
    border-radius: 4px;
    padding: 0.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    font-size: 1rem;
    font-weight: 600;

    &amp;:hover {
        background: rgba(255, 255, 255, 0.9);
    }

    ${props =&gt;
        props.inverted &amp;&amp; css`
            background: none;
            border: 2px solid white;
            color: white;
            &amp;:hover {
                background: white;
                color: black;
            }
        `};
    &amp; + button {
        margin-left: 1rem;
    }
`;


const StyledComponent = () =&gt; {
    return (
        &lt;Box color=&quot;black&quot;&gt;
            &lt;Button&gt;안녕하세요&lt;/Button&gt;
            &lt;Button inverted={true}&gt;테두리만&lt;/Button&gt;
        &lt;/Box&gt;
    );
}</code></pre>
<blockquote>
<p>VS code의 익스텐션에서 vscode-styled-components를 설치하면
글자에 색상과 자동 완성이 돼서 작성이 수월해진다.</p>
</blockquote>
<h4 id="작동-원리">작동 원리</h4>
<p>자바스크립트의 문법인 <code>템플릿 리터럴</code>를 사용해서 작동한다.</p>
<h5 id="템플릿-리터럴">템플릿 리터럴</h5>
<p>자바스크립트에서 문자열을 입력하는 방식 중 하나.
표현식, 문자열 삽입, 여러 줄 문자열, 물자열 형식화, 문자열 태깅 등의 기능을 제공한다.</p>
<h6 id="기본-문법">기본 문법</h6>
<pre><code class="language-typescript">`string text`
`string text line1
 string text line2
`

let expression;
`string text ${expression} string text`

function tag() { };
tag`string text ${expression} string text`</code></pre>
<p>ES6 이전에는 아래와 같은 방법으로 문자열을 삽입했었다.</p>
<pre><code class="language-typescript">let a = 10;
let b = 7;
let c = &quot;자바스크립트&quot;;
let text = &quot;입력한 값은 &quot; + (a + b) + &quot;이며, &quot; + c + &quot;를 사용 중입니다.&quot;;</code></pre>
<p><code>템플릿 리터럴</code>에서는 아래와 같이 <code>$</code>와 <code>{}</code>를 사용해서 삽입할 수 있다.</p>
<pre><code class="language-typescript">let a = 10;
let b = 7;
let c = &quot;자바스크립트&quot;;
let text = `입력한 값은 ${a+b}이며, ${c}를 사용 중입니다.`;</code></pre>
<h5 id="태그-템플릿">태그 템플릿</h5>
<p><code>템플릿 리터럴</code>의 발전된 형태이다.
태그를 사용하여 템플랫 리터럴을 함수로 파싱할 수 있다.</p>
<pre><code class="language-typescript">let person = &#39;Lee&#39;
let age = 28;

let tag = function(strings, personExp, ageExp) {
  console.log(strings); // 첫 번째에는 배열이 들어온다
  console.log(personExp); // 두 번째에는 person 값이 들어온다
  console.log(ageExp); // 세 번째에는 age 값이 들어온다
}
let output = tag`that ${person} is a ${age}`;</code></pre>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/0f79364d-8a60-4ced-b397-85d2a3045194/image.PNG" alt=""></p>
<pre><code class="language-typescript">function fn(strings, brand, items) {
    if(undefined === items) {
        return brand + &quot;의 라면은 재고가 없습니다!&quot;;
    } else {
        return strings[0] + brand + strings[1] + items;
    }
}

console.log(fn`구매가능한 ${ramenList[0].brand}의 라면 : ${ramenList[0].items}`);
//구매가능한 농심의 라면 : 신라면,짜파게티,참치마요,둥지냉면
console.log(fn`구매가능한 ${ramenList[1].brand}의 라면 : ${ramenList[1].items}`);
//구매가능한 삼양의 라면 : 삼양라면,불닭볶음면
console.log(fn`구매가능한 ${ramenList[2].brand}의 라면 : ${ramenList[2].items}`);
//오뚜기의 라면은 재고가 없습니다!</code></pre>
<h4 id="스타일링된-엘리먼트-만들기">스타일링된 엘리먼트 만들기</h4>
<p>styled.태그명을 사용해서 만들면 된다.</p>
<pre><code class="language-tsx">const MyComponent = styled.div`
    font-size: 1.6rem;
`;
// ...
&lt;MyComponent&gt;
    나의 컴포넌트
&lt;/MyComponent&gt;</code></pre>
<pre><code class="language-tsx">const MyComponent2 = styled(&#39;div&#39;)`
    font-size: 3rem;
    color: blueviolet;
`;
// ...
&lt;MyComponent2&gt;
    나의 컴포넌트2
&lt;/MyComponent2&gt;</code></pre>
<h4 id="스타일에서-props-쓰기">스타일에서 props 쓰기</h4>
<pre><code class="language-tsx">const Box = styled.div`
    /* props로 넣어 준 값을 직접 전달해 줄 수 있다. */
    background: ${props =&gt; props.color || &#39;blue&#39;};
    padding: 1rem;
    display: flex;
`;

//...
        &lt;Box color=&quot;black&quot;&gt;
            &lt;Button&gt;안녕하세요&lt;/Button&gt;
            &lt;Button inverted={true}&gt;테두리만&lt;/Button&gt;
        &lt;/Box&gt;</code></pre>
<p>color라는 props를 줘서 배경 색상을 지정할 수 있다.
만약 color를 주지 않았다면 기본 색상으로 파란색이 나오도록 했다.</p>
<h4 id="조건부-스타일링">조건부 스타일링</h4>
<p>기존에는 클래스에 조건부로 스타일링을 했었다면
<code>styled-component</code>에서는 props를 받아서 조건부로 스타일링 해줄 수 있다.</p>
<pre><code class="language-tsx">const Button = styled.button&lt;{inverted?: boolean}&gt;`
    /* ... */
    ${props =&gt;
        props.inverted &amp;&amp; css`
            background: none;
            border: 2px solid white;
            color: white;
            &amp;:hover {
                background: white;
                color: black;
            }
        `};
`
// ...
&lt;Button inverted={true}&gt;테두리만&lt;/Button&gt;</code></pre>
<p>스타일 코드 여러 줄을 props에 따라 넣어야 할 때는
<code>styled-component</code>에서 제공하는 <code>css</code>를 사용해야한다.</p>
<p>css를 사용하지 않아도 작동은 하지만
문자열로 취급되어서 익스텐션의 신택스 하이라이팅이 되지 않고
태그 템플릿이 아니기 때문에 해당 부분에서는 props 값을 사용할 수 없게된다.</p>
<p>조건부 스타일링 부분에 props를 참조하지 않는다면 사용하지 않아도 상관없지만,
참조할 경우에는 반드시 css를 활용해서 태그 템플릿 기능을 사용하는 것이 좋다.</p>
<h4 id="반응형-디자인">반응형 디자인</h4>
<p>반응형 디자인도 가능한데, 일반 CSS에서 사용할 때처럼 <code>media</code> 쿼리를 사용하면 된다.</p>
<pre><code class="language-tsx">/* ... */
    /* 768px 미만이되면 화면을 꽉 채운다 */
    @media (max-width: 1024px) {
        width: 768px;
    }
    @media (max-width: 768px) {
        width: 100%;
    }
/* ... */</code></pre>
<p>반응형 CSS를 함수화해서 여러 곳에서 재사용하는 방법도 있다.</p>
<pre><code class="language-tsx">const sizes = {
    desktop: 1024,
    tablet: 768,
}

type Sizes = typeof sizes;
type Media = {
    [Key in keyof Sizes]: (...args: Parameters&lt;typeof css&gt;) =&gt; CSSProp;
};
const media: Media = Object.keys(sizes).reduce((acc, label) =&gt; {
    const key = label as keyof Sizes;
    acc[key] = (...args) =&gt; css`
      @media (max-width: ${sizes[key] / 16}em) {
        ${css(...args)};
        }
    `;

    return acc;
}, {} as Media);
// ...
const Button = styled.button&lt;{inverted?: boolean}&gt;`
    /* ... */
    ${media.desktop`width: 768px;`}
    ${media.tablet`width: 100%;`}
    /* ... */

`</code></pre>
<h2 id="css-프레임워크">CSS 프레임워크</h2>
<p>위 방법 외에도  <code>Bootstrap</code> <code>Tailwind CSS</code> 등이 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Hooks.]]></title>
            <link>https://velog.io/@r0_0r/React-Hooks</link>
            <guid>https://velog.io/@r0_0r/React-Hooks</guid>
            <pubDate>Thu, 14 Nov 2024 12:32:42 GMT</pubDate>
            <description><![CDATA[<p>Hooks는 리액트 v16.8에 새로 도입된 기능으로
함수형 컴포넌트에서도 상태 관리를 할 수 있는 <code>useState</code>
마운트, 업데이트, 언마운트 시에 작업을 처리할 수 있는 <code>useEffect</code> 등
다양한 작업을 할 수 있게 됐다.</p>
<p>클래스 컴포넌트는 여러 메서드들로 생명 주기 작업 처리를 해야되는데
복잡하고 관리하기 어려워질 수도 있다는 단점이 있다.</p>
<p>함수 컴포넌트에서 React Hooks를 활용하면
<code>componentDidMount</code> <code>componentDidUpdate</code> <code>componentWillUnmount</code> 등
클래스 컴포넌트에서는 나눠서 작성해야 됐던 작업들을
<code>useEffect</code>로 통합하여 관리하기가 좀 더 수월해졌다.</p>
<hr>
<h2 id="usestate">useState</h2>
<p>리액트에서 가장 기본적인 <code>hook</code>이며, 상태를 관리할 수 있게 해준다.</p>
<pre><code class="language-jsx">function App() {
  const [count, setCount] = useState(0)

  return (
      &lt;div&gt;
        지금 카운트는 &lt;b&gt;{count}&lt;/b&gt;입니다.
      &lt;/div&gt;
  )</code></pre>
<p>useState 함수의 파라미터에는 상태의 기본값을 넣어주면 된다.
이 함수는 배열을 반환하는데
첫 번째 원소는 상태 값, 두 번째 원소는 상태를 설정하는 함수이다.</p>
<p>상태를 변화시키고 싶다면 <code>setState(x)</code>를 쓰면 된다.</p>
<pre><code class="language-jsx">setCount(1);</code></pre>
<p>값이 변했으므로, 컴포넌트는 리렌더링이 된다.</p>
<p><code>useState</code>는 하나의 상태 값만 관리할 수 있다.
여러 상태를 관리하고 싶다면 <code>useState</code>를 여러 번 쓰면 된다.</p>
<pre><code class="language-jsx">function App() {
  const [count, setCount] = useState(0)
  const [skill, setSkill] = useState(&#39;react&#39;);

  return (
      &lt;div&gt;
        지금 카운트는 &lt;b&gt;{count}&lt;/b&gt;입니다.
          지금은 {skill} 사용 중...
      &lt;button onClick={() =&gt; setSkill(&#39;flutter&#39;)}&gt;변경&lt;/button&gt;
      &lt;/div&gt;
  )</code></pre>
<h3 id="flushsync">flushSync()</h3>
<p><code>flushSync()</code>는 React 18에서 추가된 함수인데
리액트에서는 <code>setState</code>가 비동기로 상태를 변경시키는데
<code>flushSync()</code> 안에서 <code>setState</code>를 호출할 경우 바로 DOM에 반영시킨다고 한다.</p>
<p>자주 사용할 경우 비동기 업데이트의 성능 최적화를 무시하고
동기적으로 처리하기 때문에, 성능에 영향을 미칠 수 있으므로
적절한 곳에 사용해야 한다.</p>
<pre><code class="language-tsx">//...
        flushSync(() =&gt; {
            setTodos(prev =&gt; {
                const lastEl = prev[prev.length-1];
                const newData: Item = {
                    id: lastEl.id +1,
                    task: input,
                };
                return [...prev, newData];
            });
        });
//...</code></pre>
<hr>
<h2 id="useeffect">useEffect</h2>
<p> <code>useEffect</code>는 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정할 수 있는 <code>hook</code>이다.
 <code>componentDidMount</code>와 <code>componentDidUpdate</code>를 합친 형태와 비슷하다.</p>
<p> 보통 데이터를 가져오거나 이벤트를 등록할 때 많이 사용된다.</p>
<h3 id="렌더링될-때마다-실행되게-하려면">렌더링될 때마다 실행되게 하려면</h3>
<p>컴포넌트가 새로 렌더링될 때마다 <code>useEffect</code>를 실행시키고 싶다면 아래와 같이 작성하면 된다.</p>
<pre><code class="language-jsx">useEffect(() =&gt; {

  console.log(&#39;마운트&#39;);

})</code></pre>
<p>배열을 작성하지 않으면 이 <code>useEffect</code>는 렌더링될 때마다 작동하게 된다.</p>
<h3 id="마운트될-때만-실행되게-하려면">마운트될 때만 실행되게 하려면</h3>
<p>컴포넌트가 마운트될 때만 실행되게 하고 싶다면
의존성 배열을 빈 배열로 설정하면 된다.</p>
<pre><code class="language-jsx">// ...
useEffect(() =&gt; {
  // 작업할 로직 작성
  console.log(&#39;마운트&#39;);

// 의존성 배열
},[])</code></pre>
<h3 id="특정-상태-값이-변할-때만-실행되게-하려면">특정 상태 값이 변할 때만 실행되게 하려면</h3>
<p>컴포넌트에서 특정 상태 값이 변할 때만 실행되게 하려면
배열에 상태 값을 넣어주면 된다.</p>
<pre><code class="language-jsx">// ...
useEffect(() =&gt; {
  // 작업할 로직 작성
  console.log(&#39;count가 변했습니다.&#39;);

// 의존성 배열
},[count])</code></pre>
<p>count가 변할 때만 실행되게 된다.</p>
<h3 id="언마운트될-때-특정-작업을-처리하고-싶다면">언마운트될 때 특정 작업을 처리하고 싶다면</h3>
<p><code>return</code> 안에 할 작업을 작성해 주면 된다.
이를 클린업(clean-up) 함수라고 한다.</p>
<p>등록된 이벤트를 없애는 작업에 사용되곤 한다.</p>
<pre><code class="language-jsx">// ...
useEffect(() =&gt; {
  // 작업할 로직 작성
  console.log(&#39;마운트&#39;);

  return () =&gt; {
    console.log(&#39;언마운트&#39;);
  }
// 의존성 배열
},[])</code></pre>
<h2 id="uselayouteffect">useLayoutEffect</h2>
<p><code>useEffect</code>와 사용 방법은 같지만 실행 시점이 다르다.</p>
<p><code>useLayoutEffect</code>는 <code>render</code> 이후 DOM에 반영되기 전에
동기적으로 실행되는 함수이다.</p>
<p><code>useEffect</code>는 <code>render</code> 이후 DOM에 반영된 후에
비동기적으로 실행되는 함수이다.</p>
<p>보통 <code>useEffect</code>만 써도 되지만,
성능의 문제나 애니메이션 시작 등, 화면에 바로 적용되어야 하는 요소가 있다면
<code>useLayoutEffect</code>를 쓰는 걸 고려해볼 수 있다.</p>
<p><code>useLayoutEffect</code>는 동기적으로 작업되기 때문에
시간이 오래 걸리는 작업을 할 경우 작업이 끝난 후
DOM이 반영되므로 사용자 경험이 나빠질 수 있겠다.</p>
<hr>
<h2 id="usereducer">useReducer</h2>
<p><code>useReducer</code>는 <code>useState</code>와 같은 상태를 관리해주는 훅이다.</p>
<p><code>useState</code>와 하는 역할은 비슷하지만
좀 더 복잡한 상태 관리 로직이 있을 경우 바깥으로 꺼내어 작성할 수 있다.</p>
<p><code>useReudcer</code>는 Flux 패턴의 단방향 데이터 흐름을 비슷하게나마 구현할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/69f574f2-45ae-4879-8ade-830b8ec0f9f2/image.png" alt=""></p>
<blockquote>
<h3 id="flux-패턴">Flux 패턴</h3>
<p>리액트를 만든 meta(facebook)에서 제안한 패턴이다.
단방향 데이터 흐름이 핵심이다.</p>
</blockquote>
<ul>
<li>Action: 상태 변경을 요청한다.</li>
<li>Dispatcher: Action을 받아 Reducer(or Store)에게 전달한다.</li>
<li>Store: 애플리케이션의 상태를 관리하며, Action에 따라 상태를 변경한다.</li>
<li>View: 상태를 기반으로 UI를 렌더링한다.</li>
</ul>
<h3 id="action">Action</h3>
<pre><code class="language-tsx">&lt;button onClick={() =&gt; dispatch({type: &#39;INCREMENT&#39;})}&gt;
    + 1
&lt;/button&gt;
&lt;button onClick={() =&gt; dispatch({type: &#39;DECREMENT&#39;})}&gt;
    - 1
&lt;/button&gt;</code></pre>
<p><code>dispatch</code> 함수로 상태 변경을 요청하는 함수이다.</p>
<h3 id="reducer">Reducer</h3>
<pre><code class="language-tsx">const reducer = (state: number, action: Action) =&gt; {
    switch(action.type) {
        case &#39;INCREMENT&#39;:
            return state + 1;
        case &#39;DECREMENT&#39;:
            return state - 1;
        default:
            // 개발자가 지정한 action type 외에는 오류를 발생시켜서
            // state 기본 값을 보내는 것보단 잘못된 명령어임을 인지시키는 게 좋아보임.
            // 보통 오타일 경우가 많을 것 같다.
            throw new Error(`[ERROR] unknown action type - ${action.type}`);
        }
};</code></pre>
<p><code>dispatch</code>에서 보내는 <code>Action</code>의 타입을 받아서 내부 로직을 처리 후 새로운 상태를 반환한다.</p>
<h3 id="state">state</h3>
<pre><code class="language-tsx">const [state, dispatch] = useReducer(reducer, 0);</code></pre>
<p><code>reducer</code>에서 관리하는 상태이다.
첫 번째 매개변수로 reducer 함수, 두 번째 매개변수로는 초기 상태를 전달한다.</p>
<h3 id="view">view</h3>
<pre><code class="language-tsx">&lt;div&gt;
  &lt;p&gt;
      현재 카운터 값은 &lt;b&gt;{state}&lt;/b&gt;입니다.
  &lt;/p&gt;
  &lt;button onClick={() =&gt; dispatch({type: &#39;INCREMENT&#39;})}&gt;
      + 1
  &lt;/button&gt;
  &lt;button onClick={() =&gt; dispatch({type: &#39;DECREMENT&#39;})}&gt;
      - 1
   &lt;/button&gt;
&lt;/div&gt;</code></pre>
<p><code>state</code> 기반으로 UI를 렌더링하고, <code>dispatch</code>로 <code>Action</code>을 보낸다.</p>
<p>그리고 더 넓은 범위에서 상태를 공유할 수 있는 <code>useContext</code> 훅도 있다.</p>
<hr>
<h2 id="usememo-usecallback">useMemo, useCallback</h2>
<h3 id="usememo">useMemo</h3>
<p><code>useMemo</code>에서의 <code>memo</code>는 <code>memozation</code>을 뜻한다.</p>
<blockquote>
<p>Memozation
동일한 계산을 반복해야할 경우, 한 번 계산한 결과를 메모리에 저장해둬서
중복 계산을 방지하는 방법이다.</p>
</blockquote>
<p>리액트는 상태가 변할 때마다 컴포넌트를 다시 렌더링하게 되는데
만약 input의 상태를 관리하고 있다면 input의 값이 변할 때마다
<code>onChange</code>가 실행되고, <code>onChange</code> 함수는 input의 상태를 변경시키기 때문에
글자를 입력하거나, 지울 때마다 컴포넌트가 계속 리렌더링이 되어
효율이 떨어지게 된다.</p>
<p>이를 방지하기 위해서 상태의 값이 변경됐을 때만 리렌더링 할 수 있게
해주는 것이 <code>useMemo</code>이다.</p>
<pre><code class="language-tsx">const getAverage = (numbers: number[]) =&gt; {
    console.log(&#39;계산 중...&#39;);
    if (numbers.length === 0) return 0;
    const sum = (a: number, b: number) =&gt; a+b;
    return numbers.reduce(sum, 0);
}</code></pre>
<p>만약 위의 함수가 매우 무거운 작업이라고 가정하고 아래의 컴포넌트를 만들었다고 가정하면</p>
<pre><code class="language-tsx">const Average = () =&gt; {
    const [list, setList] = useState&lt;number[]&gt;([]);
    const [number, setNumber] = useState(&#39;&#39;);

    const onChnage = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
        setNumber(e.target.value);
    }

    const onInsert = () =&gt; {
       const inputNumber = parseInt(number);
       if(isNaN(inputNumber)) return;
       const nextList = list.concat(inputNumber);
       setList(nextList);
       setNumber(&#39;&#39;);
    }

    return (
        &lt;div&gt;
            &lt;input type=&quot;text&quot; value={number} onChange={onChnage}/&gt;
            &lt;button onClick={onInsert} &gt;등록&lt;/button&gt;
            &lt;ul&gt;
                {
                    list.map((value, index) =&gt; (
                        &lt;li key={index}&gt;{value}&lt;/li&gt;
                    ))
                }
            &lt;/ul&gt;
            &lt;div&gt;
                &lt;p&gt;평균 값: &lt;/p&gt; {getAverage(list)}
            &lt;/div&gt;
        &lt;/div&gt;
    )
};</code></pre>
<p>이 컴포넌트는 input에 숫자가 입력될 때마다 렌더링이 되고
그때마다 <code>getAervage</code> 함수가 실행되게 된다.</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/1b9c9486-90db-4832-96c1-3dc6f1790d46/image.PNG" alt=""></p>
<p><code>getAverage</code>가 매우 무거운 함수였다면, 렌더링되는 데 시간이 많이 소요되고
이는 사용자 경험이 나빠지는 것으로 이어지게 될 수도 있다.</p>
<p>그래서 <code>list</code>가 변할 때만 함수가 실행될 수 있도록 <code>list</code> 값을 기억해뒀다가
변경이 되었다면 <code>getAverage</code> 함수를 실행시켜서 반영시키는 것으로
최적화를 할 수 있다.</p>
<pre><code class="language-tsx">// ...
const avg = useMemo(() =&gt; {
        return getAverage(list);
    }, [list]);
return (
  // ...
  &lt;p&gt;평균 값: &lt;/p&gt; {avg}
);</code></pre>
<h3 id="usecallback">useCallback</h3>
<p><code>useCallback</code>도 <code>useMemo</code>와 비슷하다.
<code>useCallback</code>은 함수 자체를 인자로 받아서 함수가 매 렌더링 때마다
다시 생성되는 걸 방지해주는 역할을 한다.</p>
<p>리렌더링될 때마다 함수 컴포넌트 안에 있는 내용들도
다시 생성되기 때문에 매번 새로운 함수 객체를 다시 할당받는 결과가 되고
이는 효율적이지 못하다.</p>
<p>물론 대부분의 상황에서는 문제가 없겠지만
컴포넌트가 자주 렌더링되는 상황이고 개수가 많아진다면 성능이 저하될 수도 있다.</p>
<pre><code class="language-tsx">    // 컴포넌트가 처음 생성될 때만 함수를 생성
    const onChangeUseCallback = useCallback((e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
        setNumber(e.target.value);
    },[]);</code></pre>
<pre><code class="language-tsx">    // number 혹은 list가 변경되었을 때만 함수 생성
    //
    // []로 했을 경우 처음 생성됐을 상황을 기억하기 때문에
    // number나, list는 0 그리고 []이 된다.
    // 그래서 의존성 배열에 상태 값을 넣어줘야 한다.
    const onInsertUseCallback = useCallback(() =&gt; {
        const inputNumber = parseInt(number);
       if(isNaN(inputNumber)) return;
       const nextList = list.concat(inputNumber);
       setList(nextList);
       setNumber(&#39;&#39;);
    }, [number, list]);</code></pre>
<p>숫자나 문자열, 객체같이 일반 값을 재사용하는 경우에는 <code>useMemo</code>를
함수를 재사용할 경우에는 <code>useCallback</code>을 사용하면 좋다.</p>
<hr>
<h2 id="useref">useRef</h2>
<p><code>useRef</code>는 렌더링에 필요하지 않은 값을 참조할 수 있게 해준다.</p>
<p>일반 변수는 리렌더링될 때마다 다시 할당이 되지만,
ref 객체는 동일한 객체를 반환하기 때문에 생명 주기 중 일정하게 정보를 기억할 수 있다.</p>
<p><code>useState</code>로 관리되는 상태는 값이 변경될 경우 리렌더링이 되지만
ref 객체(ref.current)가 변경되더라도, 리렌더링이 되지 않기 때문에
정보를 기억할 수 있게 된다.</p>
<p>보통, DOM에 직접 접근해야될 때 DOM에 ref를 할당해서
포커스를 이동시키거나, 스크롤을 이동시키는 등의 작업을 할 수 있다.</p>
<pre><code class="language-tsx">const Average = () =&gt; {
  const inputRef = useRef(null);
  // ...
  const handleClick = () =&gt; {
    // ...
    ref.current?.focus();
  }
  return (
    &lt;div&gt;
        &lt;input ref={inputRef} value={value} onChange={onChange} /&gt;&lt;br /&gt;
          &lt;button onClick={handleClick}&gt;엔터!&lt;/button&gt;
          &lt;p&gt;입력한 값은 {value}입니다.&lt;/p&gt;
    &lt;/div&gt;
  );
}</code></pre>
<hr>
<h2 id="커스텀-훅">커스텀 훅</h2>
<p>비슷한 기능을 공유할 경우 따로 빼내어서 자신만의 훅으로 만들 수도 있다.
이름은 마음대로 지어도 되지만 보통 <code>use</code>를 앞에 붙여서 짓는다.</p>
<h3 id="useinputs">useInputs</h3>
<p>위에서 썼던 <code>useReducer</code>를 이용해서 커스텀 훅을 만든다고 한다면</p>
<pre><code class="language-typescript">import React, { useReducer } from &quot;react&quot;;

const reducer = (state: any, action: React.ChangeEvent&lt;HTMLInputElement&gt;[&#39;target&#39;]) =&gt; {
    return {
        ...state,
        [action.name]: action.value,
    };
};

const useInputs = (init: any): [any, (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; void] =&gt; {
    const [state, dispatch] = useReducer(reducer, init);
    const onChange = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
        dispatch(e.target);
    };
    return [state, onChange];
}

export default useInputs;</code></pre>
<p>이렇게 만들 수 있고, 이걸 실제 컴포넌트에서 쓸 경우에는</p>
<pre><code class="language-tsx">import useInputs from &quot;./useInputs&quot;;

const Info = () =&gt; {
    const [state, onChange] = useInputs({
        name: &#39;&#39;,
        nickName: &#39;&#39;,
    });

    const { name, nickName } = state;
    return (
        &lt;div&gt;
      &lt;div&gt;
        &lt;input name=&quot;name&quot; value={name} onChange={onChange} /&gt;
        &lt;input name=&quot;nickName&quot; value={nickName} onChange={onChange} /&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;div&gt;
          &lt;b&gt;이름:&lt;/b&gt; {name}
        &lt;/div&gt;
        &lt;div&gt;
          &lt;b&gt;닉네임: &lt;/b&gt;
          {nickName}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;   
    );
}</code></pre>
<p>이런 식으로 활용할 수 있다.
커스텀 훅을 써서 컴포넌트의 가독성이 많이 좋아진 걸 알 수 있다.</p>
<h3 id="usedebounce">useDebounce</h3>
<p>디바운스는 가장 마지막에 생성된 이벤트만 실행되는 것이라고 할 수 있다.</p>
<p>예를 들어, 검색을 한다고 했을 때
한 글자마다 검색 요청을 보내면 네트워크 요청을 많이 하게 되고,
서버나 클라이언트에 부담을 많이 주게 된다.</p>
<p>그래서 사용자가 입력을 마칠 때까지 기다린 후
입력이 끝났을 때 검색 요청을 보내서 효율적으로 검색을 할 때
디바운스를 활용할 수 있다.</p>
<pre><code class="language-typescript">// useDebounce.ts
import { useEffect, useState } from &quot;react&quot;

const useDebounce = (value: any, delay = 500) =&gt; {
    const [state, setState] = useState(value);

    useEffect(() =&gt; {
        const handler = setTimeout(() =&gt; {
            setState(value);
        }, delay);

        return () =&gt; {
            clearTimeout(handler);
        }
    },[value, delay]);

    return state;
}

export default useDebounce;</code></pre>
<pre><code class="language-tsx">import { useEffect, useState } from &quot;react&quot;;
import useDebounce from &quot;./useDebounce&quot;;

const Debounce = () =&gt; {
      const [search, setSearch] = useState(&#39;&#39;);
    const [data, setData] = useState([]);
    const debounceValue = useDebounce(search);

    useEffect(() =&gt; {
        const getData = async() =&gt; {
            return await fetch(`https://url.com/${debounceValue}`).then(res =&gt; {
                if(!res.ok) {
                    return Promise.reject(&#39;No Data&#39;);
                }
                return res.json();
            }).then(list =&gt; {
                setData(list);
            }).catch(err =&gt; console.error(err));
        }
        if(debounceValue) getData();
    },[debounceValue]);
  // ...
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트 생명주기.]]></title>
            <link>https://velog.io/@r0_0r/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0</link>
            <guid>https://velog.io/@r0_0r/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0</guid>
            <pubDate>Thu, 14 Nov 2024 12:02:53 GMT</pubDate>
            <description><![CDATA[<h1 id="생명-주기">생명 주기</h1>
<p>모든 리액트 컴포넌트에는 생명 주기가 존재한다.</p>
<p>컴포넌트가 렌더링됐을 때 특정 작업을 처리하고 싶거나
업데이트 전후로 해야될 작업이 있을 수도 있고
언마운트될 때도 해야될 작업이 있을 수도 있다.</p>
<p>이럴 때 컴포넌트의 생명 주기 활용하면 원하는 작업을 할 수 있다.</p>
<p>생명 주기 내에서는 여러 메서드가 호출되며 이를 사용할 수 있는데
클래스형 컴포넌트에서는 <code>생명 주기 메서드</code>들을 사용하고
함수형 컴포넌트에서는 <code>hook</code> 중에 하나인 <code>useEffect()</code>를 사용하는 점에서 차이가 있다.</p>
<p>현재는 함수형 컴포넌트를 사용하도록 권장하고 있기 때문에
클래스형 컴포넌트에서 쓰이는 생명 주기 메서드는 사용할 일이 없다.</p>
<hr>
<h2 id="생명-주기-종류">생명 주기 종류</h2>
<p>생명 주기는 크게 세 가지, <code>마운트</code> <code>업데이트</code> <code>언마운트</code>로 나눌 수 있다.</p>
<blockquote>
<p><strong>컴포넌트 생명 주기</strong>
<img src="https://velog.velcdn.com/images/r0_0r/post/0a1805e5-847e-4b55-9223-0eb00cc10d78/image.PNG" alt=""></p>
</blockquote>
<table>
<thead>
<tr>
<th align="left">분류</th>
<th>클래스 컴포넌트</th>
<th align="center">함수 컴포넌트</th>
</tr>
</thead>
<tbody><tr>
<td align="left">마운트</td>
<td>constructor()</td>
<td align="center">컴포넌트 내부</td>
</tr>
<tr>
<td align="left">마운트</td>
<td>render()</td>
<td align="center">return()</td>
</tr>
<tr>
<td align="left">마운트</td>
<td>componentDidMount()</td>
<td align="center">useEffect()</td>
</tr>
<tr>
<td align="left">업데이트</td>
<td>componentDidUpdate()</td>
<td align="center">useEffect()</td>
</tr>
<tr>
<td align="left">언마운트</td>
<td>componentWillUnmount()</td>
<td align="center">useEffect()</td>
</tr>
</tbody></table>
<hr>
<h3 id="생명-주기-순서">생명 주기 순서</h3>
<h4 id="마운트">마운트</h4>
<p>컴포넌트가 생성되는 걸 마운트라고 한다.</p>
<ol>
<li><code>constructor()</code>를 호출한다.
<code>constructor()</code>에서는 <code>state</code>를 초기화하는 등의 작업이 실행된다.</li>
<li><code>render()</code>를 호출한다.
<code>render()</code>에서는 화면에 표시되어야 될 내용(jsx)을 이해하고 DOM을 업데이트한다.</li>
<li>jsx가 DOM에 그려지면 <code>componentDidmount()</code>를 호출한다.
이 메서드는 마운트가 완료됐을 때 수행될 작업이 실행된다.
(API 호출, DOM 속성 변경 등)</li>
</ol>
<hr>
<h4 id="업데이트">업데이트</h4>
<p>컴포넌트가 변경되는 걸 업데이트라고 한다.</p>
<p>상태 변화를 감지하면 <code>getDerivedStateFromProps()</code>가 실행되고,
그 다음 화면에 표시될 내용을 다시 알아내기 위해 <code>render()</code> 실행된다.
그후, <code>componentDidUpdate()</code>가 실행된다.</p>
<p><code>getDerivedStateFromProps()</code>는 16.3 버전 이후에 추가된 메서드이다.
<code>props</code>로 받아온 값을 <code>state</code>에 동기화시키는 용도로 사용된다.</p>
<p>컴포넌트가 업데이트되는 경우는 네 가지가 있다.</p>
<ol>
<li>부모로부터 전달받은 props가 변경되거나.</li>
<li>state가 변경되거나.</li>
<li>부모 컴포넌트가 리렌더링될 때.</li>
<li><code>forceUpdate()</code>로 강제로 렌더링을 트리거할 때.</li>
</ol>
<p><code>forceUpdate()</code>는 클래스 컴포넌트에서만 사용 가능한 함수이다.</p>
<hr>
<h4 id="언마운트">언마운트</h4>
<p><code>componentWillUnmount()</code>가 실행되며, 컴포넌트가 화면에서 사라지는 것을 언마운트라고 한다.
이것을 활용해서 API 연결을 해제하거나, setInterval로 함수를 반복 호출을 시켰다면, clearInterval 함수로 중단시키는 작업을 주로 처리한다.</p>
<hr>
<h3 id="componentdidcatch">componentDidCatch</h3>
<p>클래스 컴포넌트의 생명 주기 메서드에서 에러가 발생했을 경우 실행되는 메서드이다.
함수 컴포넌트는 클래스 컴포넌트 생명 주기 메서드를 사용할 수 없어서
함수 컴포넌트에서 렌더링 중 에러 처리를 하고 싶다면
이 메서드를 활용한 컴포넌트를 만들고 감싸주어야 한다.</p>
<pre><code class="language-tsx">// ErrorBoundary.tsx
import { Component, ErrorInfo } from &quot;react&quot;;
import ErrorComponent from &quot;./ErrorComponent&quot;;

interface Props {
    children: React.ReactNode;
}

interface States {
    hasError: boolean;
    error: Error | null;
}

class ErrorBoundary extends Component&lt;Props, States&gt; {

    state: States = {
        hasError: false,
        error: null,
    }
    componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
        this.setState({
            hasError: true,
            error: error,
        });
        console.error({error, errorInfo});
    }
    render() {
        if (this.state.hasError) {
            return &lt;ErrorComponent error={this.state.error}/&gt;;
        }
        return this.props.children;
    }
}</code></pre>
<pre><code class="language-tsx">// App.tsx

// ...
&lt;ErrorBoundary&gt;
  &lt;MyComponent /&gt;
&lt;/ErrorBoundary&gt;
// ...</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[onPopInvoked is deprecated.]]></title>
            <link>https://velog.io/@r0_0r/onPopInvoked-is-deprecated</link>
            <guid>https://velog.io/@r0_0r/onPopInvoked-is-deprecated</guid>
            <pubDate>Tue, 22 Oct 2024 10:52:14 GMT</pubDate>
            <description><![CDATA[<p>플러터를 최신 버전으로 업그레이드하면서,
<code>context.pop()</code>이 잘 되던 페이지에서 오류가 발생했다.</p>
<p>그 이유는 안드로이드에서 뒤로가기 키를 감지하려고 쓴 <code>PopScope</code> 위젯가 이유였다.</p>
<p>업그레이드 하기 전에는</p>
<pre><code class="language-dart">PopScope(
    canPop: false, // 페이지를 pop시킬 건지
    onPopInvoked: (didPop) {
        // pop을 감지했을 때의 로직 작성
        context.pop();
      },
    child: Container(),
);</code></pre>
<p> 이런 식으로 페이지 이동을 막고 로직을 작성한 다음 <code>context.pop()</code>을 호출하는 식으로 처리했었는데</p>
<p> 업그레이드 후에는</p>
<pre><code class="language-dart"> // ...
 PopScope(
     canPop: false, // 페이지를 pop시킬 건지
     onPopInvokedWithResult: (didPop, result) {
         if(!didPop) {
            // 로직 작성
            context.pop();
        }
      },
    child: Container(),
);</code></pre>
<p><del>onPopInvoked</del>가 depreacted가 됐고, onPopInvokedWithResult로 변경되었으며,
작동 방식이 바뀌었다.</p>
<p><code>Object? result</code>인 새로운 인자를 받게 되는데 아마 <code>context.pop(&#39;text&#39;);</code>에서의
<code>&#39;text&#39;</code>를 받아오는 게 아닐까 싶다.</p>
<p><del>canPop에 false 상태로 <code>context.pop();</code>을 호출해서 `</del> is not a true`라는 오류가 나왔던 것이다.~~</p>
<p>나중에 다시 해보니, 됐던 방법이 안 돼서 다시 고생한 결과</p>
<p><code>if(!didPop)</code>으로 분기를 나눠준 뒤 페이지 이동시켜야 오류가 발생하지 않았다.</p>
<p><del>변경 로그를 읽어야겠다... 귀찮지만</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[flutter, 애니메이션 재사용하기?]]></title>
            <link>https://velog.io/@r0_0r/flutter-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%9E%AC%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@r0_0r/flutter-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EC%9E%AC%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 02 Oct 2024 17:13:26 GMT</pubDate>
            <description><![CDATA[<p>플러터에서는 애니메이션을 만들 수 있다.</p>
<p>그런데 똑같은 애니메이션인데,</p>
<p>매번 필요한 화면에서 <code>initState()</code>에서 초기화 해주고...
<code>dispose()</code>에서 해제시키고...</p>
<blockquote>
<p><del>매우 귀찮다</del></p>
</blockquote>
<p>결국 방법은 있었으니, 잊지 않기 위해서.</p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;

mixin DoubleFadeOutAnimationMixin&lt;T extends StatefulWidget&gt;
    on State&lt;T&gt;, TickerProviderStateMixin&lt;T&gt; {
  late AnimationController animationController01;
  late AnimationController animationController02;

  late Animation&lt;double&gt; animation01;
  late Animation&lt;double&gt; animation02;

  @override
  void initState() {
    super.initState();

    animationController01 = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    );

    animationController02 = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    );

    animation01 = Tween&lt;double&gt;(begin: 1.0, end: 0.0).animate(
      CurvedAnimation(
        parent: animationController01,
        curve: Curves.easeInOut,
      ),
    );
    animation02 = Tween&lt;double&gt;(begin: 1.0, end: 0.0).animate(
      CurvedAnimation(
        parent: animationController02,
        curve: Curves.easeInOut,
      ),
    );
  }

  @override
  void dispose() {
    animationController01.dispose();
    animationController02.dispose();
    super.dispose();
  }
}</code></pre>
<p>dart에 있는 <code>mixin class</code>를 사용하면 되는데</p>
<blockquote>
<p>클래스에 기능을 추가합니다. mixin은 복잡한 클래스의 계층 구조에서 코드를 재사용하는 방법 중 하나입니다.</p>
</blockquote>
<p>그냥 확장(<code>extends</code>)해서 쓰면 되지 않음...?</p>
<p>하지만 dart에서는 <code>extends</code>로 단일 상속만 지원하기 때문에
이미 <code>State&lt;ScreenWidget&gt;</code>을 확장하고 있다면... 쓸 수가 없다.</p>
<p>그래서 <code>mixin class</code>로 만들고 이걸 함께 쓰겠다는 뜻으로 <code>with</code>를 쓰면 된다.</p>
<p><code>with</code>는 여러 개를 가져다 쓸 수 있다 =&gt; <code>다중 상속</code></p>
<p>이놈을 실제 화면에서 쓰고 싶다면</p>
<pre><code class="language-dart">class ExampleFadeScreen extends StatefulWidget {
  const ExampleFadeScreen({super.key});

  @override
  State&lt;ExampleFadeScreen&gt; createState() =&gt; _ExampleFadeScreenState();
}

class _ExampleFadeScreenState extends State&lt;CoachMarkScreen&gt; with TickerProviderStateMixin, DoubleFadeOutAnimationMixin&lt;ExampleFadeScreen&gt; {

    @override
    Widget build(BuildContext context) {
        return AnimatedBuilder(
            animation: animationController01,
            builder: (context, child) {
                return Opacity(
                    opacity: animation01.value,
                    child: Text(&#39;fadeout animation 01...&#39;);
                );
            },
        );
    }
}
</code></pre>
<p>애니메이션이 똑같다면, 이런 식으로 <code>mixin class</code>로 작성해두고
재사용하면 참 좋지 않을까.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[nestjs 01]]></title>
            <link>https://velog.io/@r0_0r/nestjs-01</link>
            <guid>https://velog.io/@r0_0r/nestjs-01</guid>
            <pubDate>Mon, 16 Sep 2024 17:22:07 GMT</pubDate>
            <description><![CDATA[<h1 id="설치하기">설치하기</h1>
<h2 id="nestjs-cli">nestjs Cli</h2>
<p><code>npm install global @nestjs/cli</code>
or
<code>yarn global add @nestjs/cli</code></p>
<h1 id="프로젝트-생성">프로젝트 생성</h1>
<h2 id="아래와-같이-입력해서-cli로-생성">아래와 같이 입력해서 cli로 생성</h2>
<p><code>nest new project-name</code></p>
<p>이후 나타나는 선택지에서 사용하고 있는 패키지 관리 매니저를 선택하기
<code>npm</code> or <code>yarn</code></p>
<h1 id="컨트롤러-작성해-보기">컨트롤러 작성해 보기</h1>
<h2 id="get">@Get</h2>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/425c2852-df0a-4521-8ae5-9bca1a44c20c/image.png" alt=""></p>
<h2 id="getname">@Get(&#39;name&#39;)</h2>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/a191bdbd-2296-485b-942a-dda0f032285b/image.png" alt=""></p>
<h2 id="getnamename">@Get(&#39;name/:name&#39;)</h2>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/847a58d6-d866-4c19-bfd5-a2952532f665/image.png" alt=""></p>
<h2 id="getuser">@Get(&#39;user&#39;)</h2>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/9cc4ee06-268e-4be1-9f70-4386faaa2640/image.png" alt=""></p>
<h1 id="nest-cli로-컨트롤러-생성하기">nest-cli로 컨트롤러 생성하기</h1>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/984b4edd-0f4d-4529-a5f2-1e07954eeeb7/image.png" alt=""></p>
<p>nest-cli에서 컨트롤러, 모듈 등을 자동으로 생성해주는 명령어를 제공해준다.</p>
<h2 id="컨트롤러-생성">컨트롤러 생성</h2>
<p><code>nest g co 컨트롤러 이름</code></p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/dd85d138-4d03-4597-bb74-50b8a0a00233/image.png" alt=""></p>
<p>controller 파일과 spec 파일이 생성되었다.
spec은 테스트 용도로 생성되는 파일이다.</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/7ab892ea-3339-4a46-b462-203f83b34db4/image.png" alt=""></p>
<p>board로 컨트롤러를 생성해서, 컨트롤러 데코레이터 안에 board가 입력되어 생성된다.</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/d1b4826c-7198-43a1-a0ae-360c1541abdd/image.png" alt=""></p>
<p>또한, 자동으로 app.module.ts에 컨트롤러가 등록되어 있다.</p>
<h1 id="네임-컨벤션">네임 컨벤션</h1>
<p>nestjs에서 파일 이름을 작성할 때, camelCase가 아닌, kekab-case를 사용한다.
ex) user-profile.controller.ts</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[복사]]></title>
            <link>https://velog.io/@r0_0r/%EB%B3%B5%EC%82%AC</link>
            <guid>https://velog.io/@r0_0r/%EB%B3%B5%EC%82%AC</guid>
            <pubDate>Sat, 13 Jul 2024 14:17:03 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-typescript">let obj1 = {
  name: &#39;ganadi&#39;,
  detail: {
    age: 17,
  },
};

let obj2 = {...obj1};
let obj3 =  JSON.parse(JSON.stringify(obj1));


obj2.name = &#39;gomyami&#39;;
obj1.detail.age = 3;


console.log(obj2.name); // gomyami
console.log(obj2.detail.age); // 3
console.log(obj3.detail.age); // 17</code></pre>
<p>객체(혹은 배열)은 메모리 주소를 참조하는 식으로 변수가 할당된다.</p>
<p>그래서 <code>let objB = objA;</code> 이런 식으로 복사 효과를 바라고 했을 경우
<code>objA</code>를 수정하면 <code>objB</code>도 영향이 가고, 그 반대도 똑같다.</p>
<p>위에서 <code>obj2</code>는 <code>obj1</code>를 얕은 복사(shell copy)를 했는데,
새로운 참조 주소를 생성해서 <code>name</code> 속성을 그대로 복사해 온다.
하지만, <code>detail</code> 속성은 객체이므로, 같은 메모리 주소를 참조하게 된다.</p>
<p>그래서 obj1의 detail을 변경시키면 obj2의 detail도 변경되게 된다.</p>
<p><code>obj3</code>는 깊은 복사(deep copy)를 했다.
위에서 나온 방법은 깊은 복사를 하는 여러 방법 중 하나이다.</p>
<p>객체 안에 있는 속성을 그대로 복사하게 되므로, obj1, obj2와 서로 영향을 끼치지 않아
obj1에서 그대로 복사해온 값인 17이 그대로 출력되게 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[pentagon...]]></title>
            <link>https://velog.io/@r0_0r/pentagon</link>
            <guid>https://velog.io/@r0_0r/pentagon</guid>
            <pubDate>Mon, 17 Jun 2024 12:46:56 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-dart">
import &#39;dart:math&#39;;

import &#39;package:flutter/material.dart&#39;;

class PentagonPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.black
      ..strokeWidth = 1
      ..style = PaintingStyle.stroke;

    final path = Path();
    final center = Offset(size.width / 2, size.height / 2);
    final radius = min(size.width / 2, size.height / 2);
    const angle = (2 * pi) / 5;

    path.moveTo(
        center.dx + radius * cos(-pi / 2), center.dy + radius * sin(-pi / 2));

    for (int i = 1; i &lt;= 5; i++) {
      path.lineTo(center.dx + radius * cos(angle * i - pi / 2),
          center.dy + radius * sin(angle * i - pi / 2));
    }
    path.close();
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flow text...]]></title>
            <link>https://velog.io/@r0_0r/Flow-text</link>
            <guid>https://velog.io/@r0_0r/Flow-text</guid>
            <pubDate>Mon, 17 Jun 2024 12:45:54 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-dart">
import &#39;package:flutter/material.dart&#39;;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text(&#39;Marquee Text Example&#39;),
        ),
        body: const Center(
          child: SizedBox(
            width: 250,
            child: MarqueeText(
              text: &#39;짧은 글입니다. 짧은 글입니다. 짧은 글입니다. 짧은 글입니다.&#39;,
            ),
          ),
        ),
      ),
    );
  }
}

class MarqueeText extends StatefulWidget {
  final String text;
  final TextStyle style;
  final Duration duration;

  const MarqueeText({
    super.key,
    required this.text,
    this.style = const TextStyle(fontSize: 24),
    this.duration = const Duration(seconds: 5),
  });

  @override
  _MarqueeTextState createState() =&gt; _MarqueeTextState();
}

class _MarqueeTextState extends State&lt;MarqueeText&gt;
    with SingleTickerProviderStateMixin {
  late final ScrollController _scrollController;
  late final AnimationController _animationController;
  late final Animation&lt;double&gt; _animation;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(
        milliseconds: widget.text.length * 150,
      ),
    );
    _animation = Tween&lt;double&gt;(begin: 0, end: 1).animate(_animationController)
      ..addListener(() {
        if (_scrollController.hasClients) {
          _scrollController.jumpTo(
              _scrollController.position.maxScrollExtent * _animation.value);
        }
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          _animationController.repeat(
            reverse: true,
          );
        }
      });

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_scrollController.hasClients) {
        _animationController.forward();
      }
    });
  }

  @override
  void dispose() {
    _animationController.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ShaderMask(
      shaderCallback: (Rect bounds) {
        return const LinearGradient(
          begin: Alignment.centerLeft,
          end: Alignment.centerRight,
          colors: [
            Colors.transparent,
            Colors.black,
            Colors.black,
            Colors.transparent
          ],
          stops: [0.0, 0.05, 0.95, 1.0],
        ).createShader(bounds);
      },
      blendMode: BlendMode.dstIn,
      child: SingleChildScrollView(
        controller: _scrollController,
        scrollDirection: Axis.horizontal,
        child: Text(
          widget.text,
          style: widget.style,
        ),
      ),
    );
  }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter / Animation - Dots]]></title>
            <link>https://velog.io/@r0_0r/Flutter-Animation-Dots</link>
            <guid>https://velog.io/@r0_0r/Flutter-Animation-Dots</guid>
            <pubDate>Tue, 11 Jun 2024 13:42:25 GMT</pubDate>
            <description><![CDATA[<p>로딩 애니메이션으로 점이 점프하는 듯한 걸 만들어 보자.</p>
<pre><code class="language-dart">class JumpingDots extends StatefulWidget {
  const JumpingDots({
    super.key,
    required this.size,
    required this.dots,
  });

  final double size; // 점 크기
  final int dots; // 점 갯수

  @override
  State&lt;JumpingDots&gt; createState() =&gt; _JumpingDotsState();
}

class _JumpingDotsState extends State&lt;JumpingDots&gt;
    with TickerProviderStateMixin {

    // 각 점마다 애니메이션을 주기 위해 List로 선언했다.
    // 위젯이 생성될 때 애니메이션을 적용하기 위함.
  late List&lt;AnimationController&gt; animationControllers;
  late List&lt;Animation&lt;double&gt;&gt; animations;

  @override
  void initState() {
    super.initState();
    _initAnimations();
  }

  @override
  void dispose() {
  // 위젯이 페이지에서 사라질 때, 모든 점들의 애니메이션도 dispsoe해줘야 한다.
    for (var controller in animationControllers) {
      controller.dispose();
    }
    super.dispose();
  }

  void _initAnimations() {
  // 리스트 생성으로 애니메이션 컨트롤러 등록.
    animationControllers = List.generate(
      widget.dots,
      (index) {
        return AnimationController(
          vsync: this,
          duration: const Duration(milliseconds: 500),
        );
      },
    );

    // 생성된 컨트롤러에 애니메이션 적용.
    animations = List.generate(
      widget.dots,
      (index) {
          // begin, end가 변할 범위이다.
        return Tween&lt;double&gt;(begin: 0, end: 5).animate(
          CurvedAnimation(
            parent: animationControllers[index],
            curve: Curves.easeInOut,
          ),
        );
      },
    );

    // 애니메이션에 딜레이를 줘서 차례대로? 움직이게 하기.
    for (int i = 0; i &lt; animationControllers.length; i++) {
      Future.delayed(Duration(milliseconds: 300 * i)).then((_) {
        animationControllers[i].repeat(reverse: true);
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: List.generate(animationControllers.length, (index) {
      // 애니메이션 빌더를 이용해야 한다.
        return AnimatedBuilder(
          animation: animations[index],
          builder: (context, child) {
              // Transform 위젯으로 y 값을 변화시킨다.
            return Transform.translate(
              offset: Offset(0, animations[index].value),
              child: Container(
                margin: const EdgeInsets.symmetric(horizontal: 2),
                height: widget.size,
                width: widget.size,
                decoration: const BoxDecoration(
                  color: Colors.black,
                  shape: BoxShape.circle,
                ),
              ),
            );
          },
        );
      }),
    );
  }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[flutter / JSON Serialization, Retrofit]]></title>
            <link>https://velog.io/@r0_0r/flutter-JSON-Serialization-Retrofit</link>
            <guid>https://velog.io/@r0_0r/flutter-JSON-Serialization-Retrofit</guid>
            <pubDate>Mon, 06 May 2024 14:41:15 GMT</pubDate>
            <description><![CDATA[<h3 id="json-serialization">Json Serialization</h3>
<p>서버와 통신할 때, 데이터 모델을 직렬화하기 위해 쓰인다.</p>
<pre><code class="language-dart">class DataModel {
  final String title, nick, content;

  DataModel.fromJson(Map&lt;String, dynamic&gt; json)
      : title = json[&#39;title&#39;],
        about = json[&#39;nick&#39;],
        genre = json[&#39;content&#39;];
}</code></pre>
<p>이런 식으로 작성하여 직렬화를 한다.</p>
<p>flutter 패키지 중에 이런 작업을 자동화해주는 패키지가 있다.</p>
<pre><code class="language-dart">import &#39;package:json_annotation/json_annotation.dart&#39;;
part &#39;data_model.g.dart&#39;;

@JsonSerializable()
class DataModel {
    final String title, nick, content;

  DataModel({
  required this.title,
  required this.nick,
  required this.content,
 });

  factory DataModel.fromJson(Map&lt;String, dynamic&gt; json) =&gt; _$DataModelFromJson(json);
  Map&lt;String, dynamic&gt; toJson() =&gt; _$DataModelToJson(this);
}</code></pre>
<p>어노테이션으로 <code>@JsonSeriallizable()</code>를 클래스 위에 작성하고 저장하면
패키지가 알아서 <code>date_model.g.dart</code> 파일을 생성하고 직렬화 코드를 작성해준다.</p>
<h3 id="retrofit">Retrofit</h3>
<p>서버와 통신할 때 작성하는 코드 부분을 자동으로 생성해주는 역할을 해준다.</p>
<pre><code class="language-dart">part &#39;free_repository.g.dart&#39;;

final freeRepositoryProvider = Provider((ref) {
  final url = &#39;$baseUrl/board/free&#39;; // 통신할 주소
  final dio = ref.watch(dioProvider); // dio provider
  final repository = FreeRepository(dio, baseUrl: url);
  return repository;
});

@RestApi()
abstract class FreeRepository {
  factory FreeRepository(Dio dio, {String baseUrl}) = _FreeRepository;

  // 전체 게시글 가져오기
  @GET(&#39;/&#39;)
  Future&lt;FreeListModel&gt; getLists({@Query(&#39;page&#39;) required int page});

  // ...
}</code></pre>
<p>추상화 클래스 위에 <code>@RestApi()</code>를 작성하면, 패키지가 통신에 필요한 코드를 작성하여 새로 파일을 생성한다.</p>
<h3 id="dio-interceptor">Dio Interceptor</h3>
<p>Dio 패키지는 서버와 통신할 때 쓰는 패키지인데, 유용한 기능인 <code>Interceptor</code>가 있다.</p>
<p>뜻 그대로, 가로채는 것인데 요청 보내기 전, 요청 받기 전, 에러 받기 전에 어떤 작업을 할지 작성해 줄 수가 있다.</p>
<pre><code class="language-dart">  @override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {

    if (options.headers[&#39;accessToken&#39;] == &#39;true&#39;) {
      options.headers.remove(&#39;accessToken&#39;);

      final token = await storage.read(key: ACCESS_KEY);

      options.headers.addAll({&#39;Authorization&#39;: token});
    }

    return super.onRequest(options, handler);
  }</code></pre>
<p>다른 파일에서 서버와 통신하는 코드에서 헤더에 accessToken: true를 넣어서 보내면</p>
<p>Dio에서 서버로 보내기 전에, 헤더에 유저의 AccessToken으로 바꾼 후, 서버로 요청하게 된다.</p>
<p>로그 기록이나, 토큰이 만료됐을 경우, 리프레시 토큰을 이용하여 갱신하는 등의 작업이 가능할 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter / DefaultLayout]]></title>
            <link>https://velog.io/@r0_0r/Flutter-DefaultLayout</link>
            <guid>https://velog.io/@r0_0r/Flutter-DefaultLayout</guid>
            <pubDate>Sat, 20 Apr 2024 12:15:35 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;

class DefaultLayout extends StatelessWidget {
  const DefaultLayout({
    super.key,
    required this.child,
  });

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: child,
    );
  }
}
</code></pre>
<pre><code class="language-dart">Widget build(BuildContext context) {
return DefaultLayout(
    child: SafeArea(
        child: ...
        ),
    );
}</code></pre>
<p>공통적으로 적용시키고 싶은 레이아웃을 적용시킬 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[flutter / context??]]></title>
            <link>https://velog.io/@r0_0r/flutter-context</link>
            <guid>https://velog.io/@r0_0r/flutter-context</guid>
            <pubDate>Wed, 17 Apr 2024 11:29:19 GMT</pubDate>
            <description><![CDATA[<p>Theme 정보나, navigator 정보 등을 가져올 때 context를 통해서 가져오게 되는데
이러한 정보는 현재 가장 가까운 부모의 정보를 가져오게 되는 걸로 알고 있다.</p>
<p>showDialog의 경우, useRootNavigator의 기본값이 true이기 때문에
가장 가까운 navigator 정보가 아닌 루트(최상위) 정보를 가져오게 되는 것 같다.</p>
<p>rootNavigator가 한 개라면, 큰 문제가 발생하지 않을 듯...
하지만 여러 개일 경우(shellRoute를 사용한다든지), 문제가 발생하는 것 같다.</p>
<p>그래서 Theme 정보를 가져올 때도 오류가 발생했던 게 아닐까.</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/35690328-42f5-40a9-b4bd-92e95e38ae2c/image.png" alt=""></p>
<p>useRootNavigator가 true일 때, 최상위 위젯(Material App)이 루트가 됐다.</p>
<p><img src="https://velog.velcdn.com/images/r0_0r/post/f9b005bf-7925-4ac3-98d4-e32a4130933f/image.png" alt=""></p>
<p>useRootNavigator가 false일 때, 가장 가까운 위젯 트리의 부모(Profile)이 루트가 됐다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[flutter / Form에서 TextField 여러 개 쓰기]]></title>
            <link>https://velog.io/@r0_0r/flutter-Form%EC%97%90%EC%84%9C-TextField-%EC%97%AC%EB%9F%AC-%EA%B0%9C-%EC%93%B0%EA%B8%B0</link>
            <guid>https://velog.io/@r0_0r/flutter-Form%EC%97%90%EC%84%9C-TextField-%EC%97%AC%EB%9F%AC-%EA%B0%9C-%EC%93%B0%EA%B8%B0</guid>
            <pubDate>Wed, 10 Apr 2024 12:01:45 GMT</pubDate>
            <description><![CDATA[<p>Form에서는 여러 개의 텍스트 필드를 사용하게 되는데,
하나씩 관리하는 것보다, 한꺼번에 관리하는 게 낫지 않을까 해서, 찾아본 결과.</p>
<h3 id="텍스트-필드-생성-클래스">텍스트 필드 생성 클래스</h3>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;

class RenderTextField extends StatelessWidget {
  const RenderTextField({
    super.key,
    required this.label,
    required this.onSaved,
    required this.validator,
  });

  final String label; // 라벨 표시 ex) 아이디
  final FormFieldSetter onSaved; // 전송이나 저장 버튼 눌렀을 시 변수에 저장하는 함수
  final FormFieldValidator validator; // 값을 검증하는 함수

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          children: [
            Text(
              label,
              style: TextStyle(
                fontSize: Theme.of(context).textTheme.displayLarge?.fontSize,
                color: Theme.of(context).colorScheme.primary,
              ),
            ),
          ],
        ),
        TextFormField(
          onSaved: onSaved,
          validator: validator,
          autovalidateMode: AutovalidateMode.always, // 검증 함수를 실시간으로 실행(?)해주는 듯.
          decoration: InputDecoration(
            filled: true,
            fillColor: Colors.grey[200],
          ),
        ),
        const SizedBox(height: 20,),
      ],
    );
  }
}</code></pre>
<h3 id="실제-화면에서-사용">실제 화면에서 사용</h3>
<pre><code class="language-dart">final formKey = GlobalKey&lt;FormState&gt;();
final String id = &#39;&#39;;
final String pw = &#39;&#39;;

  renderButton() {
    return ElevatedButton(
      onPressed: () async {
        if (formKey.currentState!.validate()) { // 검증 통과 시 true가 반환되는 듯.
          debugPrint(&#39;통과됨&#39;);
          formKey.currentState!.save(); // 변수에 저장이 된다.
        }
      },
      child: const Text(
        &#39;테스트&#39;,
        style: TextStyle(color: Colors.black, fontSize: 16),
      ),
    );
  }
// ...
Form(
  key: formKey,
  child: Column(
    children: [
      RenderTextField(
        label: &#39;아이디&#39;,
        onSaved: (value) {
             setState(() {
              this.id = value;
          }
        },
        validator: (value) {
          if(value.length &lt; 1) return &#39;반드시 입력해야 합니다&#39;;
          if(value.length &lt; 4) return &#39;4글자 이상 입력해야 합니다&#39;;
          return null;
        },
      ),
      RenderTextField(
        label: &#39;비밀번호&#39;,
        onSaved: (value) {
           setState(() {
              this.id = value;
          }
        },
        validator: (value) {
          if(value.length &lt; 1) return &#39;반드시 입력해야 합니다&#39;;
          if(value.length &lt; 8) return &#39;8글자 이상 입력해야 합니다&#39;;
          return null;
        },
      ),
      const SizedBox(
          height: 10,
      ),
      renderButton(),
    ],
  ),
),
// ...</code></pre>
]]></description>
        </item>
    </channel>
</rss>