728x90
반응형

계기

  • Go 로 사이드 프로젝트 개발을 하다, 동적 쿼리가 필요한 경우도 많았고 동일한 유틸/함수들을 다양한 프로젝트에서 동일하게 사용해야 하는 경우가 너무나 많았다.
  • 문득 내가 모든 프로젝트들에 사용하는, 내가 만든 함수들을 패키지화 시키고 간편하게 사용하면 어떨까 생각하게 되었다.
    • 그렇게 '동적 쿼리 유틸'과 'DB 연결 및 쿼리' 유틸들을 오픈소스 패키지화 하기로 결정하였다.

버전 릴리즈

  • 우선 깃허브에 퍼블릭 레포를 파고 패키지 소스코드를 푸시 하였다.
  • 패키지를 임포트 해 사용할 것이기 때문에, 메인 함수의 패키지명은 사용할 패키지 명으로 설정해야 한다.
package gqbd

// ... 코드
  • 테스트 코드들로 각 함수및 유틸들이 원하는 리턴값을 가져오는지 검증을 진행하였다.

    • 여담이지만 <>_test.go 파일들을 테스트 코드로 인식한다.
    • t *testing.T 타입을 인자로 넘겨주면 테스트 진행할 함수로 인식된다.
  • 완료된 후 버전을 릴리즈했다. 최초 베타 테스트이므로 v0.1.0 으로 잡았다.

git tag v0.1.0
git push origin v0.1.0
  • 위처럼 레포에 v0.1.0으로 푸시를 했다면, 이제 Go 쪽에 해당 패키지와 버전을 알려주면 배포가 완료된다.
GOPROXY=proxy.golang.org go list -m github.com/<repo_owner>/<repo_name>@<version>
  • 이렇게 하면 GoDoc에서 내가 배포한 레포를 확인할 수 있다.
    • 다만 배포했다고 GoDoc이 빠르게 반영되는건 아니라는 함정.
    • 그리고 레포의 README.md 파일은 GODOC의 메인 설명 페이지로 나온다는 것.

결과

  • 아래는 내가 배포한 두가지 패키지들이다. 지속적으로 유지보수하면서 버전업 시킬 예정이다.
  • GQBD : Go-Query-Builder
  • GDCT : Go-Database-ConnecT
728x90
반응형
728x90
반응형

HLS

  • HLS: HTTP Live Streaming
    • 콘텐츠를 작은 세그먼트로 나누어서 .m3u8 파일과 .ts 세그먼트 파일로 구성함
    • 즉 비디오를 2-10초 정도의 작은 .ts(Transport Stream)으로 나누게 됨
    • m3u8 파일이 모든 세그먼트와 재생 정보를 관리하는 익덱스 파일
    • 적응형 비트레이트: 네트워크 상태에 따라 다양한 화질 선택 가능
  • m3u8 형식 혹은 .ts 파일이 hls 스트리밍에 사용되는 파일 형식

예시

manifest.m3u8 (메인 인덱스 파일)
|- quality_high.m3u8 (고화질 인덱스)
|  |- segment0_high.ts
|  |- segment1_high.ts
|  |- ...
|- quality_medium.m3u8 (중간 화질 인덱스)
|  |- segment0_medium.ts
|  |- ...
|- quality_low.m3u8 (저화질 인덱스)
   |- segment0_low.ts
   |- ...

FFMPEG 이용한 m3u8 파일 변환

  • 여러개의 .ts 파일과 .m3u8 파일이 생김
    • m3u8 파일이 각 ts 파일에 대한 인덱스 역할을 함
ffmpeg -i <video_file>.mp4 -profile:v baseline -level 3.0 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls <output_file>.m3u8

HTTP Streaming vs HLS

주요 차이점

  1. 적응성
    • HLS: 네트워크 상태에 따라 화질을 자동으로 조정
    • HTTP 스트리밍: 단일 화질만 제공, 적응형 기능 없음
  2. 지연 시간
    • HLS: 일반적으로 10-30초 지연 발생
    • HTTP 스트리밍: 버퍼링 시간만큼의 지연
  3. 대역폭 효율
    • HLS: 현재 네트워크 상태에 맞는 최적의 화질 제공
    • HTTP 스트리밍: 고정 화질로 네트워크 상태가 좋지 않으면 버퍼링 발생
  4. 서버 부하
    • HLS: 여러 화질의 세그먼트를 미리 생성해야 함
    • HTTP 스트리밍: 단일 파일만 제공하므로 서버 부하 낮음
  5. 구현 복잡성
    • HLS: 인코딩, 세그먼트화, 인덱스 파일 관리 등 복잡한 과정 필요
    • HTTP 스트리밍: 단순히 파일을 제공하는 것만으로 가능
  6. 확장성
    • HLS: CDN과 연계하여 우수한 확장성 제공
    • HTTP 스트리밍: CDN 활용 가능하나 적응형 기능 부재
  7. 호환성
    • HLS: iOS, Safari에서 네이티브 지원, 다른 브라우저는 추가 라이브러리 필요
    • HTTP 스트리밍: 대부분의 브라우저에서 네이티브 지원

간단한 비유

  • HLS는 도서관에서 책의 각 장을 필요할 때마다 가져오는 것과 같음
  • 일반 HTTP 스트리밍은 책 전체를 한 번에 빌려오는 것과 같음
728x90
반응형
728x90
반응형

계기

  • 이전에 만들었던 프로젝트들을 리팩토링 중, 검증 방법이 헤더의 Authorization 으로 토큰 검증을 하게 해 뒀던 걸 발견했다.
  • 뭐 잘못된 방법은 아니지만, 쿠키를 활용해서 검증하는 방법으로 변경하였다.

개요

  • 로그인 요청을 받았을 때, 이메일/패스워드 정보를 검증 후 유저 정보를 Redis 서버에서 체크한다.
    • Redis 서버에 해당 정보가 조회될 경우, 새로운 환경에서 로그인 요청을 한 것으로 판단한다.
      • 이전 로그인은 종료시키기 위해, 해당 정보를 삭제한다.
    • 조회된 정보가 없다면 이전 로그인은 만료된 것이므로 무시한다.
  • 이후 새로운 accessToken과 refreshToken을 생성한다.
    • refreshToken을 상위 키로, 하위 키로는 유저 id 로 삼아 로그인 정보를 세팅한다.
      • 키: refresh:{refreshToken}:{userId}
  • 해당 키들을 응답 쿠키에 각각 도메인, 만료시간, 토큰, secure, httpOnly 를 세팅한다.
    • accessToken의 만료 시간은 한시간, refreshToken의 만료 시간은 일주일로 세팅한다.
  • 이후 로그인 및 유저 정보가 필요한 요청은 미들웨어에서 토큰 검증 및 데이터 추출하여 사용한다.
  • 만료시간이 끝난 경우, 토큰 갱신 요청을 날려서 새로운 access token을 발급 받는다.

코드 구현

  • 백엔드는 golang, 프론트는 nextjs app router를 사용하였다.

백엔드

  • 주요 로직은 주석처리, 토큰 세팅 및 쿠키 세팅하는 로직만 유지
// 컨트롤러가 아닌, 비즈니스 로직을 담은 서비스
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("%d", userData.UserStatus), fmt.Sprintf("%d", userData.IsAdmin), time.Hour)

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

        return LoginResponse{
            Status:  http.StatusInternalServerError,
            Code:    "ULG09",
            Message: "Create JWT Error",
        }
    }

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

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

        return LoginResponse{
            Status:  http.StatusInternalServerError,
            Code:    "ULG09",
            Message: "Create JWT Error",
        }
    }

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

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

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

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

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

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

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

    refreshTokenCookie := http.Cookie{
        Name:     "refreshToken",
        Value:    refreshToken,
        Path:     "/",
        Secure:   true,
        HttpOnly: true,
        SameSite: http.SameSiteNoneMode,
    }

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

    return LoginResponse{
        Status:       http.StatusOK,
        Code:         "0000",
        AccessToken:  token,
        RefreshToken: refreshToken,
        Message:      "Login Success",
    }

프론트

  • App Router의 Server Side Component를 이용해서 쿠키 및 토큰을 관리한다.
    • 로그인 응답으로 토큰을 받고 서버 사이드 쿠키에 세팅한다.
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: 'unauthorized' }, { status: 401 });
    }
});

    const response = <LoginResponse>(await loginResponse.json());

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

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

    const res = NextResponse.json({ message: 'Login Success' }, { status: 200 });

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

    res.cookies.set('refreshToken', 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: 'server error' }, { status: 500 });
  }
}
  • 서버 사이드 컴포넌트. 받은 쿠키들을 이용해 이후 요청을 한다.
    • 만료시 토큰 리프레시를 한다.

export async function POST(request: NextRequest) {
    try {
        let tokenCookie = request.cookies.get("accessToken");
        let refreshToken = request.cookies.get("refreshToken");

        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('accessToken', response.accessToken, {
                secure: true,
                httpOnly: true,
                path:"/",
              //   domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN,
                expires: new Date(Date.now() + ONE_HOUR),
              });

              cookieStore.set('refreshToken', response.refreshToken, {
                secure: true,
                httpOnly: true,
                path:"/",
              //   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: 'unauthorized' }, { status: 401 });
            }

            const dataResponse = <ExampleResponse>await exampleResponse.json();

            const response = NextResponse.json(dataResponse);

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

    }
}
  • Middleware를 적용하여, 쿠키가 없을 시 로그인 페이지로 자동 리디렉션 시킨다.
import { NextFetchEvent, NextMiddleware, NextRequest, NextResponse } from "next/server";

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

export type MiddlewareFactory = (middleware: NextMiddleware) => NextMiddleware;

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

    const cookies = request.cookies.get("accessToken");

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

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

    if (cookies && isProtecteRoute) {
      if (cookies.value === "") {
        return NextResponse.redirect(new URL('/login', request.url));
      }
    }
    // 인증 성공, 통과
    return next(request, event);
  };
};
728x90
반응형

'기록 > 사이드 프로젝트' 카테고리의 다른 글

[NEXTJS] 사이드 컴포넌트 쿠키 처리  (0) 2025.01.22
[OAUTH] 구글 OAuth  (0) 2025.01.02

+ Recent posts