Contents

토이프로젝트 [바틀비.] 회고록

토이프로젝트로 진행했던 “바틀비.“에 대한 회고록을 남겨볼까합니다. 혼자서 진행한 첫 번째 토이프로젝트였습니다. 리액트를 사용한 경험을 쌓고자 진행했던 프로젝트였습니다. 그만큼 가벼운 마음으로 시작했었는데, 시간나는대로 틈틈이 하다보니 생각보다 완성에는 시간이 걸렸습니다.

1. 프로젝트 시작하기

여기는 개발 외적 이야기!
프로젝트에 대한 신변잡기적인 이야기이기 때문에 개발에 관한 이야기는 2번 제목으로 바로 이동해서 읽어주세요!

1-1. 개발배경, “필요하니까 만들어볼까낭”

개발이 멋진 이유는, 불편한 것을 더욱 편리하게 해주기 때문이죠. 당장 나의 불편한 상황을 해결하는 서비스를 만들고 싶었던 것이 프로젝트를 시작한 가장 중요한 이유 중 하나였습니다.

2023년 여름, 프로그래머스에 있는 코딩 테스트 연습 문제를 한창 풀고 있었습니다. 저는 코딩 테스트 연습문제를 풀 때, 비주얼 스튜디오 코드로 코드를 옮겨서 문제를 풀었는데 매번 테스트 케이스를 복사해서 붙여넣는 게 너무 번거롭더라고요. 그래서 그냥 문제의 주소만 입력하면 문제와 테스트 케이스를 복사해주는 코드를 파이썬으로 만들었습니다. 이왕 만든 거 하나의 서비스로 만들어서 배포해보면 어떨까 하는 생각이 들었고, 마침 리액트로 토이프로젝트를 해봤으면 했기 때문에 완성된 웹 서비스로 만들게 되었습니다.


1-2. 그래서 “바틀비.“란?

서비스 자체는 간단합니다. 문제 번호를 입력하면 최초 코드와 테스트 코드를 한꺼번에 복사해준다. 서비스가 간단한만큼 UI도 최대한 심플하게, 사용하기 간편하게를 모티브로 간단히 만들기 시작했습니다.

바틀비라는 이름은…

기존 내용을 가져와 보기 좋게 만들어준다는 서비스의 성격을 잘 담을 수 있는 이름이 무엇이 있을까 고민하다가 필경사와 유사한 성격인 것 같아 필경사라는 서비스 정체성을 부여했고, 필경사 하면 제일 유명한 바틀비의 이름을 붙여 “바틀비.“라는 이름 지어주었습니다. 이름을 조금 바꾸어 ‘코드 + 바틀비 = 코틀비’처럼 바틀비에서 개발자의 테이스트를 첨가한 무언가로 이름을 붙여줄까도 생각해봤지만, 기껏 고상한 이름을 붙여줬는데 어줍잖은 말장난으로 이름을 붙여 주는 것은 어울리지 않을 것 같아 그렇게 하지 않았습니다. 또, ‘사용자를 돕는다 = 집사 = 알프레드’라는 방식으로 이름이 붙여진 맥용 편의성 앱 Alfred처럼 단순히 해당 역할을 대표하는 이름을 따 붙여준 경우도 있기 때문에 바틀비라는 이름을 그대로 사용해주었습니다.

다만 바틀비와 필경사 모두 친숙하다고는 할 수 없는 이름과 단어이기 때문에 실제 상품화된 서비스였다면 마케팅적인 측면에서 다른 이름을 고려해봤을 것 같네요. 다만 바틀비의 어감이 재밌는 편이고 뭔가 신뢰를 주는 어감이라 마음에 든 것도 있고 해서 현재 프로젝트 이름으로 낙찰했습니다.

아, “바틀비.“의 링크는 여기에요!

요렇게 생겼답니다.

요렇게 생겼답니다.


1-3. 허락보다 용서가 쉽지만, 저작권의 세계는 그렇지 않지

개발에 앞서, 아무래도 원전이 되는 코딩 테스트 연습 문제는 프로그래머스의 자산이기 때문에 저작권 문제가 있을 것이라고 생각했습니다. 서비스를 제작하기 전에 프로그래머스의 문제 관련 저작권을 확인하고 제가 취급할 수 있는 수준을 확인했습니다. 프로그래머스의 FAQ를 찾아보니 다음과 같은 글 있더라고요.

프로그래머스의 문제를 외부에 게시할 수 있나요?

요컨대, 1. 비상업적, 비영리적 용도로 게시할 수 있으며, 2. 문제를 풀고 채점이 가능한 형태가 아니라면 지문 및 테스트 케이스를 게시할 수 있다.는 것이었습니다. 좀 더 확실하게 하기 위해 프로그래머스에 문의 메일을 보냈는데 다음과 같은 답장을 받았습니다.



좋아요! 메일에서 밝히고 있는 것처럼 초기코드와 테스트 케이스의 경우만 사용하면 문제가 없을 것 같네요! 다만, 문제에 대한 저작권은 확실히 표시하기 위해 앞으로 만들 사이트에서 문제의 저작권과 관련된 내용을 좀 더 확실히 표시하는 것이 좋겠다는 생각을 했습니다.


2. 그럼 본격적으로 개발해봅시다!

2-1. “어떤 기술 스택을 사용해야할까?”

나는 자바스크립트 좋아

일단 리액트를 사용해보고자 시작한 프로젝트인만큼 프론트엔드는 리액트를 사용하기로 하고, 웹 크롤링으로 프로그래머스의 테스트 코드를 가져오는 백엔드 서비스를 생각했습니다. 크롤링…하면 파이썬이죠! 파이썬에선 Beautiful Soup이라는 강력한 크롤링 라이브러리를 사용할 수 있었기 때문에 크롤링 부분은 파이썬을 사용하여 개발하기로 했습니다. 파이썬을 사용하기 때문에 백엔드 프레임워크도 막연히 파이썬으로 구성된 프레임워크를 사용해야겠다…라고 생각하면서 Django와 Flask 중에 고민을 했고, 실제로 처음 시작은 Django로 백엔드 코드를 작성했습니다.

그런데 아무리 생각해봐도 필요한 API의 수가 2-3개에 불과한 이 프로젝트에 Django는 너무 무거운 도구인 것 같았습니다. 최대한 심플하게!를 모티브로 한 이상 더 가볍게 백엔드를 만들 수 있는 방법은 없나하고 찾던 중 JavaScript로 Python을 구동할 수 있다는 것을 알게 되었습니다. 바로 child_process를 통해 말이죠! 따라서 백엔드 서버는 간편히 그리고 가볍게 사용할 수 있는 Express.js를 사용하기로 했습니다. 사실 Express.js를 사용해본 적이 없었어 이번 기회에 사용해볼 겸 하는 생각으로 사용한 것도 있답니다.


도전과제: 최소한의 외부 라이브러리 사용하기

프로젝트를 시작하면서 목표로 한 것이 또 있는데요, 심플한 서비스인만큼 최대한 외부 라이브러리 사용하지 않고 할 수 있는 것은 직접 만들어 쓴다였습니다. 따라서 UI요소들은 최대한 만들어 사용했습니다. 실제로 완성된 서비스에서 사용된 외부 라이브러리는 다음과 같습니다.

  • BeautifulSoup: Python 크롤링 라이브러리
  • Font Awesome: 아이콘 라이브러리
  • SweetAlert: 예쁜 alert
  • react-responsive: 반응형 웹 라이브러리

2-2. 아키텍처 구성도

정말 심플하죠?

정말 심플하죠?

실제 서비스는 위와 같이 구성되어 있습니다. 네, 보시면 아시겠지만 실제 배포된 서비스는 백엔드 서버를 따로 두지 않고 AWS Lambda를 통해 사용자 요청을 처리하고 있습니다. 배포를 진행하는 과정에서, 사용하는 API 수가 얼마 되지 않은 이상 AWS Lambda로 API를 사용하는 편이 비용과 편의성 측면에서 더 나을 수 있을 것 같다는 판단을 했고 위와 같은 서비스로 구성하게 되었습니다.

처음에는

  1. 현재 사용자가 선택할 수 있는 문제 풀이 언어 종류를 반환하는 API
  2. 문제의 이름을 미리보여 주는 API
  3. (주 기능) 문제를 IDE에서 사용할 수 있는 코드로 만들어 주는 API

의 세 가지 API를 사용했습니다. 그러나 개발 진행 중

  1. 1번 API의 경우,
    1. 페이지에 들어갈 때마다 API를 요청하는 것은 통신 낭비이며
    2. 선택 언어 추가는 핵심 기능(=잦은 업데이트가 되지 않는 경우)이므로 하드코딩으로 사용하여 기능이 추가될 때 업데이트 해도 되는 메이저 업데이트라고 판단했기 때문에 폐기
  2. 2번 API는 3번 API와 사실상 기능상 거의 동일한 API이기 때문에 3번 API로 편입

하는 과정을 통해 단 하나의 API만 남기게 되었습니다.

다음과 같은 이유로 Lambda로 API를 구축하는 과정에서 API를 딱 한 개로 줄이고 실제로 현재도 단 하나의 API를 사용 중입니다. 따라서 개발 중에는 Express.js로 API 요청을 했으며, 현재는 AWS Lambda를 통해 API 요청을 하고 있습니다.

또, 처음에는 Netlify를 통해 정적 사이트를 배포했는데, 후술할 이미지 로딩의 속도 문제가 있어서 리액트 배포를 S3 환경에서 진행하게 되었습니다.


2-3. 본격적인 개발 회고

그럼 이제부터 개발하면서 겪었던 내용들을 회고하며 세부적인 내용을 적어볼까 합니다. 개발하면서 겪었던 문제, 구현 과정에서의 고민과 문제 해결 과정 등 상세한 이야기를 해볼까 합니다.

1) 백엔드

Express.js로 구축했지만 실제로는 Python이 모든 기능을 담당하고 있기 때문에 Python에서 구현한 내용을 이야기 해볼까 합니다. 근데 사실 별로 할 이야기는 없네요😅. Beautiful Soup으로 사용자가 요청한 문제의 페이지에서 필요한 부분을 따와 텍스트 타입으로 언어에 맞게 테스트 케이스와 문제를 가공하여 주었습니다. 핵심 기능이지만 실제로도 반나절도 안돼서 개발을 완료했던 기억이 나네요.

특기할만한 사항이 있다면 역시 child_processspawn을 사용한 node.js <-> python의 요청 및 반환이었습니다. 해당 코드 블록은 다음과 같습니다.

1
2
3
4
5
6
	const result = spawn("python", [
		"./세부경로/파이썬_파일.py",
		매개변수1,
		매개변수2,
		매개변수3
	]);

해당 코드에서처럼 실행할 Python 파일을 경로와 함께 지정 후 넘겨 줄 매개변수를 함께 보내 요청합니다.

1
2
3
4
if __name__ == '__main__':
    전달받은_매개변수1 = sys.argv[1]
    전달받은_매개변수2 = sys.argv[2]
    전달받은_매개변수3 = sys.argv[3]

Python에서는 다음과 같이 메인 함수에서 node.js가 전달한 매개변수를 받아 요청을 처리할 수 있습니다.

더 자세한 코드는 Github 레퍼지토리에서 app.jscrawling > get_question.py를 통해 직접 확인해보실 수 있습니다.


2) 프론트엔드

2-1) 전체적인 UI와 디자인

요렇게 생겼답니다.(1)

요렇게 생겼답니다.(1)

전체적으로 톤 다운된 색감을 사용해 심플하게 디자인 했습니다. 화려한 아름다움은 높은 수준이 수반되어야 하며, 그렇지 못하다면 심플한 것이 낫다는 나름의 디자인에 대한 경험을 바탕으로 둔 지론이 있는데 수준이 낮은 저는 심플하게 하는 방향으로 했습니다… 하지만 간단하고 가벼운 사용성을 지향하는 만큼 군더더기 없는 디자인을 표방한 것도 있답니다. 때문에 검색 포털처럼 검색창 하나의 최소한의 UI를 사용했습니다.

파란색의 통일된 색깔톤을 유지했는데, 메인이 되는 클래식 블루의 경우 그냥 제가 제일 좋아했던 펜톤 올해의 색이어서 선택했습니다. 하지만 그러고 보니 inform 색과도 비슷한 톤으로 유지되어 디자인에 일관성이 있고, 바틀비라는 남성적인 어감이 주는 서비스의 성격과 어울리는데다 신뢰감을 줄 수 있는 컬러감이라 결과적으로는 좋은 선택이었다고 생각합니다. 헿

반복적으로 색상이 사용하기 위해 리액트 프로젝트 루트 경로의 App.module.css에 넣어두고 사용했습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
:root {
  /* Names */
  --classic-blue: #014b7d;

  /* Cases */
  /* --information: #004add; */
  --information: #197adb;
  --alert: rgb(242, 49, 67);
  --dark-shadow: rgba(89, 89, 89, 0.372);
  --not-selected: #b4b4b4;

  /* Shadows */
  --common-shadow: 2px 5px 2px rgba(133, 133, 133, 0.656);
  --upper-shadow: 0px -1px 10px 3px rgba(79, 79, 79, 0.692);

  /* Custom Colors */
  --footer-black: rgb(36, 36, 36);
  --tooltip-alert: #b6233a;
  --alert-text: #870000;
  --spinner-border: #f3f3f3;
}

다음과 같이 지정한 후 각 색상을 사용할 css에서 해당 색상을 꺼내 적용했습니다.

1
2
3
.custom-button {
  color: var(--classic-blue);
}

폰트는 네이버의 나눔스퀘어 네오를 사용했습니다. 역시 각지고 정돈되어 있지만 세련된 느낌이 있어 서비스의 성격과 잘 맞는다고 생각하여 선택했습니다.

최소한의 외부 라이브러리를 사용한다는 나름의 도전과제 달성을 위해 각 UI 요소들은 모두 직접 만들어 사용했습니다. 로딩 시 나타나는 스피너도 단순히 HTML과 CSS로 간단히 구현할 수 있다는 것도 이번에 처음 알았네요.

UI 컴포넌트가 과반수를 넘어요&hellip;

UI 컴포넌트가 과반수를 넘어요…

UI 요소들은 사실 단 한 번 밖에 사용되지 않았지만, 학습 목적이 큰 프로젝트인 만큼 최대한 UI 요소를 재사용한다는 가정과 함께 컴포넌트를 작성했습니다.

다음부터는 각 요소에 대해 하나하나 이야기 해볼까 합니다.

2-2) 서비스 설명 영역

설명 영역에서 특기할 점은 역시 오버레이 됐을 때 나타나는 설명 애니메이션입니다. 첫 번째 문장의 ℹ️와 두 번째 문장의 물음표는 오버레이 할 경우 해당 내용에 대한 애니메이션이 나타나게 했습니다. 글로 설명하는 것보다 훨씬 직관적이니까요.

문제상황 1. 마우스 호버 시 gif 재생이 처음부터 되지 않는다…!

해결방법: 재생하지 않을 때 정적 이미지를 넣어준다!

다만 호버 시 이미지가 노출되면서 나타난 문제가 크게 두 가지 있었습니다. 첫 번째 문제는 처음 마우스 호버 시 이미지를 나타나게 한 후에는 재생이 계속 진행 된다는 것이었습니다. 커서를 치우고 나서도 말이죠. 다시 말해

  • 원래 의도: 마우스를 올릴 때마다 GIF 이미지가 처음부터 재생됨

  • 나타난 상황: 아 그런 거 없고 마우스 처음 올릴 때부터 재생해서 이후 마우스 올리든 말든 난 끝까지 재생한다(영상 끝나면 되감기 없음)

의 상황이 나타났습니다. 의도한대로 구현하기 위해서는 별도의 조치가 필요했는데 해결 방법은 호버하지 않았을 때는 정적 이미지를 넣어주고, 마우스를 올렸을 때에 해당 gif를 넣어준다였습니다. (참고 링크) 관련 코드 조각은 다음과 같습니다.

1
2
3
4
5
6
7
8
	let mainDescriptionGif = <></>;
	if (isMouseOverOnMainInfo) {
		mainDescriptionGif = (
			<img src={mainDescriptionImg} alt="Guide for how to use Bartleby." />
		);
	} else {
		mainDescriptionGif = blackSolidImage;
	}
1
2
3
4
// 하단 jsx 영역
<div className={`${style["main-tooltip-content"]}`}>
	{mainDescriptionGif}
</div>
문제상황 2. 느려….!

해결방법: 이미지 용량을 줄인다!

사실 원래 상단에 들어간 서비스 설명 gif 파일의 경우 상당히 고화질에 프레임도 빠방한 움짤이었는데요, Netlify에 배포 후 확인해보니 다운로드 받을 때 상당히 오랜 로딩 시간(약 3초 정도)이 소요 됐습니다. 따라서 최대한 크기를 줄이고 색상 수와 프레임을 덜어내 현재의 이미지를 적용했습니다.

문제상황 2(1). 그래도 느려…!

해결방법: 물리적으로 가까운 스토리지를 사용한다!

예 여전히 느리더라고요. 처음 다운로드에서. 이유를 찾아보니 Netlify의 서버 위치상의 물리적 한계 때문에 이미지 다운로드가 느리다는 것이었습니다. 원래 AWS 서비스는 Lambda만 사용하려 했지만, 어차피 프리티어 계정 만든 김에 정적 페이지를 배포할 수 있는 AWS S3로 옮겨서 리액트를 배포했습니다. 리전은 서울로!

문제상황 2(2). 여전히 느려….!

고만 좀 느려

해결방법: 페이지가 로드 될 때 이미지도 미리 다운 받아 놓는다! (useLayoutEffect의 사용)

사실 그냥 느린 게 아니고 실제 문제 원인은 이미지에 커서를 호버 할 경우에 이미지를 로드를 하고 있었던 것이었습니다.🙄 이것은 이미지를 별로 다뤄보지 못한 무지가 빚어낸 전적인 저의 잘못… 아무튼, 따라서 이미지를 페이지가 로드될 때 같이 미리 다운로드 받아 이를 해결했습니다. 해당 방법을 위해 리액트 훅인 useLayoutEffect를 사용했습니다. 해당 훅은 useEffect훅과 사용 경우가 비슷하고 사용하는 방법도 완전히 같은데요, useEffect와 달리 브라우저가 화면에 DOM을 그리기 전에 이펙트를 수행한다는 점이 다릅니다.

리액트 훅 흐름 다이어그램 (출처: github.com/donavon/hook-flow)

상단 이미지처럼 LayOutEffect의 경우 Mount 시점에 이펙트가 수행되고 있습니다. 이를 이미지에 적용해 이미지의 로드 속도를 개선했습니다. 해당되는 코드 조각은 다음과 같습니다.

1
2
3
4
5
6
7
8
	const hoverImgPreload = () => {
		const mainDescImg = new Image();
		mainDescImg.src = mainDescriptionImg;
	};

	useLayoutEffect(() => {
		hoverImgPreload();
	}, []);

해당 영역과 관련된 코드는 이곳에서 확인 가능합니다.


2-3) 언어 선택 영역
문제상황 1. 이 모양이 아니야…! (Safari 한정) / div 및 ul, li로 나타나는 애니메이션이 있는 선택 상자 만들기

해결방법: Select가 아니라 div, ul, li로 새롭게 만들어준다!

한바퀴 빙글빙글 도는 화살표 애니메이션만 빼면 특별할 것이 없는 영역이지만 해당 영역에도 비하인드 스토리가 있습니다. 사실 해당 영역은 select 태그로 구현한 후 스타일링도 나름 예쁘게 해서 구현했지만, 개발을 진행한 Chrome이 아닌 Safari 브라우저에서 확인해보니 셀렉트 옵션들간의 간격이 생기고 생김새가 볼품없어지는 결과가 나타났습니다. 따라서, 해당 영역은 div로 새롭게 만들어 주었습니다. 기본 선택 상자 자체는 div로 만들어주며, 언어 박스 클릭 시 나타나는 목록은 ul과 li로 만들어주었습니다.

해당 과정에서 공들였던 것은 나타나는 애니메이션을 보여주는 css 였습니다. visibility css 속성 값을 통해 나타났을 때와 나타나지 않을 때 각각 visible과 hidden 값을 주고 해당 애니메이션을 transition을 통해 부드럽게 움직이는 애니메이션을 나타낼 수 있습니다. 해당되는 코드 조각은 다음과 같습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    <ul
        className={
          `${isShowUp ? style["show-up"] : style["good-bye"]}` +
          " " +
          `${style["option-list"]}`
        }
        onClick={onClickHandler}
      >
        {Object.keys(selectOptions).map((key) => (
          <li data-value={key} key={key} className={`${style["option"]}`}>
            {selectOptions[key]}
          </li>
        ))}
      </ul>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
-- 애니메이션 전체를 관장하는 className
.option-list {
  transition: 0.3s ease-in-out;
}

-- 셀렉트 옵션이 나타났을  class의 css 설정값
.show-up {
  max-height: 80px;
  visibility: visible;
}
-- 셀렉트 옵션이 사라질  class의 css 설정값
.good-bye {
  max-height: 0px;
  visibility: hidden;
}

해당 css에서 중요한 점은 .show-up 태그에서 나타난 max-height 값의 설정입니다. 해당 값이 제대로 설정되지 않는다면, 애니메이션 작동에서 자연스러운 애니메이션을 완성할 수 없습니다.

다만 다음과 같이 ul, li로 새로운 선택 상자를 만든다면, 상자의 외부 클릭 시 해당 상자가 닫히지 않는 문제를 해결해야 합니다. 해당 문제에 대한 해결은 다음과 같은 방식으로 할 수 있습니다.(스택 오버플로우에서의 해당 문제 해결 타래글)

참고로 360도 움직이는 화살표는 v모양의 이미지를 클릭 시마다 180도 회전 시켜 주는 css 애니메이션을 적용해서 구현할 수 있습니다. 전체 코드가 궁금하신 분들은 여기(jsx)여기(css)의 코드를 참고해주세요.


2-4) 검색 영역

검색 영역엔 특별한 점은 없지만 정규식을 통해 숫자만 입력 받을 수 있도록 했습니다. 프로그래머스 문제 페이지의 주소 자체를 모두 입력할 수 있게 할까 했지만, 숫자를 직접 입력하는 경우에도 최대 여섯자리만 입력하면 되기 때문에 문제 번호만 입력하는 편이 다양한 상황에서 여러모로 더 편리할 것으로 생각되었습니다. 다만, 숫자 외 유효한 주소 값을 입력받을 경우에도 처리할 수 있게 하는 등의 개선 방향도 고려 중입니다.

위의 gif 이미지에서 보이는 것처럼 숫자 외의 키를 입력하면 안내 툴팁이 나타나는데, 마지막 인풋 값을 기준으로 툴팁이 사라지는 시간이 설정될 수 있도록 디바운싱을 적용했습니다.


2-5) 가져오기 및 미리보기 영역

언어와 문제가 존재하는 번호가 입력되면 가져오기가 활성화됩니다. 하단 문제 이름 미리보기 영역은 추후 추가한 것인데, 번호만 입력할 경우 어떤 문제인지에 대한 정보가 있으면 더 편하겠다는 생각에 추가 되었습니다.

문제 이름 미리보기 영역은 개인적으로는 개발 중 제일 애먹은 부분 중 하나입니다. 문제번호를 찾을 때 빙글빙글 도는 로더가 나타나게 되는데, 이때 입력 값을 지워버릴 경우 로더는 남아 있는 채 미리보기 영역만 사라진다든가 로더가 줄어들고 있는 미리보기 영역에 따라 같이 모양이 실시간으로 작아지거나 하는 문제가 생겼습니다.

해결방법이라고 하면, 딱 이거다! 싶은 해결 방법은 없었고 전체적인 문제 로딩과 API 수신 상태에 대한 결과값 플래그 등을 제대로 정의하고 해당 상태를 전달해주어 문제를 해결했습니다.

상단 gif 이미지를 보면, 가져오기 버튼의 경우 존재하지 않는 문제 번호가 입력 됐을 때에 비활성화 되는 것을 알 수 있습니다. 즉 유효한 입력값임을 판단 됐을 때 가져오기가 활성화 되는 방식이 아니라, 유효하지 않은 값임이 판단 됐을 때 비활성화 하는 방식을 사용했습니다.

그 이유는, 실제로는 입력을 입력한 즉시 가져오기 버튼을 누르는 경우가 많아, 해당 값이 유효한 값임을 판별하기까지 기다리는 것은 사용성이 크게 떨어졌기 때문입니다. 따라서, 유효한 값임을 판별하기 전에 가져오기 버튼을 누를 수 있되, 잘못된 입력 값을 경우 별도의 Alert를 띄워 잘못됐음을 알려주었습니다. 또한, 잘못된 입력 값을 경우 가져오기 버튼을 비활성화해 불필요한 동작이 발생하는 것을 방지 했습니다.


2-6) 가져오기 기능

성공적으로 문제를 가져오면 SweetAlert2를 통해 성공적으로 문제를 가져왔음을 알리고, 클립보드에 해당 코드를 복사 해 붙여넣을 수 있게 했습니다.

클립보드 복사의 경우 navigator.clipboard APInavigator.clipboard.writeText() 메서드를 사용했습니다. 해당 메서드의 경우 일부 브라우저에서 작동하지 않으며, 사용자의 환경에 따라 다른 방법을 고려하라는 사용 지침이 있습니다. 다만, 해당 API가 현대적이고 안전한 방법으로 현재 추천되고 있는 복사/붙여넣기 방식으로 제안되고 있어 본 프로젝트에서는 해당 API를 활용해 사용자의 클립보드 복사를 구현했습니다.


2-7) 모바일은
모바일은 죄 없어요. 제가 죄에요.

모바일은 죄 없어요. 제가 죄에요.

아직 모바일 환경은 지원하지 않습니다. 모바일에서 개발을 할까…? 싶은 이유가 제일 크긴 하지만요. 일단은 모바일 뷰는 막아 놓은 상태에서 모바일 적용 방법을 생각해보려 합니다.


3) 배포! AWS!

배포 = 새로운 불 구덩이로 들어가보자

앞서 이야기한 것처럼, 배포 환경은 당초 진행했던 것과는 다르게 구성했습니다. Express.js의 환경은 AWS Lambda(및 Amazon API Gateway)로 프론트엔드 배포는 Netlify에서 S3 및 CloudFront로 말이죠.

배포한 후의 프로덕트를 확인하는 과정에서 상술한 문제 몇 문제들과 자잘한 문제들에 부딪혔지만, 배포 관련해서는 간단히 이야기하고 일일히 기술하지는 않겠습니다…테스트하면서 쌓았던 로그들로 그 슬픔의 흔적을 보여드리려고 했는데 테스트 후 리전을 옮기고 Lambda 함수를 지워서 그 마저도 없어져 버렸어요…🥺😭

3-1) AWS Lambda를 통해 serverless API 환경 구축하기

AWS 계정이 프리티어 혜택을 누릴 수 있는 상황이며 극히 적은 사용량으로 EC2와 Lambda 모두 추가 사용량이 청구되지 않을 것으로 예상되는 상황입니다. 다만, 단 하나의 API만을 사용하는 상황이며 그 API 역시 사실상 Python으로 구동되고 있으므로 Python 언어 자체로 응답을 처리할 수 있는 Lambda를 선택해 배포해보기로 결정했습니다. EC2는 간단히 사용해 본적이 있어서 새로운 서비스를 경험해보는 차원에서 선택한 이유도 있습니다.

기존에 Express.js와 연동해서 사용하던 코드를 거의 그대로 사용했습니다. 다만, Beautiful Soup 라이브러리가 필요했는데 이는 단순히 lambda의 루트 폴더에 업로드 하는 것으로 사용이 가능했습니다.

현재 사용 중인 실제 폴더 계층 구조

현재 사용 중인 실제 폴더 계층 구조

3-2) Amazon API Gateway로 API 트리거 연결하기

Amazon API Gateway를 통해서 외부의 HTTP 요청에 대해 해당 Lambda의 함수를 실행할 수 있습니다.

API Gateway의 API 유형에는 아래와 같이 HTTP와 REST 두 가지가 있습니다.

기본적으로 REST API 형태가 편리하고 지원하는 기능역시 많습니다. 다만 본 프로젝트에서는 좀 더 단순한 형태인 HTTP 방식을 선택했습니다. 이전까지의 이유들과 마찬가지로 (비용이 청구되는 서비스라고 가정했을 때) 저렴하게 서비스를 구축할 수 있기 때문입니다. AWS에서는 HTTP가 REST 대비 최대 71%까지 저렴할 수 있다는 공식 입장을 발표하고 있습니다.(관련 아티클)

또한 API Gateway를 설정하면서 API 구조를 조금 변경했습니다.

기존에 있던 1. 미리보기 이름을 반환하는 API2. 정제된 코드/테스트를 반환하는 API를 둘의 작동 로직이 완전히 같기 때문에 하나의 API로 통합했습니다. 기존에는 GET 요청으로,

  1. 미리보기 이름 반환용: /question?{문제번호=문제번호}&{미리보기 API여부=Y/N}
  2. 코드 반환용: /question?{문제번호=문제번호}&{언어종류=javascript/python}

의 두 형태를 사용했다가 쿼리 스트링으로 사용했던 문제번호를 Path Parameter로 변경하고 언어 종류와 미리보기 API 여부 만을 쿼리 스트링으로 사용하는 것으로 변경했습니다. 따라서 현재는 다음과 같은 형태로 API를 요청하고 있습니다.

  • /question/{문제번호}?{언어 종류} 혹은 {미리보기 여부}

기능상에서는 크게 달라진 것은 없지만 다음의 형식이 좀 더 RESTFUL에 가까운 형태이기에 변경해 적용했습니다.

우리들의 오랜 친구 CORS도 해당 API Gateway 관리 페이지에서 설정할 수 있습니다.

3-3) S3와 CloudFront로 정적 페이지 배포

Netlify는 Github의 레퍼지토리를 연결하는 것만으로 배포를 할 수 있어 상당히 편합니다. 하지만 상술한 이미지 문제와 같이 더 빠른 성능 향상을 기대하며 S3로 배포 환경을 이전했습니다. S3 내에서의 배포는 어려운 점은 많지 않고 CloudFront와의 연동도 어렵지 않은 편이라 비교적 손쉽게 배포 환경을 바꿔 배포할 수 있었습니다.

로컬/운영 환경에 따라 요청하는 API 주소가 다르게 설정된 .env에서의 값만 따로 신경 써주었습니다.

3-4) 별도의 도메인 이름 지정해주기

아무래도 CloudFront로 부여받은 도메인 이름은 소위 멋이 안 살기 때문에 가비아에서 이벤트로 저렴하게 판매 중인 도메인 이름을 구매해 적용해주었습니다. 마침 .pro 도메인이 이벤트 할인 중이라 저렴하게 구매하여 이름을 붙여주었습니다. 마침 프로그래머스의 은총을 받아 완성한 프로젝트이기도 하고, 마음만은 프로인 그런 느낌이니까요.

3-3과 3-4에서 수행한 내용을, 미래의 저와 방식을 알고 싶은 분들을 위해 키워드 위주로 간략히 순서를 적어두겠습니다. CI/CD 설정과는 별개로 직접 배포할 경우 다음의 과정을 거칩니다.

키워드 위주의 배포 과정

S3 내 빌드 업로드 -> CloudFront를 해당 프로젝트 배포 -> S3 내 CloudFront에 대한 권한 설정(JSON 형태) ->

CloudFront 내 오류 페이지로 지정되지 않은 path 요청에 대해 리다이렉트 설정 -> AWS를 통한 SSL 인증서 발급 요청 ->

가비아 내 해당 인증서의 CNAME 레코드 등록 -> 인증서가 발급되면 CloudFront 내의 설정을 통해 대체 도메인 이름으로 구매한 도메인 이름 등록

(재배포가 필요한 경우 CloudFront 내 무효화를 통해 무효화 생성(객체경로: /*))

본 포스팅에서는 배포 관련 내용은 위와 같이 간략히 다루겠습니다.


3. 끝!은 고도화 시작이죠

그리고 남아 있는 미래

그리고 남아 있는 미래

핵심 기능을 마무리했기에 배포까지 완료! 했지만 여전히 부족함이 많은 프로젝트입니다. 하지만 일단 핵심 기능과 배포는 끝냈으므로 일단 한 고개 넘었다고 생각하고 다음 단계로 넘어가야겠죠.

현재 프로젝트에 대한 향후 계획은 다음과 같습니다.

  1. 지원 언어 추가
    • 네 아직 JavaScript와 Python 밖에 지원하지 않습니다. 이유는 심플합니다. 제가 그 두 언어로 연습 문제를 풀어서…
    • 사실 Java 쪽 기능을 개발해보고 싶다고 했던 친구가 있어서 그 친구의 몫을 남겨두었던 것인데 여태까지 별 말이 없는 것보면 아마 안할테죠? 네 그것은 저의 새로운 과제로 하는 걸로.
  2. TypeScript로 마이그레이션 (24.01.07. 완료됨!)
    • (+) TypeScript로의 마이그레이션을 완료했습니다!🔥🔥
    • 리액트를 좀 더 다루기 위한 프로젝트였을 정도로 학습 목적이 큰 프로젝트였습니다.
    • 따라서, 현재 학습 중인 타입스크립트를 적용해 프로젝트를 TypeScript로 변환하는 작업을 하고 있습니다.
    • 서비스의 기능상의 변화와는 상관 없지만, 해당 목표를 일단 최우선적으로 적용할 계획입니다.

이외 몇 가지 개선 사항들이 있어 이에 대응하려고 합니다. 그래도 일단은 TypeScript 적용을 우선적으로 진행하고 있으며 해당 목표가 앞으로도 최우선 목표가 될 것 같습니다.

배포하면서 부끄러운 부분도 있는 서비스지만, 일단은 긍정적은 면을 보며 앞으로의 수행 동기로 삼는 것도 중요하겠죠! 정말 별 것 아닌 기능을 제공하는 서비스지만, 조금이나마 불편을 해소해준다는 점, 리액트를 활용한 프로젝트를 혼자서 배포까지 해냈다는 점, 프로덕트로서 나름의 완성도를 갖추었다는 점이 만족스러웠습니다. 남은 과제들을 해결하며 더 완성도 있는 프로젝트로 만들어 내고 향후 또 다른 프로젝트을 위한 자그마한 경험 자산으로 남겨둘까 합니다.🔥🔥