TL; DR

브라우저의 Web Workers API를 이용해
백그라운드에서 Google의MediaPipe 비전 AI 모델을 로드/초기화/추론을 진행하고
추론의 결과값을 UI에 실시간으로 표시하는 경우,

메인 스레드에서 AI 모델 로드/초기화/추론을 진행한 것보다
FPS가 감소하고,
추론 소요 시간이 증가한다.
(원인은 본문에서 알아본다)

이 때문에
비전 AI 모델을 활용하며,
이미지 추론이 실시간으로 이뤄져야하고,
그 추론의 결과 값이 UI에 또 실시간으로 반영되어야 하는 경우
브라우저의 백그라운드에서 AI 모델 연산을 실행하는 것은 적합하지 않을 수 있다.

그러나,
브라우저의 백그라운드에서 AI 모델 연산을 실행하는 것이
메인 스레드의 과부하를 감소시키는 데 도움이 되므로,
상황에 따라 실행 위치를 적절히 결정해야 한다.

 


이 글은 크래프톤 정글 부트캠프를 참여 당시
웹서비스 개발 팀프로젝트를 진행하면서 겪었던 문제와 해결책에 대한 내용을 담고 있다.

당시 시간이 부족해
검증 없이 도출한 해결책을 임시방편으로 적용했는데,
내내 찜찜한 마음을 떨칠 수 없었다.

부트캠프 수료 이후
문제를 되짚어보며 몇가지 실험을 진행해보았고,
의외로 그때의 결정이 최선의 방책이었음을 확인할 수 있었다!

실제로는 사후 검증이었지만,
글은 논리적으로 잘 읽히도록
문제 -> 가설/검증 -> 방안 도출의 순서로 작성하였다.

근데 이제 굉장히 긴 내용을 곁들인...

 

1. 문제 상황: AI 기능을 켜면 갑자기 화면이 버벅거린다. 왜?

1.1. 기대하는 결과: 영상통화가 진행중인 화면 위에, 안면/모션 인식 AI 모델의 추론 결과가 스무스하게 입혀질 것

우리가 최종적으로 기대하는 핵심 서비스의 실행 화면은 다음과 같았다. (구현은 성공함)

이 화면은 다음 5가지 조건을 한번에 충족해야 했다.

  • 조건1: 모바일 웹브라우저에서 작동할 것
  • 조건2: 여러명이 동시에 영상통화를 진행할 것
  • 조건3: 안면/모션 인식 AI를 실행하여, 사용자의 카메라에 입력된 데이터를 실시간으로 추론할 것
  • 조건4: 안면/모션 인식 AI의 추론 결과를 실시간으로 확인하여, 서비스 로직에 맞게 UI에 표현할 것
  • 조건5: 배경음악이 흐르고 있는 와중에, 조건3에서 확인한 AI 추론 결과에 기반하여 효과음을 실행할 것

고려해야 할 것이 굉장히 많았지만,
이 중에서 제일 중요한 과제는 조건 3, 4번이었다.

<제한시간 내에 고득점을 달성해야 하는 게임>의 컨셉을 가진 서비스였기 때문에
사용자가 카메라에 대고 수행하는 액션이 <실시간으로, 지연없이 반영되는 경험>을 제공하지 못하면
서비스 자체가 의미없어지는 상황이었다.

그러나 AI는 그렇게 호락호락하지 않았다.

 

1.2. 문제의 현실: AI 모델의 로딩~초기화~첫추론 과정에서 UI 렌더링이 지연됨

우리는 안면/모션 인식 기능을 구현하기 위해 Google에서 제공하는 MediaPipe의 Holistic, Pose 모델 라이브러리를 활용하였다.

이 모델로 이미지 데이터를 추론하기 위해서는 먼저 다음 두가지 과정이 선행되어야 했다.

1. 모델 활용에 필요한 각종 데이터 로드
2. 모델의 가중치 초기화

Google을 대단하게 본 것인지, 딥러닝을 만만하게 본것인지, iPhone의 성능을 믿은 것인지...
나는 위의 과정은 별 문제 없이 진행될 거라고 생각했고,
오히려 실시간 추론이 과부하를 더많이 가져올 거라 생각했다.

그러나, 우리가 모델 적용후 처음 맞이한 화면은 다음과 같았다.

얼굴에 주황색 점과 하얀선이 덮이기 전을 주목해보세요

주목할 시점은 바로 카메라에 비친 내 모습 위에,
<벡터 이미지 요소가 그려지기 전>까지의 화면이다.
문제가 느껴지시는지..?

자, 다시 이번엔 안면인식 AI를 실행한 화면을 확인해보자..

얼굴에 그물망이 덮이기 전의 시점에 주목해보세요

마찬가지이다.

여러분의 인터넷에 문제가 생긴것이 아니다.
분명 몇 초간 버벅거리는 모습이 확인된다.

이는 큰 문제였다. 왜냐하면

1. AI를 이용한 게임을 시작하기 전에 이미 영상통화가 진행된다.
   이때 다른 문제가 없는 한 유저들은 원활한 화면과 음성을 확인할 수 있다. 
   그런데 <게임만 시작하면 갑자기 화면이 버벅>거린다? 당연히 문제로 인식될 수밖에 없는 상황

2. 1.1.에서 언급한 것처럼, <시간제한을 두고 진행하는 실시간 게임인데 지연현상>이 발생한다?
    게임 시작도 전에 점수를 잃은 느낌에, 유저들의 전의를 상실하게 할 상황

이기 때문이다...

도대체 원인이 뭘까? 왜 버벅거리는 현상이 발생하는 걸까?

 

1.3. 원인: 가볍게 맞이하기엔 덩치가 너무 큰 AI 모델

위의 버벅거리는 화면이 녹화된 환경은 다음과 같다.

  • 당시 최신 크롬 웹브라우저
  • 롤해도 욕 안먹는 초고속 인터넷 통신망
  • MacBook Pro (2019) + 최신 macOS 버전

컴퓨터 성능이 의심될 수 있으나,
MacBook Pro M3에서도 같은 현상이 발생했다.
옛날 모델이지만 나의 할배컴..아직 정정함...ㅠ

즉,
모바일이라서, 인터넷이 느려서, 컴터가 후져서 버벅거리는 것이 아니라는 말.

그럼 무엇이 원인이냐?
AI 모델 로드~초기화~첫번째 추론까지의 과정에서 일어나는 일을 살펴보면 알 수 있다.

  1. AI 모델 로드
    • 우리는 MediaPipe의 Holistic, Pose 모델을 Google이 제공하는 <CDN으로부터 다운받아> 사용하는 방식으로 코드를 작성했다.
    • 여기서 Cloud로부터 로컬환경에 받아오는 과정에서 통신 시간이 발생한다.
    • 심지어 파일이 가볍지 않다.
      • node_modules에 다운받아둔 Holistic의 용량은 67MB에 달한다.
      • 이 중 제일 가벼운 모델구조와 가중치, 계산을 위한 바이너리 파일, 모델활용 인터페이스가 있는 파일만 골라내도 20MB 수준이다.
  2. 모델 초기화
    • 구멍이 송송 뚫린 모델 구조를 가중치로 채워넣는 과정.. 그것이 모델 초기화이다.
    • AI 모델은 이 과정을 통해 당장 추론이 가능한 상태로 메인 메모리에 얹어지게 되는 것이다.
    • 다행히 MediaPipe는 경량화된 모델을 제공하기 때문에, GPU를 사용할 수준의 연산량도 아니고, 웹브라우저에게 조차도 부담스러운 일이 아니라고 한다.
  3. 첫번째 추론
    • 첫번째 추론은, AI 모델 사용의 핵심이 되는 과정 중 첫번째 트라이에 해당한다.
    • 대부분 AI 모델과 마찬가지로, 이 추론 과정에서 엄청난 연산이 발생한다. 그러나 그 추론이 첫번째냐 두번째냐에 관계없이 연산량은 크게 차이가 없다.
    • 그러나 첫번째 추론은 속도가 굉장히 느리다. 왜냐?
      • 첫번째 추론이 일어나기 전까지는 메인메모리에 모든 명령어와 데이터들이 준비되어 있더라도, 그것을 연산할 CPU와는 아직 너무 멀리 떨어져 있기 때문이다. CPU까지 다 끌고 가느라 오래 걸린다.
      • 첫번째 추론이 실행되고나서야 비로소 CPU와 메인메모리 사이의 캐시메모리에 많은 데이터들이 저장되면서, 이후의 추론에 소요되는 연산 속도가 단축된다.
      • (참고) MediaPipe의 모델들은 default로 CPU를 사용하도록 설계되어 있다. GPU 쓰지 않아도 되도록 엄청나게 경량화/최적화 되어있다고 한다.

즉, AI 모델을 사용하는 것은 함수 `f(x) = 2x+3`에 5를 넣는 것처럼 단순하지 않다.
컴퓨터 입장에서 얘를 능숙하게 빠릿빠릿 사용하려면, 준비해야 할 것이 너무도 많다.

그런데 우리 서비스의 경우
여러 명과 영상통화를 실행하던 중 AI모델을 퍼뜩 실행하라 하니,
안그래도 이미 하고 있는게 많은 컴퓨터한테 다급하게 큰 일을 맡긴 꼴.

심지어 자바스크립트 엔진은 스레드가 하나짜리 아닌가?!

혼자서 이거하랴 저거하랴 그거하랴..
아무리 기계라지만 인간적으로(?) 버벅일 수밖에 없는 상황이었던 거다.

OK.. 일단 원인은 알겠다.. 근데 이걸 어찌 해결하나......????


2. 시행착오: AI 모델을 백그라운드에서 미리 준비해 놓자


핵심 문제는 다음과 같다.
다른 작업을 하는 메인 스레드에서 AI 모델 준비까지 동시에 하면 과부하가 걸린다는 것.

그래서 처음 생각한 해결방안은
AI 모델 준비 작업을 메인 스레드에서 분리하는 것이었다.

 

2.1. 시도 #1: 미리 첫추론까지 마무리 해놓은 상태의 모델을 디스크에 저장해서 재사용하자

상상력이 뛰어난 비전공자(나)는 준비완료된 모델을 그대-로 로컬에 저장할 수 있을 거라 생각했다.

전투 복장에 장전까지 모두 마친 채로 침낭에 있다가 전쟁나면 바-로 일어나서 총쏘라 하면 되는 거 아닌가?
(군대 안 다녀왔음)

첫추론까지 마무리한 '준비완료 모델'을 탑재한 상태로 앱을 배포하여,
모델이 필요한 시점에 그냥 실행만 하면 만사가 해결될 줄 알았다.

불가능이었다.

<AI 모델이 준비 완료된 상태>는
다양한 객체와 함수와 데이터의 조합이
매우 복잡한 구조를 가진채
메인 메모리와 캐시 메모리에 업데이트 되어있는,
말 그대로 살아있는, 동적인 <상태>이다.

이 상태를 '저장'하는 것은 불가능하다.

물론, 급속냉동으로 삶의 상태를 저장한 냉동인간이 있듯이...
AI 모델의 준비완료 상태를 저장하려는 다양한 연구가 진행되고 있다고 한다.

AI모델의 초기 로드에 걸리는 시간과 자원이 사용자의 경험을 악화시키는 경우가
우리에게만 일어나는 일은 아니라는 것.

그러나 아직 나같은 쪼렙이 쓸 수 있는 기술이 아니다.

아무튼 그래서 첫번째 아이디어는 나가리.

 

2.2. 시도 #2: 브라우저의 백그라운드에서 첫추론까지 마무리 해놓고, 메인에 가져와서 쓰자

chatGPT에게 물어보니 메인 스레드의 과부하를 줄이기 위해,
브라우저의 백그라운드에서 다른 작업을 실행할 수 있다고 했다.

메인 스레드 혼자 고생하는 웹브라우저의 세상에서
보이지 않는 손(?)의 필요성이 대두되었고,
맘따뜻한 천재 개발자들이 Web Workers API를 만들어 브라우저에게 달아주었다.

Web Workers는 보통은 대량의 데이터 로깅 작업이나 연산 작업을
메인 스레드 대신 수행하기 위해 활용된다 한다.

이에,
Web Workers를 이용해 백그라운드에서 미리 모델을 준비해놓고 필요할때 딱 가져다 쓸 방안을 생각하였다.

백그라운드에서 미리 AI 모델을 모두 준비시킨 뒤, 그 모델 컨텍스트를 메인스레드로 딱 가져다 놓을 생각이었다.

그러나 이것도 불가능이었다.

Web Workers와 메인 스레드 사이에 주고받을 수 있는 데이터의 형식이 한정적이었다.
(어떤게 가능한 지는 링크의 MDN 공식 문서에서 확인 가능)

리액트에서 Context API를 이용해
준비가 완료된 모델 Context를 필요한 컴포넌트들이 가져다 쓰듯,
Web Workers에서 업데이트한 Context를 메인 스레드가 받아 쓸 수 있을 줄 알았는데...

그래서 두번째 아이디어도 나가리.

 

2.3. 시도 #3: 브라우저의 백그라운드에서 로드/초기화/추론 모두를 진행하고, 결과값만 메인으로 보내자

앞서 #2에서 확인한 결과,
Web Workers와 메인 스레드 사이에서
준비된 모델의 상태는 주고 받을 수 없지만,
ImageBitmap, VideoFrame, 기본적인 자바스크립트의 객체는 통신 가능하다는 것을 알 수 있었다.

그럼 아예 이어지는 실시간 추론까지 Web Workers에 맡기고,
그 추론의 결과값만 메인 스레드가 전달 받으면
메인 스레드가 한결 홀가분해지지 않겠는가?

조와써!!!!!!!

이제 확인할 것은 Web Workers의 효능이었다.

메인 스레드와 Web Workers 각각에서
모델 로드, 초기화, 첫추론, 실시간 추론을 모두 실행한 경우

이 두가지 경우에 대해

  • 평균 FPS
  • 추론 한번에 걸리는 평균 시간
  • 첫추론을 요청한 뒤 첫 결과를 바탕으로 UI가 처음으로 렌더링되기까지 걸리는 시간

을 측정하여, Web Workers를 실제로 도입하는 게 나은지 검증을 해보기로 했다.

일단 간단하게 이 실험만 하기위해 테스트용 웹을 만들고,
Web Workers를 지원하는 MediaPipe의 tasks-vision 중 Pose 모델을 사용하였다.
(기존의 서비스를 개발할때 사용했던 Holistic과 Pise 모델은 Web Workers 지원이 미비하여 너무나 많은 에러가 발생함)

그리고 미리 촬영한 동영상을 주입하여,
같은 동작에 대한 반응성을 확인해보았다.

아래 영상의 왼쪽은 메인 스레드만 활용한 것, 오른쪽은 Web Workers를 활용한 것이다

언뜻 별차이 없어보이지만,
매의 눈으로 관찰해보면 메인 스레드의 반응성이 훨씬 좋은 것을 확인할 수 있다.

1. 영상 위에 아무것도 없던 상태에서, 모션 추론 결과를 나타내는 벡터 이미지가 처음으로 나타나는 시점이 메인 스레드가 조금 빠름
2. 메인 스레드의 영상 위에 그려지는 벡터 이미지의 업데이트 주기가 훨씬 빠름 (훨씬 자글자글한 움직임이 보임)
3. Web Workers를 이용한 영상의 모션 추론 결과 이미지가 그려지다가, 왼쪽 팔꿈치(보는 방향)의 위치가 바깥쪽에 찍히는 현상이 발생함

그러나 우리는 매가 아니기 때문에 수치로도 확인해봤다.

메인 스레드, Web Workers 각각 경우에 대해
실험을 10번 시행하여 지표값들을 평균내었는데,

컴퓨터를 오랫동안 쉬어두었다가 실행한 횟수 3번,
컴퓨터로 인터넷 서핑, 문서 작업 등 가벼운 작업을 하다가 실행한 횟수 4번,
컴퓨터로 개발 작업 등 리소스가 많이 쓰이는 작업을 하다가 실행한 횟수 3번,

이렇게 실험 환경은 그냥 느낌대로 구성해서, robust한 실험이라고 볼수는 없다..ㅎ
그럼에도 결과 수치는 꽤나 패턴을 파악할 수 있는 수준으로 나왔다.

Web Workers FPS Main Thread FPS Web Workers
Inference Time(ms)
Main Thread
Inference  Time
(ms)
Web Workers
First Delay
(ms)
Main Thread
First Delay
(ms)
21.75 58.68 45.98 17.04 907 734.2
21.72 58.3 46.04 17.15 909.5 741.2
20.09 53.26 49.78 18.78 909.9 759.3
19.84 51.94 50.4 19.25 917.9 772.1
19.78 50.37 50.55 19.85 936 795.2
19.51 49.12 51.25 20.36 941.6 805.4
19.19 45.26 52.12 22.09 957.8 809.5
16.78 45.18 59.59 22.13 973.1 856.6
16.1 43.93 62.13 22.77 976.7 878
14.95 38.04 66.88 26.28 980.6 882.8
평균 18.97 평균 49.408 평균 53.472 평균 20.57 평균 941.01 평균 803.43

놀랍게도 Web Workers와 메인 스레드 사이의 성능 차이는 크게 벌어졌다.

특히 Web Workers의 FPS는 30을 단 한번도 넘지 못했고, 추론 시간도 45ms 이하로 떨어지지를 못했다.
게다가 첫 추론 요청 시점으로부터 결과값을 기반으로 첫 렌더링을 실행할때까지 걸리는 시간은
실행환경에 따라 최대 150ms의 차이가 벌어져서 렌더링 시점의 안정성도 떨어졌다.

Web Workers와 메인 스레드 간의 성능을 비교해보면,

Web Workers의 평균 FPS
/ 메인 스레드의 평균 FPS
Web Workers 평균 추론시간
/ 메인 스레드 평균 추론시간
Web Workers 평균 첫렌더링 delay
- 메인스레드 평균 첫렌더링 delay
0.38배 2.60배 137.58ms

이거 왜 실험해봤나 싶을 정도로 Web Workers의 성능이 안 좋다..

우리에게 기대감을 주었던 Web Workers가 모욕감을 준 이유는 다음과 같다.

  • 이미지 송수신 시간 추가
    • 메인 스레드에서 비디오 영상에 대한 ImageBitmap을 추출하여 Web Workers로 보내고, 그 값을 추론한 결과 값을 다시 메인 스레드로 보내는 과정에서 통신시간이 발생한다.
    • 추론 값을 받고나서 다시 ImageBitmap을 생성하다보니 그 통신시간 만큼의 Frame을 캡쳐하지 못해 FPS가 떨어질 수밖에 없다.
    • 추론 값을 받기 전에 캡쳐하는 Frame마다 추론 요청을 보내면, 과부하가 걸려서 동영상이 재생이 한참 되고 나서야 결과값을 반영한 그림이 그려진다.
  • 평생 2인자 Web Workers
    • 브라우저에서 제일 중요한 것은 메인 스레드이다. 따라서 기본적으로 메인 스레드의 작업에 대한 최적화 성능이 더 좋다.
    • 이 때문에 추론 자체를 위한 연산 또한 메인 스레드가 훨씬 잘해내어서 추론 시간도 빨랐을 수 있다.
  • 컨텍스트 스위칭 오버헤드
    • 메인 스레드만 실행하면 브라우저에서 컨텍스트 스위칭이 일어날 필요가 없는데, Web Workers를 쓰면 컨텍스트 스위칭이 일어나다보니 이 과정에서 오버헤드가 생기고, 이 때문에 더 느려진다.

놀랍고도 슬픈 Web Workers 실험의 여정...

결론은 MediaPipe는 실시간 연산이 필요할 땐 백그라운드에서 안 쓰는 게 낫다...!

아니 그럼 어떡하냐.. 메인 스레드 혼자 불쌍해서...


3. 대안: AI 모델을 메인 스레드에서 준비하되, 화면의 버벅임을 눈치 못채게 하자


앞서 백그라운드에서 AI모델을 준비하는 것은 아닌 것으로 판명됐다.

그럼 어쩔수 없이
AI 모델을 메인스레드에서 실행해야 한다는 것.

이제는 메인스레드의 부담을 줄일 다른 방안이 필요하다.

나는 딥러닝 개발자도 아니고, 내가 대 구글이 최적화해놓은 모델을 초(?)최적화 해낼리 만무하고,
WebAssembly도 할 줄 모르고.. 컴퓨터 시스템은 더 멍충이인데...
도무지 AI 모델의 준비과정의 힘을 빼, 메인스레드의 부담을 줄일 현실적 방도가 나오질 않았다.

그래서 나는 화면의 복잡도를 낮추는 방식을 택했다.
메인스레드에게 다른 일 다 빼놓고,
AI모델 준비에만 몰두할 수 있는 환경을 제공하는 것이다.

화면이 단순한 곳을 찾다보니 로딩페이지에 당도했고,
행여 화면이 버벅거리더라도 티도 안 나겠다싶어 개꿀(?)을 외쳤다

3.1. 초기 대안: 메인 화면 데이터 로드 시점에, AI 모델도 같이 로드하자

UI가 제일 단순한 로딩 화면에서 AI 모델을 로드하기로 결정했는데...
어떤 로딩 화면이 적합할까?

사실 AI를 쓰기 바로 전인 게임 시작 전에 로딩하는 것이 가장 좋은데

각 사용자의 기기 환경에 따라 로딩 속도가 달라질 것이고,
그 시간을 함부로 예측할 수 없어서,
동시 시작해야 하는 게임의 로직을 대폭 수정해야 할 판이었다.

일단 빠른 길로 가자는 생각에
메인 페이지 로드 시점에 AI 모델도 로드하기로 결정하고 구현하였다.

그 결과...

나는 모두를 쓸데없는 로딩 지옥에 빠뜨리고 말았다.

그저 앱에 접속해서 히스토리만 확인하려해도
당장 필요업는 AI 모델을 로드하는 것이다.

정말 최악의 UX 제공 사례를 만들었다.
앱 진입도 하기 싫어지고,
기기 리소스도 쓸데없이 써서 핸드폰도 뜨거워지고...

그렇게 나는 빠른 길이 제일 멀리 가는 길이었음을 깨닫고..
다시 정석대로 수정하기로 한다.
 

3.2. 최종 대안: 게임 시작 직전에 미리 로드하되, Progress bar로 정보를 표시해주자

자, 먼길을 걸어왔으니 다시 정리해본다.

  • 문제 상황
    • 여러명이 함께 영상통화를 진행하다가, AI 모델을 사용하려하면 화면이 버벅거린다.
  • 핵심 원인
    • 브라우저의 메인 스레드에 과부하가 걸려, 무거운 AI 모델 로드 작업과 다른 작업을 동시에 진행하는 것이 어렵다.
  • 해결 방안
    • AI 모델이 필요한 시점에 모델 로딩 과정을 추가하여, 단순한 UI에서 모델 준비 작업을 실시하여 메인 스레드의 부하를 최소화 한다.

우리 서비스가 AI 모델을 이용하는 시점은
제한 시간 동안 진행되는 스피드 게임을 시작하기 바로 직전이다.

기획 초기에는 영상통화를 하다가 게임으로 바로 넘어가는 UX로 구성을 했었지만,
위 문제를 해결하기 위해,
친구들과 만나 이야기하는 것과 게임진행의 phase 사이에 로딩 구간을 삽입하고

어떤 모델이 얼마나 로드되었는지,
어떤 친구가 준비 완료되었는지 확인할 수 있도록 UI를 구성하여
로딩 시간의 지루함을 해소하고자 했다.

그렇게 완성한 결과물은 아래와 같다!


4. 결론: AI는 리소스 잡아먹는 하마이다.

- 그래서 <백그라운드에서 AI 실행하기>가 항상 좋은 대안은 아니다.

AI를 쓰면 뭐든 쉽게, 뭐든 빨리, 뭐든 멋지게 할 수 있을 줄 알았다.

그러나 생각 없이 썼다가 큰 코 다쳤다.

AI는 모델을 개발하는 것도 어렵지만,
사용하는 것도 큰 리소스가 필요하다.

그리고 내가 AI를 어떻게 활용할 것인가를 정확히 파악하고 있어야,
그 리소스를 효율적으로 사용할 수 있다.

나의 경우,
AI의 백그라운드 실행이 딱히 효과 없었던 이유는 <이미지 데이터 통신>과 <실시간 반영>에 있다.

만약 백그라운드로 이미지 캡쳐본을 계속해서 보낼 필요도 없고,
결과값의 실시간 반영이 필요한 작업이 아니었다면,
충분히 의미있는 시도였을거라고 생각한다.
(그치만 나도 덕분에 AI의 횡포도 이해해보고,, 메모리와 캐쉬의 관계도 복습하고,,, worker도 써보고,,, UX도 개선해보고... 진짜 짱 의미있었다...!)

앞으로는 내가 쓰는 AI가 어떤 특성을 가지고 있는지 더 면밀하게 따져서
AI 모델도, 그것을 쓰는 나의 서비스도,
최대의 효율을 낼 수 있는 방안을 모색하는 개발자가 되어야겠다 다짐한다.

여기까지 읽어내려오신 여러분에게도 좋은 간접경험이 되었기를 바란다..!

+ Recent posts