<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yeonjiyooo_.log</title>
        <link>https://velog.io/</link>
        <description>1 ^ 365 = 1 이지만 1.01 ^ 365 = 37쩜 어쩌고... 이다!</description>
        <lastBuildDate>Wed, 15 Oct 2025 07:00:08 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>yeonjiyooo_.log</title>
            <url>https://velog.velcdn.com/images/yeonjiyooo_/profile/64d28d56-1fe4-482b-80c0-42980724a32c/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. yeonjiyooo_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yeonjiyooo_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Spring Boot] JWT를 이용한 로그인 구현]]></title>
            <link>https://velog.io/@yeonjiyooo_/Spring-Boot-JWT%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@yeonjiyooo_/Spring-Boot-JWT%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Wed, 15 Oct 2025 07:00:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Spring Security을 적용하지 않고 JWT 인증 방식을 간단하게 구현한 과정을 설명한 글입니다.</p>
</blockquote>
<h2 id="1-jwt란-무엇인가">1. JWT란 무엇인가?</h2>
<p>JWT(JSON Web Token)이란 JSON 형식으로 인증에 필요한 정보들을 담고 비밀키로 암호화한 서명을 포함한 토큰으로 사용자에 대한 인증을 수행하기 위해 사용됩니다.</p>
<p>여기서 주목해야할 특징은 다음 두 가지입니다.</p>
<ol>
<li>인증에 필요한 정보를 담음</li>
<li>비밀키로 암호화</li>
</ol>
<p>먼저 첫 번째 특징 &quot;<strong>인증에 필요한 정보를 담음</strong>&quot; 에 대해 알아봅시다.</p>
<p>JWT는 Header, Payload, Signature 총 세 가지 부분이 연결되어 하나의 String 값으로 표현됩니다. 이 때, 인증에 피룡한 정보가 담기는 곳이 바로 <strong>Payload</strong> 입니다.
사용자나 시스템과 같은 토큰의 주체에 관한 정보를 key-value 형태로 담아 보낼 수 있습니다. 이를 통해 DB 조회 없이도 토큰의 소유자는 누군지, 이름은 무엇인지 등의 정보를 알 수 있게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/d8a855e4-bc96-4914-b330-a86544874136/image.png" alt="Token"></p>
<p>두 번째 특징으로는 &quot;<strong>비밀키를 통한 암호화</strong>&quot;가 있습니다.</p>
<p>앞서 언급한 JWT의 세 가지 부분 중 Signature에 해당됩니다. Header와 Payload를 합친 문자열을 서버의 비밀키로 암호화한 서명을 통해 토큰의 유효성을 검증할 수 있게 됩니다. 서명에 사용된 비밀키는 서버만 알고 있기 때문에 서명을 만들 수 있는 주체는 오직 서버뿐입니다. 이를 통해 토큰의 진위성을 보장해줄 수 있게 됩니다.
<br></p>
<h2 id="2-jwt를-통한-회원-인증-과정">2. JWT를 통한 회원 인증 과정</h2>
<p>그렇다면 JWT를 통해 어떻게 회원 인증을 구현할 수 있을까요? accessToken과 refreshToken 을 사용한 인증 과정을 아래와 같이 정리할 수 있습니다.</p>
<ol>
<li>클라이언트가 서버에게 로그인 정보 (ex. email, pw)를 전송합니다.</li>
<li>로그인 정보를 받은 서버는 이를 바탕으로 올바른 로그인 요청인지 검증합니다.</li>
<li>만약 <strong>유효한 로그인 정보라면 accessToken과 refreshToken을 발급</strong>하고, 이를 응답에 담아 보냅니다.</li>
<li>Token 정보를 응답 받은 클라이언트는 <strong>앞으로의 모든 요청의 헤더에 accessToken 값을 담아 보냅니다</strong>.</li>
<li>서버에서는 클라이언트 요청 헤더에 담긴 accessToken 의 유효성을 검증합니다.</li>
<li><strong>유효한 Token임이 검증된 경우에만 요청을 정상적으로 처리</strong>하고, 만약 유효하지 않은 Token이라면 (ex. expiration이 지난 경우) *<em>401 Unauthorized *</em>를 발생시킵니다.</li>
<li>401 Unauthorized 를 응답 받은 클라이언트는 refreshToken을 활용하여 accessToken 재발급을 시도합니다.</li>
<li>refreshToken 값의 유효성 여부를 검증하고 이 결과를 바탕으로 서버는 <strong>accessToken을 재발급 해주거나, token을 만료</strong>시킵니다.</li>
<li>accessToken 재발급에 성공한 경우 5번 과정부터 다시 반복하며 요청 - 응답을 반복합니다.</li>
<li>accessToken 재발급에 실패한 경우는 사용자가 직접 재인증 과정을 거쳐야 합니다.</li>
</ol>
<br>


<h2 id="3-jwt-인증을-위한-jwtutils">3. JWT 인증을 위한 JwtUtils</h2>
<p>JWT와 관련된 여러 라이브러리가 있는데 그 중 JJWT를 사용하였습니다. JJWT 깃허브에 들어가보면 README 문서화가 잘 되어 있어서 읽어가면서 공부하면 좋을 것 같습니다!</p>
<p><a href="https://github.com/jwtk/jjwt?tab=readme-ov-file">JJWT Github</a>
<img src="https://velog.velcdn.com/images/yeonjiyooo_/post/cf36f33c-0a56-4ba8-9591-b7bfb063bd46/image.png" alt=""></p>
<p>JJWT는 Java에서 JWT(JSON Web Token)를 생성, 파싱, 검증하는 데 사용되는 라이브러리로, Jwts 클래스를 사용하여 간단하게 토큰을 생성, 파싱, 검증할 수 있습니다. </p>
<p>조금 더 자세히 설명해보자면, JJWT는 토큰 생성을 위한 객체들을 <strong>추상화</strong>하기 위한 라이브러리 입니다. JJWT 라이브러리를 사용하는 개발자가 실제 토큰 생성 모듈에 직접 의존하는 것이 아닌 이를 추상화한 인터페이스에 의존하게 됨으로서, <strong>의존성을 역전(DIP)</strong> 시키고 내부 로직을 <strong>캡슐화</strong> 시킨다는 장점이 있습니다. 만약 토큰 생성 모듈 내부의 변화가 생기더라도 개발자가 자신의 코드를 직접 수정하는 일은 발생하지 않게 되는 것이죠.</p>
<h3 id="21-dependency-설치">2.1 Dependency 설치</h3>
<p>본격적으로 JJWT를 사용하기에 앞서 의존성을 설치해주어야 합니다. build.gradle 파일에 아래 코드를 작성하고, IntelliJ 기준 우측 상단의 Gradle 아이콘을 눌러 Reload 시켜주면 설치가 완료됩니다.</p>
<pre><code>dependencies {
    implementation &#39;io.jsonwebtoken:jjwt-api:0.12.3&#39;
    implementation &#39;io.jsonwebtoken:jjwt-impl:0.12.3&#39;
    implementation &#39;io.jsonwebtoken:jjwt-jackson:0.12.3&#39;
}</code></pre><h3 id="22-token-발급">2.2 Token 발급</h3>
<p>JwtUtil 클래스 내부에 다음과 같은 Token 발급 메소드를 작성했습니다.</p>
<pre><code class="language-java">public String createAccessToken(UserEntity userEntity) {
    return Jwts.builder()
            .subject(String.valueOf(userEntity.getUserId()))
            .claim(&quot;userId&quot;, userEntity.getUserId())
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + accessExpiration))
            .signWith(secretKey)
            .compact();
}</code></pre>
<p>사용된 메소드들을 하나씩 살펴보겠습니다.</p>
<ul>
<li><p><strong>builder</strong>(): JWT 빌더 객체 생성<br>  헤더와 페이로드가 비어있는 상태의 JWT 빌더 객체를 생성합니다.</p>
</li>
<li><p><strong>subject</strong>(): 표준 클레임 sub 설정
  sub: 토큰의 주체가 되는 사용자의 식별자 (ex. userId)
  JWT의 표준 클레임 중 하나인 sub를 설정하는 메소드입니다. 이 때 subject 메소드의 파라미터로는 String만 가능하기 때문에 타입을 맞춰주어야 합니다.
  위 코드에서 사용된 userEntity의 userId의 타입은 Long 이기 때문에 String.valueOf()를 통해 타입 변환을 해주었습니다.</p>
</li>
<li><p><strong>claim</strong>(): 커스텀 클레임 추가
  sub와 같은 표준 클레임 외에도 비지니스 로직과 관련된 커스텀 클레임을 추가할 수 있습니다.</p>
</li>
<li><p><strong>issuedAt</strong>(): 표준 클레임 iat 설정
iat: 토큰이 발급된 시각
Date()를 통해 토큰 생성 메소드가 실행되는 시점의 시각을 토큰 발급 시간 iat로 설정합니다.</p>
</li>
<li><p><strong>expiration</strong>(): 표준 클레임 exp 설정
  exp: 토큰이 만료되는 시간
토큰이 생성되는 현재 시각에 미리 설정한 토큰 만료 시간 accessExpiration을 더하여 만료 시간을 설정합니다.</p>
</li>
<li><p><strong>signWith</strong>(): JWS 서명 설정
  미리 설정 해놓은 secretKey 암호화에 사용되는 비밀키로 설정합니다. 이 단계에서 암호화 알고리즘을 의미하는 표준 클레임 alg이 자동으로 세팅됩니다.</p>
</li>
<li><p><strong>compact</strong>(): Header와 Payload Base64 인코딩
앞에서 설정한 Header와 Payload를 인코딩 한 후 .으로 이어 붙이고 비밀키로 암호화한 서명을 만들어 Header.Payload.Sinature 형태의 문자열을 생성합니다. </p>
</li>
</ul>
<p>위의 과정을 거쳐 최종적으로 accessToken 문자열이 생성됩니다. refreshToken도 위와 동일한 과정으로 발급됩니다. expiration 메소드의 파라미터로 accessExpiration 대신 refreshExpiration을 넣어주면 됩니다.</p>
<h4 id="➕-accesstoken--refreshtoken">➕ AccessToken / RefreshToken</h4>
<p>여기서 잠깐 accessToKen과 refreshToken의 만료 시간 설정에 대해 짚고 넘어가면 좋을 것 같습니다. 이것에 대한 이해를 위해서는 refreshToKen의 도입 계기를 먼저 알아보겠습니다.</p>
<p>AccessToken만을 이용하여 인증을 한다면, 유효기간 설정에 따른 단점이 존재하게 됩니다. 토큰의 유효 기간을 짧게 설정한다면 사용자가 인증 과정을 자주 거쳐야 합니다. 이는 당연하게도 사용자 경험의 저하로 연결됩니다. 그렇다고해서 유효 기간을 무작정 길게 설정한다면 토큰을 탈취한 공격자가 긴 만료 시간 전까지 마음껏 리소스에 접근할 수 있게 됩니다. JWT 방식의 특성상 서버가 이미 생성된 토큰에 대해서는 추적이나 제어가 불가능하기 때문입니다.</p>
<p>이러한 단점을 극복자고자 도입된 것이 <strong>refreshToken</strong>의 개념입니다.</p>
<p>보안을 위해 accessToken의 유효 기간은 짧게 설정하되, refreshToken을 통해 자동으로 accessToken을 재발급 받음으로서 사용자가 직접 재인증 과정을 거치지 않아도 되도록 만들어줍니다. 이 때 사용되는 refreshToken의 유효 기간은 상대적으로 길게 설정을 해서 사용자 경험이 저하되는 것을 방지해줍니다.</p>
<p>정리하자면 일반적으로 <strong>accessToken의 유효기간은 짧게, refreshToken의 유효기간은 길게</strong> 설정합니다. 토큰을 정확히 어떻게 다루느냐는 프로젝트나 회사마다 정책의 차이가 존재하기 때문에 명확한 정답은 없습니다.</p>
<h3 id="23-token-유효성-검증">2.3 Token 유효성 검증</h3>
<p>다음으로는 토큰의 유효성을 검증하는 메소드 입니다. 마찬가지로 사용된 메소드들을 하나씩 정리해보겠습니다.</p>
<pre><code class="language-java">public Boolean verifyToken(String token) {
    try {
        Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(token);
        return true;
    } catch (Exception e) {
        throw new CustomException(ErrorCode.UNAUTHORIZED_TOKEN);
    }
}</code></pre>
<ul>
<li><strong>parser</strong>(): JWT parser를 만들기 위한 builder 반환</li>
<li><strong>verifyWith</strong>(): 서명 검증에 사용할 비밀키 등록
  서버가 자신이 서명한 토큰이 맞는지 확인하기 위해 검증에 사용할 비밀키를 등록하는 메소드입니다. 파서는 해당 비밀키를 받아 헤더에 적힌 alg과 호환되는 키 형식인지 확인하고, 맞지 않는다면 예외(WeakKeyException)를 발생시킵니다.</li>
<li><strong>build</strong>(): JwtParser를 생성
  앞선 메소드에서 설정한 정보들을 바탕으로 JwtParser를 만들어 반환해줍니다. 토큰 검증을 위한 준비가 끝난 단계라고 생각하면 됩니다.</li>
<li><strong>parseSignedClaims</strong>(): 토큰 정보를 검증하는 메소드
생성된 JwtParser의 메소드로 토큰 문자열은 Header, Payload, Signature 세 부분으로 분해한 후 Base64 디코딩을 진행합니다. 서명은 verifyWith에서 받은 비밀키로 검증을 진행하고 Payload는 Claims로 매핑하여 반환해줍니다. 또한 exp, nbf와 같은 클레임에 대한 유효성 체크도 진행합니다.
반환값은 Jws<Claims>로 getHeader(), getPayload()로 토큰의 값에 접근할 수 있습니다.</li>
</ul>
<p>유효성 검증 과정에서 발생할 수 있는 예외는 다음과 같은 것들이 있습니다.위 코드에서는 발생하는 예외를 catch 문에서 CustomException 객체로 처리했습니다.</p>
<ul>
<li>서명 불일치/위조 : io.jsonwebtoken.security.SecurityException</li>
<li>만료: ExpiredJwtException</li>
<li>포맷/구조 이상: MalforemdJwtException</li>
</ul>
<br>

<h2 id="4-authcontroller">4. AuthController</h2>
<p>이제 각 계층 별 코드를 살펴보겠습니다. 먼저 로그인, 로그아웃 로직과 관련된 도메인의 이름을 Auth 로 설정했기 때문에 AuthController, AuthService 라고 부르겠습니다.</p>
<pre><code class="language-java">@PostMapping(&quot;/token&quot;)
public ResponseEntity&lt;DataResponseDto&lt;TokenDto&gt;&gt; login(@RequestBody LoginDto loginDto) {
      TokenDto tokenDto = authService.login(loginDto);
       return ResponseEntity.status(HttpStatus.OK)
            .body(DataResponseDto.of(HttpStatus.OK, &quot;LOGIN_SUCCESS&quot;, &quot;로그인에 성공했습니다.&quot;, tokenDto));
}</code></pre>
<p>요청 body는 String 타입의 email과 password를 필드로 가지는 LoginDto를 정의해서 사용했습니다. 
 응답에는 String 타입의 accessToken과 refreshToken을 필드로 가지는 TokenDto를 담을 수 있게 작성했습니다.
  <br></p>
<h2 id="5-authservice">5. AuthService</h2>
<pre><code class="language-java">public TokenDto login(LoginDto loginDto) {
        if (loginDto == null ||
                !StringUtils.hasText(loginDto.email()) ||
                !StringUtils.hasText(loginDto.password())) {
            throw new CustomException(ErrorCode.BAD_REQUEST);
        }
        UserEntity userEntity = userRepository.findByEmail(loginDto.email())
                .orElseThrow(() -&gt; new CustomException(ErrorCode.USER_NOT_FOUND));

        if(!userRepository.isValidUser(loginDto)) {
            throw new CustomException(ErrorCode.UNAUTHORIZED_USER);
        }

        String accessToken = jwtUtils.createAccessToken(userEntity);
        String refreshToken = jwtUtils.createRefreshToken(userEntity);

        return TokenDto.builder()
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }</code></pre>
<p>  다음으로 로그인 비지니스 로직을 처리하는 서비스 계층의 login 메서드 입니다. 먼저 로그인 정보로 들어온 email과 password의 값의 존재 여부를 확인하고 비어있다면 BAD_REQUEST 예외를 던집니다.</p>
<p>  이후에는 User DB에서 해당 로그인 정보에 매핑되는 사용자가 있는지 확인하고 유효한 사용자라면 앞서 작성한 JwtUtils의 createAccess(Refresh)Token 메소드를 통해 토큰을 발급합니다.</p>
<p>  마지막으로 TokenDto 객체를 builde() 메소드를 통해 생성하고 반환해줍니다.
  <br></p>
<h2 id="6-마치며">6. 마치며</h2>
<p>스프링을 사용한 첫 프로젝트여서 아직 코드 자체에 대한 완성도는 낮습니다! 특히 로그인 정보에 대한 유효성 검증 부분은 아직 구현하지 못한 상태라는 점 참고 부탁드립니다. 빠른 시일 내에 기능 추가 및 리팩토링을 해서 포스팅 해놓겠습니다 🙌  혹시라도 글을 읽으시면서 이상한 점이나 오류를 발견하신다면 언제든 댓글에 남겨주세요!</p>
<hr>
<p>  [이미지 출처] <a href="https://cloud.google.com/apigee/docs/api-platform/security/oauth/using-jwt-oauth?hl=ko">https://cloud.google.com/apigee/docs/api-platform/security/oauth/using-jwt-oauth?hl=ko</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] IoC (Inversion of Control) 제어의 역전]]></title>
            <link>https://velog.io/@yeonjiyooo_/Spring-IoC-Inversion-of-Control-%EC%A0%9C%EC%96%B4%EC%9D%98-%EC%97%AD%EC%A0%84</link>
            <guid>https://velog.io/@yeonjiyooo_/Spring-IoC-Inversion-of-Control-%EC%A0%9C%EC%96%B4%EC%9D%98-%EC%97%AD%EC%A0%84</guid>
            <pubDate>Mon, 29 Sep 2025 14:10:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>Spring과 첫 만남...🫠 IoC에 대해 공부해보자!</strong></p>
</blockquote>
<h2 id="1-ioc란">1. IoC란?</h2>
<p>객체 생성과 의존성 주입과 같은 과정을 개발자가 직접 하지 않고,  <strong>그 제어를 외부(프레임워크, 컨테이너)에 위임하는 것</strong>을 의미합니다. 개발자는 객체의 제어를 직접 할 필요가 없으므로 복잡한 요소들을 신경쓰지 않고 비지니스 로직에만 집중할 수 있게 됩니다.</p>
<p>과거에는 개발자들이 직접 코드 안에서 <code>new</code> 키워드를 사용해서 객체를 생성
→ 어떤 객체가 어떤 객체를 사용할 지 의존 관계 설정 필요</p>
<pre><code class="language-java">OrderService orderService = new OrderService();
PaymentService paymentService = new PaymentService();
orderService.setPaymentService(paymentService);</code></pre>
<p>IoC 를 사용하게 되면 <strong>객체 생성과 의존성 주입을 IoC 컨테이너가 대신 수행</strong></p>
<pre><code class="language-java">@Service
public class OrderService {
    //OrderService와 PaymentService는 의존 관계
    private final PaymentService paymentService;

    @Autowired
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}</code></pre>
<p><strong>🔼 코드 실행 과정</strong></p>
<ul>
<li>스프링이 @Service 가 붙은 OrderService를 Bean으로 등록하려고 함</li>
<li>이 때, OrderService는 PaymentService 라는 의존성이 필요</li>
<li>스프링이 컨테이너에서 PaymentService 타입의 Bean을 찾아 생성자의 파라미터로 넣어줌</li>
</ul>
<p>⇒ OrderService는 스스로 new PaymentService() 하지 않고도 정상적으로 동작!</p>
<h2 id="2-ioc-사용-이유">2. IoC 사용 이유</h2>
<ol>
<li><p><strong>객체 간 결합도 감소</strong>: 개발자가 직접 new 키워드를 통해 객체를 생성하지 않고 IoC 컨테이너가 객체 생성과 연결을 외부에서 처리해주기 때문에 객체 간 결합이 느슨해집니다.</p>
<p><code>OrderService</code> 는 <code>PaymentService</code> 라는 추상 타입에만 의존
 구체적인 구현체 (ex. <code>kakaoPay</code>, <code>TossPay</code>) 에는 의존하지 않음</p>
<p> 따라서 구현체가 바뀌어도 (ex. <code>kakaoPay</code> → <code>TossPay</code>) <code>OrderService</code> 수정 필요 X
 ⇒ 객체지향의 <strong>OCP 원칙</strong>과도 연결 🔗</p>
<p><strong>OPEN</strong> 확장에는 열려있음: 새로운 구현체 추가 및 변경 가능
<strong>CLOSED</strong> 수정에는 닫혀있음: OrderService는 건들이지 않음</p>
</li>
</ol>
<ol start="2">
<li><p><strong>지속가능성 및 확장성</strong>: 어플리케이션의 새로운 요구사항이나 기능에도 수정해야 하는 코드를 최소화하고 보다 간단하게 처리할 수 있게 됩니다. 이런 유연성이 지속 가능성을 보장하고 시스템이 커져도 확장성을 확보할 수 있게 해줍니다.</p>
</li>
<li><p><strong>테스트 용이성</strong>: 진짜 객체 대신 Mock 객체를 쉽게 주입할 수 있어 테스트가 용이해집니다.</p>
<p> 진짜 객체 대신 <strong>테스트용 mock 객체</strong> 주입 가능</p>
<pre><code>mock 객체: 실제 객체처럼 동작하지만, 테스트 목적에 맞게 단순화, 제어된 동작만 수행하는 객체    </code></pre><p>   → DB 연결, 외부 API 호출 같은 무거운 동작 없이 <strong>빠르게 테스트 가능</strong></p>
</li>
</ol>
<pre><code class="language-java">    PaymentService mockPaymentService = Mockito.mock(PaymentService.class);
    OrderService orderService = new OrderService(mockPaymentService);</code></pre>
<br>

<h4 id="-controller-service-repository--component">+) @Controller @Service @Repository + @Component</h4>
<p>스프링은 어플리케이션 실행 시 <strong>IoC 컨테이너</strong>를 만들고 그 안에 <strong>관리할 객체 = Bean 등록</strong>
→ 일일히 XML이나 @Bean 으로 등록할 수 도 있음 BUT 번거로움</p>
<p>⇒ <strong>Component Scan 컴포넌트 스캔 기능</strong> </p>
<p>특정 패키지를 탐색하여 @Component 계열 annotaion이 붙은 클래스를 찾아 자동으로 Bean 등록
→ <code>@Component</code> <code>@Controller</code> <code>@Service</code> <code>@Repository</code> 모두 스캔 대상</p>
<p><strong><code>@Component</code></strong> : 생성한 class를 Bean 으로 등록할 때 사용하는 가장 기본적인 annotaion</p>
<ul>
<li><strong><code>@Controller</code></strong> : 웹 계층에서의 Controller 역할(ex. HTTP 요청 처리)을 위한 객체</li>
<li><strong><code>@Service</code> :</strong> 비지니스 로직 계층에서 사용 비지니스 로직을 담당하는 서비스 클래스라는 의미</li>
<li><strong><code>@Repository</code></strong> : 데이터 접근 계층에서 사용하고 DB와 연동하는 코드에 붙임</li>
</ul>
<p>사실 전부 @Component 로 써도 Bean 등록은 됨
하지만 가독성을 향상시키고, 부가 기능을 적용하기 위해 상황에 맞는 annotation 선택</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[WEB] 웹페이지 접속 과정 살펴보기 with WireShark 🦈]]></title>
            <link>https://velog.io/@yeonjiyooo_/WEB-%EC%9B%B9%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A0%91%EC%86%8D-%EA%B3%BC%EC%A0%95-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0-with-WireShark</link>
            <guid>https://velog.io/@yeonjiyooo_/WEB-%EC%9B%B9%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A0%91%EC%86%8D-%EA%B3%BC%EC%A0%95-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0-with-WireShark</guid>
            <pubDate>Tue, 23 Sep 2025 13:26:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>브라우저에 도메인 주소를 입력했을 때의 내부 동작 과정을 정리해보았습니다. 
틀린 부분에 대한 지적과 조언은 언제든 환영입니다! 🙇🏻‍♀️</p>
</blockquote>
<p align="center"><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/b3fd09bb-3987-4915-bd32-cadfdd877e93/image.jpeg" width=400 align=center></p>

<h2 id="브라우저에-wwwkakaocom을-입력하면-어떤-과정을-거쳐-접속할-수-있을까">브라우저에 <a href="http://www.kakao.com%EC%9D%84">www.kakao.com을</a> 입력하면 어떤 과정을 거쳐 접속할 수 있을까?</h2>
<ol>
<li><strong>URL 입력 및 처리</strong><ul>
<li>브라우저 주소창에 <a href="http://www.kakao.com">www.kakao.com</a> 입력</li>
</ul>
</li>
</ol>
<ul>
<li><p>리다이렉트 확인</p>
<p>  초기 요청을 받은 서버가 URL을 다른 위치로 자동으로 보내게 설정되어 있을 수 있음
  ex. http:// 로 시작된 요청이 https://로 리다이렉트 될 수 있음</p>
<ul>
<li>공유 캐시 확인<pre><code>대규모 웹 서비스의 경우 네트워크 내에 공유 캐시(캐시 서버) 존재할 수 있음
자주 요청되는 리소스를 사용자와 물리적으로 가까운 웹 캐시 서버에 보관</code></pre></li>
</ul>
</li>
</ul>
<ol start="2">
<li><p><strong>DNS 조회</strong></p>
<ul>
<li><a href="http://www.kakao.com">www.kakao.com</a> 의 IP 주소를 찾기 위해 DNS 조회</li>
<li>로컬 DNS 캐시 조회 후 정보 확인 → 캐시에 정보가 없다면 DNS 서버에 조회 요청</li>
<li>필요에 따라 여러 DNS 서버를 거칠 수 있음</li>
</ul>
</li>
<li><p><strong>Forward Proxy 포워드 프록시</strong> <strong>(선택적)</strong></p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/f466faba-2424-4ae3-b717-64845011b5db/image.png" alt=""></p>
</li>
</ol>
<ul>
<li><p>같은 내부망에 존재하는 클라이언트의 요청을 받아 외부 서버에서 가져온 데이터로 응답</p>
</li>
<li><p><a href="http://www.kakao.com">www.kakao.com</a> 에 접속하려고 할 때 프록시 서버가 대신 리소스를 받아와 클라이언트에게 응답</p>
</li>
<li><p>자주 사용되는 리소스를 캐싱해두어 성능 향상</p>
</li>
<li><p>) Reverse Proxy 리버스 프록시</p>
<p> 클라이언트가 웹 서버에 접근시 해당 웹 서버에 직접 요청하는 것이 아닌 프록시로 요청
 → 프록시가 배후(reverse)의 서버로부터 데이터를 가져옴</p>
</li>
</ul>
<ol start="4">
<li><strong>TCP 연결 3-way handshake</strong></li>
</ol>
<ul>
<li><p>3-way handshake 클라이언트 ↔ 서버 연결을 위해 <strong>3개의 패킷</strong> 사용</p>
<ul>
<li><strong>C → S: SYN</strong>
  SYN segment: 데이터 채널을 위한 연결 파라미터 값을 실어 나름
  아직 연결 전이므로 데이터는 없고 <strong>헤더만 존재</strong></li>
</ul>
</li>
</ul>
<pre><code>- **S → C: SYN + ACK**</code></pre><p>   TCP는 <strong>reliable delivery</strong>를 해야하므로 받았으면 받았다는 확인을 해줘야 함
   서버 측에서 클라이언트에게 <strong>연결에 필요한 새로운 파라미터 값 정보인 SYN</strong> +
   앞선 클라이언트로부터 온 <strong>SYN을 잘 받았다는 ACK 정보</strong>도 함께 보냄
        → <strong>두 데이터 채널이 함께 묶여 만들어질 수 밖에 없음</strong> like 2인3각
        BUT 나중에 끊을 떄는 각각 끊을 수 있음</p>
<pre><code>- **C → S: ACK**
  S → C 채널을 위한 세 값을 헤더에 싣고 온 SYN에 대한 ACK
    별도의 ACK를 전송하지 않고 ACK 내용을 헤더에 표현한 뒤 ACK = 1로 설정해도 됨</code></pre><ol start="5">
<li><p><strong>HTTPS 보안 설정</strong></p>
<p> <img src="https://velog.velcdn.com/images/yeonjiyooo_/post/a84fa9f3-3819-48ea-8fb0-9bf199d4efd8/image.png" alt=""></p>
</li>
</ol>
<ul>
<li><p>TCP 연결이 되면 HTTPS로 통신하기 위한 <strong>TLS handshaking</strong> 수행</p>
<ul>
<li><p><strong>ClientHello (C)</strong>: 클라이언트가 지원하는 TLS, 암호화 제품군, 무작위 바이트 문자열 등을 전달</p>
</li>
<li><p><strong>ServerHello (S)</strong>: TLS 인증서, 서버가 가능한 암호화 방식, 서버 무작위 바이트 문자열, 세션 아이디 전달</p>
</li>
<li><p><strong>서버 인증서 검증 (C):</strong> 서버가 보낸 TLS 인증서 검증
  대부분의 브라우저에는 공신력있는 CA가 만든 공개키가 이미 설치되어 있음</p>
<p>  서버가 보낸 TLS 인증서가 CA가 만든 것이 맞는지 내장된 CA 공개키로 암호화된 인증서 복호화
  → 실제 인증서에 명시된 서버인지, 해당 도메인 소유자인지, 유효기간이 지나지 않았는지 확인</p>
<p>정상적으로 진행되었다면 사이트 <strong>공개키 및 정보 획득</strong></p>
</li>
<li><p><strong>Client Premaster Secret 생성 (C)</strong>: premaster secret 무작위 48바이트 문자열 전송
→ 인증서로부터 전달받은 서버의 공개키로 암호화, 서버의 개인키로만 복호화 가능</p>
</li>
<li><p><strong>Server Premaster Secret 복호화 (S)</strong>: 서버가 개인키로 복호화하여 master secret 으로 저장</p>
<pre><code>→ 이것을 사용하여 **session key** 생성 </code></pre></li>
<li><p><strong>Client Ready (C) &amp; Server Ready (S)</strong>: 세션 키로 암호화된 완료 메세지 전송
⇒ handshaking 완료 , <strong>세션키를 이용해 암호화된 통신 진행</strong></p>
</li>
</ul>
<p>TLS handshaking 이후 모든 HTTP 요청과 응답은 session key로 암호화</p>
</li>
</ul>
<br>

<p><strong>🔽 WireShark 패킷 예시</strong></p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/7bcd4480-5c17-410f-8f26-eca24442622a/image.png" alt=""></p>
<p>[407] <strong>SYN</strong>: 클라이언트가 서버에게 TCP 연결 요청</p>
<p>[414] <strong>SYN + ACK</strong>: 서버측에서 이전 SYN에 대한 응답 ACK와 연결을 위한 정보 SYN을 보냄</p>
<p>[415] <strong>ACK</strong>: 서버로부터 받은 SYN에 대한 응답 ACK를 보냄</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/9ba8a5b8-3bc0-4753-b987-ad32241673f8/image.png" alt=""></p>
<p>   [417] <strong>ClientHello</strong>: 클라이언트가 서버에게 TLS 세션 시작 요청</p>
<p>   [438] <strong>ServerHello</strong>: 서버의 응답 (실제 사용할 Cipher suite 선택, 키 교환을 위한 초기데이터 포함)</p>
<p>   [438] <strong>Change Cipher Spec</strong>: 암호화 전환 준비</p>
<p>   [439] <strong>Application Data</strong>: 서버 쪽에서 암호화된 데이터 전송 시작
   (TLS 1.3에서는 인증서도 Application Data 안에서 암호화된 상태로 전송)</p>
<p>   [441] <strong>Change Cipher Spec, ApplicatIon Data</strong>: 클라이언트도 암호화 모드로 전환</p>
<br>

<h3 id="✚-tls-transport-layer-security">✚ TLS (Transport Layer Security)</h3>
<p>인터넷 상의 커뮤니케이션을 위한 개인 정보와 데이터 보안을 용이하게 하기 위해 설계된 <strong>보안 프로토콜</strong>
SSL (Secure Sockets Layer) 라는 이전의 암호화 프로토콜에서 발전한 것</p>
<p><strong>TLS 의 역할</strong></p>
<ul>
<li><strong>암호화</strong>: 제3자로부터 전송되는 데이터를 숨김</li>
<li><strong>인증</strong>: 정보를 교환하는 당사자가 요청된 당사자임을 보장</li>
<li><strong>무결성</strong>: 데이터가 위조되거나 변조되지 않았는지 확인</li>
</ul>
<p>HTTPS: 암호화되어 데이터를 볼 수 없음
<img src="https://velog.velcdn.com/images/yeonjiyooo_/post/26fcc6b4-32fe-41db-bdaa-f84af4cf0e66/image.png" alt=""></p>
<p>HTTP: 요청과 응답에 담긴 데이터를 볼 수 있음
<img src="https://velog.velcdn.com/images/yeonjiyooo_/post/f7028d7f-cb26-475c-b9bd-e0d0d75f2f1d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TCP 완전 정복 ...ing]]></title>
            <link>https://velog.io/@yeonjiyooo_/TCP-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5</link>
            <guid>https://velog.io/@yeonjiyooo_/TCP-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5</guid>
            <pubDate>Tue, 23 Sep 2025 06:12:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 내용은 <strong>김효곤 교수님</strong>의 <a href="https://product.kyobobook.co.kr/detail/S000001732376">[인터넷 프로토콜]</a> 서적 및 강의를 기반으로 정리한 글입니다! 🙇🏻‍♀️</p>
</blockquote>
<p><strong>+</strong> 아직 완성이 안된 글입니다...! 공부 하는대로 계속 내용 추가 할 예정입니다 🔥🔥</p>
<h2 id="131-tcp-서비스-모델">13.1 TCP 서비스 모델</h2>
<p>인터넷 트래픽의 90% 이상을 TCP가 실어나름 ex. 웹, 이메일, 비디오 스트리밍 …
→ 잘 설계된 서비스 모델과 그를 위한 정교한 메커니즘 구현</p>
<p>TCP의 복잡한 메커니즘들은 모두 <strong>외부적으로 사용자에게 제공하는 서비스 모델을 구현</strong>하기 위한 장치</p>
<h3 id="1311-tcp의-특징">13.1.1 TCP의 특징</h3>
<p><strong>1. 🚚 reliable delivery 배달의 안전성 보장!</strong> </p>
<ul>
<li><strong>재전송 retransmission 메커니즘</strong>을 통한 신뢰성있는 배달</li>
<li>바로 아래 계층인 IP가 제공하는 서비스에 전혀 신뢰성 X → TCP가 극복해야함</li>
<li>여러 차례 재전송을 해서라도 꼭 배달
  → TCP에 배달을 맡긴 응용 계층은 신뢰성 있는 통신 채널을 가정하고 나머지 작업 수행</li>
</ul>
<p>⇒ 응용 계층 스스로가 성공적인 배달여부를 체크하면서 재전송할 필요 ❌ TCP에 믿고 맡김
<br></p>
<p><strong>2. 👯 in-order delivery 데이터를 보내는 순서대로!</strong></p>
<ul>
<li><p>TCP를 트랜스포트로 사용하는 응용 프로토콜들은 TCP 연결을 일종의 <strong>파이프</strong>로 생각</p>
</li>
<li><p>파이프의 입구 = 전송 소켓, 파이프의 반대편 끝 = 수신 소켓
  → 양방향으로 하나씩 존재 = <strong>데이터채널</strong></p>
</li>
<li><p>socket에 데이터를 write = 파이프에 데이터를 밀어 넣는 것</p>
</li>
<li><p>IP는 데이터의 순서 보장 X → TCP가 보장해줌으로서 응용 계층의 부담을 줄임</p>
<br>



</li>
</ul>
<p><strong>3. 🎛️  flow control 응용이 데이터를 처리할 수 있는 만큼만! 흐름제어</strong></p>
<ul>
<li><p>송신자측에서 지나치게 빠른 속도로 데이터를 보내면 수신자가 소화할 수 없음</p>
<ul>
<li>수신자가 소화하지 못하는 이유?
  → 수신 host가 처리능력이 떨어지는 느린 컴퓨터
  → TCP로 통신 중인 응용 말고도 많은 다른 응용들이 수신 측의 컴퓨터에서 수행</li>
</ul>
</li>
<li><p>다른 응용이 CPU를 점유하는 동안에는 TCP를 사용하는 응용이 도착하는 데이터를 처리 X</p>
<br>



</li>
</ul>
<p><strong>4. 🍔 Byte Stream 데이터를 바이트가 길게 줄 서있는 것처럼 생각!</strong></p>
<ul>
<li><p>응용 계층에서 내려오는 데이터를 <strong>바이트의 연속</strong>으로 생각
  → 이것을 일정 길이로 잘라 TCP 패킷을 만듦 = <strong>segment</strong></p>
</li>
<li><p>대부분의 TCP 연결은 적절한 segment 크기를 알아서 결정할 수 있음 by 경로 MTU 발견</p>
</li>
<li><p>데이터는 <strong>바이트 단위로 번호</strong>가 매겨지고, 배달 여부도 바이트 단위로 체크 (퍼즐 한 조각)
segment 단위로 묶어서 전송하긴 하지만, TCP에는 segment를 세는 기능 X
  ⇒ <strong>모든 체크는 바이트 단위로!</strong></p>
</li>
<li><p>응용에서는 1바이트부터 시작해서 데이터 크기에 제한 없이 TCP에 전송 요구 가능</p>
<br>



</li>
</ul>
<p><strong>5. 🔗 Connection-Oriented 데이터가 흐르면 연결이 필수! 연결지향</strong></p>
<ul>
<li><p>연결지향이냐 아니냐는 <strong>프로토콜 스택의 계층과 상관 X</strong> 각 프로토콜별 개별적 선택
(같은 계층의 UDP나 아래 계층의 IP는 connectionless)</p>
</li>
<li><p>데이터 전송 상태에 대한 정보를 추적해야 하기 때문에 연결지향
데이터 분실, 순서 뒤바뀜, 수신측의 TCP 버퍼의 여유공간 등<br>→ <strong>데이터 전송의 정확한 상태를 알고 있는 것</strong>으로 1, 2, 3 특징이 가능하게 만듦</p>
</li>
</ul>
<br>




<h2 id="132-tcp-헤더-형식">13.2 TCP 헤더 형식</h2>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/b3525bfd-6c0e-4e42-a789-b031c38644e7/image.png" alt=""></p>
<p><strong>40B의 option</strong> 제외 고정헤더의 길이 <strong>20B</strong></p>
<h3 id="1321-고정-헤더-구성값">13.2.1 고정 헤더 구성값</h3>
<ol>
<li><p><strong>Source Port &amp; Destination Port</strong> <strong>→ 2B * 2 = 4B</strong>
 포트번호 16bit(2B) → 수신해야 할 <strong>응용 프로세스를 찾는데 사용</strong>
 = <strong>transport 계층 주소</strong>
 두 개의 포트번호 + IP 헤더에 있는 IP 주소 → 총 4개의 주소 값으로 응용 프로세스를 찾아냄</p>
<p> <strong>&lt;IP 주소, 포트번호&gt; 쌍 = 소켓 ⭐️</strong>
 TCP 연결은 <strong>두 개의 소켓을 연결</strong>하는 것</p>
<p> <strong>+) 포트번호의 분류</strong></p>
<ul>
<li><p><strong>well known port</strong>: 우리가 <strong>잘 알고있는 서비스</strong>들이 이 범위를 사용 <strong>(0 ~ 1023)</strong>
  ex. HTTP 80번 포트
  아무나 이 포트번호를 사용할 수 없음, IANA에 등록하는 절차 필요
  이 포트번호를 아예 사용하지 못하게 하는 운영체제도 있음</p>
</li>
<li><p><strong>registered port</strong>: 각종 <strong>응용들이 IANA에 등록</strong>해서 사용하고 있는 범위 <strong>(1024 ~ 49151)</strong>
  well-known과 달리 규제가 빡세지 않음
  서버가 아닌 클라이언트 응용이 아무 포트나 잠깐 쓰려고 할 때 이 범위에서 사용하기도 함
  서버에 접속해서 원하는 데이터를 받고 연결을 끊을 때까지 잠깐 사용 → <strong>ephemeral port</strong></p>
</li>
<li><p><strong>private/dynamic port</strong>: 이 범위를 쓰는 응용은 거의 없음 <strong>(19152 ~ 65535)</strong><br>  최근 p2p응용이나 트로이 목마 같은 해킹 툴들이 사용
  <a href="https://ko.wikipedia.org/wiki/TCP/UDP%EC%9D%98_%ED%8F%AC%ED%8A%B8_%EB%AA%A9%EB%A1%9D">https://ko.wikipedia.org/wiki/TCP/UDP%EC%9D%98_%ED%8F%AC%ED%8A%B8_%EB%AA%A9%EB%A1%9D</a></p>
</li>
</ul>
</li>
</ol>
<ol start="2">
<li><p><strong>Sequence Number → 4B</strong></p>
<p>Byte Stream이라는 TCP의 특성에 의해 각 <strong>바이트는 일련번호</strong>를 하나씩 받음
이 번호를 통해 유실, 순서 뒤바뀜 등을 대처
TCP segment의 헤더에 적힌 시퀀스 번호 
= 세그먼트에 의해 옮겨지고 있는 바이트 중 <strong>가장 앞의 시퀀스 번호</strong> → <strong>대표 번호</strong></p>
</li>
</ol>
<ol start="3">
<li><p><strong>Acknowledgement Number ACK → 4B</strong> ⭐️ ⭐️</p>
<p>마지막으로 받은 <strong>연속적인 바이트의 시퀀스 번호 + 1</strong>
ex. 시퀀스 번호 2번까지 잘 받았다면 ACK = 3 
잘 받았다 = 무사히 <strong>수신 소켓 버퍼</strong>에 들어감! 
→ 언제든지 <strong>응용이 소켓 read 함수를 통해 퍼갈 수 있음</strong></p>
<p>ACK x를 받으면 전송 소켓 버퍼에서 번호가 x-1인 바이트까지는 삭제 가능
→ 재전송이 필요 없어졌기 때문!</p>
</li>
</ol>
<ol start="4">
<li><p><strong>HDL → 0.5B</strong></p>
<p> 최대 40B option 때문에 헤더 길이가 가변 → 헤더길이 명시 필요<br> HDL * 4 = 실제 TCP 헤더 길이 
 4bit → 0 ~ 15까지 표현 가능 4 * 15 = 60B (최대)
 만약 헤더가 4의 배수가 아니라면 padding 필요</p>
</li>
</ol>
<ol start="5">
<li><p><strong>Flags → 1B</strong></p>
<p> 각 flag당 1 bit씩 총 <strong>8개의 flag</strong> 존재</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/e8ebd22b-4c93-4643-b4ea-137dce76e6fb/image.png" alt=""></p>
<p> <img src="https://velog.velcdn.com/images/yeonjiyooo_/post/ff44613a-6390-4fc4-89a5-77e651d251bf/image.png" alt=""></p>
</li>
</ol>
<ol start="6">
<li><p><strong>Window Size → 1B</strong></p>
<p>window = <strong>수신 측 소켓 버퍼의 빈 공간</strong>
처음에 공간을 잡아놓고 활용률을 봐가면서 소켓 크기 자체를 연결 도중 재조정 하기도 함
→ <strong>autosizing</strong></p>
<p>전송 측이 수신 측 버퍼를 넘치게 하지 않는 하에서 <strong>몇 바이트나 더 전송할 수 있는지</strong>를 파악하게 함<br>→ <strong>흐름 제어 flow control</strong></p>
</li>
</ol>
<ol start="7">
<li><p><strong>Checksum → 1B</strong></p>
<p>  전송 중 오류가 생겼는지 확인하는 전형적인 인터넷 체크섬으로 <strong>1의 보수 덧셈 결과를 1의 보수</strong> 취함      </p>
<pre><code> UDP처럼 TCP도 **pseudo header** 사용 (IP주소와 IP프로토콜 필드값이 체크섬에 포함)</code></pre><p> 다만 UDP와 달리 <strong>TCP 체크섬은 의무</strong>
 <strong>TCP segment 전체</strong>에 대해 계산되고, NIC에서 해줄 수 있으면 위임도 가능</p>
<pre><code> → OS는 체크섬 필드를 0으로 하고 보낸 후 NIC가 하드웨어적으로 계산해서 채워줌</code></pre></li>
</ol>
<ol start="8">
<li><p><strong>Urgent Pointer → 1B</strong></p>
<p>   flags 필드에서 URG flag = 1일때 <strong>긴급한 데이터의 끝 위치</strong>를 명시
   <strong>현재 segment 시퀀스 번호로부터 얼마나 떨어져 있는지 바이트 단위</strong>로 알려줌</p>
</li>
</ol>
<br>

<h2 id="1322-tcp-options">13.2.2 TCP Options</h2>
<p>TCP option도 <strong>TLV = Type(1B) + Length(1B) + Value 형식</strong>을 따름
종류에 따라 Value 필드의 길이 달라짐</p>
<p>Length는 Type와 Length 필드의 길이까지 포함한 <strong>해당 옵션의 전체 길이</strong></p>
<img src="https://velog.velcdn.com/images/yeonjiyooo_/post/91d0eff8-7fb5-43b5-828d-1c8e4c260d76/image.png" width=500>

<ol>
<li><p><strong>End of Option List (EOL) → 1B</strong></p>
<p> 이 뒤로는 더 이상 옵션이 없다는 뜻
 필수 사용 X → 헤더 길이 알기 때문에 option의 길이도 앎
 4배수가 아닌 경우 NOP으로 메꾸면 되기 때문에 꼭 EOL을 사용할 필요는 없음</p>
<p> BUT 만약 EOL을 넣었다면, 4배수를 맞춰야 하는 경우 뒤에 <strong>0</strong>(NOP X)으로 패딩
 → NOP도 결국 option의 일종이기에 EOL의 정의와 모순됨
 <img src="https://velog.velcdn.com/images/yeonjiyooo_/post/dd984242-96e7-4ae4-bde3-7db36c4ffa8f/image.png" alt=""></p>
</li>
</ol>
<pre><code>MSS(4) + NOP(1) + WS(3) + SACK(2) = 10B → 4배수가 되려면 2B필요
따라서 뒤에 EOL(1) + 0으로 padding(1) 필요한 것</code></pre><ol start="2">
<li><p><strong>No Operation (NOP) → 1B</strong></p>
<p> 헤더 길이가 4배수가 되도록 끼워넣는 값으로 option 앞이나 뒤에 어디든 붙여도 상관 없음
4배수만 맞추면 되므로 3개를 초과하지 않음</p>
</li>
</ol>
<ol start="3">
<li><p><strong>Maximum Segment Size (MSS) → 4B</strong></p>
<p> TCP 연결이 만들어질 때 <strong>어떤 세그먼트 크기를 쓸 것인지</strong> peer에게 알려줄 때만 사용
 일반적인 TCP segment = TCP 헤더 + 응용 계층 데이터
 BUT MSS는 <strong>TCP 헤더 부분을 제외한 응용 계층 데이터 부분의 길이만을 얘기함</strong></p>
<p> TCP 연결이 사용하는 <strong>호스트의 인터페이스 MTU 크기</strong>에 의해 결정 <strong>(링크계층의 MTU)</strong></p>
<p> 링크계층 MTU의 payload에 TCP segment를 실은 IP 데이터그램이 들어가는 경우,<br> TCP 헤더와 IP 헤더가 최소크기를 가져야 데이터 부분의 길이가 최대가 됨</p>
<p> → <strong>MTU - (20B + 20B) = MSS</strong>가 됨 </p>
<p> BUT 경로 상의 어떤 MTU가 작아질 수 있으므로 <strong>최종 MSS는 아님</strong>
 MTU가 MSS+40B보다 작은 경우 peer들이 MSS를 더 작은 값으로 조정하지 않으면 <strong>IP fragment **
 → IP 헤더 DF = 1로 설정해서 MTU를 탐색하는 **PMTU discovery 사용</strong></p>
</li>
<li><p><strong>Window Scaling Factor → 3B</strong></p>
<p> 윈도우 확장 옵션으로 <strong>수신 소켓 버퍼의 크기가 64KB보다 큰 경우</strong> peer에게 알려줌
 TCP 고정헤더에서 Window Size는 16bit → 2 ^ 16 - 1 = 65535B까지만 표현 가능</p>
<p> <strong>수신 소켓 버퍼 (TCP Buffer)</strong> 
 = <strong>네트워크에서 들어오는 데이터의 속도 &amp; 응용의 속도차를 완충</strong>해주는 공간</p>
<p> 고속의 연결에서는 속도차가 커질 수 있어 <strong>버퍼가 더 커야할 필요</strong>가 있음</p>
<p> 이 옵션에 들어있는 값의 명칭: scale factor
 고정헤더의 <strong>window size를 몇 번 왼쪽 shift해서 해석해야 하는지</strong>를 의미 </p>
<p> window size *=  2 ^ (scale facor)</p>
</li>
<li><p><strong>Selective ACK (SACK) Permitted → 2B</strong></p>
<p> TCP 연결 단계에서 peer TCP에게 자신은 Selective ACK를 사용할 수 있다고 알림
 peer TCP도 SACK을 사용할 수 있다고 하면 <strong>데이터 전송 단계가 시작되었을 때</strong> SACK 사용 가능</p>
</li>
</ol>
<ol start="6">
<li><p><strong>Selective ACK → 2 + 8*k (k &lt; 5)</strong></p>
<p> SACK option은 송신자 TCP가 여러 segments를 보낼 때 일부가 유실되었더라도 어떤 것을 못받았는지 알려줌
  → <strong>잃어버린 segment만을 보내는 효율적인 재전송</strong>을 하게 함</p>
<p> 비교적 나중에 추가된 option으로 TCP 성능 향상에 지대한 영향을 줌
 <strong>데이터 전송이 시작된 후에 쓰이는 옵션</strong>이므로 TCP <strong>연결 시점에서는 나타나지 않음</strong></p>
<p> <img src="https://velog.velcdn.com/images/yeonjiyooo_/post/bd9034c9-65cb-4252-b592-9555defe7bc8/image.png" alt=""></p>
</li>
</ol>
<pre><code>value 필드에 **불연속 바이트 블록** → **left edge, right edge** → **길이 4B짜리 바이트 시퀀스 번호** 

left edge: 불연속 블록의 **시작 시퀀스 번호**
right edge: 불연속 블록의 **마지막 시퀀스 번호 + 1**

(5841 ~ 8761) &amp; (11681 ~ 12991) 이고 ACK = 4800인 경우
→ **(4801 ~ 5840) &amp; (8761 ~ 16680) 이 블록에 해당하는 바이트가 유실되었다!**

SACK는 못받은 것이 아닌 **받은 것을 확인**

수신자는 송신자에게 SACK option을 통해 이미 수신한 두 불연속 블록을 빼고 재전송 할 것을 요구 
→ **불필요한 재전송을 피함으로써 TCP 전송 속도 UP!** 

하나의 불연속 블록의 표현 = **left edge(4B) + right edge(4B) = 8B**

TCP option의 최대는 40B지만 5개 블록을 표현할 수 없음 Why?
→ TLV에 따라 **Type과 Length를 표현하는 2B가 포함되어야 하기 때문** → 따라서 **최대 4개**

블록 4개 = 4 * 8 + 2(TL) = 34 → 2B Padding 필요 (NOP 2개 or EOL 1개 + 0 1B)</code></pre><p><strong>8. Timestamp → 10B</strong></p>
<p>전송되는 segment에 달아서 보내고 수신측 TCP는 이 <strong>값을 그대로 복사</strong>해서 보냄
→ 송신자 측은 <strong>수신측 TCP까지의 왕복시간 = RTT (Round-Trip Time)</strong>을 알게됨</p>
<p>측정 목적: TCP가 전송한 segment가 ACK되어 돌아올 만한 시간을 추정해야하기 때문</p>
<p><strong>정밀하고 빠른 RTT 측정</strong>을 목표로 함</p>
<p>어떤 segment를 전송했을 때, <strong>평균 RTT 시간이 훨씬 지났는데도 ACK가 안돌아오는 경우</strong>
→ 네트워크에서 해당 segment가 <strong>유실되었다고 판단 + 재전송</strong> </p>
<p><strong>기존 RTT를 측정하던 방법:</strong></p>
<p>시스템에 한 flag(T)를 두고 패킷이 전송될 때 <strong>T = 0</strong>이면 그 패킷이 출발한 시간 t1 저장
→ <strong>T = 1</strong>로 바꾸어 RTT가 측정중임을 표시 
(<strong>이 시기에 전송되는 다른 패킷에 대해서는 측정 X</strong> → 기존 방식의 대표적인 한계)
→ t1에 출발했던 패킷이 ACK가 되어 돌아올 때의 시간 t2
→ t2 - t1 = RTT의 sample
→ T = 0으로 다시 바꿔서 RTT 측정과정 반복</p>
<p>⇒ 한계점: <strong>한 RTT당 한 RTT sample</strong>만 얻을 수 있기 때문에 <strong>RTT가 매우 큰 경우</strong>는 <strong>측정 빈도, 샘플 down</strong></p>
<p>RTT가 빠르게 변화하는 상황에서는 RTT의 평균과 분산 추정의 정확성이 매우 떨어짐
반면 timestamp는 한 RTT당 한 smaple이 아니라 <strong>매 패킷마다 RTT sample</strong>을 얻을 수 있음
→ <strong>측정 sample수가 많아지면서 정확한 RTT 추정 가능</strong></p>
<p><strong>28. User Timeout → 4B</strong></p>
<p>전송된 데이터가 <strong>ACK로 돌아오기를 기다리는 최대 시간 조절</strong>
peer TCP에게 <strong>얼마 이상의 시간 동안 ACK가 없으면 연결을 끊어라</strong>!는 것을 미리 알려줌</p>
<p>많은 클라이언트를 상대하는 서버라면 ACK와 같은 정상적인 반응을 하지 않는 TCP 연결 유지 BAD</p>
<p>→ <strong>UTO 값을 아주 작게 설정</strong>해서 클라이언트 TCP에게 통보
→ 살아있는 TCP 클라이언트는 어떻게든 UTO가 넘기 전에 패킷 전송 등의 행동 취함
→ 연결 종료 예방</p>
<p>반대로 <strong>반응이 없어 보여도 연결을 끊지 말아</strong>달라고 요구하려면 <strong>매우 큰 UTO</strong>를 통보하면 됨</p>
<p><strong>⇒ ⭐️ 모든 TCP 옵션은 반드시 TCP 연결 시에 양쪽 TCP peer가 모두 동의해야 사용 가능 ⭐️</strong></p>
<p>만약 버전이 안맞아서 자신이 인식할 수 없는 옵션이면 무시 + length 값만큼 건너뛰어 다음 옵션 처리
커널이 업데이트 되면서 TCP가 새로운 버전으로 교체될 때 해결됨</p>
<p>→ <strong>하위 호환성 backward compatibility</strong>를 보장 + <strong>자유로운 옵션추가 가능</strong>으로 <strong>feature scalability</strong> 
<br></p>
<h2 id="133-tcp-연결-제어">13.3 TCP 연결 제어</h2>
<p>TCP는 <strong>연결 지향 Connection Oriented 프로토콜</strong>! 
→ 연결이 이루어진 후에야 데이터가 흐를 수 있음 </p>
<p>TCP는 <strong>전이중 full-duplex</strong> 방식으로 작동 = 두 TCP peer 사이에 <strong>양방향 데이터 채널</strong>이 열려있음</p>
<p>두 TCP가 각 데이터 채널을 통한 <strong>데이터 전송 상태에 대해 일치하는 이해</strong>를 가짐 = <strong>동기화 Synchronize</strong></p>
<p>동기화를 위해 필요한 정보:</p>
<ul>
<li><p><strong>Initial Sequence Number (ISN)</strong></p>
<p>  바이트 스트림의 맨 첫 번째 바이트에 할당되는 SN은 0이나 1이 아님<br>  TCP 연결의 납치(hijack)를 막기 위해 <strong>무작위 같은 수</strong> 채택
  → <strong>자신이 고른 ISN을 peer TCP 끝점에 알려주어야 함</strong> (어디서부터 byte sequence가 시작되는지)</p>
</li>
<li><p><strong>Window Size</strong></p>
<p>  수신 소켓 버퍼 안의 빈 공간 크키
  연결 시작 지점에서는 소켓 버퍼가 비어있으므로 윈도우 크기 = 소켓 버퍼 자체의 크기<br>  소켓 버퍼를 얼마로 할당할 것인지는 사용자가 결정하므로 전송 시작 전에 알려줘야함!</p>
<p>  <strong>flow control 흐름 제어</strong>를 위해 peer TCP가 window size를 알고 있어야 함</p>
</li>
<li><p><strong>Maximum Segment Size (MSS)</strong></p>
<p>  이 채널이 <strong>사용하기를 희망하는 TCP segment의 크기</strong> (헤더 제외)
  ISN, Window Size → 각 채널에서 <strong>독립적인 별도의 값</strong> 사용 
  MSS → 양 채널이 고른 값의 <strong>최소값으로 통일</strong> → IP fragment를 방지</p>
<p>  각자의 첫 MSS =  호스트 인터페이스 MTU - 40B </p>
<p>  <strong>MSS = min(MSS a→b, MSS b→a)</strong></p>
<p>  MSS는 option에 해당하므로 생략할 수도 있는데 이 경우는 최악의 MTU = 576B, MSS = 536B</p>
<p>  <strong>두 TCP peer가 동일한 서브넷에 있는 경우 MSS 정보가 필요 없음</strong>
  BUT 통상적으로는 SYN segment에 MSS 옵션 포함!</p>
<p>  MSS는 <strong>TCP, IP 헤더 크기가 최소임을 가정</strong>하고 구한 값
  → 옵션이 들어간 경우 IP fragment가 발생할 수도 있지 않은가?
  → 이 경우 TCP 송신자는 <strong>MSS에서 option 크기를 제외한 숫자의 데이터 바이트만을 전송</strong>
  → IP fragment를 피함!</p>
<ul>
<li>MTU = 1500B</li>
<li>MSS = 1460B (20B IP + 20B TCP 기준)</li>
<li>실제 TCP 옵션이 12B 있다면 → TCP는 <strong>payload를 1448B까지만 실어서 보냄</strong></li>
</ul>
</li>
</ul>
<p>  <img src="https://velog.velcdn.com/images/yeonjiyooo_/post/8ca30a95-054f-49c0-a92a-266811735901/image.png" alt=""></p>
<pre><code>ISN, MSS: 정방향, W: 역방향에 쓰임 (A의 window size는 B → A 일 때 고려히기 때문)
TCP는 ISN, W, MSS 이외에도 끊임 없이 추적, 관리해야 할 값들이 많음
→ 매 연결마나 커널 안에 연결을 관리하기 위한 **TCB 제어 블록(TCB)**을 만듦


연결 중에는 TCB로 값들을 추적할 수 있고 연결이 종료되면 해당 TCB도 사라짐

TCB: 연결을 식별하는 두 개의 소켓 번호 (IP, Port), RTT 추정치, RTT 편차,
전송 및 수신 소켓 버퍼에 대한 포인터, 다음에 전송할 시퀀스 번호, 윈도우 크기 등등 많은 값 포함
⇒ **데이터 전송과 수신을 위한 모든 제어에 관련된 값들의 집약**</code></pre><br>

<h3 id="1331-연결-맺기">13.3.1 연결 맺기</h3>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/17ee4bcd-e033-45e9-8f26-31948aa6f244/image.png" alt="">
<strong>3-way handshake</strong></p>
<p>클라이언트 🙋🏻‍♀️: 연결 맺자~
서버 🙆🏻: 그래~</p>
<p><strong>🔗 연결:</strong> <strong>3개</strong>의 패킷 사용</p>
<ul>
<li><p>*<em>C → S: SYN | *</em></p>
<p>  SYN segment: 데이터 채널을 위한 연결 파라미터 값 3개를 실어 나름</p>
<p>  아직 연결 전이므로 데이터는 없고 <strong>헤더만 존재</strong></p>
</li>
<li><p>*<em>S → C: SYN + ACK | *</em></p>
<p>  TCP는 <strong>reliable delivery</strong>를 해야하므로 받았으면 받았다는 확인을 해줘야 함</p>
<p>  서버 측에서 클라이언트에게 <strong>연결에 필요한 새로운 파라미터 값 정보인 SYN</strong> + 앞선 클라이언트로부터 온 <strong>SYN을 잘 받았다는 ACK 정보</strong>도 함께 보냄 
  → 두 데이터 채널이 함께 묶여 만들어질 수 밖에 없음 (BUT 나중에 끊을 떄는 각각 끊을 수 있음)</p>
</li>
<li><p>*<em>C → S: ACK | *</em></p>
<p>  S → C 채널을 위한 세 값을 헤더에 싣고 온 SYN에 대한 ACK</p>
<p>  별도의 ACK를 전송하지 않고 ACK 내용을 헤더에 표현한 뒤 ACK = 1로 설정해도 됨</p>
</li>
</ul>
<h3 id="1332-연결-끊기">13.3.2 연결 끊기</h3>
<p>연결을 맺을 때는 두 개의 데이터 채널이 동시에 만들어져야함
BUT <strong>연결을 끊을 때</strong>는 데이터 채널을 <strong>따로따로 끊을 수 있음</strong>
→ 한 개의 데이터 채널을 끊은 뒤에도 남은 반대 채널은 계속 쓸 수도 있음 (이론적으론 가능, 그치만 굳이?)</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/b889fc42-28a8-41af-9c6d-ddc01a6fe768/image.png" alt=""></p>
<p><strong>⌛️ 종료: 4개</strong>의 패킷 사용</p>
<ul>
<li><p><strong>C → S: FIN</strong>
  FIN 패킷이 가는 방향의 데이터 채널의 연결을 끊음
  어느쪽이든 먼저 FIN 패킷을 보낼 수 있음
  BUT 먼저 FIN segment를 보낸 쪽이 더 큰 부담을 가짐</p>
</li>
<li><p><strong>S → C: ACK</strong>
  끊자고 하는 FIN segment가 수신자 측에 정상적으로 도달이 완료되었음을 알림</p>
</li>
<li><p><strong>S → C: FIN</strong>
 FIN 패킷이 가는 방향의 데이터 채널의 연결을 끊음</p>
</li>
<li><p><strong>C → S: ACK</strong>
  <strong>먼저 끊자고 한 쪽</strong>이 TCP 연결에서 보내는 <strong>마지막 패킷 = ACK</strong> 을 보내게 되어있음
  TCP에는 <strong>데이터의 재전송은 있어도 ACK의 재전송은 없음</strong>
  만약 #4: ACK가 가다가 유실되면 peer는 ACK를 기다리다 <strong>timeout</strong> 
  → <strong>FIN segment 재전송</strong>
  ⇒ 마지막 ACK를 전송하는 측은 <strong>ACK 보내고 바로 끝내버릴 수 없음</strong> (ex. 자료구조 정리, TCB 정리)</p>
<p>  재전송 될지 모르는 FIN segment를 기다려야 함 = <strong>TIME_WAIT</strong> 
  TIME_WAIT 시간 = Maximum Segment Life (MSL) * 2
  <strong>MSL</strong>: 원칙적으로 segment가 <strong>배달이 되지 않은 상태로 네트워크 안을 떠돌 수 있는 시간</strong> 
  MSL의 표준을 2분으로 생각했을 때 TIME_WAIT는 무려 4분으로 꽤 긴 시간!
  반면 <strong>끊음을 당하는 쪽은 마지막 ACK를 받자마자 끝</strong></p>
</li>
</ul>
<p>TCB 등 연결마다 만들어진 메모리 자원, 다수의 타이머 이벤트 처리 등을 데이터 전송이 끝난 후에도 유지해야 함
→ TIME_WAIT는 <strong>한 순간에도 수 많은 클라이언트</strong>들을 상대해야 하는 대형 서버에게는 큰 부담
⇒ 데이터를 모두 보내고 먼저 연결을 끊어야 할 때 <strong>RST</strong>를 이용하기도 함</p>
<p>(RST: TCP 헤더의 flags 중 하나로 kill 여부 결정)</p>
<h2 id="134-tcp-흐름-제어">13.4 TCP 흐름 제어</h2>
<p>TCP는 응용으로부터 내려오는 바이트 스트림을 잘라서 segment를 만듦 
→ IP에 내려보내 데이터그램의 형태로 목적지에 배달</p>
<p>BUT 목적지까지의 파이프가 계속 용량이 변하고, 목적지에서도 수신 응용이 데이터를 소화하는 속도가 계속 변함</p>
<p><strong>경로 상의 네트워크 용량</strong>이 계속 변하는 것을 추적 &amp; 전송 속도 조절 → <strong>혼잡제어 conjestion control</strong></p>
<p>목적지에서 <strong>수신 응용의 처리 속도</strong> 추적 &amp; 전송 속도 조절 → <strong>흐름제어 flow control</strong></p>
<p>소켓 자료구조: 두 쌍의 소켓 식별자 (나와 peer까지해서 2개) + 소켓의 현재 상태 + 소켓 버퍼
소켓 버퍼는 각 TCP의 두 데이터 채널에 연결</p>
<p><strong>응용 → 전송 소켓 버퍼 → 네트워크 → 수신 소켓 버퍼 → 응용</strong></p>
<p>흐름제어의 목적</p>
<ul>
<li>수신자쪽에서 데이터가 넘치치 않도록 조절</li>
<li>파이프가 비지 않도록 충분히 데이터를 전송
⇒ 데이터 유실이 일어나지 않는 선에서 충분히 빠르게 데이터를 전송해야함</li>
</ul>
<p>TCP 수신자가 <strong>수신 상태에 대한 연락</strong>을 잘 해줘야 함 = ACK</p>
<p>고정헤더의 흐름제어 관련 필드: <strong>ACK, Window Size</strong>
→ 방향 데이터 채널의 ACK 정보는 ← 방향으로 옴</p>
<p>ACK 정보는 TCP 고정 헤더의 두 필드를 사용
← 방향으로 올 데이터가 있으면 그것의 헤더에 담아 보냄
← 방향으로 올 정보가 없으면 헤더만 있는 ACK segment를 만듦</p>
<h3 id="1341-ack-번호">13.4.1 ACK 번호</h3>
<p>ACK 번호의 본질은 sequence 번호
TCP 연결시에 전송되는 <strong>SYN segment도 ISN을 할당받음</strong></p>
<p>ISN은 SYN segment 자체가 할당 받는 것이기 때문에 데이터 바이트는 하나도 싣고 있지 않아도 주어짐
SYN segment 자체도 네트워크 안에서 유실되면 재전송 되어야 하기 때문
<img src="https://velog.velcdn.com/images/yeonjiyooo_/post/b21e9730-031d-4f2f-89d8-21f17047b7ed/image.png" alt=""></p>
<p>#6 패킷이 SN을 1 소모 → #8 패킷 Seq = 1
TCP 연결 종료시에 전송되는 FIN segment도 ISN 하나 소모함</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/a5e62554-cc8a-413c-b4e8-f8f010788f9a/image.png" alt=""></p>
<p>#665 FIN 패킷이 SN 1 소모 Seq = 320 → 같은 방향으로 가는 #674 SN Seq = 321
SYN과 FIN을 제외하면 오직 데이터 바이트만이 Sequence Number를 소모할 수 있음
→ 데이터를 하나도 싣지 않은 ACK segment들은 SN을 증가시키지 않음</p>
<p>Sequence Number는 <strong>이전까지 전송한 바이트수</strong>라고 해석하면 됨!</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/c946d8bb-2a32-4854-a943-67c90ef886ac/image.png" alt=""></p>
<p>전체 K바이트를 나르는 경우 <strong>FIN의 Seq = ISN + k + 1</strong></p>
<p>reliable delivery를 위한 ACK 
ACK = x → x-1 번째 데이터 바이트까지의 모든 데이터가 수신 소켓 버퍼에 무사히 도착!</p>
<p>만약 데이터가 유실되어 순서가 아닌 SN을 가진 데이터가 왔다면 <strong>이전에 보냈던 ACK번호를 그대로 보냄</strong>
→ ACK 번호 = <strong>수신 소켓 버퍼에</strong> <strong>들어간 마지막 바이트의 SN + 1</strong>
→ 데이터를 보냈음에도 전과 동일한 ACK가 도착했다 
= 새로 보낸 데이터는 <strong>순서가 잘못되어 수신 버퍼에 넣지 않음</strong>
→ 대신 <strong>reordering buffer라는 별도의 커널공간</strong>에 저장</p>
<p>이후 재전송된 segment들이 들어와 벌어졌던 gap이 메꿔지면 <strong>한 번에 ACK 증가시킴</strong>
재전송된 segment들도 우선 reordering buffer에 들어가고 한 번에 수신 소켓 버퍼로 이동!</p>
<p><strong>Cumulative ACK 누적ACK</strong></p>
<p><strong>마지막 연속 바이트의 시퀀스 번호 + 1</strong>을 송신자에게 ACK 하는 방식
어떤 ACK segment가 네트워크 안에서 유실되어도 그 <strong>다음 ACK가 전달된다면 문제가 없어짐 = 견고성</strong></p>
<p>ACK segment도 IP에 실려가므로 유실될 수 있음
만약 <strong>각 segment별로 ACK</strong>를 보낸다면 <strong>유실된 ACK에 대해 모두 재전송</strong>이 필요
BUT 누적 ACK의 경우 마지막 segment의 ACK만 잘 간다면 문제 없음!</p>
<h3 id="1342-윈도우-크기">13.4.2 윈도우 크기</h3>
<p>Window Size = <strong>수신자가 ACK 하는 시점</strong>에 수신 <strong>소켓 버퍼에 존재하는 빈공간의 크기</strong>
→ 그 당시의 ACK 번호와 윈도우 크기를 캡처해서 보내는 것이라고 생각</p>
<p>윈도우 크기시 주의할 점: <strong>윈도우 크기를 절댓값으로 해석하지 말 것 ⭐️</strong>
ex. 윈도우 크기가 2KB라고 알려줬을 때 그대로 2KB를 더 보내면 소켓 버퍼가 넘칠 수 있음
→ 전송을 하려는 시전에 수신자의 상황이 달라질 수 있기 때문!</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/6f8333f5-1c9f-4af2-9710-c29410441ee5/image.png" alt=""></p>
<p>수신자가 보낸 ACK = a, window size = b → ACK를 보낼 당시의 값
송신자가 ACK를 받았을 때는 이미 SN = b 까지 추가적으로 데이터를 보냄</p>
<p>만약 <strong>b+w &gt; a+w-1</strong> 이면 수신 소켓 버퍼 넘침!
⇒ 송신자가 더 보낼 수 있는 총량 = <strong>w-(b-a)-1B</strong></p>
<p>(위 그림에서 노란색 부분에 속하는 양)</p>
<p>Byte Stream에 있는 데이터 바이트의 구분</p>
<ul>
<li><strong>already ACKed</strong>: 전송되었고 이미 ACK를 받은 바이트</li>
<li><strong>already send but not ACKed</strong>: 전송되었지만 아직 ACK를 받지 못한 바이트 
= 미확인 outstanding</li>
<li><strong>cannot be sent yet</strong>: 아직 전송되지 않은 바이트</li>
</ul>
<p>ACK가 도착하면 <strong>window를 오른쪽으로 움직임</strong></p>
<p>ACK→ window의 왼쪽 엣지 <strong>LE를 오른쪽</strong>으로
window size → 오른쪽 엣지 <strong>RE = 새로 업데이트된 LE + window size</strong>
⇒ <strong>sliding window</strong>내에서만 전송</p>
<p>TCP 흐름제어를 수행하여 수신 소켓 버퍼가 넘쳐 데이터가 유실되는 일이 없게 함</p>
<h3 id="1342-윈도우-업데이트">13.4.2 윈도우 업데이트</h3>
<p>window update: window size를 전달하기 위한 수단으로 사용되는 ACK
TCP의 ACK 정보에는 ACK 번호 뿐만 아니라 window 크기도 포함됨 </p>
<p>TCP의 윈도우 업데이트 규칙: <strong>강제 ACK 전송 규칙</strong></p>
<p>윈도우 업데이트 시점에서 ACK를 보낼 수 없는 상태
(어짜피 ACK를 할 수 있는 시점에서는 윈도우 업데이트 필요 X)</p>
<ul>
<li>ACK를 할 계기를 줄 새로운 데이터의 도착이 없음</li>
<li>Delayed ACK에 의해 ACK가 억제당하고 있음
  응용이 열심히 퍼내서 소켓 버퍼에 빈공간이 생겼는데도 송신 측이 모르는 경우
  파이프에서 데이터가 빠져 나갔는데 채워넣지 못함</li>
</ul>
<p>→ 이러한 상황이라도 <strong>데이터 흐름에 손해를 보게될 정도가 되면 강제로 ACK</strong>시켜 이를 해결</p>
<pre><code class="language-markdown">1. 송신자 → 수신자: 데이터 보냄 (예: 3000 bytes)
2. 수신자: 다 받음 → 버퍼 거의 다 참 (윈도우 = 0)
3. 수신자: ACK 보냄 + “윈도우 0” (더 보내지 마!)
4. 수신자 내부에서 처리됨 → 버퍼가 비어감 (윈도우 증가)
5. BUT 새로운 데이터는 안 옴 → delayed ACK 상태
6. 시간이 지나도 송신자는 ‘윈도우 0’으로 착각 → 데이터 안 보냄
7. 수신자: “이거 안 되겠는데?” → **강제로 윈도우 update ACK 전송!**
8. 송신자: “오! 윈도우 생겼네?” → 데이터 전송 재개</code></pre>
<p>송신 측이 알고 있는 윈도우의 <strong>RE가 2MSS 이상 오른쪽으로 이동</strong>할 수 있다면 <strong>즉시 ACK</strong>를 보냄</p>
<h3 id="1343-발신-버퍼와-수신-버퍼가-tcp의-성능에-끼치는-영향">13.4.3 발신 버퍼와 수신 버퍼가 TCP의 성능에 끼치는 영향</h3>
<p>흐름제어 성능에 영향을 끼치는 중요한 값 = <strong>수신 측 소켓 버퍼의 크기</strong>
그에는 못미치지만 발신 측 소켓 버퍼의 크기도 영향을 미침!</p>
<p><strong>데이터 채널의 용량</strong> = <strong>대역폭 * 지연</strong> Bandwidth-delay product BDP</p>
<p>대역폭 [bits/s]: <strong>초당 파이프에 넣을 수 있는 데이터 양 (B)</strong>
지연 [s]: 데이터가 <strong>파이프를 끝까지 통과하는데 걸리는 시간 (L)</strong>
⇒ <strong>BDP</strong> = <strong>파이프 안에 동시에 들어있을 수 있는 데이터의 총량</strong></p>
<p>만약 수신자가 데이터를 받을 수 없는 상황이되면?</p>
<p>수신 소켓 버퍼에서 데이터를 퍼내던 응용 프로세스가 CPU 할당을 받지 못하는 경우
→ 파이프에서 움직이던 <strong>BDP에 해당하는 양 만큼의 데이터 유실!</strong>
⇒ 수신자는 항상 수신 소켓 버퍼의 크기를 <strong>2*BDP</strong>를 감당할 수 있을 정도의 크기로 마련</p>
<p>BDP만큼을 더 받더라도 더 이상 받을 수 없다는 연락을 보내는 동안 <strong>또 BDP만큼이 올 수 있기 때문</strong>
→ 2 * BDP</p>
<p>BUT TCP 연결에서 B, L은 제각각이고 연결 도중 바뀔 수도 있음</p>
<p>현재의 TCP는 계속 P의 크기를 모니터링하며 수신 소켓 버퍼의 크기를 조절할 수 있음</p>
<p><strong>2P = 2 * L * B</strong></p>
<p>B = 2P / 2L → B_bottleneck = 병목의 속도, 분모: RTT, 분자: 바람직한 수신 소켓 크기</p>
<p>만약 2P를 바람직한 크기보다 훨씬 작게 설정해놓았다면? B_bottleneck보다 값이 작아짐
→ 실제 병목이 제공할 수 있는 용량보다 <strong>더 낮은 속도</strong>
⇒ 이런 문제를 해결하기 위해 <strong>상당히 큰 수신 소켓의 크기</strong>를 사용해야함</p>
<p>BUT 수신 소켓 버퍼가 너무 커지면 여기에 쌓인 데이터는 <strong>응용이 퍼내가기 전까지 또 다른 큐잉 지연의 원인</strong>!</p>
<p>→ <strong>단대단 end-to-end 지연이 커져</strong> 사용자 경험에 좋지 않
⇒ <strong>수신 소켓이 무조건 크다고 좋은 것은 아님</strong></p>
<p>그럼 어느정도의 크기로 소켓 버퍼를 설정해야하는가?</p>
<p>TCP 연결마다 BDP를 수용할 수 있는 크기로 버퍼 설정
Dynamic Right Sizing (DRS)
수신자가 TCP 패킷 헤더와 타임스탬프 옵션을 기반으로 BDP 추정 → window size에 반영
Auto-tuning
DRS와 달리 BDP 추정과는 상관 X
소켓 버퍼에 할당할 수 있는 시스템 메모리 크기의 여유를 봐가며 버퍼 크기 조절
버퍼가 가득 차면 더 크게 만들어서 계속 데이터가 흐를 수 있게 해줌
간접적으로 TCP 성능을 높임</p>
<p><strong>발신 소켓 버퍼가 너무 작으</strong>면 응용 프로세스가 <strong>소켓에 데이터를 쓸 때 버퍼가 가득 차있을 수 있음</strong> 
→ <strong>커널 block</strong></p>
<p>네트워크나 수신측 peer 때문에 데이터가 나가지 못하는 경우
→ 발신 소켓 버퍼의 데이터가 다시 실려 나감</p>
<p>BUT 버퍼가 너무 작으면 <strong>비어있던 파이프를 채울 정도로 데이터가 충분 X</strong>
⇒ 수신자가 막혀있어도 그 동안 <strong>송신자가 버퍼에 계속 쓸 수 있어야 함!</strong>
⇒ 발신 소켓 버퍼도 <strong>충분히 크게</strong> 잡아야함</p>
<h2 id="135-에러-제어">13.5 에러 제어</h2>
<p>TCP segment는 <strong>IP 데이터그램</strong>에 실려 전송되므로 <strong>유실 위험성</strong> 존재</p>
<h3 id="1351-ack-규칙">13.5.1 ACK 규칙</h3>
<p>segment 헤더에 적힌 SN = s, 정상적인 SN = a, segment의 크기 = L</p>
<ol>
<li><p><strong>기대했던 SN = a</strong></p>
<p> 정상적으로 받은 데이터를 수신 소켓 버퍼에 넣음</p>
<p> <strong>→ ACK = a + L 전송 계획</strong> (delayed ACK로 일단 계획)</p>
</li>
<li><p><strong>기대보다 큰 SN &gt; a</strong> → 통상적인 의미에서의 에러 제어가 필요한 경우</p>
<ul>
<li><p>어떤 <strong>segment가 네트워크 안에서 유실</strong>된 후, 그 뒤의 segment는 정상 도착한 경우
  순서가 맞지 않는 segment → <strong>reordering buffer</strong>에 넣음</p>
</li>
<li><p><strong>SN = a+1인 짧은 segment</strong>가 오는 경우
  <strong>Persist Probe</strong>에 해당하는 데이터
  <strong>0-window ACK</strong>가 있었던 경우 송신자가 수신 소켓 버퍼에 여유가 생겼는지 주기적으로 질문하기 위함
  0-window ACK =  window size가 0이라고 적어서 ACK 한 것
  → <strong>송신측 window의 LE = RE</strong>, RE를 넘어서 데이터를 전송했다!</p>
</li>
<li><p><em>→ 두 경우 모두 ACK = a 즉시 전송*</em> (긴급한 상황이므로 delayed ACK 적용 X)</p>
</li>
</ul>
</li>
<li><p><strong>기대보다 작은 SN &lt; a</strong></p>
<p> 자주 일어나지 않는 특별한 경우 <strong>Keepalive Probe</strong>
 이미 받아서 수신 소켓 버퍼로 들어간 데이터에 해당하는 SN을 달고 새로운 segment가 온 것
 일부러 비정상적인 SN을 설정해보냄으로서 <strong>수신자가 살아있는지 체크</strong>
 보낼 데이터가 없으므로 헤더만 전송</p>
<p> <strong>→ ACK = a 즉시 전송</strong></p>
</li>
</ol>
<br>

<h3 id="1352-segment-유실의-발견">13.5.2 segment 유실의 발견</h3>
<p>TCP 송신자가 segment 유실을 알게되는 경우</p>
<ol>
<li><p><strong>재전송 타이머 Retransmission Timer</strong></p>
<p> TCP segment가 전송됐는데 <strong>RTT가 지나도록 ACK가 도착하지 않는</strong> 것을 알 수 있도록 <strong>타이머 존재</strong>
 타이머의 초기값: <strong>RTO Retransmission Time Out = avg(RTT) + 4 * dev(RTT)</strong>
 전송된 segment는 <strong>재전송 큐에 복사 + 전송된 시간 기록</strong></p>
<p> 커널이 주기적으로 큐를 체크하면서 <strong>현재 시각과 전송된 시간 차이가 RTO가 넘는 segment</strong> 체크
 ACK가 도착한 경우 ACK된 모든 segment는 큐에서 삭제</p>
<p> <strong>P[RTO &lt; RTT] ≤ 1/16</strong> by Chebyshev</p>
</li>
<li><p><strong>중복 ACK duplicate ACK (Fast Retransmit)</strong></p>
<p> 수신측에서 비정상적인 SN을 받은 경우 앞서 정상적으로 받았을 때의 ACK와 동일한 값 전송
 → 중복 ACK 발생</p>
<p> <strong>중복 ACK가 3개</strong> 발생하면 <strong>ACK되고 있는 SN를 가진 segment가 유실</strong>되었다고 판단</p>
<p> segment가 3(경험적으로 판단)개나 도착할 동안에도 아직 도착하지 않음
 → <strong>라우팅의 문제로 순서가 뒤집어진게 아니라 아예 유실된 것</strong>이라고 생각</p>
<p> 재전송 타이버로 유실 사실을 발견하고 재전송 하는 것보다 빠름 → Fast Retransmit</p>
<p> FR: RTT + 3d
 RT: avg(RTT) + 4 * dev(RTT) (수십, 수백ms 가능)</p>
<p> → 3d &lt;&lt; 4 * dev(RTT) <strong>해상도가 낮은 타이머를 쓸 수록 더 차이가 커짐</strong></p>
<p> <strong>Fast Retransmit을 사용할 수 있는 조건</strong></p>
<ul>
<li><p><strong>window size가 커서</strong> 여러 segment가 한 번에 전송될 정도여야 함
  유실된 segment 이후로도 <strong>3개의 segment가 더 들어와야 하기 떄문</strong></p>
</li>
<li><p>혼잡이 발생하여 유실이 일어난 <strong>라우터의 혼잡 정도가 극심해서는 안됨</strong>
  한 segemnt가 <strong>유실된 직후의 다음 segment는 정상적으로 도착</strong>해야함!</p>
</li>
</ul>
</li>
</ol>
<pre><code>⇒ **Fast Retransmit이 아예 재전송 타이머를 완전히 대체할 수 있는 것 X**</code></pre><br>

<h2 id="136-tcp-타이머">13.6 TCP 타이머</h2>
<h3 id="1361-delayed-ack-타이머">13.6.1 Delayed ACK 타이머</h3>
<p>TCP 수신자는 <strong>즉각적으로 ACK를 하지 않음</strong>
    → ACK 패킷을 보내기 싫기 때문</p>
<p>   ACK 패킷은 TCP 헤더 + IP 헤더로만 구성되어도 <strong>최소 40B</strong>
    ⇒ <strong>단독으로 ACK 패킷을 보내지 말고 해당 방향으로 가는 데이터가 있을 때 같이 전송</strong>!
    사실 대역폭이 증가한 오늘날에는 딱히 필요는 없지만 여전히 기본 구성으로 존재</p>
<p>   TCP segment가 도착하면 타이머가 시작
    만약 타이머가 돌고 있다면 새로 시작하지는 않음
    ⇒ 지연 값이 <strong>도착하는 모든 패킷에 정확히 적용되는 것은 아님</strong>!</p>
<p>   delayed ACK가 타임아웃 될 때까지는 <strong>지연된 segement들이 증가</strong>
    결국 타임아웃이 되면 <strong>N:1의 비율로 ACK 발생</strong> (N ≥ 1)</p>
<p>   사람들이 ACK가 두 segment당 하나로 온다고 오해할까?
    <strong>Delayed ACK 알고리즘과 window update 규칙의 상호작용</strong>으로 인한 것!</p>
<p>   윈도우 업데이트 규칙: 응용이 수신 소켓 버퍼에서 데이터를 퍼가서,
    <strong>2MSS 이상의 추가 공간</strong> 발생시 무조건 송신자에게 ACK 보냄
    = 송신 측 window의 RE가 오른쪽으로 2MSS 이상 움직일 수 있는 경우
    ⇒ 수신 측 응용이 열심히 퍼가고 있다면 윈도우 업데이트 규칙에 의해 ACK 발생
    (Delayed ACK가 오버라이드 됨)</p>
<p>   서버에서 <strong>커다란 파일</strong>을 다운로드 받을 때, <strong>응용이 프로세서를 대부분 점유</strong>할 수 있다면 발생하는 일!</p>
<p>   Delayed ACK로 ACK를 억누를 수 있는 최대시간은 시스템 마다 다름
    표준으로는 500ms까지 가능 BUT 최근에는 시간을 대폭 축소하는 경향이 있음</p>
<h3 id="1362-persist-타이머">13.6.2 Persist 타이머</h3>
<p>   <strong>ACK의 window size 값이 0</strong>으로 왔을 때 TCP 송신자가 시작하는 타이머</p>
<p>   window size = 0 → <strong>ACK를 할 당시 수신 측 소켓 버퍼가 꽉 찼다</strong>는 뜻 = <strong>0-윈도우 ACK</strong>
    0-윈도우 ACK를 받으면 송신자는 더 이상 데이터를 보낼 수 없음 (흐름제어)
    TCP 수신자로부터의 윈도우 업데이트를 기다려야함</p>
<p>   BUT <strong>윈도우 업데이트 = ACK 패킷</strong>
    → IP 데이터그램에 실려 운반되므로 유실 될 수 있음!</p>
<p>   만약 윈도우 업데이트 내용을 담은 ACK 패킷이 유실된다면 서로 데이터를 보내지 못하는 상황
    → Persist 타이머를 설정하여 이 값이 타임아웃되면 혹시 ACK가 유실된 것인지 의심
    → 수신자에게 작은 데이터 조각을 보내봄 1B
    (흐름제어상 1바이트도 더 보내면 안되지만 이 경우 유일한 에외)</p>
<p>   window의 RE 너머 1바이트를 보냄 = persist probe</p>
<p>   Persist Probe를 보냈을 때 수신 측의 반응</p>
<ul>
<li><p><strong>다시 0-window ACK 보냄 + 1B도 수신할 수 없었음을 ACK 번호로 표현</strong>
   persist 하는 동안 전혀 상황의 변화가 없었던 것
   수신 소켓의 버퍼가 여전히 가득 차있음!</p>
</li>
<li><p><strong>다시 0-window ACK 보냄 + 1B는 수신</strong>
   수신자의 상황 변화가 있었지만 윈도우 업데이트를 보낼 정도의 공간 확보는 X
   1B정도는 받을 수 있어서 받고 ACK 번호 1증가
   수신자는 약간의 공간이 있더라도 1MSS 보다 작으면 거짓말을 함
   = Silly Window Syndrom (SWS) 회피
   상황 변화가 있긴 했으므로 타이머의 값을 2배로 올리진 않음</p>
</li>
<li><p><strong>0이 아닌 window 값을 보내며 1B 수신</strong>
   윈도우 업데이트 ACK가 실제로 유실됐었던 경우</p>
</li>
</ul>
<h3 id="1363-keepalive-타이머">13.6.3 Keepalive 타이머</h3>
<p>  KeepAlive 타이머는 표준에는 없음
  BUT 실제 시스템에서 서버들에서 불필요한 자원 낭비를 막기 위해 많이 사용</p>
<p>TCP의 <strong>half-open</strong> 상태: <strong>클라이언트가 혼자 조용히 down된 경우</strong> 서버는 그것을 알 수 없음</p>
<p>연결이 정상적으로 종료되었으면 서버는 연결에 할당되었던 모든 자원 수거
BUT half-open시 수거하지 못하고 <strong>자원을 낭비</strong>하고 있게 됨!</p>
<p>클라이언트에서는 큰 문제가 되지 않지만, TCP 연결의 수가 아주 많은 서버의 경우 문제가 됨
⇒ 서버는 <strong>오랫동안 활동이 없는 클라이언트가 half-open 상태가 아닌지 체크</strong></p>
<p>마지막으로 패킷 교환이 있었던 시점에 시작 → 2시간 후에 타임아웃 (OS마다 다름)</p>
<p>이 때 서버는 <strong>Keepalive probe</strong> 패킷을 보내봄</p>
<p>이미 2시간이나 서로 데이터를 주고 받지 않았기에 보낼 데이터가 없음, <strong>헤더만 보냄</strong>
비활동 상태의 클라이언트라면 놀라서 반응해야함
→ 그러려면 <strong>이미 ACK를 받은 옛날 것</strong>을 보냄
→ 살아있는 client는 즉시 <strong>바른 ACK 번호를 제시하는 ACK 패킷</strong>을 보냄
→ 만약 죽었다면 응답이 없음</p>
<p>⇒ <strong>1분 내외의 시간 간격</strong>을 두고 Keepalive Probe를 <strong>3~5번 정도 보내보고 응답이 안오면 연결 정리</strong>!</p>
<h2 id="137-혼잡-제어">13.7 혼잡 제어</h2>
<p>인터넷 프로토콜 스위트 중 가장 복잡한 TCP 중 가장 복잡한 부분!</p>
<h3 id="1371-혼잡-제어-알고리즘의-시작">13.7.1 혼잡 제어 알고리즘의 시작</h3>
<h3 id="1372-van-jacobson-알고리즘">13.7.2 Van Jacobson 알고리즘</h3>
<p>목적: <strong>현재 TCP 경로가 받아들일 수 있는 전송 속도</strong>를 알아내는 것!</p>
<p>너무 작게 설정하면 사용자에게 낮은 throughput
너무 크게 설정하면 병목 라우터(TCP 경로 상에서 가장 용량이 작은 곳)에사 패킷이 대량으로 유실</p>
<p>도착하는 ACK = 다음 전송할 데이터 세그먼트 <strong>출발 신호를 주는 TCP 동작</strong></p>
<p>데이터 세그먼트 하나가 수신자에게 배달이 되어 파이프를 빠져나감
→ ACK 돌아옴 = 수신 측에서 잘 빠져나갔다는 뜻
→ 빠져나간 데이터 세그먼트만큼을 채워넣음
⇒ <strong>평형 유지 equilibrium</strong></p>
<p>TCP 채널애 할당할 수 있는 대역폭 * 송신자에서 수신다까지 지연 = 파이프에서 이동 중인 데이터양
혼잡 윈도우 conjestion window: 네트워크의 최대 용량을 추정하는 송신자의 추정치</p>
<p>네트워크가 소화할 수 있는 RTT당 전송률을 파악하기 위해 사용하는 윈도우
TCP 수신자가 보내주는 수신 소켓 버퍼 여유 공간인 window size와 다름!</p>
<p>W: 세그먼트 단위</p>
<ul>
<li>ACK가 오는 경우 → W = W + 1
  한 RTT동안 전송한 W 세그먼트가 모두 ACK되어 돌아오는 겅우 1 RTT가 지난 후 1 세그먼트만큼 커짐 
= additive increas AI</li>
</ul>
<ul>
<li>ACK가 오지 않고 재전송 타임아웃 발생
→ w = 1</li>
</ul>
<h3 id="1373-slow-start-알고리즘">13.7.3 Slow Start 알고리즘</h3>
<h3 id="1374-fast-retransmit-알고리즘">13.7.4 Fast Retransmit 알고리즘</h3>
<h3 id="1375-fast-recovery-알고리즘">13.7.5 Fast Recovery 알고리즘</h3>
<br>


<h2 id="138-작은-세그먼트-방지와-강건성-원칙">13.8 작은 세그먼트 방지와 강건성 원칙</h2>
<p>TCP는 일부러 세그먼트를 <strong>MSS보다 작게 만들어 보낼 이유가 없음</strong> 
→ TCP/IP 헤더 오버헤드가 상대적으로 커지기 때문
데이터만 전송 소켓 버퍼에 충분히 있다면 언제나 TCP는 MSS 크기를 가진 segment를 보냄</p>
<p><strong>Silly Window Syndrom SWS 회피</strong></p>
<p>TCP가 <strong>작은 데이터 segment를 양산하지 않기 위해 하는 행동</strong>
수신측: <strong>1MSS가 되지 않는 소켓 버퍼 공간은 아예 없는 척</strong>!
→ 그렇지 않으면 송신자가 1MSS 이하의 segment를 전송할 수 있기 때문</p>
<p>송신측: <strong>Nagle 알고리즘</strong>을 통한 작은 segment 전송 방지</p>
<p><strong>Nagle 알고리즘</strong></p>
<p><strong>이미 1MSS 보다 작은 segment를 전송하여 그에 대한 ACK를 받지 못한 상황</strong>이면
<strong>또 다른 작은 segment를 내보내지 않도록 하는 것</strong>!</p>
<p>→ 해당 ACK가 돌아올 때까지 작은 데이터들이 버퍼에 쌓이고 나중에 한 번에 가게 됨</p>
<p>혼자 비싸게 택시타고 가기 vs 다같이 버스타고 가기</p>
<p>작은 segment에 대해 <strong>ACK가 도착하지 않아도 1MSS 이상의 바이트가 모였다면 상관없이 전송!</strong></p>
<p>ex. ‘a’글자를 꾹 누르고 있는 상황
컴퓨터 입장에서는 응용으로부터 매우 천천히 ‘a’ 파이트들이 발신 소켓 버퍼에 떨어지는 중
이 때는 뒤의 입력을 기다렸다가 여러 바이트씩 묶어서 한 번에 segment로 전송!
⇒ <strong>TCP/IP 헤더 오버헤드 비율이 대폭 개선</strong></p>
<p><strong>RTT가 작은 연결</strong>에서는 <strong>ACK가 금방 돌아오므로 크게 효과 X</strong></p>
<p>RTT가 작다 = 일반적으로 연결 경로가 짧고 인터넷의 가장 복잡한 부분을 거치치 않음</p>
<p>ex. 두 컴퓨터가 LAN을 통해 통신 → <strong>대역폭이 커서 헤더 오버헤드로 인한 낮은 효율이 크게 문제X</strong>
반대로 RTT가 큰 연결 → 인터넷의 혼잡한 지점을 지나면서 <strong>주어진 TCP 연결에 할당된 대역폭 작음</strong>
→ 더 많은 라우터를 거칠 수록 이 가능성은 UP</p>
<p>온라인 게임 같은 실시간 응용은 Nagle 알고리즘이 방해가 되기도 함
⇒ 응용에 따라서는 Nagle 알고리즘을 비활성화하는 경우도 있음</p>
<p>왜 송신 측에서 Nagle 알고리즘이 잘 수행되고 있는데 굳이 수신 측에서 SYS 회피를 해야되냐?</p>
<p>TCP 설계에서 Postel의 <strong>강건성 원칙 robustness principle</strong></p>
<p>“Be conservative in what you do, be liberal in what you accept from others.”
<strong>패킷 전송에 있어서 남이 규칙을 지킬 것이라고 믿지 말되 너는 규칙을 지켜라</strong></p>
<p>SWS는 peer 중 어느 한 쪽이라도 규칙을 지키면 작은 segment의 남발 문제를 해결 할 수 있음</p>
<p><strong>Nagle 알고리즘 ↔ Delayed ACK 알고리즘</strong></p>
<p>TCP는 full-duplex 프로토콜이므로 두 개의 데이터 채널 존재
만약 Nagle이 작은 segment의 전송을 막고 있었음
그런데 Delayed ACK 타이머가 눌려 그동안 억눌렸던 ACK 전송
작은 segment를 만들만큼의 데이터 밖에는 발신 소켓 버퍼에 없는 상황
→ 그럼 ACK는 데이터 없이 헤더만 보낼까? Nagle을 따라야 하니까? → NO ❌</p>
<p>이왕 ACK를 보내는 김에 데이터도 같이 보내버림
어차피 Nagle 알고리즘은 헤더로 인한 오버헤드를 줄이기 위한 알고리즘인데
거꾸로 헤더 오버헤드를 늘리는 결정을 하는 것은 말이 안됨!</p>
<p><strong>윈도우 업데이트 규칙 ↔ SWS</strong></p>
<p><strong>윈도우 업데이트 규칙이 되는 2MSS</strong>와 <strong>수신측 SWS에서 조건이 되는 1MSS</strong></p>
<p>윈도우 업데이트 규칙  → 1) <strong>새롭게 발생한 공간</strong>에 대해 2) <strong>ACK를 보낼까 말까</strong>
SWS → 1) <strong>소켓 버퍼에 이미 존재</strong>하지만 <strong>너무 작은 공간</strong>을 2) <strong>제대로 알려줄까 말까</strong></p>
<p>⇒ 데이터 도착에 의해 ACK를 하게 되었다면 이미 윈도우 업데이트 규칙은 해당 X</p>
<p>이 경우 소켓 버퍼에 있는 공간의 크기가 1MSS보다 크냐 작냐의 문제만 남음
→ 작으면 0으로 보냄</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java #1] JDK(Java Development Kit) + JRE, JVM ]]></title>
            <link>https://velog.io/@yeonjiyooo_/Java-1-JDKJava-Development-Kit-JRE-JVM</link>
            <guid>https://velog.io/@yeonjiyooo_/Java-1-JDKJava-Development-Kit-JRE-JVM</guid>
            <pubDate>Tue, 09 Sep 2025 15:10:57 GMT</pubDate>
            <description><![CDATA[<p><strong>Java</strong>와의 <strong>첫 만남</strong> 👋
<img src="https://velog.velcdn.com/images/yeonjiyooo_/post/329e51ce-6cf6-46d1-899b-6090b672bc9b/image.png" alt=""></p>
<hr>
<h2 id="☕️-java">☕️ Java</h2>
<p>Java는 썬 마이크로시스템즈에서 1995년에 개발된 언어로, 플랫폼 독립성을 가지고 있고 강력한 라이브러리 생태계를 갖춘 객체지향 프로그래밍 언어이다.</p>
<p><strong>✅ Java의 대표적인 특징 두가지</strong></p>
<ol>
<li><strong>플랫폼 독립성</strong> <strong>Write Once, Run Anywhere</strong> ↔ Write Once, Compile Anywhere</li>
<li><strong>객체지향</strong> → 코드 재사용, 유지보수 용이</li>
</ol>
<p>C언어로 작성된 소스 코드 → <strong>컴파일러</strong> → 기계어
java 소스 코드 → <strong>자바 컴파일러</strong> → 바이트코드 .class 파일 → <strong>JVM 인터프리팅</strong> → 기계어</p>
<p>Java는 자바 컴파일러를 통한 소스코드 → 바이트코드로의 변환과 JVM을 통한 바이트코드 → 기계어의 변환을 거친다. 즉, 컴파일러 언어와 인터프리터 언어의 특성을 모두 가지고 있어 <strong>하이브리드 언어</strong>라고도 불린다.</p>
<br>


<h2 id="🧳-jdk-java-development-kit">🧳 JDK Java Development Kit</h2>
<p>JDK란 Java 어플리케이션을 개발하기 위한 컴파일러, 라이브러리, 실행환경(JRE)을 포함한 개발 도구 모음을 의미한다.</p>
<p>어플리케이션 개발을 위해서는 코드, 그것을 실행할 환경, 컴파일러, 디버깅 등등 다양한 과정이 진행되며 이것들을 위한 도구들이 필요하다. 이러한 도구들을 통합하여 빠르고 안정적인 개발을 가능하게 만들어 준 것이 바로 JDK이다.</p>
<p>JDK = <strong>JRE + Development Tools</strong></p>
<br>


<h3 id="⏰-jre-java-runtime-environment">⏰ JRE Java Runtime Environment</h3>
<p>JRE는 Jave 어플리케이션을 실행하기 위한 환경이다. JRE는 실행만을 위한 최소한의 구성을 제공하므로 시스템 자원 절약이 가능해진다는 장점이 있다. 개발자에게 있어 한정된 자원을 효율적으로 사용하는 것은 중요한 문제이다. 과거는 더더욱이 자원이 부족했기에 JRE와 Development Tool을 분리하여 JDK를 구성한 것으로 보인다.</p>
<p><strong>🦾 JRE의 구성 요소</strong></p>
<ul>
<li>JVM 가상 머신</li>
<li>Java Class Library: 자주 사용되는 기능들의 표준 API 집합 (ex. 입출력, 문자열)</li>
</ul>
<p>JRE = <strong>JVM + Java Class Library</strong></p>
<p>개발이 완료된 프로그램을 단순히 실행만 하면 되는 경우에는 JRE만 설치하면 된다. 하지만 추가적인 개발이나 컴파일 작업 등이 필요한 경우는 JDK를 설치해야 한다.</p>
<br>

<h3 id="🌐-jvm-java-virtual-machine">🌐 JVM Java Virtual Machine</h3>
<p>JVM은 자바 바이트 코드를 실행하기 위한 가상 머신이다. Java의 OS 독립성을 보장해주기 위한 가장 핵심적인 부분이며 메모리 누수와 같은 부수적인 프로그램에 대한 관리도 가능하게 해준다.</p>
<p>JVM은 자바 이외에도 코틀린,스칼라 등과 같은 다른 언어에서도 사용 가능한데 이는 다른 언어들도 자바 바이트 코드로 컴파일되도록 만들어졌기 때문이다.</p>
<p><strong>🌀 JVM에서 바이트 코드를 처리하는 과정</strong></p>
<ol>
<li>javac(자바 컴파일러)가 생성한 <strong>.class 파일</strong>을 JVM의 <strong>클래스로더가 메모리에 올림</strong></li>
<li><strong>인터프리팅 + JIT 컴파일러</strong> 방식을 사용해 특정 환경의 기계어로 변환<ul>
<li>인터프리팅: 바이트코드를 한 줄씩 기계어로 변환하여 실행</li>
<li>JIT 컴파일러: 런타임에 기계어로 컴파일해두고 캐싱</li>
</ul>
</li>
</ol>
<br>

<p>그럼 JVM에서는 왜 인터프리팅을 사용할까? 이는 Write One, Run Anywhere 를 구현하고자 하는 자바의 철학과 동일선상에서 생각해볼 수 있다. 컴파일러는 결국 플랫폼에 종속되기 때문에 이를 피하고자 인터프리터를 사용한 것이다. 또한 인터프리터는 초기 실행 속도가 빠르다는 장점이 있어 JIT과 함께 사용하며 속도적인 측멱에서의 단점도 최소화 하였다. </p>
<p>Java가 OS 독립적일 수 있는 이유는 Java로 작성된 소스파일은 JVM을 거쳐 OS와 상호작용하기 때문이다. 호스트 OS의 종류와 관계 없이 JVM만 설치되어 있다면 동일한 바이트코드를 수정없이 사용할 수 있다.</p>
<p>반면 컴파일러 언어의 경우, OS 별로 별도의 컴파일이 필요하다. (Write Once, Compile Anywhere) 또한 OS API를 직접 호출하는 코드가 있다면 OS 별로 코드 작성을 다르게 해야한다는 단점이 있다. 이는 단순히 OS별 재컴파일만 해서 해결되는 문제가 아닌, 코드를 작성하는 과정에서도 번거로움이 생기는 것이다.
<br></p>
<p>✚ <strong>thread 생성으로 보는 예시 상황</strong></p>
<p>C 소스 코드</p>
<pre><code class="language-cpp">// Windows API
#include &lt;windows.h&gt;
CreateThread(...);

// Linux API
#include &lt;pthread.h&gt;
pthread_create(...);</code></pre>
<p>Java 소스 코드</p>
<pre><code class="language-cpp">//어떤 OS든 동일
new Thread(() -&gt; {
    System.out.println(&quot;Hello&quot;);
}).start();</code></pre>
<br>

<p><strong>💊  JIT 컴파일러</strong></p>
<p>JVM에서 바이트코드가 기계어로 변환될 때에는 인터프리팅 방식으로 진행된다. 결론적으로 java로 작성된 소스코드는 두 번의 변환 과정을 거쳐 기계어로 번역되는 것이고 이로 인한 속도저하가 발생한다. 이러한 속도 측면의 한계를 극복하고자 등장한 것이 JIT 컴파일러이다. </p>
<p>자주 실행되는 코드를 런타임에 기계어로 변환하고 이후에는 캐싱을 통해 바로 실행하여 속도를 향상시키는 것이다. 런타임에 진행되는 특성을 반영하여 Just-In-Time JIT 이라는 이름이 붙여졌다.</p>
<br>

<hr>
<p>본격적으로 Java를 공부하기 전 Java 언어의 특성과 JDK, JRE, JVM에 대한 개념을 정리하는 시간을 가져보았다. JVM에 대한 개념과 왜 JIT 컴파일러와 인터프리터를 함께 사용하는지 이해하는 과정이 다소 어려웠다. 가상머신에 대해서는 학부 운영체제 수업때 들어 알고는 있었지만 이를 JVM 적용시키려니 조금 헷갈리는 부분이 있었던 것 같다 😢 JVM에 대해서는 다음 글에서 더 자세히 공부해봐야겠다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2024 카카오 겨울 인턴십 코딩테스트] 도넛과 막대 그래프 (C++)]]></title>
            <link>https://velog.io/@yeonjiyooo_/2024-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EA%B2%A8%EC%9A%B8-%EC%9D%B8%ED%84%B4%EC%8B%AD-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8F%84%EB%84%9B%EA%B3%BC-%EB%A7%89%EB%8C%80-%EA%B7%B8%EB%9E%98%ED%94%84-C</link>
            <guid>https://velog.io/@yeonjiyooo_/2024-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EA%B2%A8%EC%9A%B8-%EC%9D%B8%ED%84%B4%EC%8B%AD-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8F%84%EB%84%9B%EA%B3%BC-%EB%A7%89%EB%8C%80-%EA%B7%B8%EB%9E%98%ED%94%84-C</guid>
            <pubDate>Thu, 14 Aug 2025 11:28:51 GMT</pubDate>
            <description><![CDATA[<h3 id="✍-문제">✍ <strong>문제</strong></h3>
<p>문제는 프로그래머스에서 확인해주세요!
<a href="https://school.programmers.co.kr/learn/courses/30/lessons/258711">프로그래머스 2024 KAKAO WINTER INTERNSHIP 도넛과 막대 그래프</a></p>
<hr>
<h3 id="✍-제한-사항">✍ <strong>제한 사항</strong></h3>
<p>1 ≤ edges의 길이 ≤ 1,000,000
edges의 원소는 [a,b] 형태이며, a번 정점에서 b번 정점으로 향하는 간선이 있다는 것을 나타냅니다.
1 ≤ a, b ≤ 1,000,000</p>
<p>문제의 조건에 맞는 그래프가 주어집니다.
도넛 모양 그래프, 막대 모양 그래프, 8자 모양 그래프의 수의 합은 2이상입니다.</p>
<hr>
<h3 id="✅-코드">✅ <strong>코드</strong></h3>
<pre><code class="language-cpp">#include &lt;stdio.h&gt;
#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;map&gt;

using namespace std;

vector&lt;int&gt; solution(vector&lt;vector&lt;int&gt;&gt; edges) {
    vector&lt;int&gt; answer;
    map&lt;int, vector&lt;int&gt;&gt; inout;
    int edgeNum = edges.size();     //간선의 개수
    int vertex;                     //생성된 정점
    int graphNum;                   //전체 그래프의 개수

    for (int i = 0; i &lt; edgeNum; i++) {
        //edges[i][0] -&gt; edges[i][1] 간선 존재
        int from = edges[i][0];
        int to = edges[i][1];
        if (inout.find(from) != inout.end()) inout[from][1]++;
        else inout[from] = {0, 1};
        if (inout.find(to) != inout.end()) inout[to][0]++;
        else inout[to] = {1, 0};
    }

    for (auto v: inout) {
        if(v.second[0] == 0 &amp;&amp; v.second[1] &gt;=2) {
            vertex = v.first;
            graphNum = v.second[1];
            break;
        }
    }

    for (int i = 0; i &lt; edgeNum; i++) {
        if (edges[i][0] == vertex) inout[edges[i][1]][0]--;
    }

    int donut = 0;
    int stick = 0;
    int eight = 0;

    for (auto v: inout) {
        if (v.second[0] == 0 &amp;&amp; v.first != vertex) stick++;
        if (v.second[0] == 2 &amp;&amp; v.second[1] == 2) eight++; 
    }
    donut = graphNum - stick - eight;

    answer = {vertex, donut, stick, eight};

    return answer;
}</code></pre>
<h3 id="✅-코드풀이">✅ <strong>코드풀이</strong></h3>
<p>이 문제에서 가장 핵심이 되는 아이디어는 <strong>각 정점별 나가는 들어오는 간선의 개수, 나가는 간선의 개수</strong>로 그래프 종류를 파악하는 것이다. </p>
<p>결과적으로 구해야 하는 속성은 총 4가지이다.</p>
<ul>
<li>그래프와 무관하게 생성한 정점의 번호</li>
<li>도넛 그래프 개수</li>
<li>막대 그래프 개수</li>
<li>8자 모양 그래프 개수</li>
</ul>
<p>각 속성을 구별할 수 있는 특징을 정리하면 아래와 같다.</p>
<ul>
<li>그래프와 무관하게 생성한 정점의 번호<ul>
<li>들어오는 간선의 개수 = 0</li>
<li>나가는 간선의 개수 &gt;= 2 (문제 조건: 총 그래프의 수는 2개 이상)</li>
</ul>
</li>
</ul>
<p>아래 그래프의 특성은 생성한 정점으로 인해 생긴 간선은 제외하고 정의한 것이다.</p>
<ul>
<li>도넛 그래프 개수<ul>
<li>모든 정점이 들어오는 간선의 개수 = 1 and 나가는 간선의 개수 = 1</li>
</ul>
</li>
<li>막대 그래프 개수<ul>
<li>들어오는 간선의 개수 = 0 or 나가는 간선의 개수 = 0 인 정점 존재</li>
<li>나머지 정점은 들어오는 간선의 개수 = 1 and 나가는 간선의 개수 = 1</li>
</ul>
</li>
<li>8자 모양 그래프 개수<ul>
<li>들어오는 간선의 개수 = 2 or 나가는 간선의 개수 = 2 인 정점 한 개 존재</li>
<li>나머지 정점은 들어오는 간선의 개수 = 1 and 나가는 간선의 개수 = 1</li>
</ul>
</li>
</ul>
<br>
각 정점 별로 들어오고 나가는 간선의 개수를 저장하기 위해 map 자료 구조를 사용하였다.

<p>key: 정점(노드) 번호 <strong>int</strong>
value: 들어가는 간선 개수[0], 나가는 간선 개수[1] 를 원소로 가지는 <strong>vector</strong> </p>
<pre><code class="language-cpp">map&lt;int, vector&lt;int&gt;&gt; inout;

/*
inout[3][0]  = 2 
3번 정점으로 들어오는 간선의 개수 = 2
inout[3][1] = 1
3번 정점에서 나가는 간선의 개수 1
*/</code></pre>
<p>위와 같이 정의한 간선 개수 정보를 담은 inout map과 4가지 속성의 특성을 고려했을 때 다음과 같은 순서대로 각 속성의 값을 구할 수 있다.</p>
<ol>
<li>edges 벡터를 순회하며 각 정점별 in, out 간선의 개수를 inout map에 저장한다.</li>
<li>inout map을 순회하며 <strong>생성한 정점의 번호(속성1)</strong>를 찾는다.
 inout[i][0] == 0 &amp;&amp; inout[i][1] &gt;= 2 를 만족하는 i가 생성한 정점의 번호가 된다.
 이 때 이 정점에서 <strong>나가는 간선의 개수가 전체 그래프 개수의 합</strong>이 된다.</li>
<li>생성한 정점에서 시작된 간선을 삭제한다.
 ➡️ edges 벡터를 순회하며 해당 정점에서 가리키는 정점의 inout[][0] 값을 1 감소시킨다.</li>
<li>다시 inout map을 순회하며 <strong>막대 그래프의 개수(속성3)</strong>과 <strong>8자 모양 그래프의 개수(속성4)</strong>를 구한다.
 ➡️ 들어오는 간선의 개수 = 0 인 정점의 개수가 막대 그래프의 개수가 된다.
 ➡️ 들어오는 간선의 개수 = 2 and 나가는 간선의 개수 = 2 인 정점의 개수가 8자 모양 그래프의 개수가 된다.<ol start="5">
<li>2번에서 구한 전체 그래프의 개수에서 막대, 8자 모양 그래프의 개수를 빼 <strong>도넛 모양 그래프(속성2)</strong>의 개수를 구한다.</li>
</ol>
</li>
</ol>
<hr>
<p>❗ 문제를 풀 때 처음에는 개별 간선 자체를 이용하여 문제를 접근하려고 했다. 그러다보니 풀이 설계가 어려워지고, 구현도 너무 복잡해져서 갈피를 잃어버렸다. 결국 카카오테크 블로그에서 올려준 공식 해설을 보고 나서야 풀 수 있었다. 엄청 복잡한 문제인 줄 알았는데 간선 개수라는 포인트만 잡으면 이후는 엄청 간단했던 문제라서 혼자서 해결하지 못한 것이 너무 아쉬웠다...😵‍💫 또 3개의 개수를 각각 구하지 않고도 전체에서 나머지 2개를 뺌으로서 마지막 남은 하나의 개수도 구할 수 있다는 생각도 잘 떠올리지 못했다! </p>
<p>문제를 더 많이 풀어보면서 <strong>생각의 유연성</strong>을 길러야겠다!! 💪</p>
<p><strong>[참고자료]</strong></p>
<p><a href="https://tech.kakao.com/posts/6103">카카오테크 블로그 공식 해설</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] #4 Error |  ERR_ABORTED 504 (Outdated Optimize Dep)]]></title>
            <link>https://velog.io/@yeonjiyooo_/Error-ERRABORTED-504-Outdated-Optimize-Dep</link>
            <guid>https://velog.io/@yeonjiyooo_/Error-ERRABORTED-504-Outdated-Optimize-Dep</guid>
            <pubDate>Thu, 24 Jul 2025 05:29:33 GMT</pubDate>
            <description><![CDATA[<p>OFS 프로젝트 중 캘린더 페이지를 구현하고 있었는데, moment 패키지를 삭제하고 dayjs 패키지를 새롭게 설치하고 적용하는 과정에서 아래와 같은 에러가 발생하였다.</p>
<p>🔽 <strong>504 Outdated Optimize Dep</strong></p>
<pre><code class="language-bash">GET http://localhost:5173/node_modules/.vite/deps/dayjs.js?v=5465f0dc net::ERR_ABORTED 504 (Outdated Optimize Dep)</code></pre>
<h3 id="🤔-오류-원인">🤔 오류 원인</h3>
<p>Vite 서버가 오래된 번들(새로 업데이트되지 않은)을 로드했기 때문에 발생한 문제이다. </p>
<h4 id="💼-번들링">💼 번들링?</h4>
<p>번들링이란 여러 개의 JavaScript, CSS, 이미지 파일 등을 하나의 파일 또는 여러 최적화된 파일로 묶는 과정을 말한다. 웹 브라우저는 HTTP 요청이 많아지면 느려지기 때문에, 여러 파일들을 하나로 합치고 최적화하여 속도와 성능이 개선시킬 수 있다.</p>
<h4 id="🪡-vite가-번들링하는-방식">🪡 Vite가 번들링하는 방식?</h4>
<p>Vite는 개발 환경에서 빠르게 로딩하도록 설계되었다. 기존의 Webpack과 같은 번들러는 앱을 실행할 때 전체 코드를 한번에 번들링 하는 방식으로 속도가 다소 느리다는 단점이 있었다. </p>
<p>반면 Vite는 ES Module 방식을 이용하여 요청이 들어올 때만 필요한 파일을 번들링해서 제공하는 방식으로 작동한다. import된 모듈 (ex. dayjs)를 미리 변환하여 .vite 캐시에 저장하고 브라우저가 요청하면 .vite/dayjs.js 같은 경로로 제공하는 것이다.</p>
<p>.vite 폴더는 node_modules/.vite 경로 상에 있고 dependencies를 미리 최적화해서 저장해두는 캐시 디렉토리이다. 이 폴더 안에는 dayjs, react, react-dom 같은 패키지를 브라우저에 빨리 서빙하기 위한 번들 결과가 들어있다.</p>
<p>문제 상황에서는 새롭게 darm -rf node_modules/.viteyjs를 설치 했음에도 불구하고 .vite 캐시는 이전 상태를 기억하고 있어 오래된 번들을 로드하려고 했기 때문에 오류가 발생한 것이다.</p>
<h3 id="🥳-오류-해결">🥳 오류 해결</h3>
<p>.vite 캐시를 삭제하고 서버를 재시작하면 오래된 번들 상태는 삭제되고 현재 상태에 맞는 새로운 번들 결과가 .vite에 저장될 것이다.</p>
<h4 id="vite-캐시-삭제">.vite 캐시 삭제</h4>
<pre><code class="language-bash">rm -rf node_modules/.vite</code></pre>
<h4 id="서버-재시작">서버 재시작</h4>
<pre><code class="language-bash">npm run dev</code></pre>
<hr>
<p>vite의 번들링 과정에 대해 간단히 알아볼 수 있었던 좋은 기회였다. 개발을 하다보면 늘 오류와 싸워야 하지만 그 과정에서 얻어가는 것이 꼭 있으니, 오류를 두려워 하지 않고 배움의 기회로 삼아 열심히 해보자!! 💪</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] #3 useState 비동기 동작 처리하기]]></title>
            <link>https://velog.io/@yeonjiyooo_/React-3-useState-%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%8F%99%EC%9E%91-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yeonjiyooo_/React-3-useState-%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%8F%99%EC%9E%91-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 11 Jul 2025 14:49:28 GMT</pubDate>
            <description><![CDATA[<p>React 컴포넌트의 상태관리를 위해 정말 자주 사용하는 <strong>useState ⭐️</strong></p>
<p>하지만 반드시 주의해야 할 점은 &quot;useState는 <strong>비동기적으로 동작</strong>&quot; 한다는 것이다.</p>
<h2 id="🔥-비동기란">🔥 비동기란?</h2>
<p>비동기적으로 동작한다는 것의 의미는 특정 코드의 동작이 끝날 때까지 기다리지 않고 다음 코드의 동작을 바로 실행시키는 방식을 말한다. 이를 useState의 동작에 적용시켜 본다면, 상태 변화 함수를 통해 <strong>state를 변화시키더라도 그것을 바로 적용시키지 않고 다음 렌더링 시점까지 기다리는 것</strong>이다. </p>
<p>간단한 예시를 통해 useState의 비동기 동작 과정을 자세히 알아보자.</p>
<pre><code class="language-javascript">const [count, setCount] = useState(0); //state의 초기값 = 0

const handleClick = () =&gt; {
  setCount(count + 1);     //✅ 업데이트 요청, 실제로 값에는 반영 X
  console.log(count);      //❌ 여기서는 아직 이전 값 (0)
};
</code></pre>
<p>1️⃣ <strong>count라는 상태 변수</strong>와 <strong>2️⃣ handleClick라는 이벤트 핸들러 함수</strong>가 있다.</p>
<p>본래 의도는 handleClick 함수 내의 setCount 상태 변화 함수가 실행되면서 count값이 1 증가하고, 
1 증가한 값을 console.log로 확인하는 것이다.</p>
<p>하지만 기대와는 달리 console에 찍힌 값은 여전히 0일 것이다! 🤔</p>
<p>그 이유는 바로 useState의 비동기적 특성 때문이다. </p>
<p>handleClick 함수 내에서 setCount가 실행되더라도 상태 값인 count의 변화는 바로 적용되지 않는다. <strong>count 값이 여전히 0인채로 console.log(count)에 도달</strong>하게 된다. 이후 이벤트 핸들러 함수가 완전히 종료되고 <strong>⭐️해당 컴포넌트를 다시 렌더링 하는 시점⭐️</strong> 에서야 count 값의 변화가 적용된다. </p>
<p>setCount가 이벤트 핸들러 함수 내부에서 실행될 당시의 동작은 단순히 <strong>상태값 변화를 &quot;요청&quot;</strong>하는 것으로 이해하면 된다. 그리고 그 요청을 <strong>실제로 &quot;수행&quot;</strong>하는 것은 그 즉시가 아닌, <strong>다음 렌더링 시점</strong>인 것이다!</p>
<p>따라서 이벤트 핸들러 함수 내부의 console.log(count)가 실행될 때에는 아직 이벤트 핸들러 함수의 동작이 끝나기 전이므로 count 변수는 이전 상태 값인 0을 값으로 가지게 되고, 이후에 컴포넌트가 렌더링 된 후 1로 변화하게 된다.</p>
<h2 id="😓-로그인-상황에서-비동기로-인한-오류-수정">😓 로그인 상황에서 비동기로 인한 오류 수정</h2>
<p>이제부터 내가 로그인 페이지 개발 중 useState의 비동기적 특성을 고려하지 않아 발생했던 오류와 해결 방법을 작성해보겠다!</p>
<h4 id="✅-구현하고자-했던-로직">✅ 구현하고자 했던 로직</h4>
<p>1) 로그인 버튼 클릭
2) 입력받은 이메일(아이디)가 유효한 형식인지 확인
3-1) 유효한 이메일이라면 로그인 로직 실행
3-2) 유효하지 않은 이메일이라면 경고 메세지 출력</p>
<p>아직 UI를 구현하는 단계였어서 로그인 과정까지 구현하지는 않았고 3-1의 로그인 로직은 console에 &quot;로그인 진행 중...&quot; 을 출력하는 것으로 코드를 짰다!</p>
<pre><code class="language-javascript">//로그인 제출과 관련된 상태 객체
const [submission, setSubmission] = useState({
      isSubmitted: false,
      isValidEmail: false,
});

//로그인 버튼 클릭시 실행되는 함수
const handleSubmit = (e) =&gt; {
      e.preventDefault();
      setSubmission(() =&gt; ({
         isSubmitted: true,
         isValidEmail: validateEmail(info.address),
      }));
      if (!submission.isValidEmail) {
         console.log(&quot;이메일 형식 오류&quot;);
         return;
      }
      console.log(&quot;로그인 진행 중...&quot;);
      console.log(info);
      //로그인 로직 진행 (ex. API 요청)
};</code></pre>
<h4 id="⚠️-오류-발생">⚠️ 오류 발생</h4>
<p>그런데 버튼을 처음 클릭했을 때는 유효한 이메일 형식이더라도 console에 이메일 형식 오류가 출력되었고, 두 번을 눌러야만 로그인이 정상적으로 진행되는 오류를 확인하였다.</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/5b26bd11-c301-4efd-bd22-5a2f4a607489/image.png" alt=""></p>
<p>해당 오류는 useState의 비동기 동작을 고려하지 않아 발생했던 것이다 🤯</p>
<p>handleSubmit 함수를 보면 setSubmission 상태 변화 함수가 실행되고, 해당 함수 내부에서 이메일 형식이 유효한지에 대한 정보를 담고있는 <strong>isValidEmail 상태 변수가 업데이트</strong>(실제로는 요청만!) 된다.</p>
<p>그런데 앞서 말했듯, 이 코드가 실행되어도 isValidEmail 변수는 <strong>즉각적으로 값이 업데이트 되지 않기 때문에</strong> 처음으로 handleSubmit이 실행되는 경우에 if (!submission.isValidEmail) 조건문의 <strong>isValidEmail은 여전히 초기값이 false로 저장</strong>되어 있던 것이다.</p>
<p>이후 handleSubmit 함수 실행이 완전히 종료되고 컴포넌트가 리렌더링 되면서 isValidEmail은 그제서야 true 값을 가지므로, 두 번째로 로그인 버튼을 눌렀을 때는 이메일의 형식이 유효하다고 옳게 판단하여 로그인 로직을 진행하게 된다. </p>
<h4 id="😊-오류-해결">😊 오류 해결</h4>
<p>이처럼 useState 업데이트 직후에는 해당 값에 의존하는 코드를 작성하면 안된다. </p>
<p>따라서 조건문과 같이 특정 값에 대한 연산이 필요한 경우 <strong>해당 값을 저장하는 javascript 변수를 선언</strong>하여 사용하면 된다! 아래는 이메일 형식 검사의 결과값을 담는 <strong>isValid 변수</strong>를 선언하고 그것을 조건문에 사용한 코드이다.</p>
<p>해당 코드를 실행시키면 이메일 형식이 유효한 경우 로그인 버튼을 누르면 바로 로그인 로직이 동작하는 것을 확인할 수 있다!</p>
<pre><code class="language-javascript">const handleSubmit = (e) =&gt; {
      e.preventDefault();

      const isValid = validateEmail(info.address);

      setSubmission(() =&gt; ({
         isSubmitted: true,
         isValidEmail: validateEmail(info.address),
      }));
      if (!isValid) {
         console.log(&quot;이메일 형식 오류&quot;);
         return;
      }
      console.log(&quot;로그인 진행 중...&quot;);
      console.log(info);
      //로그인 로직 진행 (ex. API 요청)
   };</code></pre>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/af631602-fd73-4c78-bd07-580bf05efaa0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/2cafceb1-838b-4e1e-b116-0c374ddd259d/image.png" alt=""></p>
<hr>
<p>useState를 공부하며 비동기에 관한 내용을 공부했었는데 막상 코드 짜면서는 고려를 하지 않고 막 쓰다가 드디어(ㅎ) 오류를 만난 덕분에 다시금 중요한 개념을 짚고 넘어갈 수 있었다. </p>
<p>아무리 열심히 공부를 해도 기억하려고 노력하지 않으면 금방 까먹게 되는 것 같다. 개발을 하면서도 무작정 하는 것이 아니라 그동안 배웠던 것을 끄집어내고 체화시키는 과정도 함께한다면 더 나은 개발자가 될 수 있을 것 같다 💪💪 파이팅!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] #2 React Naming Convention 알아보기]]></title>
            <link>https://velog.io/@yeonjiyooo_/React-2-React-Naming-Convention-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@yeonjiyooo_/React-2-React-Naming-Convention-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 09 Jul 2025 10:15:01 GMT</pubDate>
            <description><![CDATA[<p>여느 때와 다름없이 개발 중이었는데 너무 지저분한 className에 놀라서 작성해보는 글이다 😓</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/3c09d025-97f9-4db7-a968-b626a45e30ec/image.png" alt=""></p>
<hr>
<h2 id="🗣️-component">🗣️ Component</h2>
<p>각 컴포넌트의 역할을 명확하게 보여줄 수 있도록 이름을 짓고 PascalCase 를 사용한다.</p>
<p>PascalCase: 각 단어의 첫 글자를 대문자로 하며 가장 첫 글자도 대문자로 시작하는 규칙</p>
<pre><code class="language-javascript">// ✅
const SubmitBtn = () =&gt; { ...

// ❌
const submitBtn = () =&gt; { ...
const button = () =&gt; { ...</code></pre>
<h2 id="📂-file">📂 File</h2>
<p>해당 파일 내의 컴포넌트 이름과 매칭시키고 PascalCase를 사용한다.</p>
<pre><code class="language-javascript">// ✅
[SubmitBtn.jsx]
const SubmitBtn = () =&gt; { ...

// ❌
[submit.jsx]
const SubmitBtn = () =&gt; { ...</code></pre>
<h2 id="🗿-constant">🗿 Constant</h2>
<p>상수는 모든 글자를 대문자로 표현하고 underscore _ 을 사용한다.</p>
<pre><code class="language-javascript">// ✅
const API_URL = &quot;https://api.example.com&quot;;
const MAX_COST = 100;

// ❌
const api_URL = &quot;https://api.example.com&quot;;
const maxCost = 100;</code></pre>
<h2 id="🚚-props">🚚 Props</h2>
<p>목적을 명확하게 나타낼 수 있는 descriptive 한 이름을 사용한다. 해당 프로젝트에서 널리 사용되는 약어나 줄임말이 아닌 이상 줄여쓰는 것은 피해야 한다.</p>
<pre><code class="language-javascript">// ✅
user, button, ...

// ❌
usr, btn, ...</code></pre>
<h2 id="🐟-state-변수">🐟 State 변수</h2>
<p>boolean 값을 가지는 상태 변수에는 is, has, should 같은 접두어를 붙인다. </p>
<pre><code class="language-javascript">// ✅
isActive, hasError, shouldRender</code></pre>
<h2 id="🐍-classname">🐍 className</h2>
<p>여러 스타일이 있지만 <strong>BEM: Block, Element, Modifier</strong> 방식을 사용하기로 했다. 
<strong>BEM</strong>: 큰 컴포넌트를 block, 그 안의 구성요소를 element, 상태나 변형을 modifier로 나누는 명명 규칙</p>
<p>className을 보고 어떤 역할을 하는 지 알 수 있어 가독성이 좋고 modifier만 붙이면 다양한 상태를 구현할 수 있어 확장성이 좋다는 장점이 있다. </p>
<pre><code class="language-css">block__element--modifier</code></pre>
<table>
<thead>
<tr>
<th>구성</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>Block</td>
<td>컴포넌트 단위</td>
<td><code>login</code></td>
</tr>
<tr>
<td>Element</td>
<td>Block 내부 요소</td>
<td><code>login__input</code></td>
</tr>
<tr>
<td>Modifier</td>
<td>상태나 변형</td>
<td><code>login__input--error</code></td>
</tr>
</tbody></table>
<p>⚠️ <strong>주의점</strong>
요소 안에 또 요소를 작성하거나 많은 modifier을 사용하지 않는다.</p>
<pre><code class="language-css">login__input__label ❌
login__lable or login__input--label</code></pre>
<pre><code class="language-html">&lt;button className=&quot;login__button login__button--primary&quot;&gt;로그인&lt;/button&gt;</code></pre>
<p>위 코드처럼 2개 이상의 class를 사용하여 여러 요소에 공통적으로 적용해야할 스타일과 특정 요소의 상태나 역할에 따른 추가 스타일을 지정할 수 있다. 이 때 class 이름 사이에는 공백을 넣어야 한다.</p>
<hr>
<p>물론 100% 정답이고 반드시 지켜야만 하는 사항은 아니지만, 다른 사람들과 협업을 해야하는 개발자의 입장에서 명확한 규칙을 가지고 그것을 지켜가며 개발하는 것도 중요한 역량이라는 생각이 든다!</p>
<p>나~중에 현업에 들어가게 되면 내가 속한 조직에서 쓰는 규칙에 맞게 해야될테니까 혼자서 개발할 때에도 규칙을 세우고 번거롭더라도 그것을 고려해가며 개발해야겠다 💪</p>
<blockquote>
<p>[참고자료]
<a href="https://getbem.com/naming/">https://getbem.com/naming/</a>
<a href="https://dev.to/kristiyanvelkov/react-js-naming-convention-lcg">https://dev.to/kristiyanvelkov/react-js-naming-convention-lcg</a>
<a href="https://stackoverflow.com/questions/57221878/react-classname-naming-convention">https://stackoverflow.com/questions/57221878/react-classname-naming-convention</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] #1 왜 React일까?]]></title>
            <link>https://velog.io/@yeonjiyooo_/%EC%99%9C-React%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@yeonjiyooo_/%EC%99%9C-React%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Mon, 07 Jul 2025 14:31:50 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드 개발자에게 가장 사랑받는 라이브러리 중 하나인 <strong>React</strong> 🌐</p>
<p>어떤 매력이 있길래 많은 개발자들의 <strong>Pick!</strong>을 받을 수 있었던 것일까?
<img src = "https://velog.velcdn.com/images/yeonjiyooo_/post/4e0815d9-6560-411f-a6a9-80e889cd1b3f/image.png" width = "300"></p>
<h2 id="🖐️-react의-등장">🖐️ React의 등장</h2>
<h4 id="multi-page-application-mpa">Multi Page Application MPA</h4>
<p>초기의 웹 애플리케이션은 Multi Page Application MPA 방식으로 구성되어 있었다. 사용자가 페이지를 이동할 때마다 서버에서 HTML, CSS, JS를 전부 다시 받아야와야 했던 것이다. 이는 매 페이지 전환마다 전체 페이지를 다시 로딩해야 하므로 속도가 느렸고, 그에 따른 사용자 경험도 매끄럽지 않았다. </p>
<h4 id="single-page-application-spa">Single Page Application SPA</h4>
<p>MPA의 단점을 극복하고자 등장한 것이 Single Page Application SPA이다. SPA는 HTML, CSS, JS를 최초 1회만 로딩하고 이후에는 JS를 통해 동적으로 화면(DOM)을 업데이트 하는 방식으로 작동한다. 페이지 전환시에 전체 페이지를 다시 로딩하지 않아도 된다는 점에서 속도가 증가하였고 서버 부하도 감소하였다.</p>
<p>SPA를 적용한 초기 프레임워크에는 <strong>Google의 Angular.js</strong>가 있다.</p>
<img src = "https://velog.velcdn.com/images/yeonjiyooo_/post/b221dc71-8f36-4926-9424-8564e559aa49/image.png" width = "500">

<br>

<p>Angular는 양방향 데이터 바인딩을 지원하여 데이터가 변경되었을 때 DOM을 자동 업데이트 할 수 있게 하였다. 또한 모듈 단위로 앱을 구성하는 방식을 채택하여 구조화된 대규모 앱 작성이 가능하였다.</p>
<p>하지만 점점 앱의 사이즈가 커지면서 양방향 데이터 바인딩은 오히려 데이터의 흐름을 복잡하게 만들었고, 상태 변화가 많아지는 경우 디버깅이 어렵다는 단점이 있었다. 또한 DOM의 변경이 많을 수록 성능 저하가 발생하는 문제도 있었다.</p>
<h4 id="⭐️-react">⭐️ React!</h4>
<p>앞서 살펴본 여러 문제점을 보완하고자 나온 것이 우리가 공부하고자 하는 React 이다.</p>
<p>React는 메타에서 개발한 오픈소스 자바스크립트 라이브러리로 2025년이 된 지금까지도 프론트엔드 개발의 대표적인 라이브러리로 자리매김하였다.</p>
<p>React의 특징을 살펴보며 어떻게 기존의 웹 애플리케이션의 단점을 극복할 수 있었는지 알아보자!</p>
<br>

<h2 id="🤓-react의-특징">🤓 React의 특징</h2>
<h3 id="1-컴포넌트-기반-아키텍처-🧩">1. 컴포넌트 기반 아키텍처 🧩</h3>
<p>페이지를 구성하는 모든 요소들을 컴포넌트라는 단위로 모듈화하여 개발하기 때문에 재사용성이 좋고 유지보수에 용이하다!</p>
<p>예를 들어, 여러 페이지에 동일하게 렌더링되는 header라는 부분이 있다면, 이를 하나의 독립된 컴포넌트로 작성하고 해당 컴포넌트를 필요로 하는 페이지에 불러오기만 하면 된다.</p>
<p>여러 페이지에서 사용된다고 해서 그 페이지들마다 따로 작성할 필요가 없기 때문에 중복 코드 발생을 막을 수 있다.  </p>
<h3 id="2-단방향-데이터-흐름-one-way-data-binding-🔁">2. 단방향 데이터 흐름 (one-way data binding) 🔁</h3>
<p>앞서 Angular.js의 특징 중 하나가 양방향 데이터 바인딩을 지원하는 것이라고 하였다. </p>
<p>이와 달리 react는 단방향 데이터 바인딩을 선택하여 부모 → 자식 컴포넌트, 또는 상태 → 화면(UI) 방향으로만 데이터가 흐르도록 만들었다. 이에 따라 사용자의 입력은 별도의 이벤트 핸들러를 통해 처리해야 하기 때문에 코드의 길이나 구현 방법이 조금 복잡해 질 수 있다. </p>
<p>하지만 데이터가 한 방향으로만 흐르기 때문에 데이터의 흐름을 예측하기 쉬워지고, 상태 추적과 유지 보수가 훨씬 용이해졌다. 웹의 크기가 커짐에 따라 데이터의 추적이 쉽다는 장점이 더욱 강력하게 작용하므로 단방향 바인딩을 채택한 것으로 보인다.</p>
<h3 id="3-선언형-ui-구성-🧪">3. 선언형 UI 구성 🧪</h3>
<p>웹에서 업데이트란 사용자의 행동 (클릭, 드래그)에 따라 웹 페이지가 스스로 모습을 바꿔 상호작용 하는 것을 말한다. React에서는 상태(state) 기반 렌더링 방식을 사용하므로 컴포넌트의 state 값만 바꾸면 해당 값에 따른 UI가 렌더링 될 수 있다.</p>
<p>따라서 업데이트를 위한 복잡한 동작을 정의할 필요 없이 특정 변수의 값을 바꾸는 것만으로도 화면을 업데이트 시킬 수 있기 때문에 UI 코드가 훨씬 간결하고 직관적이다.</p>
<h3 id="4-virtual-dom-도입-🧠">4. Virtual DOM 도입 🧠</h3>
<p>React에서는 virtual DOM을 도입함으로써 렌더링 과정의 병목을 감소시키고 성능을 크게 향상시킬 수 있었기에 vitual DOM은 React의 가장 중요한 특징 중 하나로 꼽힌다.</p>
<p>virtual DOM을 이해하기 전에 DOM과 브라우저의 렌더링 과정을 먼저 살펴보자!</p>
<p><strong>4.1 🌳 DOM Document Object Model</strong></p>
<p>DOM이란 HTML 코드를 일종의 객체 모델로 표현한 것으로 문서의 구조화된 표현을 제공한다. HTML 문서가 브라우저에 로드되면 브라우저는 이를 파싱하여 자신이 이해할 수 있는 형태인 객체모델 = DOM을 구성한다. DOM은 <strong>HTML의 각 요소를 객체</strong>로 나타내고, <strong>계층적인 구조</strong>를 가지고 있다.</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/dd6d241a-0b4f-4b06-83ba-36f0c2595f92/image.png" alt="이미지 출처: 토스페이먼츠">
(이미지 출처: <a href="https://docs.tosspayments.com/resources/glossary/dom">https://docs.tosspayments.com/resources/glossary/dom</a>)</p>
<p>이렇게 생성한 DOM을 통해 JavaScript와 같은 스크립트 및 프로그래밍 언어가 웹 페이지에 접근할 수 있게 된다. 즉 DOM은 웹 페이지와 프로그래밍 언어를 연결시켜주는 중간 역할이라고 생각할 수 있게 된다!</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/4fb10b0e-fa24-44d5-8174-c671ebe73a0d/image.png" alt="이미지 출처: 토스페이먼츠">
(이미지 출처: <a href="https://docs.tosspayments.com/resources/glossary/dom">https://docs.tosspayments.com/resources/glossary/dom</a>)</p>
<p>언어마다 HTML에 접근하는 코드는 다르지만 대표적으로 자바스크립트의 경우 
<code>document.getElementByld(&quot;#id&quot;)</code> 으로 접근할 수 있다.</p>
<p>CSS도 HTML의 DOM과 유사하게 CSSOM (CSS Object Model)을 생성한다.</p>
<p><strong>4.2 📄 웹 페이지의 렌더링 과정</strong></p>
<p>우리가 작성한 코드는 아래와 같은 과정을 통해 화면에 렌더링 된다.</p>
<ol>
<li>HTML의 DOM, CSS의 CSSOM 생성</li>
<li>Render Tree 생성</li>
<li>Layout</li>
<li>Painting</li>
<li>Compositing</li>
</ol>
<p>(1단계는 앞서 살펴본 DOM을 생성하는 과정이니 설명은 생략한다.)</p>
<p>2단계에서는 DOM과 CSSOM을 합쳐 Render Tree를 생성하게 된다. Render Tree란 화면에 렌더링 되어야 하는 모든 요소들의 정보를 포함하는 것으로 <strong>웹 페이지의 설계도</strong> 정도로 이해할 수 있겠다.</p>
<p>3단계는 Render Tree를 바탕으로 브라우저가 <strong>각 요소의 위치와 크기를 계산</strong>하는 단계이다. 각 요소가 화면의 어느 위치에 어떤 크기로 렌더링되어야 하는 지를 결정한다.</p>
<p>4단계는 <strong>각 요소의 스타일과 내용</strong>을 바탕으로 화면에 픽셀을 그리는 단계이다. 각 요소에 적용된 스타일 속성을 시각적으로 표현하여 픽셀 데이터를 생성한다.</p>
<p>5단계에서는 painting 과정에서 생성된 여러 개의 레이어를 하나의 화면으로 결합한다. 여러 요소들이 겹쳐져있는 경우, 그것들의 순서를 고려하여 정확한 화면을 렌더링 할 수 있게  만들어주는 과정이다.</p>
<p>이렇게 모든 단계를 거쳐 작성한 코드가 웹 브라우저 화면에 렌더링 되는 것인데, 이 단계들 중 layout, painting 단계는 매우 오래걸리는 무거운 작업이다.</p>
<p>만약 DOM을 수정하게 되면 1~5단계의 과정을 다시 거쳐야 하는데, DOM의 수정이 자주 발생하게 된다면 layout을 다시 하는 <strong>reflow</strong>와 painting을 다시하는 <strong>repaint</strong> 과정을 반복적으로 거쳐야 하므로 성능이 매우 저하된다.</p>
<p>이를 극복하기 위해 등장한 것이 <strong>virtual DOM</strong>이라는 개념이다!</p>
<p><strong>4.3 🌐 virtual DOM</strong></p>
<p>virtual DOM에 대한 이해를 돕는 영상자료이다.
<a href="https://youtu.be/BYbgopx44vo?si=aOC4U5vEm4J-IAC5">https://youtu.be/BYbgopx44vo?si=aOC4U5vEm4J-IAC5</a></p>
<p>React에서는 렌더링이 발생될 상황에 놓이게 되면 새로운 화면에 들어갈 내용이 담긴 virtual DOM을 생성한다. virtual DOM은 실제 DOM의 가벼운 복사본으로 메모리 상에 존재하며, JavaScript 객체 형태이다.</p>
<p>React는 렌더링 이전의 virtual DOM과 렌더링 이후의 virtual DOM, 총 2개의 virtual DOM을 가지고 이를 비교하여 변경된 부분만 실제 DOM에 적용함으로서 불필요한 전체 DOM 업데이트를 방지해준다.</p>
<p>변경 사항을 메모리 상에 있는 virtual DOM을 통해 먼저 처리하고, 실제 DOM을 변경할 때는 최소한의 변경만 적용하므로 무거운 작업(layout, paiting)을 최소화할 수 있게 되었고 이를 통해 성능을 크게 향상시킬 수 있었다!</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/93de1e01-f172-4ca0-88e2-e43952168197/image.png" alt="">
(이미지 출처: <a href="https://callmedevmomo.medium.com/virtual-dom-react-%ED%95%B5%EC%8B%AC%EC%A0%95%EB%A6%AC-bfbfcecc4fbb">https://callmedevmomo.medium.com/virtual-dom-react-%ED%95%B5%EC%8B%AC%EC%A0%95%EB%A6%AC-bfbfcecc4fbb</a>)</p>
<p>이렇듯 React는 단순히 새로운 UI 라이브러리가 아니라, 복잡한 웹 앱 개발에서의 <strong>데이터 흐름과 UI 성능 문제</strong>를 해결하기 위한 대안으로 등장한 기술이다. Angular.js가 보여준 SPA의 가능성에 <strong>예측 가능성과 성능</strong>을 더해 React는 대규모 웹 애플리케이션 개발의 새로운 표준으로 자리잡게 되었다.</p>
<hr>
<p>React 이전의 방식들의 단점과 이를 보완하는 React의 특징을 살펴보니 왜 개발자들에게 많은 사랑을 받는 라이브러리가 될 수 있었는지 이해가 된다. </p>
<p>웹 개발을 한다고 하면 당연히 React를 배워야 한다고 하는데 왜 굳이 React 인지가 궁금하기도하고, 내가 지금 공부하는 것이 어떤 특징을 가지는지 정도는 아는 것이 좋을 것 같아 포스팅해보았다 😗</p>
<p>잘못된 내용이 있다면 언제든지 댓글 남겨주세요! 🙇🏻‍♀️</p>
<blockquote>
<p><strong>[참고자료]</strong>
<a href="https://docs.tosspayments.com/resources/glossary/dom">https://docs.tosspayments.com/resources/glossary/dom</a>
<a href="https://yozm.wishket.com/magazine/detail/2909/">https://yozm.wishket.com/magazine/detail/2909/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 2748번 피보나치 수2 (C++)]]></title>
            <link>https://velog.io/@yeonjiyooo_/%EB%B0%B1%EC%A4%80-2748%EB%B2%88-%ED%94%BC%EB%B3%B4%EB%82%98%EC%B9%98-%EC%88%982-C</link>
            <guid>https://velog.io/@yeonjiyooo_/%EB%B0%B1%EC%A4%80-2748%EB%B2%88-%ED%94%BC%EB%B3%B4%EB%82%98%EC%B9%98-%EC%88%982-C</guid>
            <pubDate>Wed, 04 Jun 2025 05:36:13 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/2748">백준 2748번 피보나치 수2</a></p>
<h3 id="✍-문제">✍ <strong>문제</strong></h3>
<p>피보나치 수는 0과 1로 시작한다. 0번째 피보나치 수는 0이고, 1번째 피보나치 수는 1이다. 그 다음 2번째 부터는 바로 앞 두 피보나치 수의 합이 된다.</p>
<p>이를 식으로 써보면 Fn = Fn-1 + Fn-2 (n ≥ 2)가 된다.</p>
<p>n=17일때 까지 피보나치 수를 써보면 다음과 같다.
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597</p>
<p>n이 주어졌을 때, n번째 피보나치 수를 구하는 프로그램을 작성하시오.</p>
<hr>
<h3 id="✍-입력">✍ <strong>입력</strong></h3>
<p>첫째 줄에 n이 주어진다. n은 90보다 작거나 같은 자연수이다.</p>
<hr>
<h3 id="✍-출력">✍ <strong>출력</strong></h3>
<p>첫째 줄에 n번째 피보나치 수를 출력한다.</p>
<hr>
<h3 id="✅-코드">✅ <strong>코드</strong></h3>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;iostream&gt;

using namespace std;

long long fibo[91];

int main() {

    int n;
    cin &gt;&gt; n;

    fibo[0] = 0; fibo[1] = 1;

    for (int i = 2; i &lt;= n; i++) fibo[i] = fibo[i-1] + fibo[i-2];

    cout &lt;&lt; fibo[n];

    return 0;
}</code></pre>
<h3 id="✅-코드풀이">✅ <strong>코드풀이</strong></h3>
<p>정말 간단한 문제지만 주의해야 할 점이 있다!</p>
<p>피보나치를 구하는 문제에서 보통 <strong>재귀</strong>를 많이 이용하는데 (재귀를 배울 때 피보나치는 거의 백프로 예시로 사용되기 때문) 이 문제의 <strong>시간 제한은 1초</strong>다. 따라서 함수호출이 많고 연산이 많은 재귀의 특성을 고려했을 때 시간 초과가 날 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/1a03c761-414a-4eb4-85c9-fd65597f6dc3/image.png" alt=""></p>
<p>따라서 시간초과에 걸리지 않으려면  피보나치 수를 저장할 <strong>배열을 선언</strong>하고** 반복문**을 사용해 구현해야 한다. (구현은 어렵지 않으니 설명 PASS)</p>
<p>또 한 가지 주의해야 할 점은 n번째 피보나치 수를 구해야하는데 n의 최댓값은 90이다. </p>
<p>따라서 반드시 피보나치 수 배열의 자료형은 <strong>long long</strong>으로 선언해줘야한다. 46번째 피보나치 수까지 int 범위이고 그 다음부터는 int 범위를 넘어가는 값이기 때문이다.</p>
<hr>
<p>❗ 브론즈니까 아무 생각 없이 막 풀다보면 기본적인 조건들에서 걸릴 수 있었던 문제였다. 항상 PS 할 때는 시간복잡도나 자료형을 꼼꼼히 확인하는 습관을 들이자! 💪</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 2143번 두 배열의 합 (C++)]]></title>
            <link>https://velog.io/@yeonjiyooo_/%EB%B0%B1%EC%A4%80-2143%EB%B2%88-%EB%91%90-%EB%B0%B0%EC%97%B4%EC%9D%98-%ED%95%A9-C</link>
            <guid>https://velog.io/@yeonjiyooo_/%EB%B0%B1%EC%A4%80-2143%EB%B2%88-%EB%91%90-%EB%B0%B0%EC%97%B4%EC%9D%98-%ED%95%A9-C</guid>
            <pubDate>Tue, 03 Jun 2025 17:29:46 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/2143">백준 2143번 두 배열의 합</a></p>
<h3 id="✍-문제">✍ <strong>문제</strong></h3>
<p>한 배열 A[1], A[2], …, A[n]에 대해서, 부 배열은 A[i], A[i+1], …, A[j-1], A[j] (단, 1 ≤ i ≤ j ≤ n)을 말한다. 이러한 부 배열의 합은 A[i]+…+A[j]를 의미한다. 각 원소가 정수인 두 배열 A[1], …, A[n]과 B[1], …, B[m]이 주어졌을 때, A의 부 배열의 합에 B의 부 배열의 합을 더해서 T가 되는 모든 부 배열 쌍의 개수를 구하는 프로그램을 작성하시오.</p>
<p>예를 들어, A = {1, 3, 1, 2}, B = {1, 3, 2}, T=5인 경우, 부 배열 쌍의 개수는 다음의 7가지 경우가 있다.</p>
<p>T(=5) = A[1] + B[1] + B[2]
      = A[1] + A[2] + B[1]
      = A[2] + B[3]
      = A[2] + A[3] + B[1]
      = A[3] + B[1] + B[2]
      = A[3] + A[4] + B[3]
      = A[4] + B[2] </p>
<hr>
<h3 id="✍-입력">✍ <strong>입력</strong></h3>
<p>첫째 줄에 T(-1,000,000,000 ≤ T ≤ 1,000,000,000)가 주어진다. 다음 줄에는 n(1 ≤ n ≤ 1,000)이 주어지고, 그 다음 줄에 n개의 정수로 A[1], …, A[n]이 주어진다. 다음 줄에는 m(1 ≤ m ≤ 1,000)이 주어지고, 그 다음 줄에 m개의 정수로 B[1], …, B[m]이 주어진다. 각각의 배열 원소는 절댓값이 1,000,000을 넘지 않는 정수이다.</p>
<hr>
<h3 id="✍-출력">✍ <strong>출력</strong></h3>
<p>첫째 줄에 답을 출력한다. 가능한 경우가 한 가지도 없을 경우에는 0을 출력한다.</p>
<hr>
<h3 id="✅-코드">✅ <strong>코드</strong></h3>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;iostream&gt;
#include &lt;algorithm&gt;
using namespace std;

int t, n, m;
int a[1001], b[1001];
long long sa[500501], sb[500501]; //부분합 배열

int main() {

    ios::sync_with_stdio(false);
    cin.tie(NULL); cout.tie(NULL);

    int temp;
    cin &gt;&gt; t;

    cin &gt;&gt; n;
    for (int i = 0; i &lt; n; i++) {
        cin &gt;&gt; temp;
        a[i] = temp;
    }

    //배열 a의 부분합 배열 sa 계산
    int aidx = 0; int sum_a = 0;
    for (int i = 0; i &lt; n; i++) {
        sa[aidx++] = a[i];
        sum_a = a[i];
        for (int j = i+1; j &lt; n; j++) {
            sum_a += a[j];
            sa[aidx++] = sum_a;
        }
    }

    cin &gt;&gt; m;
    for (int i = 0; i &lt; m; i++) {
        cin &gt;&gt; temp;
        b[i] = temp;
    }

    //배열 b의 부분합 배열 sb 계산
    int bidx = 0; int sum_b = 0;
    for (int i = 0; i &lt; m; i++) {
        sb[bidx++] = b[i];
        sum_b = b[i];
        for (int j = i+1; j &lt; m; j++) {
            sum_b += b[j];
            sb[bidx++] = sum_b;
        }
    }

    //부분합 배열 오름차순 정리
    sort(sa, sa+aidx);
    sort(sb, sb+bidx);

    int al = 0; int br = bidx -1;
    long long sum = 0;
    long long result = 0;

    while (al &lt; aidx &amp;&amp; br &gt;= 0) {
        sum = sa[al] + sb[br];

        if (sum &lt; t) al++;
        else if (sum &gt; t) br--;
        else if (sum == t) {
            long long same_a = 1;
            long long same_b = 1;
            for (int i = al + 1; i &lt; aidx; i++) {
                if (sa[i] == sa[al]) same_a++;
                else break;
            }
            for (int i = br - 1; i &gt;= 0; i--) {
                if (sb[i] == sb[br]) same_b++;
                else break;
            }
            result += same_a * same_b;
            al += same_a;
            br -= same_b;
        }
    }

    cout &lt;&lt; result;

    return 0;
}</code></pre>
<h3 id="✅-코드풀이">✅ <strong>코드풀이</strong></h3>
<p>이 문제에서의 핵심은 *<em>시간복잡도⏰ *</em>를 고려하는 것이다.</p>
<p>가장 무식한(?) 방법으로는 배열의 원소를 1개, 2개 ... t개 사용하는 경우의 수를 각각 구해서 더해서 구할 수 있다. 그런데 T의 범위의 최대값이 <strong>10억</strong>이다. 각 경우의 수가 10개씩만 나와도 100억번 이상의 연산을 해야된다는 건데 C++ 기준 1초에 1억번의 연산을 진행한다고 가정하면 제한 시간인 2초 안에 실행하는 것은 불가능하다.</p>
<h4 id="🤔-어떻게-하면-시간-복잡도를-줄일-수-있을까">🤔 어떻게 하면 시간 복잡도를 줄일 수 있을까?</h4>
<p>우리가 구하고자 하는 것이 <strong>&quot;부분합의 합&quot;</strong> 임을 떠올려보자. 배열 A, B에서 나올 수 있는 부분합들의 합을 T로 만드는 경우의 수를 구하는 것이므로 <strong>각 배열의 부분합의 값을 저장하는 배열</strong>(sa, sb)을 선언하고 그 값을 채워볼 수 있다.</p>
<p><strong>🚨 이 때 주의할 점! 
**
입력으로 받는 배열 원소의 범위는 -10억 이상 10억 이하이다. 그렇다면 그 원소들의 합으로 계산되는 부분합 배열의 원소는 int 범위를 초과할 수 있다. 따라서 부분합 배열의 원소 자료형은 반드시 **long long</strong>으로 선언해야 한다.</p>
<p>(int형은 대략 절댓값 21억 이하의 값들까지 표현가능 함을 외워두자)</p>
<p>이 문제를 풀기위한 중요 아이디어 중 하나인 부분합 배열의 선언까지 생각해냈다면 거의 다 온 것이다! 이제는 이 부분합 배열을 이용하여 결론적으로 그 합을 T로 만드는 경우의 수를 구하면 된다.</p>
<h4 id="🤔-하나는-작은-쪽부터-하나는-큰-쪽부터">🤔 하나는 작은 쪽부터, 하나는 큰 쪽부터!</h4>
<p>a의 부분합 배열(sa)의 원소 하나와 b의 부분합 배열(sb)의 원소 하나를 더해 T를 만들면 되는 상황이다. 이 때 떠올릴 수 있는 아이디어는 <strong>sa 배열은 원소가 작은 것부터, sb 배열은 원소가 큰 것</strong>부터 탐색하는 것이다.</p>
<p>이 경우에 만약 두 배열에서 고른 원소의 합이 <strong>T보다 크다면</strong> 값을 줄여야 하므로 내림차순으로 탐색하는 <strong>sb 배열의 index를 줄이면 되고</strong>, 반대로 <strong>작다면</strong> 오름차순으로 탐색하는 <strong>sa 배열의 index를 키우면 된다</strong>!</p>
<p>이것을 구현하기 위해 두 부분합 배열을 sort를 이용해 오름차순 정리를 해주고, 탐색할 index의 값을 al = 0 (가장 작은 원소 위치), br = bidx - 1 (가장 큰 원소 위치)로 초기화시켜준다.</p>
<pre><code class="language-cpp">while (al &lt; aidx &amp;&amp; br &gt;= 0) {
        sum = sa[al] + sb[br];

        if (sum &lt; t) al++;
        else if (sum &gt; t) br--;
        else if (sum == t) {
            long long same_a = 1;
            long long same_b = 1;
            for (int i = al + 1; i &lt; aidx; i++) {
                if (sa[i] == sa[al]) same_a++;
                else break;
            }
            for (int i = br - 1; i &gt;= 0; i--) {
                if (sb[i] == sb[br]) same_b++;
                else break;
            }
            result += same_a * same_b;
            al += same_a;
            br -= same_b;
        }
    }</code></pre>
<p>그리고 while문을 돌며 경우의 수를 세주면 되는데 이 내부에서 시간을 줄일 수 있는 한 가지 방법이 더 있다. sum = t인 경우, 즉 원소의 합이 우리가 원하는 값으로 계산된 경우, 만약 앞으로 탐색을 진행할 방향에 동일한 원소가 있다면 그 개수를 세어 한 번에 경우의 수를 계산하는 것이다. </p>
<p>글로 설명하자니 이해가 어려울 것 같아 그림으로 표현해보았다! </p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/9e0aa7bc-5458-4694-a21c-b96e5b9f6fe9/image.jpeg" alt=""></p>
<p>위 그림에 해당하는 부분이 else if (sum == t) 블록 안의 코드이다.</p>
<h4 id="🔐-최종-정리를-해보자">🔐 최종 정리를 해보자!</h4>
<ol>
<li>배열 a, b의 부분합 배열 sa, sb 계산</li>
<li>sa는 오름차순, sb는 내림차순으로 탐색하며 경우의 수 계산</li>
<li>합이 T가 되는 경우의 수를 찾은 경우, 그 원소와 값이 동일한 원소가 있는지 확인</li>
</ol>
<hr>
<p>❗ 떠올려야 하는 아이디어가 두 개나 있어서 풀기 어려웠던 문제였다. 다양한 문제를 풀며 적절한 아이디어를 떠올릴 수 있는 연습을 해야되겠다는 생각이 든다! 그럴려면 기본적으로 알고리즘이나 유용한 자료구조에 대해 잘 이해하고 많이 연습해봐야 할 것 같다 😵‍💫</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 25206번 너의 평점은 (C++)]]></title>
            <link>https://velog.io/@yeonjiyooo_/%EB%B0%B1%EC%A4%80-25206%EB%B2%88-%EB%84%88%EC%9D%98-%ED%8F%89%EC%A0%90%EC%9D%80-C</link>
            <guid>https://velog.io/@yeonjiyooo_/%EB%B0%B1%EC%A4%80-25206%EB%B2%88-%EB%84%88%EC%9D%98-%ED%8F%89%EC%A0%90%EC%9D%80-C</guid>
            <pubDate>Sat, 17 May 2025 12:57:36 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/25206">백준 5800번 성적 통계</a></p>
<h3 id="✍-문제">✍ <strong>문제</strong></h3>
<p>인하대학교 컴퓨터공학과를 졸업하기 위해서는, 전공평점이 3.3 이상이거나 졸업고사를 통과해야 한다. 그런데 아뿔싸, 치훈이는 깜빡하고 졸업고사를 응시하지 않았다는 사실을 깨달았다!</p>
<p>치훈이의 전공평점을 계산해주는 프로그램을 작성해보자.
전공평점은 전공과목별 (학점 × 과목평점)의 합을 학점의 총합으로 나눈 값이다.</p>
<p>인하대학교 컴퓨터공학과의 등급에 따른 과목평점은 다음 표와 같다.</p>
<table>
<thead>
<tr>
<th>학점</th>
<th>과목평점</th>
</tr>
</thead>
<tbody><tr>
<td>A+</td>
<td>4.5</td>
</tr>
<tr>
<td>A0</td>
<td>4.0</td>
</tr>
<tr>
<td>B+</td>
<td>3.5</td>
</tr>
<tr>
<td>B0</td>
<td>3.0</td>
</tr>
<tr>
<td>C+</td>
<td>2.5</td>
</tr>
<tr>
<td>C0</td>
<td>2.0</td>
</tr>
<tr>
<td>D+</td>
<td>1.5</td>
</tr>
<tr>
<td>D0</td>
<td>1.0</td>
</tr>
<tr>
<td>F</td>
<td>0.0</td>
</tr>
</tbody></table>
<p>P/F 과목의 경우 등급이 P또는 F로 표시되는데, 등급이 P인 과목은 계산에서 제외해야 한다. 과연 치훈이는 무사히 졸업할 수 있을까?</p>
<hr>
<h3 id="✍-입력">✍ <strong>입력</strong></h3>
<p>20줄에 걸쳐 치훈이가 수강한 전공과목의 과목명, 학점, 등급이 공백으로 구분되어 주어진다.</p>
<hr>
<h3 id="✍-출력">✍ <strong>출력</strong></h3>
<p>치훈이의 전공평점을 출력한다.</p>
<p>정답과의 절대오차 또는 상대오차가 10^-4 이하이면 정답으로 인정한다.</p>
<hr>
<h3 id="✅-코드">✅ <strong>코드</strong></h3>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;iostream&gt;
#include &lt;string&gt;

using namespace std;

double grade2num(string grade) {
    double num;
    if (grade == &quot;A+&quot;) num = 4.5;
    else if (grade == &quot;A0&quot;) num = 4.0;
    else if (grade == &quot;B+&quot;) num = 3.5;
    else if (grade == &quot;B0&quot;) num = 3.0;
    else if (grade == &quot;C+&quot;) num = 2.5;
    else if (grade == &quot;C0&quot;) num = 2.0;
    else if (grade == &quot;D+&quot;) num = 1.5;
    else if (grade == &quot;D0&quot;) num = 1.0;
    else num = 0;
    return num;
}

int main() {

    string subject, grade;
    double credit;

    double sum = 0;
    double total_credit = 0;
    //학점 = sum / total_credit

    for (int i=1; i&lt;=20; i++) {
        cin &gt;&gt; subject &gt;&gt; credit &gt;&gt; grade;
        if (grade == &quot;P&quot;) continue; //학점이 P인 경우는 계산에 포함 X
        else {
            total_credit += credit;
            double score = grade2num(grade);
            sum += credit * score;
        }
    }

    cout&lt;&lt;fixed;
    cout.precision(6);
    cout &lt;&lt; sum / total_credit;

    return 0;

}</code></pre>
<h3 id="✅-코드풀이">✅ <strong>코드풀이</strong></h3>
<p>풀이 방법 자체는 별 게 없다. 20줄짜리 과목명, 학점, 등급 input을 잘 처리해서 전체 학점과 전체 평점점수를 구해 나눠주기만 하면 된다. 몇 가지 주의해야할 점은 &quot;등급이 P인 과목은 계산에서 제외한다&quot;, &quot;오차범위가 10^-4이내여야 한다&quot; 정도 있겠다.</p>
<h4 id="✔️-c-출력-소수점-자리수-설정">✔️ C++ 출력 소수점 자리수 설정</h4>
<p>C++에서는 별도의 설정이 없다면 소수를 출력하는 경우 소수점 6번째 자리수에서 반올림하여 소수점 5번째 자리까지 출력한다. </p>
<pre><code class="language-cpp">double a = 3.123456789;
cout &lt;&lt; a;</code></pre>
<p>따라서 문제 조건에 따라 출력해야 하는 소수점 자리수를 설정해야 한다면 <strong>fixed와 precision</strong>을 사용해야한다.</p>
<pre><code class="language-cpp">cout &lt;&lt; fixed;
cout.precision(3); //소수점 3번째 자리까지 출력 (소수점 4번째 자리에서 반올림)</code></pre>
<p>이 때 주의해야할 점은 precision과 함께 <code>cout &lt;&lt; fixed;</code> 를 꼭 작성해줘야 한다.</p>
<p>만약 fixed를 사용하지 않는 경우는 정수 부분 자릿수까지 포함하여 해당 인자만큼의 숫자 개수를 출력하게 된다. precision(n)은 전체 출력값의 숫자 개수를 n개로 설정한다고 보면 된다.</p>
<pre><code class="language-cpp">//fixed와 precision을 함께 사용하는 경우
cout &lt;&lt; fixed;
cout.precision(3);
cout &lt;&lt; 3.123456789;
output: 3.123

//precision만 사용하는 경우
cout.precision(3);
cout &lt;&lt; 30.123456789;
output: 30.1
</code></pre>
<h4 id="✔️-c-타입-변환-번외">✔️ C++ 타입 변환 (번외)</h4>
<p>이 문제의 풀이에서는 사용되지 않았지만 덧붙이는 이유는...</p>
<p>갑자기 처음 구현당시 소수점까지 string으로 받아서 처리하느라 진땀을 뺐기 때문이다. (지금 생각하면 어이 X) 일단 처음 내가 생각했던 방법은 이러하다.</p>
<ol>
<li>입력으로 들어오는 credit은 <code>1.0, 2.0, 3.0, 4.0</code> 이므로 가장 앞 문자만 보면 된다.</li>
<li>string 타입의 변수 credit에 해당 입력을 저장한다.</li>
<li>credit[0]을 double로 타입 변환을 하여 저장한다.</li>
</ol>
<p>다시 봐도 황당하지만; 어쨌든 이렇게 진행하면서 문제가 되었던 부분은 3번 단계이다.</p>
<p>int 🔄 double 형변환이 아닌 <code>char 🔄 double</code> 이기 때문에 아무리 credit[0] 문자 값이 3이더라도 변환값은 3이 아니다. 그것의 ** 아스키코드 ** 값인 51이 반환값이 된다.</p>
<p>따라서 내가 원하는 3으로의 형변환을 하려면 다음과 같은 로직이 추가적으로 필요하다.</p>
<pre><code class="language-cpp">string credit = &quot;3.0&quot;; //credit[0] = 3
double credit_num = double(credit[0]) - double(&#39;0&#39;); //숫자0의 아스키코드 빼기
cout &lt;&lt; credit_num; //output: 3
</code></pre>
<p>그런데 너무 오랜만에 사용해서 그런지 이 사실을 망각한채 <strong>외안되</strong>만 500번 생각했다.</p>
<p>뜬금없이 이상하게 처리하려고 하다가 까먹었던 내용 다시 알 수 있었으니까 좋긴한데 10번 다시 생각해도 왜 소수점을 굳이굳이굳이 string으로 받으려고 했을까는 의문이다...^^* </p>
<p>(실버니까 풀이 보기도 쫀심 상해서 알아차리는데 상당한 시간을 썼다 ♨️)</p>
<br>


<p>마지막으로 한 가지 더 짚고 넘어가자면, 사실 <code>int(a), double(b)</code> 와 같은 형변환 방식은 C++ 에서는 권장되지 않는 방법이고, <strong><code>static_cast&lt;Type&gt;(a), dynamic_cast&lt;Type&gt;(a)</code></strong> 를 쓰는 것이 좋다.</p>
<p>전자의 경우 잘못된 형변환일 때도 컴파일 타임에 잡아내지 못할 수 있기 때문이다. </p>
<p>하지만 후자의 경우 <strong>타입 간의 호환성을 컴파일 시에 확인</strong>하므로 잘못된 형변환의 경우를 컴파일 시에 막을 수 있다! 따라서 웬만하면 후자를 사용하는 것으로 습관을 들여야겠다.</p>
<hr>
<p>❗ 이제 실버쯤은 눈 감고도 풀 수 있겠다고(과장법) 방심하고 있었는데 간만에 좀 헤매는 문제가 나와서 정리해보았다. 역시 배움엔 끝이 없고 방심하는 순간 뒤처지는 건 금방이겠구나... 생각이 들었다 ◞‸◟💧 그래도 이 문제 덕분에 코테 준비도 꾸준히 해야겠다고 다시금 다짐하게 되는 좋은 계기가 되었다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[C언어 정리]]></title>
            <link>https://velog.io/@yeonjiyooo_/C%EC%96%B8%EC%96%B4-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@yeonjiyooo_/C%EC%96%B8%EC%96%B4-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 24 Apr 2025 06:51:26 GMT</pubDate>
            <description><![CDATA[<p>%d : int 타입 정수형
%c : char 타입 문자형
%s : char* 타입 문자열
%lf : double 타입 실수
%f : float 타입 실수</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[kakao tech] 주니어 FE 개발자의 색상 추출 라이브러리 개발기 | 정리글]]></title>
            <link>https://velog.io/@yeonjiyooo_/%EC%A3%BC%EB%8B%88%EC%96%B4-FE-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%83%89%EC%83%81-%EC%B6%94%EC%B6%9C-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EA%B0%9C%EB%B0%9C%EA%B8%B0</link>
            <guid>https://velog.io/@yeonjiyooo_/%EC%A3%BC%EB%8B%88%EC%96%B4-FE-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%83%89%EC%83%81-%EC%B6%94%EC%B6%9C-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EA%B0%9C%EB%B0%9C%EA%B8%B0</guid>
            <pubDate>Mon, 10 Mar 2025 05:58:39 GMT</pubDate>
            <description><![CDATA[<p>⬇️ <strong>아래 tech blog를 읽고 흥미로워 정리해본 글 입니다</strong> ⬇️</p>
<p>원문: <a href="https://tech.kakao.com/posts/627">https://tech.kakao.com/posts/627</a></p>
<hr>
<h2 id="💡-광고-템플릿-프로젝트">💡 광고 템플릿 프로젝트</h2>
<ul>
<li><p>광고주가 광고 이미지와 문구만 입력하면 카카오에서 미리 정의한 다양한 템플릿으로 조합하여 광고를 노출</p>
</li>
<li><p>광고주는 완성된 형태의 광고를 등록할 필요 X, 광고에 필요한 최소 정보만 제공</p>
<p>  → 광고주 부담 ⬇</p>
</li>
<li><p>카카오에서 검수한 디자인에 따라 광고지면에 알맞은 크기와 형태로 광고 노출</p>
<p>  → 안정적인 광고 노출 보장</p>
</li>
</ul>
<br>

<h2 id="🧐-문제-정의">🧐 문제 정의</h2>
<h3 id="1-광고-색상-추출-작업의-목표">1. 광고 색상 추출 작업의 목표</h3>
<p>광고 이미지와 어울리는 색상을 추출하여 광고 템플릿에 적용</p>
<p>→ 여백 최소화, 전체 광고 일관성</p>
<p>그렇다면 “광고 이미지와 어울리는 색상” 이라는 것을 누가 어떻게 정의할 것인가?</p>
<p>광고주가 제출하는 경우</p>
<p>→ 아이디어 구현은 쉬워짐 BUT 색상이 채워진 광고에 대한 효과 입증이 안된 상태에서 요구하기엔 무리라고 판단</p>
<p>→ 광고주에게 별도의 요청 X</p>
<p>광고를 그려주는 템플릿에서 광고 이미지를 기반으로 적절한 색상을 추출하는 것을 구현</p>
<h3 id="2-대표-색상-정의">2. 대표 색상 정의</h3>
<p><strong>방법 1) 이미지 전체의 수치적 평균</strong></p>
<p>전체 이미지가 비슷한 톤이라면 OK</p>
<p>BUT 여러 색상을 이루어진 이미지의 경우 엉뚱한 색상이 도출</p>
<p><strong>방법 2) 전체 픽셀 RGB 값 중 최빈값</strong></p>
<p>단색 위주의 이미지는 OK
BUT 사람 눈엔 비슷해보이지만 실제 RGB 값에 미세한 차이가 있는 경우는 전혀 다른 결과 도출</p>
<p><strong>⭐️결론⭐️</strong> 사람이 보기엔 비슷한 색상이지만 RGB 값에 미세한 차이가 있는 색상들을 <strong>“하나의 그룹”</strong>으로 묶은 후, </p>
<p><strong>“가장 많은 픽셀”</strong>이 포함된 그룹을 선택하자!</p>
<h3 id="⇒-k-means-clustering">⇒ K-means Clustering</h3>
<br>

<h2 id="🥳-문제-해결">🥳 문제 해결</h2>
<p>색상은 RGB / HSV 3차원 값으로 표현 가능</p>
<p>3차원 색상 정보 간의 거리를 사용하여 거리가 짧은 픽셀끼리 <strong>군집화</strong></p>
<ul>
<li>몇 개의 그룹으로 묶을 것인가?</li>
<li>데이터의 유사도를 어떻게 정의할 것인가?</li>
</ul>
<img src = "https://velog.velcdn.com/images/yeonjiyooo_/post/180ee9d9-6ca3-44d9-aa8f-5edf97f418c0/image.png" width=400>

<p>[이미지 출처] (<a href="https://tech.kakao.com/posts/627">https://tech.kakao.com/posts/627</a>)</p>
<h3 id="k-means-clustering">K-means Clustering</h3>
<p>⬇️ K-means Clustering에 대해서는 별도의 페이지를 통해 정리하겠습니다!</p>
<p><a href="https://www.notion.so/K-means-Clustering-b1df06dfe3d04cdb8619f98684f323bc?pvs=21">K-means Clustering</a></p>
<br>

<h2 id="👍-발전-단계">👍 발전 단계</h2>
<h3 id="1-스토리북을-이용한-웹-테스터">1. 스토리북을 이용한 웹 테스터</h3>
<p><a href="https://storybook.js.org/">https://storybook.js.org/</a></p>
<ul>
<li>패널에 원하는 값을 넣어 테스트 할 수 있는 기능 제공</li>
<li>디자이너와 협업할 일이 있을 때 활용해보면 좋을 것 같음👌</li>
</ul>
<h3 id="2-사내-라이브러리로-개발">2. 사내 라이브러리로 개발</h3>
<p>라이브러리 형태에 적절한 input, ouput 정의</p>
<p>→ 기존 광고 템플릿 내에 적요했던 logic을 라이브러리 형태로 개발!</p>
<hr>
<h3 id="💻-글을-마무리-하며">💻 글을 마무리 하며!</h3>
<p>문제의 정의 → 해결 → 발전의 단계가 명확하게 드러나 있어서 어떤 것을 <strong>개발</strong>하는 행위에서의 가장 핵심적인 요소들이 잘 전달되었다. 더 나은 서비스를 제공하려는 과정에서 문제를 정의하고, 그 문제를 해결하기 위해 고려해야 하는 사항들을 통해 적절한 결과를 도출해내었다는 것이 인상깊었다. 또 마지막에 사내 라이브러리로 개발하여 해당 업무를 담당하는 팀에서도 효율적으로 해당 기술을 사용할 수 있게 했다는 것을 보며 조직에 속해있을 때 어떤 식으로 개발 업무가 진행되는지 정말 야아악간 엿볼 수 있었다…! 또 스토리북이라는 새로운 도구를 알게 되었는데 언젠가 써먹어보고 싶다! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS/Error] unrecognized selector sent to class]]></title>
            <link>https://velog.io/@yeonjiyooo_/iOSError-unrecognized-selector-sent-to-class</link>
            <guid>https://velog.io/@yeonjiyooo_/iOSError-unrecognized-selector-sent-to-class</guid>
            <pubDate>Tue, 25 Feb 2025 02:03:18 GMT</pubDate>
            <description><![CDATA[<h2 id="🚫-오류-내용">🚫 오류 내용</h2>
<pre><code class="language-bash">Exception    NSException *    &quot;+[TapAndAlarm.SubjectTimePicker timeButtonTapped]: unrecognized selector sent to class 0x102861c38&quot;    0x0000600000c84630</code></pre>
<p><em><strong>unrecognized selector sent to class ~</strong></em></p>
<p>이 오류는 &#39;timeButtonTapped&#39; 라는 메서드를 호출하려고 했는데, 해당 메서드를 &#39;TapAndAlarm.SubjectTimePicker&#39;라는 클래스에서 찾을 수 없다는 뜻이다.</p>
<p>오류가 발생한 코드를 통해 원인을 알아보자!</p>
<pre><code class="language-swift">
class SubjectTimePicker: UIView  {

    private let timeButton: UIButton = {
        let button = UIButton()
        button.setTitle(&quot;35분&quot;, for: .normal)
        button.setTitleColor(UIColor.black, for: .normal)
        button.titleLabel?.font = UIFont.systemFont(ofSize: 24, weight: .semibold)
        button.layer.cornerRadius = 8
        //오류 발생 구간
        button.addTarget(SubjectTimePicker.self, action: #selector(timeButtonTapped), for: .touchUpInside)
        //오류 발생 구간
        return button
    }()

     @objc private func timeButtonTapped() {
         pickerView.isHidden.toggle()
    }

}</code></pre>
<h2 id="🤔-오류-원인">🤔 오류 원인</h2>
<p>swift에서 addTarget의 첫 번째 인자는 <strong>인스턴스</strong>가 되어야 한다. 해당 인스턴스에서 #selector(_) 에 명시된 메소드를 찾고 실행시킨다.</p>
<p>하지만 오류 코드에서 addTarget의 첫 번째 인자는 SubjectTimePicker.self로 <strong>클래스</strong>이다. 실행시키고자 하는 timeButtonTapped는 인스턴스 메소드로 클래스에서 직접 호출할 수 없다. 따라서 메소드를 찾지 못하고 unrecognized selector 오류가 발생하는 것이다.</p>
<h2 id="🛠️-오류-해결">🛠️ 오류 해결</h2>
<p>addTarget의 인자 설정에 문제가 있었던 것이므로 첫 번째 인자를 인스턴스를 뜻하는 <strong>self</strong>로 고쳐주면 된다!</p>
<pre><code class="language-swift">button.addTarget(self, action: #selector(timeButtonTapped), for: .touchUpInside)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git] fatal: Authentication failed | github token 발급]]></title>
            <link>https://velog.io/@yeonjiyooo_/Git-fatal-Authentication-failed-github-token-%EB%B0%9C%EA%B8%89</link>
            <guid>https://velog.io/@yeonjiyooo_/Git-fatal-Authentication-failed-github-token-%EB%B0%9C%EA%B8%89</guid>
            <pubDate>Tue, 18 Feb 2025 01:41:36 GMT</pubDate>
            <description><![CDATA[<p>얼마 전 깃허브에서 날아온 메일 한 통...</p>
<img src =" https://velog.velcdn.com/images/yeonjiyooo_/post/7c331397-3eea-4935-b82f-589ab4e77f81/image.jpeg">

<p>내용인 즉슨, access token 곧 만료 되니까 처리하쇼~ 라는 건데 난 대충 보고 나중에 하지 뭐 하고 무시했다. </p>
<p><strong>그.런.데</strong></p>
<p>아무 생각 없이 github에 push 하려고 하니까 바로 <strong>fatal: Authentication failed</strong>가 떠버리는 것이다 ㅜ.ㅜ 이는 기존 token의 유효기간이 만료되어 더 이상 해당 token으로는 인증을 할 수 없다는 뜻이다.</p>
<p>알고보니 곧 만료된다는게 3시간 안에 만료된다는 것이었다...^^* 사실 이 전에도 몇 번 메일 날라왔던 것 같은데 무시한 제 잘못 입니다. </p>
<p>이럴 땐 당황하지 않고 personal access token을 재발급 받으면 되는데 친절하신 깃허브님께선 메일에 <strong>기존 token과 동일한 권한을 가지는 token을 바로 재생성 할 수 있게 해주는 링크</strong>도 함께 보내주신다. </p>
<br>

<p>보내준 링크로 들어가보면 아래와 같은 화면으로 이동된다.</p>
<img src = "https://velog.velcdn.com/images/yeonjiyooo_/post/145a6645-a4af-4fb6-a415-f2d888e79661/image.png">

<p><strong>regenrate token</strong> 버튼을 누르면 token 생성 화면으로 넘어가는데, 이 때 모든 설정은 기존 token과 동일하게 되어있어서 따로 변경할 것 없이 <strong>update token</strong>을 누르면 끝난다!</p>
<br>
혹시 token 발급이 처음이거나, 나처럼  메일을 제대로 안읽고 냅다 새로운 token을 받는 사람들은(...^^*) 아래 과정대로 하면 된다!

<blockquote>
<p>깃허브 메인 페이지 ➡️ 프로필 ➡️ Settings ➡️ Developer Settings ➡️ Personal Access Token ➡️ Tokens(classic)</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/3ffe7cda-e1ad-40e9-9ed7-2df8d0ec3a59/image.jpeg" alt=""></p>
<p>이후 우측 상단에 generate new token 버튼을 누르면 token에 대한 몇 가지 설정을 할 수 있다. token의 이름은 원하는 형식으로 작성해주면 되고, Expiration도 발급 목적에 따라 적절한 기간을 설정하면 된다. </p>
<p>Expiration의 경우 custom도 가능하고, 아예 없애버릴 수도 있는데 <strong>보안상의 이유로 유효기간을 없애는 것은 추천하지 않는다</strong>고 한다! 물론 개인적인 프로젝트 정도야 문제 없겠지만 그래도 나는 90일로 설정하는 편이다.</p>
<p>Select Scope은 해당 token의 접근 범위에 대한 설정인데 대표적으로 repo가 저장소와 관련된 권한이다! 이 역시도 목적에 맞게 설정하면 되지만 귀찮아서 다 권한을 허용하는 분들도 계신 것 같다. 개취!</p>
<br>

<p>위 절차를 통해 token을 발급받으면 생성된 key를 알려주는 창으로 넘어가는데, 이는 💥** 새로고침하거나 나가면 다시 확인할 수 없어서 반드시 바로 복사해야 한다!**💥</p>
<p>마지막으로 터미널로 돌아오면 Username과 Password를 입력하라고 할 텐데, username에는 본인 깃허브 아이디를 입력하고 <strong>password 부분에 방금 복사한 key를 붙여넣기</strong> 해주면 끝!!</p>
<p><align = "center>
<img src = " https://velog.velcdn.com/images/yeonjiyooo_/post/341530c1-3775-4f3a-9351-760b573f1c3b/image.jpeg" width = 300>
 </p>



<hr>
<p>계속 깃헙 token 발급 받으면서도 매번 아 어떻게 하더라...(저능) 하는 나를 발견하고 정리해본 token 발급 과정! 다음 번엔 제발 까먹지 말고 스무스하게 발급받자 <del>.</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 3020번 개똥벌레 (C++)]]></title>
            <link>https://velog.io/@yeonjiyooo_/%EB%B0%B1%EC%A4%80-3020%EB%B2%88-%EA%B0%9C%EB%98%A5%EB%B2%8C%EB%A0%88-C</link>
            <guid>https://velog.io/@yeonjiyooo_/%EB%B0%B1%EC%A4%80-3020%EB%B2%88-%EA%B0%9C%EB%98%A5%EB%B2%8C%EB%A0%88-C</guid>
            <pubDate>Sun, 09 Feb 2025 14:22:08 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/3020">백준 3020번 개똥벌레</a></p>
<h3 id="✍-문제">✍ 문제</h3>
<p>개똥벌레 한 마리가 장애물(석순과 종유석)로 가득찬 동굴에 들어갔다. 동굴의 길이는 N미터이고, 높이는 H미터이다. (N은 짝수) 첫 번째 장애물은 항상 석순이고, 그 다음에는 종유석과 석순이 번갈아가면서 등장한다.</p>
<p>(중략)</p>
<p>이 개똥벌레는 장애물을 피하지 않는다. 자신이 지나갈 구간을 정한 다음 일직선으로 지나가면서 만나는 모든 장애물을 파괴한다.</p>
<p>(중략)</p>
<p>동굴의 크기와 높이, 모든 장애물의 크기가 주어진다. 이때, 개똥벌레가 파괴해야하는 장애물의 최솟값과 그러한 구간이 총 몇 개 있는지 구하는 프로그램을 작성하시오.</p>
<hr>
<h3 id="✍-입력">✍ 입력</h3>
<p>첫째 줄에 N과 H가 주어진다. N은 항상 짝수이다. (2 ≤ N ≤ 200,000, 2 ≤ H ≤ 500,000)</p>
<p>다음 N개 줄에는 장애물의 크기가 순서대로 주어진다. 장애물의 크기는 H보다 작은 양수이다.</p>
<hr>
<h3 id="✍-출력">✍ 출력</h3>
<p>첫째 줄에 개똥벌레가 파괴해야 하는 장애물의 최솟값과 그러한 구간의 수를 공백으로 구분하여 출력한다.</p>
<hr>
<h3 id="✅-코드">✅ 코드</h3>
<pre><code class="language-cpp">#include &lt;stdio.h&gt;
#include &lt;iostream&gt;
#include &lt;algorithm&gt;

using namespace std;

long long tree[2100000];
int idx; //첫 번째 leaf node의 index값

//구간 i-j에 +1
void edit(int i, int j) {
    int l = idx + i - 1;
    int r = idx + j - 1;

    while (l &lt;= r) {
        if (l % 2 == 1) tree[l++] += 1;
        if (r % 2 == 0) tree[r--] += 1;

        l /= 2;
        r /= 2;
    }
}

//i 위치의 장애물 개수
int get(int i) {
    int result = 0;
    int nidx = idx + i - 1;

    while (nidx &gt; 0) {
        result += tree[nidx];
        nidx /= 2;
    }

    return result;
}

int main() {

    ios::sync_with_stdio(false);
    cin.tie(NULL);

    int n, h, o; //n: 길이, h:높이, o:장애물
    cin &gt;&gt; n &gt;&gt; h;

    for (idx = 1; idx &lt; h; idx *= 2);

    for (int i = 1; i &lt;= n; i++) {
        cin &gt;&gt; o;
        if (i % 2 == 1) edit(1, o);
        else edit(h - o + 1, h);
    }

    //장애물의 최솟값, 그 구간의 수
    int min = 200001;
    int min_num = 1;

    for (int i = 1; i &lt;= h; i++) {
        int io = get(i);
        if (io &lt; min) {
            min_num = 1; //...^^*
            min = io;
        }
        else if (io == min) min_num++;
    }

    cout &lt;&lt; min &lt;&lt; &quot; &quot; &lt;&lt; min_num;

    return 0;
}</code></pre>
<br>

<h3 id="✅-코드풀이">✅ 코드풀이</h3>
<p>구간갱신, 단일쿼리의 <strong>index tree</strong>를 이용한 구현이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[PS] 순열과 조합]]></title>
            <link>https://velog.io/@yeonjiyooo_/PS-%EC%88%9C%EC%97%B4%EA%B3%BC-%EC%A1%B0%ED%95%A9</link>
            <guid>https://velog.io/@yeonjiyooo_/PS-%EC%88%9C%EC%97%B4%EA%B3%BC-%EC%A1%B0%ED%95%A9</guid>
            <pubDate>Sun, 09 Feb 2025 14:16:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>👋 순열과 조합의 구현을 시간복잡도의 관점에서 생각해보자!</strong></p>
</blockquote>
<hr>
<h2 id="✏️-순열-permutation">✏️ 순열 permutation</h2>
<p><strong>nPk</strong> : n개의 수 중에서 k개를 골라 <strong>순서를 고려</strong>하여 나열하는 경우의 수</p>
<p>순열과 조합을 구분하는 기준은 &quot;<strong>순서를 고려할 것인가</strong>&quot; 이다.</p>
<p>예를 들어, 회장-부회장-차장 각 1명씩 총 3명을 뽑는 경우와 대표 3명 뽑는 경우가  각각 순열과 조합의 예시가 될 수 있다. </p>
<h3 id="backtracking을-이용한-순열-구현-on">Backtracking을 이용한 순열 구현: O(N!)</h3>
<p><a href="https://www.acmicpc.net/problem/5568">백준 5568 카드 놓기</a></p>
<p>가장 간단한 순열의 구현 방식으로는 <strong>backtracking</strong>이 있다. 길이 5 짜리의 모든 순열을 만든다고 할 때 현재 자리에 아직 사용되지 않은 숫자 중 가장 작은 것부터 넣은 후, 남은 자리에 대해 permutation 함수를 재귀 호출한다.</p>
<p>이 코드에서 주의해야 할 점은 특정 값에 대해 방문 처리를 하고 해당 값을 포함한 모든 순열을 만든 이후에는 다시 그 값의 <strong>방문 처리를 해제</strong> 해야된다는 점이다!</p>
<pre><code class="language-cpp">#include &lt;stdio.h&gt;
#include &lt;iostream&gt;
#include &lt;set&gt;
#include &lt;cmath&gt;

using namespace std;

string input[11];
bool visited[11] = {false, };
set&lt;string&gt; p;

int n, k;
int cnt = 0;

void permutation(int length, string num) {

    if (length == k) {
        if (p.find(num) == p.end()) cnt++; //기존에 만들어지지 않은 정수인 경우
        //cout &lt;&lt; num &lt;&lt; &quot;생성\n&quot;;
        p.insert(num);
        return;
    }

    ⭐ ⭐ ⭐ ⭐ ⭐ ⭐ ⭐ ⭐ ⭐ ⭐ ⭐ ⭐
    for (int i=1; i&lt;=n; i++) {
        if (!visited[i]) {
            visited[i] = true;      //방문 처리
            permutation(length+1, num + input[i]);
            visited[i] = false;     //방문 처리 해제
        }
    }
    ⭐ ⭐ ⭐ ⭐ ⭐ ⭐ ⭐ ⭐ ⭐ ⭐ ⭐ ⭐

    return;
}

int main(){
    ios::sync_with_stdio();
    cin.tie(NULL);

    string m;
    cin &gt;&gt; n &gt;&gt;  k;

    for (int i=1; i&lt;=n; i++) {
        cin &gt;&gt; m;
        input[i] = m;
    }

    permutation(0, &quot;&quot;);
    cout &lt;&lt; cnt;

    return 0;
}
</code></pre>
<p>backtracking을 이용하는 경우의 시간 복잡도는 <strong>O(N!)</strong> 이다. 가능한 모든 경우를 다 탐색하기 때문에 N개의 수로 만들 수 있는 모든 순열의 개수인 N! 만큼의 연산이 필요하다.</p>
<p>따라서 문제에서 주어지는 N의 값이 큰 경우 시간초과 등의 이유로 사용이 부적절 할 수 있다. N이 20만 되더라도 20! = 2,432,902,008,176,640,000 이다. </p>
<h3 id="backtracking-제외-다른-방법">Backtracking 제외 다른 방법</h3>
<p><a href="https://www.acmicpc.net/problem/1722">백준 1722번 순열의 순서</a></p>
<p>위 문제가 대표적으로 backtracking을 이용한 구현이 불가능한 경우이다.</p>
<p>반복문을 통해 1! ~ 20!의 값을 계산하여 배열에 담아놓고 꺼내 쓰는 식으로 구현을 했는데, 어디서 틀린 건지 아직 해결이 안돼서 나중에...^^* 수정해놓겠습니다!!! 혹시 이 글을 보시는 분들 중 제 코드에서 오류를 발견해주시는 분이 계시다면 그 쪽 방향으로 절 한 번 올리겠습니다.</p>
<pre><code class="language-cpp">#include &lt;stdio.h&gt;
#include &lt;iostream&gt;

using namespace std;

int n, m, value;
long long k;

bool visited[21] = {false, };
int num[21];
int input[21];

long long factorial[21];

void permutation(long long order) {
    int now = 0;
    for (int i=1; i&lt;=n; i++) {
        for (int j =1; j&lt;=n; j++) {
            if (visited[j]) continue;

            if (now + factorial[n-i] &lt; order) {
                now += factorial[n-i];
            } else {
                num[i] = j;
                visited[j] = true;
                break;
            }
        }
    }
    for (int i=1; i&lt;=n; i++) cout &lt;&lt; num[i] &lt;&lt; &quot; &quot;;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(NULL);


    cin &gt;&gt; n &gt;&gt; m; //1부터 n까지의 자연수

    factorial[0] = 1;

    for (int i=1; i&lt;=n; i++) {
        factorial[i] = factorial[i-1] * i;
    }

    if (m == 1) { //k번째 순열 구하기
        cin &gt;&gt; k;
        permutation(k);
    }
    else if (m == 2) { //입력받은 순열의 순서 구하기

        for (int i=1; i&lt;=n; i++) {
            cin &gt;&gt; value;
            input[i] = value;
        }

        long long result = 1;

        for (int i=1; i&lt;=n;i++){
            for (int j=1; j&lt;input[i]; j++) {
                if (visited[j] == false) result += factorial[n-i];
            }
            visited[input[i]] = true;
        }
        cout &lt;&lt; result;
    }

    return 0;
}

</code></pre>
<hr>
<h2 id="✏️-조합-combination">✏️ 조합 Combination</h2>
<p>*<em>nCr *</em>: n개의 수 중에서 k개를 고르는 경우의 수</p>
<p>n개의 것들 중 k개를 고르는 경우의 수를 조합이라고 하고, 조합은 고르기만 할 뿐 순서를 고려하여 나열하지 않는다. </p>
<p>조합은 <strong>파스칼의 삼각형</strong>이라는 성질을 따르는데, 이를 공식으로 표현하면 다음과 같다.
<em>nCr = n-rCr-1 + n-1Cr</em></p>
<p>파스칼의 삼각형은 이항계수를 삼각형 모양으로 배열한 것으로 어떠한 자리의 값은 그 자리 윗 줄의 왼쪽 + 오른쪽 값으로 구할 수 있다는 성질이다. 밑에 삽입한 그림을 보면 이해가 쉬울 것이다! 
(이미지 출처: 위키피디아 &quot;파스칼의 삼각형&quot;)</p>
<p><img src="https://velog.velcdn.com/images/yeonjiyooo_/post/12cd6be4-c286-497d-8aab-854d8c142914/image.gif" alt="위키피디아 파스칼의 삼각형"></p>
<br>

<h3 id="ncr--n--n-rr-정의에-따른-구현">nCr = n! / (n-r)!r! 정의에 따른 구현</h3>
<p>추가 작성 예정입니다!!</p>
<p>to be continued...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] 사소한 주의사항 모음집 !!]]></title>
            <link>https://velog.io/@yeonjiyooo_/iOS-%EC%82%AC%EC%86%8C%ED%95%9C-%EC%A3%BC%EC%9D%98%EC%82%AC%ED%95%AD-%EB%AA%A8%EC%9D%8C%EC%A7%91</link>
            <guid>https://velog.io/@yeonjiyooo_/iOS-%EC%82%AC%EC%86%8C%ED%95%9C-%EC%A3%BC%EC%9D%98%EC%82%AC%ED%95%AD-%EB%AA%A8%EC%9D%8C%EC%A7%91</guid>
            <pubDate>Mon, 27 Jan 2025 10:48:02 GMT</pubDate>
            <description><![CDATA[<h3 id="🍎-iboutlet--ibaction-삭제">🍎 IBOutlet / IBAction 삭제</h3>
<p>코드 상에서만 없앤다고 되는 것이 아니다! 연결한 객체에서 우클릭 후 아래 사진에서 동그라미 친 부분을 눌러 완전히 삭제해야 오류가 발생하지 않는다.
<img src = "https://velog.velcdn.com/images/yeonjiyooo_/post/658c290b-fad8-487a-bf13-ae2e00163c66/image.png" width=400></p>
<h3 id="🍎-특정-모서리만-둥글게-만들기">🍎 특정 모서리만 둥글게 만들기</h3>
<pre><code class="language-swift">view.layer.cornerRadius = 8
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
</code></pre>
]]></description>
        </item>
    </channel>
</rss>