부트캠프를 수료한 지 벌써 반년이 훌쩍 지났다...
수료 이후 여러 팀 단위 사이드 프로젝트를 하면서 최대한 비는 시간 없이 꾸준히 무언가를 해왔는데, 그마저도 슬슬 마무리 단계에 들어가면서 새로운 동기부여가 필요해지고 있었다.
그렇게 시간이 흐르고 5월 어느 날,
스터디 단톡방에서 해커톤에 같이 나가보자는 이야기가 나왔다.
'해커톤? 코딩 대회? 내가 그런 걸 나갈 실력이 되나?'라는 생각이 먼저 들었고 조금 주저하게 되었다. 하지만 텅 빈 이력서를 보면서 '한번쯤은 도전해봐도 괜찮지 않을까?'라는 생각에 톡방에 올라온 링크를 클릭했다.
앱손이면 일본 프린터 회사 아닌가? 여기서 해커톤을?
조금 찾아보니 이미 일본과 미국에서 여러 번 열린 적 있었고 한국에서는 처음 열리는 챌린지라고 한다. K-Culture/교육/이커머스 3개의 대주제 중 1개를 선택해 2~6명의 팀원과 함께 약 한 달간 서비스를 만들어 발표회를 진행하는 방식이었고, 그 과정에서 Epson connect API를 필수로 사용하여 앱손의 복합기를 연결해야 했다.
이 정도면 평소에 하던 팀 프로젝트랑 크게 다를 거 없는데? 🤔
생각보다 할만하겠다 싶어 함께 스터디를 하고 있던 기훈님, 은상님과 함께 프론트 3인 팀을 결성하고, 앱손 챌린지 디스코드, 인프런 등에서 수소문한 끝에 백엔드 도훈님과 성욱님, UX/UI 하영님을 포함한 총 6인 팀을 결성하게 되었다!
앱손 챌린지는 팀을 만들고 기획서를 제출한 뒤, 심사를 거쳐 30팀을 추려내 본 대회를 진행하는 방식이었다. 즉, 기획을 확실하게 하지 않으면 팀을 꾸린 보람도 없이 아무것도 못해보고 끝이라는 것이다.
기획까지 해놓고 떨어지는 팀은 어떡하라고 이렇게 진행하는건가 싶었는데 본선 진출 30팀에게는 각 팀당 1대씩 무상으로 앱손 프린터를 주기 때문에 앱손 측에서도 리턴을 위한 좋은 기획안을 가진 팀을 선발하는것이 중요하겠구나 싶었다.
기획을 하기에 앞서 3개의 대주제 중 하나를 정하고 시작해야 하는데, 사실 팀원을 모집하기 전에 프론트 3명이서 대주제를 미리 K-culture로 정하고 모집을 시작했다.
교육이나 이커머스는 잘 모르는 분야이기도 하고 번뜩이는 아이디어가 쉽게 떠오르지 않을 것 같다는 공통된 의견이 있었기 때문이었다.
6명의 팀원이 모인 후 몇 차례의 기획 회의를 진행하면서 K-culture와 앱손의 프린터를 적극 활용할 수 있는 아이디어를 구체화했다.
K-pop을 좋아하는 글로벌 팬과 아티스트의 손편지 소통 플랫폼 + 손편지 번역 텍스트를 통한 맞춤형 한국어 교육자료 제공 서비스
이렇게 최종 기획안으로 결정되었다.
K-culture와 프린터만 놓고 보면 생각나는 게 많지 않을 수도 있었지만 요즘 화두인 AI를 접목하기로 하면서 다양한 의견이 나왔고, 하영님의 케이팝을 좋아하는 외국인 친구가 손편지를 쓰면서 겪은 언어적 고충에 관한 이야기와 합쳐지면서 이런 아이템이 나오게 되었다.
팀 명으로는 AIGOO (아이고) 라는 이름이 붙여졌는데, 외국인들이 아이고라는 말을 귀엽게 받아들인다고 한다. I Go 라는 의미부여도 가능한 점은 덤.
아이디어가 정해지고 나서 기획서 제출까지 일사천리로 진행되었고, 최종 30팀에 들면서 본격 작업에 들어가게 되었다!
여담으로, 첫 기획 회의를 진행하면서 진행자를 맡게 되었는데 초반에 눈에 띄는 행동을 하게 된 죄(?)로 이후 팀장까지 하게 되었다 ㅎㅎ... 😅
6/12일 한국앱손 강남 본사에서 열리는 네트워킹 데이에 참여했다. 간단하게 앱손의 기기들을 구경하고 설명을 듣는 투어와 비대면 기획 멘토링, 저녁식사로 일정이 계획되어 있었다.
우리팀은 나, 하영님, 기훈님, 은상님 총 4명이 네트워킹 데이에 참가했다.
앱손의 다양한 복합기, 라벨프린터, 빔프로젝터 등을 구경하고 앱손의 기술력에 대한 소개를 15~20분정도 듣고 나서 회의실로 이동해 30분정도 기획 멘토링을 받게 되었다.
기획서를 기반으로 강조해야 하는 점은 무엇인지, 비즈니스적 관점 등을 토대로 프로젝트를 어떻게 진행하면 좋을지에 대한 내용을 들을 수 있었다.
저녁 식사는 맛있는 뷔페였다~~!
본격적으로 프로젝트 제작에 들어가기 이전에 기술 스택부터 정해야 했다.
프론트엔드 기술 스택은 빠르게 프로젝트를 진행하기 위해 3명이 공통적으로 사용해본 기술인 Next.js
, Zustand
, React Query
, Tailwindcss
로 결정하게 되었다.
프로젝트 제작에 있어 가장 주요한 기능 다섯 가지는 다음과 같았다:
이 중 2, 3, 4번의 경우 OCR과 AI를 활용하는데, 백엔드의 성욱님이 AI로 해커톤 수상 경력이 있고 이해도가 높으신 편이라 백엔드에서 처리를 하기로 하였다.
프론트엔드에서는 대체로 평이한 UI 작업이 있었는데, 나는 그 중에서 가장 복잡할 것으로 생각되는 편지 내 키워드 선택 파트를 맡게 되었다. 편지 내용 안에서 키워드를 선택해 서버로 보내기까지의 작업이다.
먼저 편지 상세보기 페이지를 보면 각 문장별로 원문과 번역문이 매칭되어있고, 문장을 누르면 문장에 포함된 키워드들의 해석, 예문, 반의어, 동의어를 보고 원하는 키워드를 선택할 수 있는 페이지로 넘어가게 된다. 키워드를 선택하면 해당 키워드는 문장에서 빨간색으로 하이라이트 처리되며, 최종적으로 키워드를 저장하는 버튼을 눌러 서버에서 맞춤형 학습자료를 만들게 된다.
백엔드에 편지 내용을 요청하면 응답으로 편지의 정보와 함께 내용을 받게 되는데 원문인 originText
, 번역문인 translatedText
로 주어져 있고 한글 문장의 경우 아래와 같이 주요 키워드를 표시해 받게 된다.
*(과일)*에는 *(사과)*와 *(배)*와 *(딸기)*가 있습니다.
그래서 정규식을 이용해 각 문장의 키워드를 분리하고 스타일을 다르게 주도록 만들었다.
const REG_EXP = /\*\(([^)]+)\)\*/g;
const renderStyledSentence = (sentence: string) => {
let match;
let lastIndex = 0;
const styledSentence = [];
while ((match = REG_EXP.exec(sentence)) !== null) {
const [wordWithMark, onlyWord] = match;
const startIndex = match.index;
// 키워드의 앞부분
styledSentence.push(
<span key={lastIndex} className="text-text-info">
{sentence.substring(lastIndex, startIndex)}
</span>
);
// 키워드
styledSentence.push(
<span
key={lastIndex + 1}
className={`${keywords.includes(onlyWord) ? "text-primary-7" : "text-text-info"} font-bold`}
>
{onlyWord}
</span>
);
// lastIndex 밀기
lastIndex = startIndex + wordWithMark.length;
}
// 다음 match 키워드가 없는 경우
styledSentence.push(
<span key={lastIndex} className="text-text-info">
{sentence.substring(lastIndex)}
</span>
);
return <p>{styledSentence}</p>;
};
정규식의 exec
를 이용해 *( )*
내부의 키워드를 찾아 별도의 스타일링을 적용하도록 로직을 구성했다. 정규식에 일치하는 string
값을 찾으면 선택한 키워드 배열인 keywords
에 포함되어있는지 여부에 따라 하이라이트 색상을 다르게 지정하고 나머지 string은 모두 일반 문장 스타일을 적용했다.
편지 상세 페이지에서 각 문장을 누르면 문장에 속한 키워드들이 나열되고, 각 키워드를 선택할 수 있는 페이지로 이동하게 된다.
이때, 각 키워드별 해석, 용례, 반의어, 동의어를 ChatGPT API를 사용하여 보여주도록 하였다.
const useGetGPTDataQueries = (keywords: string[]) => {
return useQueries({
queries: keywords.map((keyword) => {
return {
queryKey: ["GPTData", keyword],
queryFn: () => getAITranslate(keyword),
};
}),
});
};
React Query
의 useQueries
훅을 활용해 각 키워드들에 개별 GPT 해석 데이터를 받아올 수 있도록 처리하였고 개별 데이터가 병렬로 처리되어 각 키워드별 렌더링에 영향을 미치지 않도록 하였다.
다만 한 가지 문제가 있었는데, Open AI API Key를 환경변수에 담아두더라도 API 호출 시 브라우저에 API key가 그대로 노출된다는 것이었다.
이를 해결하기 위해 Next.js
의 API Routes
를 사용했다. API Routes
는 서버 사이드에서 API요청을 처리하며 서버 환경변수를 사용해 외부 서비스에 안전하게 접근할 수 있다.
app/api
경로에 원하는 API 디렉토리를 만들고 내부에 route.ts
파일을 만든다. route.ts
내부에 비동기 함수를 작성해 준다.
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export const POST = async (req: NextRequest) => {
try {
const { searchParams } = req.nextUrl;
const keyword = searchParams.get("keyword");
if (!keyword) {
return new Response(JSON.stringify({ error: "키워드가 필요합니다." }), { status: 400 });
}
const response = await openai.chat.completions.create({
response_format: { type: "json_object" },
model: "gpt-4o",
messages: [
{
role: "system",
content: prompt,
},
{
role: "user",
content: keyword,
},
],
temperature: 0,
max_tokens: 300,
top_p: 0,
frequency_penalty: 0,
presence_penalty: 0,
});
const completion = response.choices[0].message.content;
return new Response(JSON.stringify({ completion }));
} catch (error) {
console.error("Error in handler:", error);
return new Response(JSON.stringify({ error: `${error}` }));
}
}
기훈님이 이전에 chatGPT API를 사용해 보신 경험이 있어 많은 도움을 받아 기능을 구현할 수 있었다.
GPT 4o 모델을 사용하였고 결과를 JSON
형태로 받아 파싱하여 데이터를 렌더링하였다.
const cleanJsonString = (jsonString: string) => {
return jsonString.replace(/```json|```|`/g, "").trim();
};
const parsedData = data ? JSON.parse(cleanJsonString(data)) : null;
그 결과 이렇게 키워드별 데이터를 chatGPT에게서 받아올 수 있게 되었다.
프로젝트 제작도 중요했지만, 24일까지 제출해야 하는 발표 자료도 정말 중요했다. 29일에 있을 데모데이에서 발표 자격이 주어지는 최종 10팀을 선발하기 위해 소스코드와 발표 자료로 심사를 하기 때문이다.
우리는 23일까지 전체적인 플로우에 맞춰 개발을 거의 끝냈지만, 발표 자료 준비가 미흡한 상태였다. 오후 1시부터 회의를 하면서 발표 자료를 구상하는 동시에, 기능이 미흡한 부분을 계속 수정하고 다듬어갔다. 그때는 알지 못했다. 이 회의가 다음날 아침 8시까지 이어질 줄은...
발표용 PPT를 만들어 주시기로 했던 하영님의 퇴근이 늦어지면서, 다음날 출근해야 하는 상황임에도 불구하고 밤을 새워가며 PPT 작업을 하셨다. 그동안 개발진들은 배포한 페이지에 오류가 없는지, 스캔과 인쇄는 잘 되는지 확인하는 작업을 이어갔다.
PPT가 완성된 오전 8시가 되어서야 발표 자료를 운영 측에 제출하고 잠을 잘 수 있었다. 하영님은 출근해서 몰래 쪽잠을 주무셨다고 한다 😭.
최종 발표 행사인 데모데이 당일이 되었다. 행사는 잠실 롯데월드 타워 세미나실에서 열렸고 오전 10시부터 9시간가량 이루어지는 긴 일정이었다.
명찰과 티셔츠를 받고 본격 행사가 시작되었다.
선착순으로 먼저 도착한 팀부터 심사위원분들 앞에서 약 5분가량 간단하게 서비스 시연과 설명을 할 수 있는 피칭 기회가 주어졌다. 우리 팀은 11번째로 피칭을 진행하였는데, 아무런 대본 없이 갑자기 심사위원 앞에서 말하려니까 꽤 긴장되는 자리였다.
간단한 서비스 설명을 마치고 실제 서비스 시연을 하는 과정에서 AI 관련 내용을 설명하거나 질문에 답을 할 때는 도훈님과 성욱님의 도움을 받아 무사히 피칭을 완료하게 되었다.
2시간가량의 피칭 시간이 끝나고 연어 샐러드, 버섯 소불고기, 김치전, 파전, 과일 등이 들어있는 도시락으로 점심 식사를 하였다.
오후에는 본격적으로 메인 행사에 돌입했다. 본선에 진출한 10개 팀의 발표를 듣고 시상까지 하게 되는데 우리 팀은 나름 서비스 완성도와 아이디어에 자신이 있었기 때문에 발표자인 하영님이 열심히 발표 준비를 하고 계셨다.
그리고 최종 10팀의 명단이 공개가 됐는데...
명단에 우리 팀은 없었다.
발표 직후에 믿을 수 없다는 생각 반, 저 10팀이 얼마나 잘했는지 궁금하다는 생각 반이었다. 물론 첫 해커톤 참가인 사람이 대다수인 팀이었고 참가에 의의를 두기로 했지만, 프로젝트를 제작하면서 다들 열심히 해준 덕분에 기대감이 한껏 높아져 있던 건 사실이었다. 이렇게 열심히 준비했는데 발표도 못 해본다는 게 믿겨지지 않았다. 다른 팀원들도 실망감을 100% 드러내진 않았지만 정말 아쉬워했을 것 같다.
아쉬움을 뒤로하고 최종 10팀의 발표를 3시간가량 집중해서 보았다. 역시 최종 발표답게 다들 아이디어와 발표 내용이 깔끔하고 좋았다. 특히 3팀 정도는 우열을 가리기 힘들다 싶을 정도로 잘 만든 것 같았다.
물론 우리 팀 작품도 10팀에 붙었다면 장려상 정도는 노릴만하지 않았을까 싶긴 한데...
모든 팀의 발표를 마치고 조개국, 돼지불고기, 산적, 깻잎전, 호박죽, 잡채 등이 차려진 한 상으로 저녁 식사를 하였다.
식사 이후 시상식이 진행되었다. 최우수 1팀, 우수 1팀, 장려 2팀, 총 4팀을 시상하였고 예상했던 3팀 중 2팀이 각각 최우수상과 우수상을 수상하였다. 솔직히 이 2팀은 이 짧은 기간에 만든 서비스가 맞나 싶을 정도의 서비스 완성도와 발표 내용이었다. 🏆
럭키 드로우 추첨과 기념사진 촬영을 끝으로 Epson Innovation Challenge가 마무리되었다.
짧다면 짧고 길다면 긴 한 달간 다 같이 열심히 작업한 만큼 시원섭섭한 기분이 들었다. 너무 좋은 팀원들을 만나게 되어서 정말 재밌게 작업했고 새로운 것들을 많이 알아가면서 한 걸음 더 성장한 개발자가 될 수 있을 것 같다.
새로운 인연들을 만나면서 함께 성장하는 것이 해커톤의 또 다른 묘미가 아닐까 싶은 생각과 함께 해커톤 후기를 마친다!
✨ 감사합니다! ✨
멋진 아이고 팀 응원합니다!!!!!!!! 짝짝짝짝짝