<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>DH Developement</title>
    <link>https://danisworld.tistory.com/</link>
    <description>비전공자의 개발자 성장기</description>
    <language>ko</language>
    <pubDate>Thu, 25 Jun 2026 13:19:01 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>dongburiii</managingEditor>
    <image>
      <title>DH Developement</title>
      <url>https://tistory1.daumcdn.net/tistory/2781022/attach/77ebd590168b4a3da67c2857e98835ee</url>
      <link>https://danisworld.tistory.com</link>
    </image>
    <item>
      <title>[GO] Graceful Shutdown</title>
      <link>https://danisworld.tistory.com/102</link>
      <description>&lt;h2&gt;Graceful Shutdown&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;취소 가능한 컨텍스트 생성&lt;ul&gt;
&lt;li&gt;컨텍스트와 취소 함수가 리턴됨&lt;/li&gt;
&lt;li&gt;종료 시그널 감지 시 취소 함수 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;ctx, cancel := context.WithCancel(context.Background()) defer cancel()&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;신호 감지&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;ctrl + c -&amp;gt; 운영체제에서 취소 신호(interrupt 신호)를 프로세스로 넘겨줌&lt;/li&gt;
&lt;li&gt;signal.Notify는 신호를 감지하면 받아서 sigChan 이라는 채널로 넘겨줌&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;sigChan := make(chan os.Signal, 1) 
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;신호 감지 고루틴&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;신호가 들어올때까지 sig := &amp;lt;- sigChan은 기다림&lt;ul&gt;
&lt;li&gt;Buffered Channel 로 생각하면 될듯&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;취소 신호(interrupt 신호)를 감지하면 signal.Notify로 sigchan 채널에 취소 신호를 넘김&lt;/li&gt;
&lt;li&gt;sigChan 채널의 값을 sig 변수에 채널을 담고, 해당 블록은 해제됨&lt;/li&gt;
&lt;li&gt;로그를 찍은 후 컨텍스트 취소 메서드를 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;go func() { 
    sig := &amp;lt;-sigChan 
    log.Printf(&amp;quot;Received signal %s, shutting down...&amp;quot;, sig) 
    cancel() 
}()&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;컨텍스트 완료 대기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;컨텍스트 취소 함수 (cancel())가 실행되기 전까지 대기 (블록)&lt;/li&gt;
&lt;li&gt;신호 감지 고루틴을 통해 취소 함수가 실행되면 대기중인 context의 채널이 닫힘&lt;ul&gt;
&lt;li&gt;이렇게 되면 해당 context를 사용중인 모든 함수들에 취소 신호가 전파됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;컨텍스트가 취소되면 ctx.Done() 이 실행됨&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;&amp;lt;-ctx.Done()&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;완성본&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;전체 코드와 흐름은 아래와 같다.&lt;/li&gt;
&lt;li&gt;취소 신호를 받아 컨텍스가 취소되기 전까지 실행할 함수를 넣어주면 된다.&lt;ul&gt;
&lt;li&gt;실행할 함수에도 취소 가능한 context(ctx) 를 넘겨줘서, Graceful 하게 고루틴을 종료시킬 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;
func main() {
    // 취소 가능한 context 생성
    ctx, cancel := context.WithCancel(context.Background())

    defer cancel() // 종료 취소 보장

    // 신호 채널 생성
    sigChan := make(chan os.Signal, 1)

    // 취소 신호 감지 시 채널에 넘겨주는 용도
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        sig := &amp;lt;-sigChan // 신호가 들어올 때 까지 대기. 신호가 들어오면 해제됨
        log.Printf(&amp;quot;Received signal %s, shutting down...&amp;quot;, sig)
        cancel() // context 취소 함수 실행
    }()

    // 실행할 함수
    lib.Download(ctx, url, fileName)

    &amp;lt;-ctx.Done()

    // 종료 로그 등, 종료
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>백엔드 Backend/Golang</category>
      <category>go</category>
      <category>gochannel</category>
      <category>Golang</category>
      <category>goroutine</category>
      <category>graceful</category>
      <category>shutdown</category>
      <category>signal</category>
      <category>고랭</category>
      <category>고언어</category>
      <category>안전한종료</category>
      <author>dongburiii</author>
      <guid isPermaLink="true">https://danisworld.tistory.com/102</guid>
      <comments>https://danisworld.tistory.com/102#entry102comment</comments>
      <pubDate>Fri, 25 Apr 2025 15:35:50 +0900</pubDate>
    </item>
    <item>
      <title>[GO] 채널</title>
      <link>https://danisworld.tistory.com/101</link>
      <description>&lt;h2&gt;Channel&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;채널은 고루틴 간의 데이터 파이프라인이다.&lt;/li&gt;
&lt;li&gt;종류는 Unbuffered channel과 buffered channel로 구분된다.&lt;ul&gt;
&lt;li&gt;Unbuffered Channel: 동기적&lt;ul&gt;
&lt;li&gt;수신 채널쪽에서 송신될 때 까지 채널이 묶는다. 즉 Block을 시키고 대기한다.&lt;/li&gt;
&lt;li&gt;수신 채널이 준비되지 않다면 deadlock 에러가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Buffered Channel: 비동기적&lt;ul&gt;
&lt;li&gt;수신 채널이 데이터를 받을 준비가 되지 않더라도 지정된 버퍼만큼 데이터를 보내고 다음 작업을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;main() 함수는 가장 메인으로 생성되는 Goroutine이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;채널 예시1&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;수신자는 goroutine으로부터 데이터가 채널을 통해 들어올 때 까지 대기한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
    channel := make(chan int) // 정수 타입을 받는 채널 생성

    go func() {
        channel &amp;lt;- 123 // 채널에 123을 보냄
    }()

    var i int

    // goroutine에서 데이터를 전송할 때까지 계속 대기
    i = &amp;lt;- channel // 채널의 123을 수신한다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;채널 예시2&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;수신자와 송신자가 서로 기다리기 때문에, Goroutine 이 끝날 때 까지 기다리게 할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
    done := make(chan bool)

    go func() {
        for i := 0; i &amp;lt; 10; i +=1 {
            fmt.Println(i)
        }

        done &amp;lt;- true
    }()

    &amp;lt;- done // 위 익명함수 Goroutine이 종료될 때까지 대기
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Channel Buffering&lt;/h2&gt;
&lt;h3&gt;Unbuffered Channel&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;하나의 수신자가 데이터를 받을 때까지 송신자의 채널에 묶임&lt;/li&gt;
&lt;li&gt;즉 위에서 보여준 예시가 Unbuffered Channel 임&lt;/li&gt;
&lt;li&gt;만약 수신자가 준비되지 않았다면 에러 발생&lt;ul&gt;
&lt;li&gt;Deadlock&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
    channel := make(chan int)
    channel &amp;lt;- 1 // 수신 Goroutine이 없으므로 DEADLOCK
    fmt.Println(&amp;lt;- channel) // 별도의 Goroutine이 없으므로 DEADLOCK
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Buffered Channel&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;버퍼 채널을 만들어서 사용하면 수신자가 없어도 데이터를 보낼 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
    channel := make(chan int, 2) // 두개의 버퍼 채널 생성

    channel &amp;lt;- 10 // 수신자가 없더라도 보낼 수 있음

    fmt.Println(&amp;lt;- channel)

}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;송수신 역할 분리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;채널은 기본적으로 송신과 수신 역할 전부 다 할 수 있다.&lt;/li&gt;
&lt;li&gt;그러나 특정 역할만 수행하게 정해줄 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
    channel := make(chan string, 1) // Buffered Channel
    sendChannel(channel) // 전송
    receiveFromChannel(channel) // 수신
}

// 채널에 데이터 입력하는 역할만 수행
func sendToChannel(ch chan &amp;lt;- string ){
    ch &amp;lt;- &amp;quot;data&amp;quot;
    // data := &amp;lt;- ch 에러 발생
}

// 채널의 데이터를 수신하는 역할만 수행
func receiveFromChannel(ch &amp;lt;- chan string) {
    data := &amp;lt;- ch
    fmt.Println(data) // &amp;quot;data&amp;quot; 반환
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;채널 닫기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;채널을 닫은 이후 더 이상 해당 채널로 데이터 전송은 불가하다&lt;/li&gt;
&lt;li&gt;그러나 남은 데이터가 있다면 수신은 가능하다.&lt;/li&gt;
&lt;li&gt;channel의 리턴값은 두개이다&lt;ul&gt;
&lt;li&gt;채널 내의 데이터 값과 수신 성공 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
    channel := make(chan int, 2)

    channel &amp;lt;- 1 // 채널에 데이터 전송
    channel &amp;lt;- 3 // 채널에 데이터 전송

    close(channel) // 채널 종료

    fmt.Println(&amp;lt;-channel) // 남은 데이터가 채널에 있다면 수신
    fmt.Println(&amp;lt;-channel) // 남은 데이터가 채널에 있다면 수신

    if _, success := &amp;lt;- channel; !success { // 채널에 남아있는 데이터가 없다면 false
        fmt.Println(&amp;quot;데이터 없음&amp;quot;)
    }

}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>bufferedchannel</category>
      <category>channel</category>
      <category>go</category>
      <category>unbufferedchannel</category>
      <category>채널</category>
      <author>dongburiii</author>
      <guid isPermaLink="true">https://danisworld.tistory.com/101</guid>
      <comments>https://danisworld.tistory.com/101#entry101comment</comments>
      <pubDate>Mon, 7 Apr 2025 09:58:38 +0900</pubDate>
    </item>
    <item>
      <title>[GOLANG] 고루틴(GoRoutine)</title>
      <link>https://danisworld.tistory.com/100</link>
      <description>&lt;h2&gt;Goroutine&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Go 런타임이 관리&lt;/strong&gt;하는 가상(논리) 쓰레드&lt;ul&gt;
&lt;li&gt;go 키워드로 고루틴 실행 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비동기적으로 함수 루틴을 수행&lt;/strong&gt;하며, 동시 동작 수행에 사용됨&lt;/li&gt;
&lt;li&gt;OS 스레드보다 훨씬 가볍고, 생성할 때 비용이 적음&lt;ul&gt;
&lt;li&gt;OS 스레드는 1mb의 메모리 스택, goroutine은 kb 단위 (동적 증가 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;기본적으로 CPU 1개를 시분할하여 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;다중 처리&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Go는 기본적으로 CPU 개를 사용한다&lt;/li&gt;
&lt;li&gt;다중 병렬 처리를 위해 CPU를 여러개 사용하기 위해선 GOMAXPROCS로 증가시킬 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;runtime.GOMAXPROCS(4) // CPU 4개 사용&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;익명함수 (go func)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;익명 함수 go func() {}() 으로 비동기 실행 가능  &lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;func main() {  
  var wait sync.WaitGroup // Go 루틴 대기열 두개 생성  
  wait.Add(2)


  go func (parameter string) {
      defer wait.Done()
      //
  } (&amp;quot;parameterValue&amp;quot;)

  wait.Wait() // Go 루틴 끝날 때 까지 대기
 }&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Graceful Shutdown&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Go 언어에서 여러 고루틴을 운영할 때 Graceful Shutdown 로직이 필요한 이유는, Go 애플리케이션이 종료 신호를 받았을 때 하위 고루틴들이 메인 고루틴보다 먼저 종료될 가능성이 있기 때문&lt;/li&gt;
&lt;li&gt;쉽게 말하면, 작업 지시서이다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Context&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;동시성을 관리&lt;/li&gt;
&lt;li&gt;여러 goroutine에 값을 전달&lt;/li&gt;
&lt;li&gt;취소 신호 전파&lt;/li&gt;
&lt;li&gt;분산 시스템 / 서비스 간의 상호 작용에서 아용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;context.Background&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;모든 context 의 기본이 되는 빈 context 를 반환&lt;/li&gt;
&lt;li&gt;대부분의 context 는 이를 기반으로 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;context.TODO()&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;아직 구현되지 않은 부분을 나타내는 context 를 반환&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>백엔드 Backend/Golang</category>
      <category>conTeXt</category>
      <category>go</category>
      <category>goroutine</category>
      <category>병렬처리</category>
      <author>dongburiii</author>
      <guid isPermaLink="true">https://danisworld.tistory.com/100</guid>
      <comments>https://danisworld.tistory.com/100#entry100comment</comments>
      <pubDate>Fri, 4 Apr 2025 16:53:32 +0900</pubDate>
    </item>
    <item>
      <title>[패키지] 오픈소스 패키지 발행</title>
      <link>https://danisworld.tistory.com/99</link>
      <description>&lt;h2&gt;계기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Go 로 사이드 프로젝트 개발을 하다, 동적 쿼리가 필요한 경우도 많았고 동일한 유틸/함수들을 다양한 프로젝트에서 동일하게 사용해야 하는 경우가 너무나 많았다.&lt;/li&gt;
&lt;li&gt;문득 내가 모든 프로젝트들에 사용하는, 내가 만든 함수들을 패키지화 시키고 간편하게 사용하면 어떨까 생각하게 되었다.&lt;ul&gt;
&lt;li&gt;그렇게 &amp;#39;동적 쿼리 유틸&amp;#39;과 &amp;#39;DB 연결 및 쿼리&amp;#39; 유틸들을 오픈소스 패키지화 하기로 결정하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;버전 릴리즈&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;우선 깃허브에 퍼블릭 레포를 파고 패키지 소스코드를 푸시 하였다.&lt;/li&gt;
&lt;li&gt;패키지를 임포트 해 사용할 것이기 때문에, 메인 함수의 패키지명은 사용할 패키지 명으로 설정해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package gqbd

// ... 코드&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;테스트 코드들로 각 함수및 유틸들이 원하는 리턴값을 가져오는지 검증을 진행하였다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;여담이지만 &amp;lt;&amp;gt;_test.go 파일들을 테스트 코드로 인식한다.&lt;/li&gt;
&lt;li&gt;t *testing.T 타입을 인자로 넘겨주면 테스트 진행할 함수로 인식된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;완료된 후 버전을 릴리즈했다. 최초 베타 테스트이므로 v0.1.0 으로 잡았다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;git tag v0.1.0
git push origin v0.1.0&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;위처럼 레포에 v0.1.0으로 푸시를 했다면, 이제 Go 쪽에 해당 패키지와 버전을 알려주면 배포가 완료된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;GOPROXY=proxy.golang.org go list -m github.com/&amp;lt;repo_owner&amp;gt;/&amp;lt;repo_name&amp;gt;@&amp;lt;version&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;이렇게 하면 GoDoc에서 내가 배포한 레포를 확인할 수 있다.&lt;ul&gt;
&lt;li&gt;다만 배포했다고 GoDoc이 빠르게 반영되는건 아니라는 함정.&lt;/li&gt;
&lt;li&gt;그리고 레포의 README.md 파일은 GODOC의 메인 설명 페이지로 나온다는 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;결과&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;아래는 내가 배포한 두가지 패키지들이다. 지속적으로 유지보수하면서 버전업 시킬 예정이다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/donghquinn/gqbd&quot;&gt;GQBD&lt;/a&gt; : Go-Query-Builder&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/donghquinn/gdct&quot;&gt;GDCT&lt;/a&gt; : Go-Database-ConnecT&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>백엔드 Backend/Golang</category>
      <category>gdct</category>
      <category>go</category>
      <category>gqbd</category>
      <category>OpenSource</category>
      <category>Package</category>
      <author>dongburiii</author>
      <guid isPermaLink="true">https://danisworld.tistory.com/99</guid>
      <comments>https://danisworld.tistory.com/99#entry99comment</comments>
      <pubDate>Mon, 31 Mar 2025 08:22:01 +0900</pubDate>
    </item>
    <item>
      <title>[미디어] HLS</title>
      <link>https://danisworld.tistory.com/98</link>
      <description>&lt;h2&gt;HLS&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;HLS: HTTP Live Streaming&lt;ul&gt;
&lt;li&gt;콘텐츠를 작은 세그먼트로 나누어서 .m3u8 파일과 .ts 세그먼트 파일로 구성함&lt;/li&gt;
&lt;li&gt;즉 비디오를 2-10초 정도의 작은 .ts(Transport Stream)으로 나누게 됨&lt;/li&gt;
&lt;li&gt;m3u8 파일이 모든 세그먼트와 재생 정보를 관리하는 익덱스 파일&lt;/li&gt;
&lt;li&gt;적응형 비트레이트: 네트워크 상태에 따라 다양한 화질 선택 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;m3u8 형식 혹은 .ts 파일이 hls 스트리밍에 사용되는 파일 형식&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;예시&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;manifest.m3u8 (메인 인덱스 파일)
|- quality_high.m3u8 (고화질 인덱스)
|  |- segment0_high.ts
|  |- segment1_high.ts
|  |- ...
|- quality_medium.m3u8 (중간 화질 인덱스)
|  |- segment0_medium.ts
|  |- ...
|- quality_low.m3u8 (저화질 인덱스)
   |- segment0_low.ts
   |- ...&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;FFMPEG 이용한 m3u8 파일 변환&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;여러개의 .ts 파일과 .m3u8 파일이 생김&lt;ul&gt;
&lt;li&gt;m3u8 파일이 각 ts 파일에 대한 인덱스 역할을 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;ffmpeg -i &amp;lt;video_file&amp;gt;.mp4 -profile:v baseline -level 3.0 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls &amp;lt;output_file&amp;gt;.m3u8&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;HTTP Streaming vs HLS&lt;/h2&gt;
&lt;h3&gt;주요 차이점&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;적응성&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HLS&lt;/strong&gt;: 네트워크 상태에 따라 화질을 자동으로 조정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP 스트리밍&lt;/strong&gt;: 단일 화질만 제공, 적응형 기능 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;지연 시간&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HLS&lt;/strong&gt;: 일반적으로 10-30초 지연 발생&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP 스트리밍&lt;/strong&gt;: 버퍼링 시간만큼의 지연&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;대역폭 효율&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HLS&lt;/strong&gt;: 현재 네트워크 상태에 맞는 최적의 화질 제공&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP 스트리밍&lt;/strong&gt;: 고정 화질로 네트워크 상태가 좋지 않으면 버퍼링 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;서버 부하&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HLS&lt;/strong&gt;: 여러 화질의 세그먼트를 미리 생성해야 함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP 스트리밍&lt;/strong&gt;: 단일 파일만 제공하므로 서버 부하 낮음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;구현 복잡성&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HLS&lt;/strong&gt;: 인코딩, 세그먼트화, 인덱스 파일 관리 등 복잡한 과정 필요&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP 스트리밍&lt;/strong&gt;: 단순히 파일을 제공하는 것만으로 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;확장성&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HLS&lt;/strong&gt;: CDN과 연계하여 우수한 확장성 제공&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP 스트리밍&lt;/strong&gt;: CDN 활용 가능하나 적응형 기능 부재&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;호환성&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HLS&lt;/strong&gt;: iOS, Safari에서 네이티브 지원, 다른 브라우저는 추가 라이브러리 필요&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP 스트리밍&lt;/strong&gt;: 대부분의 브라우저에서 네이티브 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;간단한 비유&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;HLS는 도서관에서 책의 각 장을 필요할 때마다 가져오는 것과 같음&lt;/li&gt;
&lt;li&gt;일반 HTTP 스트리밍은 책 전체를 한 번에 빌려오는 것과 같음&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기록/업무일지</category>
      <category>HLS</category>
      <category>Media</category>
      <category>Streaming</category>
      <category>스트리밍</category>
      <category>영상</category>
      <author>dongburiii</author>
      <guid isPermaLink="true">https://danisworld.tistory.com/98</guid>
      <comments>https://danisworld.tistory.com/98#entry98comment</comments>
      <pubDate>Fri, 28 Mar 2025 18:25:54 +0900</pubDate>
    </item>
    <item>
      <title>[AUTH] 검증 방법 변경 - Http Only Cookie</title>
      <link>https://danisworld.tistory.com/97</link>
      <description>&lt;h2&gt;계기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;이전에 만들었던 프로젝트들을 리팩토링 중, 검증 방법이 헤더의 Authorization 으로 토큰 검증을 하게 해 뒀던 걸 발견했다.&lt;/li&gt;
&lt;li&gt;뭐 잘못된 방법은 아니지만, 쿠키를 활용해서 검증하는 방법으로 변경하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;개요&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;로그인 요청을 받았을 때, 이메일/패스워드 정보를 검증 후 유저 정보를 Redis 서버에서 체크한다.&lt;ul&gt;
&lt;li&gt;Redis 서버에 해당 정보가 조회될 경우, 새로운 환경에서 로그인 요청을 한 것으로 판단한다.&lt;ul&gt;
&lt;li&gt;이전 로그인은 종료시키기 위해, 해당 정보를 삭제한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;조회된 정보가 없다면 이전 로그인은 만료된 것이므로 무시한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이후 새로운 accessToken과 refreshToken을 생성한다.&lt;ul&gt;
&lt;li&gt;refreshToken을 상위 키로, 하위 키로는 유저 id 로 삼아 로그인 정보를 세팅한다.&lt;ul&gt;
&lt;li&gt;키: refresh:{refreshToken}:{userId}&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;해당 키들을 응답 쿠키에 각각 도메인, 만료시간, 토큰, secure, httpOnly 를 세팅한다.&lt;ul&gt;
&lt;li&gt;accessToken의 만료 시간은 한시간, refreshToken의 만료 시간은 일주일로 세팅한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이후 로그인 및 유저 정보가 필요한 요청은 미들웨어에서 토큰 검증 및 데이터 추출하여 사용한다.&lt;/li&gt;
&lt;li&gt;만료시간이 끝난 경우, 토큰 갱신 요청을 날려서 새로운 access token을 발급 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;코드 구현&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;백엔드는 golang, 프론트는 nextjs app router를 사용하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;백엔드&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;주요 로직은 주석처리, 토큰 세팅 및 쿠키 세팅하는 로직만 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 컨트롤러가 아닌, 비즈니스 로직을 담은 서비스
func LoginService(request LoginRequest, response http.ResponseWriter, request *http.Request) {    
    /* 
        DB 에서 유저 정보 조회 및 패스워드 매칭
    */

    // Access Token 생성
    token, tokenErr := auth.CreateAccessToken(userData.UserId, uuid.String(), userData.Email, fmt.Sprintf(&amp;quot;%d&amp;quot;, userData.UserStatus), fmt.Sprintf(&amp;quot;%d&amp;quot;, userData.IsAdmin), time.Hour)

    if tokenErr != nil {
        log.Printf(&amp;quot;[LOGIN] Create Token Error: %v&amp;quot;, tokenErr)

        return LoginResponse{
            Status:  http.StatusInternalServerError,
            Code:    &amp;quot;ULG09&amp;quot;,
            Message: &amp;quot;Create JWT Error&amp;quot;,
        }
    }

    // Refresh Token 생성
    refreshToken, refreshTokenErr := auth.CreateRefreshToken(&amp;quot;&amp;quot;,, time.Hour*24*7)

    if refreshTokenErr != nil {
        log.Printf(&amp;quot;[LOGIN] Create Refresh Token Error: %v&amp;quot;, refreshTokenErr)

        return LoginResponse{
            Status:  http.StatusInternalServerError,
            Code:    &amp;quot;ULG09&amp;quot;,
            Message: &amp;quot;Create JWT Error&amp;quot;,
        }
    }

    // Redis 서버에서 유저 정보 조회 - 이전 로그인 만료 여부 체크
    userDataFromRedis, getRedisErr := GetRefreshUserInfoFromRedis(refreshToken, userData.UserId)

    if getRedisErr != nil {
        return LoginResponse{
            Status:  http.StatusNotFound,
            Code:    &amp;quot;ULG08&amp;quot;,
            Message: &amp;quot;Get User Data from redis Error&amp;quot;,
        }
    }

    // 유저 정보가 조회되었을 시
    if (userDataFromRedis != LoginUserInfo{}) {
        // 조회된 Redis 정보 삭제
        delerr := DeleteAlreadySetToken(refreshToken, userData.UserId)

        if delerr != nil {
            return LoginResponse{
                Status:  http.StatusInternalServerError,
                Code:    &amp;quot;ULG08&amp;quot;,
                Message: &amp;quot;Delete Already Set Data from redis Error&amp;quot;,
            }
        }
    }

    // Redis 서버에 새로운 로그인 정보 세팅
    setErr := SetRefreshToken(refreshToken, userData.Email, fmt.Sprintf(&amp;quot;%d&amp;quot;, userData.UserStatus), userData.UserId, userData.UserName, fmt.Sprintf(&amp;quot;%d&amp;quot;, userData.IsAdmin))

    if setErr != nil {
        return LoginResponse{
            Status:  http.StatusInternalServerError,
            Code:    &amp;quot;ULG10&amp;quot;,
            Message: &amp;quot;Set JWT Error&amp;quot;,
        }
    }

    accessTokenCookie := http.Cookie{
        Name:     &amp;quot;accessToken&amp;quot;,
        Value:    token,
        Path:     &amp;quot;/&amp;quot;,
        Secure:   true,
        HttpOnly: true,
        SameSite: http.SameSiteNoneMode,
    }

    refreshTokenCookie := http.Cookie{
        Name:     &amp;quot;refreshToken&amp;quot;,
        Value:    refreshToken,
        Path:     &amp;quot;/&amp;quot;,
        Secure:   true,
        HttpOnly: true,
        SameSite: http.SameSiteNoneMode,
    }

    http.SetCookie(response, &amp;amp;accessTokenCookie)
    http.SetCookie(response, &amp;amp;refreshTokenCookie)

    return LoginResponse{
        Status:       http.StatusOK,
        Code:         &amp;quot;0000&amp;quot;,
        AccessToken:  token,
        RefreshToken: refreshToken,
        Message:      &amp;quot;Login Success&amp;quot;,
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;프론트&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;App Router의 Server Side Component를 이용해서 쿠키 및 토큰을 관리한다.&lt;ul&gt;
&lt;li&gt;로그인 응답으로 토큰을 받고 서버 사이드 쿠키에 세팅한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export async function POST(request: NextRequest) {
  // const cookieStore = cookies();

  // 로그인 데이터 확인 - 요청 조건 검증
  const loginSchema = z.object({
    email: z.string().email(),
    password: z.string().min(6).max(16),
  });

  try {
    const _loginData = await request.json();
    const loginData = await loginSchema.parseAsync(_loginData);

    const loginResponse = await login({ ...loginData });

    if (!loginResponse.ok) {
      return NextResponse.json({ message: &amp;#39;unauthorized&amp;#39; }, { status: 401 });
    }
});

    const response = &amp;lt;LoginResponse&amp;gt;(await loginResponse.json());

    if (response.status !== 200) {
      return NextResponse.json({ message: &amp;#39;Login Failed&amp;#39; }, { status: 302 });
    }

    // 브라우저에 리턴할 쿠키 설정
    const ONE_HOUR = 60 * 60 * 1000;
    const ONE_WEEK = 60 * 60 * 1000 * 24 * 7;

    const res = NextResponse.json({ message: &amp;#39;Login Success&amp;#39; }, { status: 200 });

    // 쿠키 설정
    res.cookies.set(&amp;#39;accessToken&amp;#39;, response.accessToken, {
      secure: true, // 프로덕션 환경에서는 true로 설정
      httpOnly: true,
      domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
      expires: new Date(Date.now() + ONE_HOUR),
    });

    res.cookies.set(&amp;#39;refreshToken&amp;#39;, response.refreshToken, {
      secure: true,
      httpOnly: true,
      domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
      expires: new Date(Date.now() + ONE_WEEK),
    });

    return res;
  } catch (error) {
    console.log(error);
    return NextResponse.json({ message: &amp;#39;server error&amp;#39; }, { status: 500 });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;서버 사이드 컴포넌트. 받은 쿠키들을 이용해 이후 요청을 한다.&lt;ul&gt;
&lt;li&gt;만료시 토큰 리프레시를 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;
export async function POST(request: NextRequest) {
    try {
        let tokenCookie = request.cookies.get(&amp;quot;accessToken&amp;quot;);
        let refreshToken = request.cookies.get(&amp;quot;refreshToken&amp;quot;);

        let cookie: string;
        let refreshCookie: string;

        if (!tokenCookie) {
            /** 
                refresh token 갱신 요청 로직
            */

            const ONE_HOUR = 60 * 60 * 1000;
            const ONE_WEEK = 60 * 60 * 1000 * 24 * 7;

            cookieStore.set(&amp;#39;accessToken&amp;#39;, response.accessToken, {
                secure: true,
                httpOnly: true,
                path:&amp;quot;/&amp;quot;,
              //   domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
                expires: new Date(Date.now() + ONE_HOUR),
              });

              cookieStore.set(&amp;#39;refreshToken&amp;#39;, response.refreshToken, {
                secure: true,
                httpOnly: true,
                path:&amp;quot;/&amp;quot;,
              //   domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
                expires: new Date(Date.now() + ONE_WEEK),
              });


            cookie = response.accessToken;
            refreshCookie = response.refreshToken;
        } else {
            cookie = tokenCookie.value;
            refreshCookie = refreshToken.value;

            // 기술통계 요청 데이터 확인
            const exampleScheme = z.object({
                // 요청 검증 필드
            });

            const _exampleData = await request.json();
            const exampleData = await exampleScheme.parseAsync(_exampleData);

            const exampleResponse = await example({ ...exampleData }, cookie, refreshCookie);

            if (!exampleResponse.ok) {
                return NextResponse.json({ message: &amp;#39;unauthorized&amp;#39; }, { status: 401 });
            }

            const dataResponse = &amp;lt;ExampleResponse&amp;gt;await exampleResponse.json();

            const response = NextResponse.json(dataResponse);

            return response;
        }
    } catch (error) {
        console.log(error);
        return NextResponse.json({ message: &amp;#39;server error&amp;#39; }, { status: 500 });

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Middleware를 적용하여, 쿠키가 없을 시 로그인 페이지로 자동 리디렉션 시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { NextFetchEvent, NextMiddleware, NextRequest, NextResponse } from &amp;quot;next/server&amp;quot;;

// 인증 없이 방문 불가 페이지
const PROTECTED_ROUTE: string[] = [
  // 페이지 리스트들
];

export type MiddlewareFactory = (middleware: NextMiddleware) =&amp;gt; NextMiddleware;

export const authMiddleware: MiddlewareFactory = (next: NextMiddleware) =&amp;gt; {
  return async (request: NextRequest, event: NextFetchEvent) =&amp;gt; {
    // URL 에서 언어 값 분리
    const [ , , ...segments ] = request.nextUrl.pathname.split(&amp;#39;/&amp;#39;);

    const cookies = request.cookies.get(&amp;quot;accessToken&amp;quot;);

    // 체크사항
    // 1. 로그인 유무
    // 2. 인증 없이 방문 불가 페이지
    const isProtecteRoute = PROTECTED_ROUTE.includes(request.nextUrl.pathname);

    if (!cookies &amp;amp;&amp;amp; isProtecteRoute) {
      return NextResponse.redirect(new URL(&amp;#39;/login&amp;#39;, request.url));
    }

    if (cookies &amp;amp;&amp;amp; isProtecteRoute) {
      if (cookies.value === &amp;quot;&amp;quot;) {
        return NextResponse.redirect(new URL(&amp;#39;/login&amp;#39;, request.url));
      }
    }
    // 인증 성공, 통과
    return next(request, event);
  };
};&lt;/code&gt;&lt;/pre&gt;</description>
      <category>기록/사이드 프로젝트</category>
      <category>auth</category>
      <category>cookie</category>
      <category>go</category>
      <category>JavaScript</category>
      <category>jwt</category>
      <category>login</category>
      <category>nextjs</category>
      <category>refresh</category>
      <category>검증</category>
      <category>로그인</category>
      <author>dongburiii</author>
      <guid isPermaLink="true">https://danisworld.tistory.com/97</guid>
      <comments>https://danisworld.tistory.com/97#entry97comment</comments>
      <pubDate>Fri, 31 Jan 2025 09:41:56 +0900</pubDate>
    </item>
    <item>
      <title>[NEXTJS] 사이드 컴포넌트 쿠키 처리</title>
      <link>https://danisworld.tistory.com/96</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Nextjs App Router 서버 사이드 요청&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Nextjs의 App Router를 통해 서버사이드에서 요청 날리게끔 구성되어 있다.&lt;/li&gt;
&lt;li&gt;요청에 쿠키를 담아 서버쪽에서 검증 받는 흐름이지만, 쿠키가 계속 안 들어왔다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;credentials: &quot;include&quot; 인데도 들어오지 않는 것에 의문을 가졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결론은 서버 사이드에서의 요청일 경우 직접 쿠키를 담아줘야 한다는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쿠키 양식&lt;/h3&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;Cookie&quot;: &quot;accessToken=tokendata; refreshToken=refreshToken&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경된 코드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;헤더에 직접 입력&lt;/li&gt;
&lt;li&gt;API 라우트에서 쿠키를 뽑아와 인자로 던짐&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;클라이언트 사이드 요청 함수&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;react query를 이용해 요청 및 관리&lt;/li&gt;
&lt;li&gt;이 때는 쿠키를 담을 수 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 사이드에서는 쿠키를 가져올 수 없음. 브라우저 범위이기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export interface ReferenceListResponse {
    status: number;
    code: string;
    message: string;
    result: ReferenceListItem[];
    totalCount: number;
}

export interface ReferenceListItem {
    referenceSeq: string;
    referenceName?: string;
    referenceUrl: string;
    referenceMemo?: string;
    referenceCategory: string;
    createdAt: string;
}

export const referenceListRequest = async (props: ReferenceListRequest) =&amp;gt; {

    const response = await jsFetch&amp;lt;ReferenceListResponse&amp;gt;(`/api/reference/list`, {
        method: 'post',
        cache: 'no-store',
        body: JSON.stringify(props),
        headers: {
            'content-type': 'application/json',

        },        
    });

    return response;

};&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;서버 사이드 요청 함수&lt;/h4&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;export interface ReferenceListRequest {
    category: string;
    page: number;
    pageSize: number;
}


export const referenceList = async (request: ReferenceListRequest, accessToken: string, refreshToken: string) =&amp;gt; {

    const response = await fetch(
    `${ process.env.NEXT_PUBLIC_BASE_API }/reference/list`, {
        method: 'post',
        cache: 'no-store',
        body: JSON.stringify({ category: request.category, page: request.page, pageSize: request.pageSize }),
        headers: {
            'content-type': 'application/json',
            &quot;Cookie&quot;: `accessToken=${ accessToken }; refreshToken=${ refreshToken }`
        },
        credentials: 'include', // 쿠키를 포함해서 요청 전송
        next: { tags: [ 'search-word' ] },
    });

    return response;
};&lt;/code&gt;&lt;/pre&gt;</description>
      <category>기록/사이드 프로젝트</category>
      <category>cookie</category>
      <category>JavaScript</category>
      <category>nextjs</category>
      <category>SSR</category>
      <category>서버사이드</category>
      <category>자바스크립트</category>
      <category>쿠키</category>
      <category>프론트엔드</category>
      <author>dongburiii</author>
      <guid isPermaLink="true">https://danisworld.tistory.com/96</guid>
      <comments>https://danisworld.tistory.com/96#entry96comment</comments>
      <pubDate>Wed, 22 Jan 2025 09:22:32 +0900</pubDate>
    </item>
    <item>
      <title>[AUTH] Cookie를 이용한 검증</title>
      <link>https://danisworld.tistory.com/95</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;로그인 시 발급되는 access token을 쿠키에 세팅&lt;/li&gt;
&lt;li&gt;미들웨어에서 쿠키의 access token 을 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cookie 세팅&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;생성된 토큰을 쿠키에 세팅한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func Login(res http.ResponseWriter, req *http.Request) {

    /*
        로그인 로직
    */

    accessTokenCookie := http.Cookie{
        Name: &amp;quot;accessToken&amp;quot;,
        Value: accessToken,
        Path: &amp;quot;/&amp;quot;,
        Secure: true, // HTTPS 만
        HttpOnly: true, // 브라우저에서 쿠키 조작 불가하게 세팅
        SameSite: http.SameSiteNoneMode,
    }

    refreshTokenCookie := http.Cookie{
        Name: &amp;quot;refreshToken&amp;quot;,
        Value: refreshToken,
        Path: &amp;quot;/&amp;quot;,
        Secure: true, // HTTPS 만
        HttpOnly: true, // 브라우저에서 쿠키 조작 불가하게 세팅
        SameSite: http.SameSiteNoneMode,
    }

    http.SetCookie(res, &amp;amp;accessTokenCookie)
    http.SetCookie(res, &amp;amp;refreshTokenCookie)

    /*
        응답 보내기
    */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;검증 미들웨어&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;쿠키에서 토큰을 추출해서 컨텍스트에 담는다&lt;/li&gt;
&lt;li&gt;담은 컨텍스트의 정보를 컨트롤러에서 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 사용자 정의 키 타입을 사용하여 컨텍스트 충돌 방지

type contextKey string

const (
    // JWT 서명에 사용할 비밀 키 (환경 변수로 관리하는 것이 좋습니다)

    // jwtSecret = configs.GlobalConfig.JwtKey // 실제 배포 시 환경 변수로 관리

    // 컨텍스트에 사용자 정보를 저장할 키
    userContextKey = contextKey(&amp;quot;user&amp;quot;)
)

// 사용자 정보 구조체

type User struct {
    UserId string
    UserEmail string
    UserStatus string
}

var excludeRouteList = []string{
    &amp;quot;/&amp;quot;, &amp;quot;/api&amp;quot;, 
    &amp;quot;/user/signup&amp;quot;, &amp;quot;/user/login&amp;quot;,
}

// AuthMiddleware는 accessToken 쿠키를 추출하고 JWT를 검증하는 미들웨어입니다.
func AuthMiddleware(next http.Handler) http.Handler {

    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

        // 제외할 경로는 바로 다음 핸들러로 넘김
        for _, route := range excludeRouteList {
            if r.URL.Path == route {
                log.Printf(&amp;quot;Found Match Route: %s&amp;quot;, route)
                next.ServeHTTP(w, r)
                return
            }
        }

        // accessToken 쿠키 추출
        cookie, err := r.Cookie(&amp;quot;accessToken&amp;quot;)

        log.Printf(&amp;quot;Cookie: %s&amp;quot;, cookie)

        if err != nil {
            log.Printf(&amp;quot;Get Cookie Error :%v&amp;quot;, err)  

            if err == http.ErrNoCookie {
                response.Response(w, response.CommonResponseWithMessage{
                    Status: http.StatusUnauthorized,
                    Code: &amp;quot;AUTH001&amp;quot;,
                    Message: &amp;quot;No access token provided&amp;quot;,    
                })

                return
            }

            // 다른 쿠키 에러 처리

            response.Response(w, response.CommonResponseWithMessage{
                Status: http.StatusUnauthorized,
                Code: &amp;quot;AUTH002&amp;quot;,
                Message: &amp;quot;Invalid cookie format&amp;quot;,
            })

            return
        }

        accessToken := cookie.Value

        userId, userEmail, userStatus, validateErr := auth.ValidateJwtTokenFromString(accessToken)


        if validateErr != nil {
            log.Printf(&amp;quot;ValidateErr Cookie Error :%v&amp;quot;, validateErr)

            // 토큰 만료에 대한 응답
            if strings.Contains(validateErr.Error(), &amp;quot;token expired&amp;quot;) {
                response.Response(w, response.CommonResponseWithMessage{
                    Status: http.StatusUnauthorized,
                    Code: &amp;quot;AUTH003&amp;quot;,
                    Message: &amp;quot;Token expired&amp;quot;,
                })

                return
            }

            // 일반적인 JWT 검증 실패 응답
            response.Response(w, response.CommonResponseWithMessage{
                Status: http.StatusUnauthorized,
                Code: &amp;quot;AUTH004&amp;quot;,
                Message: &amp;quot;Invalid token&amp;quot;,
            })

            return        
        }

        // 사용자 정보를 구조체로 생성

        user := User{
            UserId: userId,
            UserEmail: userEmail,
            UserStatus: userStatus,
        }

        // 사용자 정보를 컨텍스트에 추가
        ctx := context.WithValue(r.Context(), userContextKey, user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}



// 사용자 정보를 핸들러 및 컨트롤러에서 가져오는 헬퍼 함수
func GetUserFromContext(ctx context.Context) (User, bool) {
    user, ok := ctx.Value(userContextKey).(User)
    return user, ok
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;컨트롤러에서 추출된 정보들 사용&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;
func SampleController(res http.ResponseWriter, req *http.Request) {
    user, ok := middlewares.GetUserFromContext(req.Context())

    if !ok {    
        dto.SetErrorResponse(res, 401, &amp;quot;01&amp;quot;, &amp;quot;JWT Verifying Error&amp;quot;, nil)

        return
    }
    /*
        이후 로직
    */

    dto.SetResponse(res, 200, &amp;quot;01&amp;quot;)
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>백엔드 Backend/Golang</category>
      <category>auth</category>
      <category>cookie</category>
      <category>go</category>
      <category>Middleware</category>
      <category>검증</category>
      <category>고</category>
      <category>미들웨어</category>
      <category>쿠키</category>
      <author>dongburiii</author>
      <guid isPermaLink="true">https://danisworld.tistory.com/95</guid>
      <comments>https://danisworld.tistory.com/95#entry95comment</comments>
      <pubDate>Tue, 21 Jan 2025 10:21:17 +0900</pubDate>
    </item>
    <item>
      <title>[CORS] CORS 설정 - 라이브러리 이용</title>
      <link>https://danisworld.tistory.com/94</link>
      <description>&lt;h2&gt;기존&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;기존에는 직접 CORS 체크 미들웨어를 작성해 사용했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func CorsMiddlewares(next http.Handler) http.Handler {
    return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {

        origin := req.Header.Get(&amp;quot;Origin&amp;quot;)

        if origin == &amp;quot;&amp;quot; {
            origin = &amp;quot;unknown&amp;quot;
        }

        for _, o := range originList {
            if o == origin {    
                log.Printf(&amp;quot;Allowed Origin: %s&amp;quot;, origin)
                res.Header().Set(&amp;quot;Access-Control-Allow-Origin&amp;quot;, origin)
                res.Header().Set(&amp;quot;Access-Control-Max-Age&amp;quot;, &amp;quot;86400&amp;quot;)
                res.Header().Set(&amp;quot;Access-Control-Allow-Methods&amp;quot;, &amp;quot;POST, GET, OPTIONS&amp;quot;)
                res.Header().Set(&amp;quot;Access-Control-Allow-Headers&amp;quot;, &amp;quot;Content-Type, Authorization&amp;quot;)
                res.Header().Set(&amp;quot;Access-Control-Allow-Credentials&amp;quot;, &amp;quot;true&amp;quot;)
                break
            }
        }

        // Handle preflight request
        if req.Method == http.MethodOptions {
            res.WriteHeader(http.StatusOK)
            return
        }

        next.ServeHTTP(res, req)
    })
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;변경&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;그러나 조금 더 심플하고 편하게 설정할 수 있는 패키지가 있어 가져와 사용했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;get -u github.com/rs/cors&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;이후 세팅은 이렇게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func CorsHanlder() *cors.Cors {
    corHandler := cors.New(cors.Options{
        AllowedOrigins: originList,
        AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodOptions},
        AllowedHeaders: []string{&amp;quot;Origin&amp;quot;, &amp;quot;Accept&amp;quot;, &amp;quot;Content-Type&amp;quot;, &amp;quot;Authorization&amp;quot;},
        AllowCredentials: true,
        MaxAge: 86400,        
        Debug: false,
    })

    return corHandler
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;미들웨어 등록하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;현재 표준 라이브러리인 net/http 에서 gorilla/mux 로 변경한 상태이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func OpenServer() *http.Server {
    router := mux.NewRouter()

    routers.DefaultRouter(router)

    routers.UploadImageController(router)

    routers.AdminUserRouter(router)

    routers.AdminPostRouter(router)

    routers.UserRouter(router)

    routers.PostRouter(router)

    router.Use(middlewares.AuthMiddleware)

    // CORS 핸들러
    handler := middlewares.CorsHanlder().Handler(router)

    // 기존 방식  
    // handler := middlewares.CorsMiddlewares(router)

    serving := &amp;amp;http.Server{
        Handler: handler,
        Addr: configs.GlobalConfig.AppHost,
        WriteTimeout: 30 * time.Second,
        ReadTimeout: 30 * time.Second,
    }

    return serving
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>백엔드 Backend/Golang</category>
      <category>cors</category>
      <category>go</category>
      <category>Middleware</category>
      <category>미들웨어</category>
      <author>dongburiii</author>
      <guid isPermaLink="true">https://danisworld.tistory.com/94</guid>
      <comments>https://danisworld.tistory.com/94#entry94comment</comments>
      <pubDate>Mon, 20 Jan 2025 09:42:58 +0900</pubDate>
    </item>
    <item>
      <title>[TLS] TLS 핸드세이크?</title>
      <link>https://danisworld.tistory.com/93</link>
      <description>&lt;h2&gt;TLS 핸드세이크&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;클라이언트 - 서버가 교환하는 일련의 데이터그램 == 메시지&lt;/li&gt;
&lt;li&gt;사용되는 키 교환 알고리즘의 종류 및 양측에서 지원하는 암호 모음에 따라 달라짐&lt;ul&gt;
&lt;li&gt;1.3 버전 이전에는 RSA 키 교환 알고리즘을 사용했지만, 안전하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;TLS 핸드세이크 작동 방식&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;비대칭 암호화(공개 키 - 개인 키) 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vjBTu/btsLKNmN6oy/N8iLMolK0ScO0J43XfxR7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vjBTu/btsLKNmN6oy/N8iLMolK0ScO0J43XfxR7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vjBTu/btsLKNmN6oy/N8iLMolK0ScO0J43XfxR7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvjBTu%2FbtsLKNmN6oy%2FN8iLMolK0ScO0J43XfxR7K%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TLS 핸드세이크&lt;ul&gt;
&lt;li&gt;클라이언트 헬로 메시지&lt;ul&gt;
&lt;li&gt;클라이언트가 서버로 Hello 메시지 전송하여 핸드세이크 개시&lt;/li&gt;
&lt;li&gt;이 때 TLS 버전, 지원 암호 제품군, 클라이언트 무작위 바이트 문자열 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서버 헬로 메시지&lt;ul&gt;
&lt;li&gt;클라이언트 헬로 메시지에 대한 응답으로 서버의 SSL 인증서, 서버에서 선택한 암호 제품군, 그리고 서버에서 생성한 무작위 문자열 바이트 포함한 메시지 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인증&lt;ul&gt;
&lt;li&gt;클라이언트가 서버의 SSL 인증서를 인증서 발행 기관을 통해 검증&lt;/li&gt;
&lt;li&gt;인증서에 명시된 서버인지, 상호작용중인 서버가 실제 해당 도메인의 소유자인지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;예비 마스터 암호&lt;ul&gt;
&lt;li&gt;클라이언트가 무작위 바이트 문자열 하나 더 전송&lt;/li&gt;
&lt;li&gt;공개키로 암호화 되며, 서버 개인 키로만 해독 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;공개키는 서버의 SSL 인증서를 통해 클라이언트에 전달됨&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;개인키 사용&lt;ul&gt;
&lt;li&gt;서버가 예비 마스터 암호를 해독함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;세션 키 생성&lt;ul&gt;
&lt;li&gt;클라이언트와 서버 모두 클라이언트 무작위, 서버 무작위, 예비 마스터 암호 이용해 세션 키 생성&lt;/li&gt;
&lt;li&gt;모두 동일한 결과가 나와야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;클라이언트 준비 완료&lt;ul&gt;
&lt;li&gt;클라이언트가 세션 키로 암호화된 완료 메시지 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서버 준비 완료&lt;ul&gt;
&lt;li&gt;서버가 세션 키로 암호화된 완료 메시지 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;안전한 대칭 암호화 성공&lt;ul&gt;
&lt;li&gt;핸드세이크 완료, 세션키를 통한 통신이 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; 데이터가 암호화되고 인증되면, 메시지 인증 코드(MAC: Message Authorization Code)와 함께 서명됨&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>네트워크</category>
      <category>handshake</category>
      <category>TLS</category>
      <category>tls핸드세이크</category>
      <category>네트워크</category>
      <category>암호화</category>
      <category>통신</category>
      <category>핸드세이크</category>
      <author>dongburiii</author>
      <guid isPermaLink="true">https://danisworld.tistory.com/93</guid>
      <comments>https://danisworld.tistory.com/93#entry93comment</comments>
      <pubDate>Tue, 14 Jan 2025 09:42:45 +0900</pubDate>
    </item>
  </channel>
</rss>