project/teamProject

Team Project - Remember Me

부엉이사장 2025. 1. 26. 10:33
Introduction
안녕하세요? Nurd Worker입니다! 반갑습니다.😊
이번에 소개해드릴 프로젝트는 RememberMe 프로젝트입니다!

저는 다른 언어 공부를 할때 따로 A4용지같은데 적어서 공부를 하는데요. 단어를 정리해서 적어놓는것도 시간이 많이 걸리고 가끔은 잃어버리는 경우도 있어서 단어장 어플리케이션을 만들어봐야겠다 생각하고 시작하게 된 프로젝트입니다ㅎㅎ

완료된지 얼마안된 프로젝트라 아직 어학공부보다 포트폴리오준비에 전념하고있는데요.
조만간 취직 후, 틈틈히 사용할 예정입니다^^

Remember Me 프로젝트는 제가 취직할때 정말 많은 점을 어필 할 수 있는 프로젝트라고 생각합니다.
포트폴리오 프로젝트 중 메인 프로젝트라고 보시면 되겠네요 ㅎㅎ

미리 간단히 말씀드리자면,

- React + Typescript 첫 경험
- 서버리스(Lambda) 서비스
- 성능 & 비용 최적화
- 유지보수 최적화
- 팀프로젝트 진행(팀장)

이런 점들을 주목하셔서 포스팅을 읽어주시면 감사드리겠습니다^^

참고로 RememberMe 프로젝트는 현재 배포가 되어있는 상태입니다~
궁금하시면 링크를 들어오셔서 확인 가능하세요 ^^
그럼 시작하겠습니다~

 

시작하기 전에


 

remember me 프로젝트는 팀프로젝트로 분류해놨지만, 두 가지 버전으로 나뉩니다!

demo버전은 팀프로젝트로 진행을 했구요~ 이게 초기버전입니다. 인프라팀원분들 두 분, 개발팀원(nurd worker) 이렇게 만든 프로젝트입니다.

이후에 nurd worker가 따로 좀더 손본 버전 advanced버전입니다.

 

advanced버전은 test기능을 추가했고 타입설정이라든지 이런것들을 더 추가한 버전입니다.

 

 


Remember Me 링크

Remember Me introduction video : https://www.youtube.com/watch?v=MEIIWAcPjt0

 

 

Remember Me published web site link : https://rememberme.nurd.work/

 

Remember Me

A vocabulary memorization app to help you efficiently learn and retain words.

rememberme.nurd.work

 

 

 

Remember Me github Link

 

Remember Me advanced github Link : 

 

GitHub - nurdworker/rememberme-advanced-front

Contribute to nurdworker/rememberme-advanced-front development by creating an account on GitHub.

github.com

 

GitHub - nurdworker/rememberme-advanced-back

Contribute to nurdworker/rememberme-advanced-back development by creating an account on GitHub.

github.com

 

Remember Me demo github organization Link : https://github.com/vocaAppServerless

 

Remember Me

Remember Me has 5 repositories available. Follow their code on GitHub.

github.com

 

 

 

 

Remember Me Project Development Tech Stacks

 

remember me 프로젝트에서 nurd worker가 개발쪽을 100퍼센트 담당을 하였는데요.

람다로 서버리스 서비스를 구성했습니다. 사실 람다가 이 프로젝트의 가장 어려운 부분이었어요.

aws sam cli이라고 있는데 아이콘이 생소하시죠? 다람쥐가 도토리를 들고있는 아이콘입니다.

한국에서는 많이들 안써보신 툴일겁니다. 포스팅에 자세히 설명드리겠지만, 로컬환경에서 lambda환경을 구성하기위해 사용한 aws툴입니다.

 

프론트앤드는 react + type script, 백엔드는 node js, 데이터베이스는 mongo db를 사용헀습니다.

react와 typescript는 처음으로 사용해봤습니다 ㅎㅎㅎ

기타 aws 리소스로는 api gateway와, cloud watch, SNS, secret manager를 사용했네요~

 

참고로 현재 테크스택들은 개발담당이었던 제가 사용했던 스택들입니다.

팀프로젝트에서 팀원분들이 사용하신 스택들은 따로 있습니다. 테라폼, cicd, kibana등 어마무시한것들이 많아요 ㅎㅎㅎ

 

  • Frontend : React
  • Backend: : Node.js, AWS SAM CLI
  • Database : MongoDB
  • CI/CD : GitHub Actions
  • Cloud(AWS) : Lambda, API Gateway, S3, CloudFront, Route53, WAF, Parameter Store, Secrets Manager, Budgets, Chatbot
  • IaC : Terraform(HCP Terraform)
  • Logging : CloudWatch, Logstash, Elasticsearch, Kibana
  • ETC : Git/GitHub, Slack, Notion

 

 

팀프로젝트 스택 내용은 demo버전 github organization의 readMe 파일에 에 정리되어있습니다^^

 

 

 

 

Lambda에서 개발환경 & 배포환경 구분

제가 보통 사용하는 백엔드 개발 환경만 그림으로 간단하게 그려봤는데요.

node index.js

자바스크립트로 코드를 런타임으로 돌리려면 이런식으로 node명령어로 파일을 실행시킵니다.

저는 저장만 하면 새로 코드를 다시 실행시켜주는 nodemon을 주로 사용합니다.

express 프레임워크로 개발한 백엔드 서버의 경우는 스크립트를 nodemon으로 실행하면 로컬에 포트로 서버가 띄워지죠.

리액트같은경우는 npm start로 로컬에 프로세스를 띄우고 개발을 하곤하죠?

완전 개발을 처음 접하시는분들도 익숙한 개념입니다.

 

하지만 Lambda로 백엔드 서비스를 구성해야하는 remember me 서비스는 어떻게 개발환경과 배포환경을 구분해야할까요?

쉽게 상상이 안됩니다.

이 때 사용한 툴이 aws sam cli입니다. 

 

aws sam cli는 정말 생소한 툴인데요. 서버리스 서비스를 로컬에서 돌아가게 만들어주는 aws에서 배포한 툴입니다.

 

일반적으로 lambda로 serverless 백엔드 서비스를 람다로 구축하시기위해선 api gateway랑 연결한 lambda로 저런 아키텍쳐로 운영을 하게되는데요.

SAM CLI로 로컬에서 api gateway, lambda 백엔드 서비스가 구축이 됩니다!!

신기하죠?

 

람다는 컨테이너 단위입니다. 때문에 aws sam cli로 로컬환경에서 백엔드 서비스를 만드려면 도커를 설치하셔야해요.

sam build --no-cached
sam local start-api --env-vars ./env.json --port 포트번호

react나 vue, nest js 를 사용하신분들은 build가 익숙하실텐데, sam cli도 사용하시려면 빌드를 먼저해야합니다.

이후 에 start-api명령어로 api gateway+lambda환경을 실행 할 수 있습니다.

 

build가 되었네요~
start api명령어를 치면 단일진입점(api gateway endpoint)이 로컬에 생깁니다~

 

 

명령어 구분

 

위와같이 코드를 수정을 할때마다 build와 start api명령어로 계속 컨테이너를 띄워줘야 했는데요.

때문에 개발할때 명령어도 따로 나눠야했어요.

//node 명령어
node -e "require('./src/handlers/test').handler(require('./event.json'), null).then(console.log)"

간단하게 코드만 테스트하기위해서는 node명령어를 사용헀구요.

 

//event.json
{
  "queryStringParameters": {
    "request": "connectDb"
  }
}

더해서 람다에 전달할 event를 json으로 만들어야하구요.

sam build --no-cached
sam local invoke TestFunction --env-vars ./env.json --event ./event.json

실제 람다 컨테이너하나만 호출하려면 invoke명령어로 해야합니다. (event.json설정해야함)

sam build --no-cached
sam local start-api --env-vars ./env.json --port 포트번호

이건 전체 람다를 띄우는 커맨드입니다.

 

또한 환경변수도 나눠야했어요.

.env가 있고, env.json이 있습니다.

.env는 node명령어에선 됐는데 build후 람다컨테이너를 띄우면 못가져가더라구요.. 그래서 env.json으로 했습니다.

 

또한 중요한건 template.yaml로 띄울 람다 정의와 이것저것 설정을 해야하는데요

 

backend/template.yaml at main · vocaAppServerless/backend

Contribute to vocaAppServerless/backend development by creating an account on GitHub.

github.com

이 template.yaml형식대로 정리를 해야해요.

이 template.yaml을 기준으로 sam이 build를 한답니다.

참고로 cicd팀원분도 이 template.yaml을 기준으로 cicd코드를 코딩하셨어요.

 

 

sam cli를 추천하시나요?

 

 

sam은 일단 코드 수정후에 저장한다고 바로 환경에 적용이 안됩니다. ㅡㅡ

코드를 한 글자만 수정을 하더라도 적용시키려면 build+start api를 다시 해줘야하는데 이 과정이 5분씩 잡아먹어요

그리고 제 노트북은 5년된 간신히 숨만 붙어있는 산송장 노트북이라 리소스를 엄청 잡아먹는 sam을 돌릴때마다 팬이 아주 미친듯이 돌아갔답니다...

 

개발환경과 배포환경을 완벽히 분리했지만 너무 힘들었어요.

때문에 위에 명령어들 줄줄히 적어놓은게 어떻게든 sam cli를 활용해서 개발을 하려고 발버둥쳤던 흔적입니다..

환경변수도 node명령어 실행때, sam으로 컨테이너 띄웠을때, 실제 배포했을때 전부 다르게 설정해야했습니다.

 

지금까지 개발할때마다 control + S를 누르면 바로바로 코드가 프로세스에 적용하게 해줬던 nodemon과 live server등 개발도구들의 소중함을 너무나도 절실히 깨달은 경험이었습니다.. 진짜 여러분들 이거 진짜 체감못하실거에요.. 저 이거하고 탈모왔어요

 

근데 사실 sam말고 어떻게 람다환경에서의 개발 배포환경을 구분해야할지는 다른 좋은방법이 생각이 안납니다.

실제 컨테이너 이미지를 만들어서 해야할까요? 아니면 aws에 private네트워크를 만들고 거기서 lambda, api gw를 띄워야할까요? 시도는 안해봤지만 많이 어려울것 같다고 생각드네요.

 

암튼 너무 어렵다기보단 짜증이났던 경험이었습니다.. 구글링해도 sam cli에 대한 자료가 거의 없구 gpt도 수집할 자료가없어서인지 엄청 틀리더라구요. 하나하나 조금씩 수정하며 테스트해봐야했는데 테스트할때마다 5분씩 걸리니...하.. 암튼 결국 완성은 했지만요.

 

 

 

 

 

 

 

람다에 대해서

 

일단 이번 포스팅에서 제가 전해드리고 싶은 내용을 말씀드리기위해 개념부터 정리하는 식으로 차근차근 포스팅하겠습니다.

 

위에서도 말씀드렸지만 람다를 보통 함수? 라고 생각하시는데요. 람다는 정확히는 컨테이너입니다.

aws에서도 람다함수가 하나 호출되면 해당 람다함수의 컨테이너가 띄워지고 이벤트를 받고 처리후에 응답을 주는 구조랍니다.

지금부터 lambda의 특성중 제가 주목한 특성 세 가지를 먼저 소개해드릴게요.

매우 중요한 부분입니다. 성능최적화에 관련이 있는 내용입니다.

 

 

1. Lambda는 세가지 state가 있습니다. - clod start & warm state & idle state

 

cold start : 람다가 막 시작해서 초기화부터 이뤄져야할 상태에요. 한동안 람다에 이벤트가 없어서 컨테이너가 없는 상태라 다시 만들어야하는 상태입니다. 

 

warm state : 이미 cold start가 이뤄진 컨테이너라 다음 이벤트를 받을때 컨테이너 초기화가 필요없는 상태에요

 

idle state : 한동안 람다에 요청이 없어서 삭제대기중인 상태입니다. 람다는 삭제될수 있다는걸 알려드리고 싶어서 추가했어요. 별로 중요하진않아서 그냥 이런상태가 있다~정도만 생각하시면되요.

 

예시를 들어드릴게요

먼저 무찌가 보낸 이벤트는 람다가 cold start상태라서 초기화가 필요해요. 컨테이너가 생성되야하죠. 그래서 이벤트가 10초나 걸려서 처리가 됐죠.

근데 이후에 도리가 이 람다에 또다시 요청을 합니다. 이때는 무찌가 이미 람다를 따뜻하게 해놨으므로 warm state상태입니다.

이벤트 처리가 5초밖에 안걸렸네요. warm state인 람다는 이벤트를 더욱 빠르게 처리해줘요. 이미 cold start때 썼던 메모리룰 기억하고있어서 재사용하면 되거든요~

 

중요한건 람다는 처음 요청을 받으면 cold start상태로 시작되고 초기화가 필요하기때문에 이벤트에대한 응답을 비교적 늦게줍니다는 점이에요. 이후로 오는 이벤트는 이미 한번 사용된 람다 컨테이너를 재사용할수 있으니까 warm state상태라서 더욱 빠른 응답을 주는거죠. 이 부분을 기억해주세요. 밑에 설명할 캐싱기능을 이 람다의 특성들을 고려해서 만들었거든요.

 

또한 람다는 여러 상태가 있는데 람다컨테이너가 사라지는 타이밍은 저희가 가늠할수 없습니다. aws만의 규칙이 있겠죠? 이 점도 중요합니다.

 

2. 람다의 이벤트 처리방식

그림처럼 상황을 가정해봅시다.

거의 동시에 두 이벤트를 클라이언트한테 받았다고 하면요.

람다는 먼저 들어온 이벤트를 처리하고 있겠죠? 근데 또다른 이벤트가 무찌람다에 요청을 하게되니까 람다는 이걸 동기처리하든, 비동기처리하든 해야한답니다.

결론적으로 람다는 한번에 한 이벤트만 처리합니다!! 매우 중요합니다.

만약 먼저온 이벤트를 처리중인데 다른이벤트가 오면 받질 않아요.

 

 

 

3. 그럼 람다에 여러 이벤트가 오면 무한히 기다려야하나요? - 람다의 오토스케일링

 

상황을 가정해볼게요.

한 클라이언트가 람다를 호출했어요. 그럼 람다가 클라이언트의 이벤트를 처리중이겠죠?

 

이벤트를 처리중에 두 클라이언트가 갑자기 이벤트를 보냈어요. 그런데 람다는 클라이언트의 이벤트를 처리중이니 이 이벤트들을 받지 못할겁니다.

 

이때 람다는 요청을 처리하기위해 자동으로 오토스케일링이 됩니다!! 똑같은 람다 컨테이너를 여러개 더 만드는거죠.

이렇게 람다가 여러 이벤트를 컨테이너 하나당 한 이벤트 라는 규칙을 가지고도 동시에 처리가 가능합니다. js의 비동기처럼요.

멀티프로세싱같은거라고 보시면 되요.

참고로 새로 생긴 람다는 새로 만들어지니 초기화가 이뤄져야하니 cold start일겁니다.

 

이 람다의 오토스케일링 특성도 매우 중요합니다.

 

지금 이 세가지 람다의 특징을 꼭 기억해주세요!! 밑에 설명할 캐싱기능과 매우 밀접한 관계가 있습니다.

 

 

 

 

람다의 특징을 고려한 캐싱


nurd worker는 두가지 요소에 대해서 캐싱기능을 고려했습니다.

시크릿디비 커넥션캐싱했어요. 하나씩 나눠서 설명드릴게요

 

Secret부터~~

 

위에서 말씀드렸듯이 sam cli를 사용했어서 이번 remember me 프로젝트는 환경변수를 세가지로 나눴는데요.

환경변수가 뭐 중요한값이니 시크릿이라고 보면됩니다.

개발환경에선 .env와 env.json을 읽어와서 사용하니 딱히 캐싱까지 할필요없어요. 그냥 그때그때 필요할때마다 읽어와서 갖다쓰면 되니까요.

 

중요한건 배포환경일때 aws secret manger에서 비밀값을 가져온다는 점이었습니다.

 

 

< secret manger의 과금방식 >

 

aws 시크릿매니저는 여러가지 과금요소가 있는데요. aws서비스는 다들 api로 이뤄집니다.

secret manager에서 시크릿값을 가져오는것 또한 api요청으로 받게되는데요

과금요소중 하나가 이 api요청의 횟수에 있습니다!!

 

 

그림처럼 secret값을 필요로하는 이벤트가 매번 발생할때마다 secret manager에 api접근을 한다면 과금이 엄청 되겟죠?

때문에 캐싱을 구현한겁니다.

 

< 어떤 식으로 캐싱을 구현했냐면요.. >

//전역에 이렇게 시크릿 빈객체를 선언해둠.
let cachedSecrets = {};



// main handler에서 secret 값을 가져오는 함수호출
exports.handler = async (event) => {
  // caching
  cachedSecrets = (await checkCachedSecrets(cachedSecrets)).secrets;

  // response by request
  switch (requestType) {
    case "type1":
      return data;
    default:
      console.log("Invalid request type on lambda:", requestType);
      return respond(400, { message: "Invalid request from lists get lambda" });
  }
};

일단 코드를 최대한 짧게 보여드릴게요.

이렇게 전역에 cachedSecrets이라는 빈객체를 만들어놓구요.

메인핸들러(람다가 호출되었을때 실행되는 코드)에서 해당 객체에 secret을 체크하는 함수를 호출해서 시크릿값을 가져옵니다.

인수로 현재 cachedSecret객체를 넣어줬죠?

그럼 저 checkCachedSecrets함수는 어떻게생겼냐면

const checkCachedSecrets = async (cachedSecrets) => {
  try {
    if (cachedSecrets.dbSecrets && cachedSecrets.oauthSecrets) {
      return { message: "there is cached secrets", secrets: cachedSecrets };
    } else {
      const nonCheckedSecrets = await getSecrets();
      if (!nonCheckedSecrets.dbSecrets || !nonCheckedSecrets.oauthSecrets) {
        throw new Error("There are some empty secrets from aws");
      } else {
        const secrets = nonCheckedSecrets;
        return {
          message: "there is cached secrets",
          secrets: secrets,
        };
      }
    }
  } catch (err) {
    throw new Error("Failed to retrieve or cache secrets: " + err.message);
  }
};

만약 인수로 받은 secret들이 이미 존재하고 있다면 그대로 리턴을 해주고요.

만약 빈객체다? 이러면 getSecrets함수를 호출해서 secret매니저에 api요청을 보내는겁니다.

 

자 그럼 아까 말씀드렸던 람다의 cold start와 warm start를 살펴봅시다.

 


< secret  캐싱의 플로우는~ >

그림처럼

cold start시에 전역에 선언한 secret객체가 비어있는걸 확인후에 secret manager에 api요청을 보내서 시크릿값들을 가져옵니다.

이후에 또 다른 이벤트가 발생하면 아까 cold start시에 사용해놨던 secret값이 남아있으므로 이 값을 재사용하는거죠.

 

람다가 언제 사라질지는 aws맘대로라고 말씀드렸잖아요? 이렇게 코드를 설계하면 한 람다 컨테이너는 이벤트가 매번 오더라도 살아있는 동안 딱 한번만 secret manager에 api요청을 보내면 되니까 비용이 매우 절약되겠죠?

 

전역변수로 사용한 cachedSecrets 객체는 warm start시에는 데이터가 남아있답니다. 이 특성을 활용하여 캐싱을 구현하였어요.

 

 

데이터베이스 커넥션 캐싱 !
//전역에 null이 할당된 cachedDb
let cachedDb = null;

// main handler
exports.handler = async (event) => {
  cachedSecrets = (await checkCachedSecrets(cachedSecrets)).secrets;
  cachedDb = (await getDb(cachedDb, cachedSecrets)).db;
};

 

데이터베이스 커넥션 캐싱도 마찬가지로 코드가 이렇게 되어있어요. 필요없는건 다 짤랐는데 위에 구조랑 비슷하죠?

같은 원리입니다.

데이터베이스 커넥션은 객체형태입니다~ 그래서 아까 cold start시에 사용했던 커넥션 객체가 메모리에 남아있다면 이 커넥션을 계속 사용해서 그대로 데이터베이스 접근이 가능한거죠~

데이터베이스 커넥션은 비용적인 측면에선 제 무찌 클러스터에서 존재하니까 비용걱정은 없지만 매번 데이터베이스에 접근할때마다 커넥션을 새로 생성한다면 성능저하가 일어난답니다.

때문에 커넥션을 캐싱해서 사용했어요.

 

 

 

< 왜 pool안썼어요? 바보세요? >

 

일단 커넥션풀의 목적을 알아봅시다.

커넥션풀을 백엔드서버에서 보통 사용하시는데요(ORM말고..)

 

첫번째로 커넥션의 재사용입니다.

db요청을 할때마다 커넥션을 만들고 끊고 하는건 낭비입니다. 위에 설명드렸듯이요.

그래서 풀에 커넥션을 따놓고 db요청이 있을때마다 재사용을 할 수 있죠.

 

두번째로는 동시에 여러 요청이 왔을때 비동기적으로 코드가 돌아갈때(웹 api처리같은거요) db요청을 비동기적으로하기 위함입니다. 비동기적으로 처리하려면 여분의 커넥션이 여러개 필요하겠죠?

때문에 커넥션풀에 데이터베이스에 여러 커넥션을 따와놓죠. 그림에선 다섯개를 따왔네요.

 

그림처럼 여러 클라이언트가 짧은간격으로 db요청을 한다면 여러 api요청을 처리할텐데요. 커넥션 하나를 여러 실행컨텍스트에서 나눠쓸수 없으니 아까 미리 쟁여왔던 커넥션을 하나씩 갖다 쓰게 해주는거죠.

 

이게 커넥션 풀 개념인데요.

이 커넥션 풀을 만약 람다에 적용하면 어떻게될까요?

 

 

 

람다 컨테이너가 커넥션 다섯개를 미리 따왔다고 가정해볼게요

근데 혹시 기억하시나요? 람다 컨테이너 하나는 이벤트를 동시에 하나밖에 처리못합니다

 

 

바로 이 이유입니다. 람다는 이벤트를 동시에 하나밖에 처리못해요. 그래서 커넥션 다섯개를 따왔어도 어차피 한개만 쓸수밖에없다는거에요.

저는

커넥션 풀의 커넥션을 재사용한다는 점은 람다에 적용하고싶어서 커넥션 객체를 캐싱하는 방식으로 코드를 구현했구,

커넥션을 여러개 안정적으로 확보해둔다는 풀의 특징은 쓰기 싫어서 풀을 사용하지 않았습니다.

 

 

만약에 람다가 오토스케일링을 한다면 한 람다 컨테이너마다 커넥션을 다섯개씩 따올테니 그만큼 커넥션 낭비는 심해질겁니다.

 

 

 

 

백엔드 람다코드 템플릿화


백엔드에서 중요한점만 정리하는데도 많네요 ㅠㅠ 읽느라 고생하십니다

이번엔 백엔드 람다코드의 템플릿화를 말씀드릴건데요

 

// import necessary functions
const {
  checkCachedSecrets,
  getDb,
  auth: { getOauthMiddleWareResult },
  apiResource: { respond },
} = require("./rbm-helper");

// declare cached data
let cachedSecrets = {};
let cachedDb = null;

// handlers
const handlerFuncs1 = async (event, authResult, email) => {
  try {
	핸들러 코드 실행~
  } catch (error) {
	에러처리
  }
};

const handlerFuncs2 = async (event, authResult, email) => {
  try {
	핸들러 코드 실행~
  } catch (error) {
	에러처리
  }
};

// main handler
exports.handler = async (event) => {
  //get params request and email
  const requestType = event.queryStringParameters?.request;
  const email = decodeURIComponent(event.queryStringParameters?.email);

  // caching
  cachedSecrets = (await checkCachedSecrets(cachedSecrets)).secrets;
  cachedDb = (await getDb(cachedDb, cachedSecrets)).db;

  //auth middle ware
  const authResult = await getOauthMiddleWareResult(
    event,
    email,
    cachedSecrets,
    cachedDb
  );
  if ([400, 401, 419, 500].includes(authResult.code)) {
    return respond(authResult.code, {
      authResponse: authResult.authResponse,
    });
  }

  // response by request
  switch (requestType) {
    case "요청타입1":
      return handlerFuncs1(event, authResult, email);
    case "요청타입2":
      return handlerFuncs2(event, authResult, email);
    default:
      console.log("Invalid request type on this lambda:", requestType);
      return respond(400, { message: "Invalid request from this lambda" });
  }
};

최대한 짧게 요약했는데도 이러네요.. 

remember me 프로젝트의 모든 람다코드는 이렇게 템플릿화를 해놨어요.

 

그림으로 보여드리자면 이렇습니다. 모든 람다코드가 이렇게 템플릿으로 규격화 되어있어요.

나중에 유지보수를 할때 추가기능이 있다면 추가적인 핸들러만 추가하면되고, 버그난 플로우는 해당부분만 찾아서 손보면 되겠죠?

 

또한 유저 인증기능을 아직은 설명안드렸는데요~ 유저인증이 필요한 기능이있으면 저렇게 미들웨어 코드를 거치게됩니다.

remember me 프로젝트는 개인단어장 어플이다보니회원가입뺴고 거의 다 인증요청이에요. 

유저인증관련한건 밑에서 더 설명드릴예정입니다.

 

 

 

 

frontend

 

자 이제 좀 머리좀 식히고 갈게요.

저는 이번 프로젝트에서 타입스크립트와 리액트를 처음 사용해봤습니다.

처음 쓰는거라 많이 서투르지만 제가 가장 중요하게 생각했던건 전역데이터 설정이었어요.

 

1. auth.ts

인증관련 함수와 인터셉터가 있어요. 정적으로 return해줍니다.

 

2. staticData.ts

정적인 데이터나 함수, 큐만드는 생성자 클래스가 있습니다.

 

3. queueContext.tsx

큐 데이터를 전역에서 참조하기 위한 큐를 useContext로 전역에 뿌렸어요. queue는 useState훅으로 만들어서 상태로 사용했습니다.

 

4. fucs.ts

전역에서 쓰는 함수지만 얘네들은 redux스토어 값에 접근 및 수정이 필요한 함수들입니다. 때문에 커스텀훅으로 만들었습니다.

 

5. store.ts

리덕스 상태값들이 저장되어있어요.

 

 

저도 처음 리액트를 사용해보는지라 미숙하지만 이렇게 데이터와 상태들을 전역에서 참조가능하게 사용했어요.

 

 

 

프론트엔드의 성능 및 비용 최적화 - batch queue

 


일단 들어가기 앞서서 람다의 과금요소부터 생각해볼게요

람다는 과금요소가 여러가지 있지만 호출횟수에 따라서도 과금이 됩니다.

물론 평생 free tier로 운영되는 서비스지만 제한호출횟수를 넘길지 혹시 모르잖아요?

제가 걱정스러웠던 경우는 단어나 단어장에 대한 업데이트 기능이었어요. 그림과같이 단어 뜻을 여러번 바꾸거나 할일이 많을텐데요.

단어장 특성상, 단어를 수정하거나 오답노트에 추가한다거나, 삭제한다거나 여러번 수정기능이 있거니까요.

이럴때마다 람다를 호출하면 비용이 꽤 많이나오겠죠?

그래서 데이터를 쌓아서 한번에 보내는 방식이 필요했습니다.

 

그럼 데이터가 더 커지잖아? 생각하실수도 있는데 우리가 평소 보내는 api요청 객체에 비해서는 새발의 피입니다.

api 요청 객체가 생각보다 커요

새발의 피만큼 용량적은 axios 객체를 열번보내는것보다 새발의피 열방울 들어있는 axios객체 하나보내는게 훨씬 용량이 적거든요.

 

단어 수정 정보를 큐에 쌓자~

이렇게 단어를 수정할때마다 큐에 정보를 쌓았습니다.

 

word queue는 트리거로 람다함수 호출을 등록했고 큐 트리거 발동조건을 queue데이터 10개로 설정했습니다.

이제 이전에는 10번 보내야할 람다 호출이 한번으로 줄어들었네요~

 

 

추가적인 queue 최적화

그림처럼 무찌를 바보라고 등록했다가

생각이 바뀌어서 똥개로 바꿨다가

마지막으로 멍청이 라고 수정을 했다고 하면 이전에 수정했던 두 데이터는 필요가 없겠죠? 가장 마지막 결과만 있으니까요.

때문에 큐데이터를 추가할때마다, 같은 단어의 수정기록이 있다면 삭제해주게 프로그래밍 해놨어요.

 

그럼 효율이 batch queue를 도입함으로써 열배 좋아졌었는데, 이렇게 중복수정도 처리한다면 열배이상으로 더 좋아지겠죠?

 

 

트리거 발동조건 (큰 실수..)

 

예를들어 사용자가 탭을 닫거나 꺼버릴때 이 큐가 트리거가 발동되기 전이라면 데이터베이스에는 이 수정 기록이 안남을겁니다.

그래서 전 

  window.addEventListener("beforeunload", (event) => {
    if (
      window.location.href.includes(
        "https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount"
      )
    ) {
      return;
    }
    saveListsQueueDataAtDb();
    saveWordsQueueDataAtDb()
      .then(() => {
        event.preventDefault();
      })
      .catch((error) => {
        console.error("Error saving data:", error);
        event.preventDefault();
      });
  });

이렇게 beforeunload라고 윈도우 전역객체에서 이벤트리스너를 만들었는데요.

이게 모바일에서 할때는 적용이안되더라구요 ㅠㅠ

브라우저 호환성을 고려안한 제 잘못이죠..

아직까지 마땅한 트리거가 생각이안나서 임시방편으로 라우터 이동시에 큐를 람다에 보내는 코드를 추가해놓긴 했습니다.

  useEffect(() => {
    console.log("라우터가 변경되었습니다!", location.pathname);
    if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
      saveListsQueueDataAtDb();
      saveWordsQueueDataAtDb();
    }
  }, [location, saveListsQueueDataAtDb, saveWordsQueueDataAtDb]);

라우터 변경시 람다호출하는 코드를 임시방편으로 추가해놨어요 ㅠㅠ(코드에선 아이폰만해놨지만 지금은 모바일로바꿨어요)

제 실수죠 뭐.

 

 

 

 

유저인증 플로우

 

먼저 회원가입, 유저인증, 인증처리 등의 유저인증에 관한 기능은 google oauth를 사용하여 jwt방식으로 구현했습니다~

이전에 아빠 또갈까 프로젝트도 cognito와 google oauth로 jwt를 사용했지만, access token만 사용했거든요. 코그니토는 refresh토큰을 적용하기 복잡합니다.

 

하지만 이번에 remember me 프로젝트는 refresh 토큰 또한 사용했습니다~

 

'security' 카테고리의 글 목록

지극히 이기적인 개발 블로그입니다.

jacobowl.tistory.com

jwt방식에 대한건 security카테코리의 포스팅을 참고해주시면 됩니다.

 

 

어떤점이 특별하냐면요

 

jwt방식으로 유저인증을 구현하시는 방법은 개발자분들마다 다르더라구요.

nurd worker는 최대한 사용자의 편의성을 최고로 중요시하거든요~

 

만약 이런 상황이 있다고 한다면 어떻게 될까요?

 

무찌가 어제까지 remember me 프로젝트로 단어공부를 헀답니다

그리고 자고 일어나서 다시 공부를 하려고해요

 

 

그럼 이렇게 access token이 만료가 되어있는 상태일겁니다.

 

 

화면이 그대로입니다. remember me는 개인단어장용 어플리케이션이므로 인증된 요청이어야만 보이는 화면이죠.

새로고침을 안한채로 다음날 다시 보니까 인증이 필요한 화면이 그대로 남아있습니다. 하지만 access token은 만료된 상태일겁니다.

 

이런 상황에서 무찌가 만약 인증이 필요한 버튼 을 클릭하면 어떻게될까요?

remember me 프로젝트에서는 refresh 토큰 갱신과 함께 기존 요청에 대한 응답도 해주게됩니다!!

 

 

어떻게 했냐면..

 

 

rememberme-advanced-front/front/src/auth.ts at main · nurdworker/rememberme-advanced-front

Contribute to nurdworker/rememberme-advanced-front development by creating an account on GitHub.

github.com

 

응답인터셉터를 보시면 이렇습니다.

코드를 가져오면 너무 지저분해질것같아 설명만 드릴게요.

 

응답을 받았을때 419응답을 받으면 기존에 보냈던 axios요청을 통째로 다시보냅니다.

다만 헤더값에 refresh토큰을 추가해서 다시보내죠

 

 

rememberme-advanced-back/backend/src/handlers/rbm-helper.js at main · nurdworker/rememberme-advanced-back

Contribute to nurdworker/rememberme-advanced-back development by creating an account on GitHub.

github.com

그럼 백엔드 인증 미들웨어에서 해당요청을 검증하고 처리한후, authResult에 새로운 access token을 담아서 보냄과 동시에

answer프로퍼티에 기존 했던 요청에 대한 데이터도 응답해주게 됩니다.

 

몇몇 서비스들은 로그아웃을 하게 한다거나 버벅거림이 있겠지만 nurd worker는 사용자 편의성을 중요시하므로 이렇게 사용자가 access token이 만료된걸 전혀 느끼지 못하게 인증기능을 구현을 하였습니다.

 

때문에 코드를 보시면 아시겠지만 따로 refresh갱신을 위한 api자체가 없습니다. 걍 미들웨어에서 다 처리해요.

 

 

토큰 로그 db 저장

 

람다쪽 미들웨어 코드를 보시면 아시겠지만 토큰로그를 저장해요.

보안이 더욱 좋아졌습니다.

사실 구현할생각은 없었는데 google oauth api가 access token을 검증하는 api를 보낼때

access token이 만료된거든 위조된거든 똑같은 invalid token이라는 응답을 줍니다 ㅡㅡ

 

제 jwt포스팅 시리즈에서도 보셨듯이 전 두가지 조건을 동시에 만족해야 새로운 acess token을 발급해줬거든요

1. refresh 토큰이 유효해야한다

2. access token이 만료되었어야한다

 

이 두가지 조건을 and조건으로 만족해야 준다고했는데

google oauth가 2번 조건을 명확하게 해주질 못했어요

 

때문에 db에 token로그라는 collection을 따로 만들어서 토큰을 기록해두고 이 기록에따라서 만료조건을 체크했습니다.

 

 

 

 

팀프로젝트 - 첫 팀장으로써 진행

휴.. 지금까지 읽어주셔서 감사합니다.

이제부터는 팀프로젝트에 대해서 짧게 설명드릴게요

 

저희는 노션을 사용을 주로 하였어요.

프로젝트가 급하게 시작이 되어서 노션제작을 후딱 한다음 회의하면서 사용방법을 알려드렸어요

사진에 보이는것보다 더욱 많은데, 자세한 내용은 직접 독자분과 만날 기회가 있다면 알려드리겠습니다 ^^

 

 

이렇게 캘린터로 팀원분들의 진행상황을 확인했습니다.

이 방식은 이전에 아빠 또갈까 프로젝트에서 task 방식으로 협업하는걸 보고 저도 적용했습니다 ㅎㅎ(저는 고라파덕입니다..ㅋㅋ)

 

이런식으로 개인마다 task를 정리해서 기록했어요.

저 혼자 작성한 task만 저 사진에 보이는 task의 다섯배정도 됩니다 ㅋㅋㅋㅋ

 

실제로 nurd worker가 취준할떄 면접관이셔서 증명을 원하시거나 혹은 친해지시면 자세히 노션을 보여드릴 수 있습니다~

저기에 서로 사용했던 중요데이터나 이런정보도 있어서 노션을 배포해서 블로그에 공개를 하기는 어렵네요

 

 

 

시연영상 촬영

제가 특별하게 요청드렸던것은 본인이 개발한 분야에대한 시연영상을 제작해달라고 말씀드렸습니다.

왜이랬냐면, 사실 저희가 프로젝트를 만들고나서는 보통 대게는 서비스를 중지하게됩니다.

깃허브에 코드만 달랑 남겨두거나 하게되죠. 실제로 demo버전은 프로젝트 종료후 바로 배포중지가 되었어요~

 

만약 나중에 프로젝트로 사용하려면 까먹기도하고 사용했던 툴들이 갖가지 버전문제로 에러를 일으킬수도 있어요. 실제 로컬에라도 배포가 되어야 증명이 될텐데.. 난감해지죠.

 

예를들어 무무티비프로젝트도 VM이 다섯개에 컨테이너만 열개이상 띄워져있는데 1년전에 프로젝트를 다시 띄우고 하려면 기억도안나고 에러도 많이 날겁니다. 그래서 nurd worker도 무무티비 포스팅에 사용된 영상 소스들을 모두 당시에 프로젝트 종료후 바로 촬영해놨던겁니다.

 

영상으로 기록을 남기는건 중요하다고 봅니다. 가장 좋은 방법은 실제 배포해놓는것이지만 현실적으로 어렵거든요.

취직을 목표로 하는 경우 면접관분들이 진짜 개발했다는걸 믿지 않을확률이 크거든요. (사실 저도 잘 안믿습니다 ㅋㅋ)

그래서  nurd worker 또한 프로젝트마다 영상작업을 하고있고 구현영상남기는걸 굉장히 중요하게 생각해요. 

나중에 이 프로젝트를 다시 사용하실때 필요하실거라고 판단되서 오더를 드렸었습니다.

저한테 증명해서 뭐가중요한가요 전 테라폼같은 고급 스킬 볼줄도 모르는데요ㅠㅠ

팀원분들이 좀 독특한 팀장을 만나 고생을 좀 하시긴 한것같기도 하네요 ㅎㅎㅎ

 

프로젝트 피드백

nurd worker가 팀원분들에게 받은 피드백입니다~

이러한 평가를 받았습니다.

스터디는 많이 진행해봤었으나,

엔지니어로써 팀을 이끌기는 처음이라 많이 미숙했던점도 많았고 반성할점도 많았던것 같습니다 ^^

 

 

 

 

아키텍쳐

 

demo 버전 아키텍쳐

 

인프라 팀원분들이 그려주신 demo버전 아키텍쳐입니다. 역시 인프라팀분들 실력이 어마어마합니다 ㄷㄷ

demo 버전은 프론트서버를 s3버킷에서 정적호스팅 하였구요.

그리고 백엔드api gateway와 람다 구조로 이용하였구요.

데이터베이스몽고디비를 사용하였습니다.

기타 등등 여러가지 있답니다 ㅎㅎ 저도 잘 모르는것들이 많네요~

 

 

 

advanced 아키텍쳐

현재 배포된 remember me 프로젝트 advanced버전의 아키텍쳐입니다.

 

프론트 웹서버는 무찌 클러스터에서 운영이 되어있구요.

백엔드 AWS의 api gateway + lambda구조로 운영되고있습니다.

또한 추가적으로 cloud watch와 SNS를 사용한 알람을 구현해놨습니다^^ (과금 무섭..)

데이터베이스무찌 클러스터의 mongo db를 사용하고 있답니다

 

 

 

 


Conclusion
사실 remember me 프로젝트는 acsap프로젝트의 약간 상위버전? 으로 간단하게
react와 type script를 사용한 어플리케이션으로 간단하게 준비하려고 한 개인 프로젝트였습니다.

근데 마침 너드림덱 프로젝트가 막 끝났을때 이전 네트워크 국비과정을 함께하셨던 분들이 연락이 오셨어요.
프로젝트를 같이 하자고 하시길래 얼떨결에 팀프로젝트로 변해버린 프로젝트입니다. ㅋㅋ
온라인으로 진행한 프로젝트였고 팀원한분은 프로젝트 시작하자마자 취업합격통보를 받으셨는데 책임감있게 해주셔서 감사드리네요.

암튼 하다보니 막 욕심도 나고 해서 여러가지 고려사항을 많이 녹여낸 프로젝트가 되었고
취업할때 굉장히 많은 포인트들을 어필할 수 있는 프로젝트라고 생각이 드네요. (때문에 포스팅도 토나오게 많이적었네요....)

긴 글 읽어주셔서 감사합니다 ^^

 

 

'project > teamProject' 카테고리의 다른 글

Team Project - 아빠또갈까?  (0) 2025.01.24