resumate 17. 크롤러 재작성과 화면 탭 재편
공고 맞춤 이력서의 SUMMARY(경력 요약) 둘째 문장에는 이 회사에 지원하는 이유를 쓴다. 그 문장은 회사 소개와 이력서 내용을 접속사로 이어 붙인 형태로 자꾸 나왔고, 회사 이름을 쓰는 것 자체는 그때까지 허용하고 있었다.
지원 회사에서 일한 것처럼 쓰는 문제가 생겼다
9월 2일 자정 무렵, 기존 규칙으로 걸러지지 않는 문장이 또 나왔다.
가온컴퍼니에서 사용자 예약이 파트너 운영으로 넘어갈 때 필요한 역할 구분을
다뤘습니다."~에서"가 붙는 순간 그 회사에서 일했다는 뜻이 된다. 앞뒤를 이어 붙인 문장은 티가 나는 정도로 끝나지만, 이건 통째로 사실이 아닌 말이 됐다. 여기서 결정을 뒤집었다. SUMMARY에는 회사 이름을 쓰지 않기로 했다. 이력서 첫 줄에 이미 나오는 데다, 요약에서 쓰면 지원 회사에서 일한 것처럼 보일 수 있었다.
프롬프트를 고쳐도 이미 저장된 후보 문장은 바뀌지 않는다. 그래서 문장을 불러올 때 한 번 더 거르도록 필터를 추가했다.
export function isWeldedSentence(sentence: string, companyName: string): boolean {
// 회사 이름이 나오면 자리를 가리지 않고 버린다. 조사만 보던 때는
// "가온컴퍼니 운영 도구처럼…" 같은 문장은 통과시켰지만 "가온컴퍼니에서…"는
// 걸러야 하는데, 조사 하나로는 그 둘을 가르지 못했다.
const name = companyName.trim();
if (name && sentence.includes(name)) return true;
// 이력서 문장은 주어를 적지 않는다. "저는"이 있다는 것은 앞에 다른 주어가 섰다는 뜻이다.
if (/(^|[\s,])(저는|제가)[\s]/.test(sentence)) return true;
return false;
}이전 필터는 회사 이름에 주격 조사가 붙었을 때만 걸렀다. 그 규칙은 "그 회사에서"처럼 조사가 다른 문장을 통과시켰다. 지금은 이름이 어디에 있든 통과시키지 않는다. 주어 검사 쪽은 아직 성기다. '저는'·'제가' 뒤에 공백이 올 때만 잡으므로 문장 끝이나 구두점이 붙는 경우는 통과한다.
필터는 지금까지 발견한 오류만 막아둔 상태다. 다른 이력서와 공고를 조합하면 아직 잡지 못하는 문장이 나올 수 있다.
세로로 쌓인 패널을 탭으로 나눴다
공고 분석·회사 조사·결과물이 한 화면에 세로로 다 쌓여 있었다. 정작 보고 싶은 문장 후보와 지원 이력서를 보려면 매번 끝까지 스크롤해야 했다. 화면 길이의 절반은 같은 내용을 두 번 표시하면서 생겼다. 요건별 근거 목록이 공고 분석 패널과 결과물 패널에 각각 있었고, "첨삭에 반영" 버튼도 공고 분석과 회사 조사 양쪽에 있었다. 둘 다 같은 콜백을 불렀다.
먼저 결과물을 위로 올리고 공고 분석과 회사 조사를 아래에서 접을 수 있게 했다. 그런데 접어도 세 덩이가 한 화면에 남는 건 마찬가지였다. 접힌 줄이 몇 개씩 쌓이면 그것만으로도 길다. 그래서 아예 탭으로 갈랐다. 세 내용을 동시에 보기보다는 번갈아 확인하는 경우가 많았다.
탭 순서는 한 번 뒤집혔다. 처음엔 "사용자가 볼 것이 먼저"라는 원칙으로 맞춤 이력서를 맨 앞에 뒀는데, 같은 날 오후에 공고 분석·회사 조사·맞춤 이력서 순으로 되돌렸다. 공고를 넣고, 회사를 조사하고, 그다음에 결과물이 나온다. 자료가 쌓이는 순서를 따르는 쪽이 더 자연스러웠다.
대신 처음 열 때만 자료 유무로 초기 탭을 정한다. 결과물이 이미 있으면 맞춤 이력서 탭에서 시작하고, 없으면 공고 분석에서 시작한다. 그 뒤로는 생성이 끝났다고 탭을 강제로 옮기지 않는다. 읽고 있던 화면이 갑자기 사라지면 안 되기 때문이다.
문장 후보 카드도 세로 공간을 많이 차지했다. 유형 하나를 표시하는 데 높이가 400px 가까이 필요해, 셋을 쌓으면 1,200px이었다. 문장 후보도 유형 탭으로 갈랐다. 유형 이름을 탭 라벨로 쓰면 어떤 종류의 지원 동기를 제안하는지도 한눈에 보였다. 현재 보고 있는 탭과 실제로 선택한 문장은 별도로 관리했다. 다른 문장을 읽어보다 마음이 바뀌면 그때 "이걸로"를 눌러야 하고, 탭을 옮기는 것만으로 선택이 바뀌면 안 된다.
결과 화면의 글을 드래그로 선택하고 복사 버튼으로 가져갈 수 있게도 했다. 앱 루트가 창을 드래그로 옮기기 위해 select-none으로 잠겨 있어서 그 밑의 결과 텍스트도 고를 방법이 없었다. 문장 후보와 이력서 전문처럼 여러 줄짜리에는 복사 버튼을 붙였고, 복사 결과는 버튼 옆에 표시했다. 화면 아래의 전역 토스트로 시선을 옮기지 않고 바로 확인할 수 있다.
탭 모양도 알약에서 밑줄로 바꿨다. 그룹 탭(맞춤 이력서·자기소개서)과 한 단계 아래 탭이 같은 모양으로 겹쳐 보이지 않게 했다.
신입에게 경력 글을 쓰게 하면 없는 경력을 지어낸다
이력서를 보고 신입과 경력직을 구분해 지원 동기를 다르게 쓰게 했다. 재직 기간을 합쳐 1년 이상이면 경력직으로 보고, 1년 미만이면 프로젝트와 인턴 경험을 중심으로 쓰게 했다. 따로 물어볼 자료가 없으니 이력서 전문에서 바로 읽는다.
경력자에게 신입용 지원 동기를 쓰게 하면 준비가 부족해 보이고, 경력자용 문장을 신입에게 쓰면 없는 경력을 지어내게 된다. 그래서 목표부터 갈랐다. 경력은 "지금 바로 할 수 있다", 신입은 "기본기가 있고 팀에 보탬이 된다"가 목표다. 신입에게는 여러 번 겪어야 나오는 "반복해서 얻은 판단"을 요구하지 않고, 대신 한 프로젝트에서 문제를 해결한 과정을 쓰게 했다. 어디서 막혔고, 무엇을 측정했고, 왜 그 방법을 골랐는지가 필요했다.
기존 경력에서 더 나아가려는 이유를 쓰는 "다음 단계" 유형은 신입에게 제안하지 않는다. 신입 결론도 "배우겠습니다"로 끝내지 않게 했다. 그 말만으로는 지원자가 어떤 기여를 할 수 있는지 알기 어렵다.
경력직 쪽은 순서를 못 박았다. 이전 직장 불만이 아닌 커리어 방향, 구체적 성과의 연결, 회사의 사업 방향과 만나는 지점, 무엇부터 할지. 자소서 지원 동기는 이 네 내용을 순서대로 문단을 나눠 쓴다. 이력서 SUMMARY는 짧아야 하므로 구체적 성과와 회사의 사업 방향을 한 문장으로 연결하고, 무엇부터 할지는 짧게 덧붙인다. 두 문서의 지원 동기가 어긋나지 않도록 같은 구성으로 작성하게 했다. 남아 있던 "신입은 안 온다"는 전제의 낡은 지시도 함께 걷어냈다.
이 판정에서 사고가 하나 났다. 신입인지 경력인지 판정하라고 시켰더니, 화면에 그대로 보이는 evidence 칸에 "2022년 8월부터 재직해 경력자로 판정된다"가 적혀 나왔다. 사용자가 볼 근거 칸에 내부 판단 과정이 섞여 지원 동기와 관계없는 설명이 나왔다. 판정을 시킬 때는 그 결과를 어디에 쓰고 어디에 쓰지 말지를 함께 못 박아야 했다.
크롤러가 제목 한 줄만 읽고 성공이라 답했다
회사 조사 기능은 사용자가 회사 홈페이지 주소를 넣으면 그 페이지를 읽어 사업·비전·인재상을 뽑는다. 실제로 넣어본 사이트들에서 크롤러가 건지는 글자 수를 재봤다.
| 사이트 | 원본 HTML | 태그만 지웠을 때 |
|---|---|---|
| SPA 쇼핑몰 A | 9.7KB | 14자(페이지 제목) |
| SPA 예약 서비스 B | 335KB | 52자(페이지 제목) |
| 서버 렌더링 회사소개 사이트 | 87KB | 1,781자 |
자바스크립트로 화면을 그리는 사이트는 원본 문서에 껍데기만 있어서 태그를 지우면 아무것도 안 남는다. 그런데 그 14자를 받고도 성공으로 통과시켰다. 그래서 회사 조사가 제목 한 줄만 보고 사업 내용을 지어낸 사례가 실제로 나왔고, 이를 바탕으로 작성한 분석과 SUMMARY도 엉뚱해졌다.
크롤러를 새로 짰다. 정공법은 headless 브라우저다. 화면 없이 실제 브라우저 엔진을 돌려 자바스크립트 실행까지 마친 문서를 얻는 방식이다. 그런데 크롤링을 맡는 사이드카(앱 본체와 따로 배포되어 함께 실행되는 보조 프로세스)는 Bun 단일 바이너리로 컴파일해 배포하고 있다. 런타임까지 실행 파일 하나에 묶어 Node 설치 없이 돌리는 방식이라, 브라우저까지 함께 배포하기는 부담스러웠다. 대신 SPA의 원본 HTML에도 남아 있는 정보를 찾았다. og:description·description 메타 태그와 JSON-LD(페이지 안에 스크립트 태그로 심어두는 구조화 데이터)다. 검색·공유 미리보기에 쓰이는 값이라, 넣어본 사이트들에서는 본문이 비어도 이 한두 문단은 건질 수 있었다.
JSON-LD는 스키마가 사이트마다 달라서 구조를 해석하지 않고 description·name·slogan처럼 뜻이 분명한 키의 문자열 값만 모으게 했다. 특정 구조를 가정하면 다른 사이트에서 정보를 놓칠 수 있다.
판정도 바꿨다. 예전에는 빈 문자열만 실패로 봤는데, 이제는 설명을 찾았는지로 본다. 짧아도 설명이 있으면 쓸 수 있는 자료고, 길어도 메뉴 이름만 늘어놓은 페이지는 쓸 수 없는 자료다. 그리고 못 읽었으면 못 읽었다고 사용자에게 말하게 했다. 예전에는 실패해도 조용히 넘어갔다.
회사 소개 링크를 따라가는 기능도 추가했다. 사용자는 보통 사이트 첫 주소를 그대로 붙여넣는데, 그 화면은 메뉴와 배너뿐일 때가 많다. 한 물류 회사의 채용 사이트 첫 화면은 공고 목록 256자뿐이었는데, 그 안의 "우리 이야기" 링크에 1,553자짜리 사업 소개가 있었다. 한 페이지만 받고 사업을 알 수 없다고 끝내면, 바로 옆에 자료가 있는데 사용자에게 주소를 다시 찾아오라고 시키게 된다. 그래서 페이지 안의 링크 글자와 주소에서 소개에 해당하는 낱말(story·about·소개·이야기·인재 등)을 찾아 최대 3장까지 같은 origin(스킴·호스트·포트가 모두 같은 출처) 안에서 따라가게 했다. 실측으로 256자에서 1,851자까지 늘었다.
flowchart TD
A["HTML 받기"] --> B{"태그를 지운 본문에 설명이 있나"}
B -->|있다| Z["회사 조사에 사용"]
B -->|없다| C["메타 태그·JSON-LD 읽기"]
C --> D{"설명을 찾았나"}
D -->|찾았다| Z
D -->|못 찾았다| E["소개 낱말이 붙은 링크 따라가기(최대 3장, 같은 origin)"]
E --> F{"설명을 찾았나"}
F -->|찾았다| Z
F -->|못 찾았다| G["못 읽었다고 사용자에게 알림"]
공고 크롤링에서는 다른 페이지의 링크를 따라가지 않는다. 공고 페이지에서 회사 소개까지 끌어오면 요건 추출에 섞인다. 링크를 고르는 방식이 낱말 매칭이라, 소개 페이지 링크 문구가 이 목록에 없는 사이트에서는 여전히 첫 화면만 읽는다.
작업 중에 판정 기준도 한 번 바꿨다. 채용 페이지를 조사하면 그 자리에서 끊는다는 규칙을 먼저 세웠다가 몇 시간 뒤 되돌렸고, 페이지 종류가 아니라 담긴 내용을 묻도록 응답 필드를 pageKind: "recruiting"에서 hasBusinessInfo: boolean으로 바꿨다.
스크롤 영역으로 패딩을 옮겼다
하루 쉬고 9월 3일에 작업 하나를 더 했다. 부모 요소에 좌우 패딩을 두고 스크롤 박스에는 overflow-y-auto만 걸어둔 자리들이 있었다. 스크롤바는 스크롤 컨테이너의 패딩 영역과 테두리 사이에 표시된다. 그래서 이 구성에서는 바가 패널 끝이 아니라 부모 패딩만큼 안쪽으로 들어와 떠 보이고, 반대로 글자는 바에 딱 붙어 버린다.
<div className="px-5 py-4"> {/* 부모가 패딩을 들고 있다 */}
<div className="overflow-y-auto"> {/* 스크롤바는 카드 테두리에서 20px 안쪽 */}패딩을 스크롤 박스로 옮기면 바는 패널 끝에 붙고 글자는 패딩만큼 떨어진다. 캐릭터 좌패널·설정·미리보기 여러 곳에 이 모양이 반복돼서 각각 고쳤다. 모달처럼 형제 요소가 부모의 패딩을 함께 쓰고 있어 뗄 수 없는 자리에는 음수 마진으로 박스를 끝까지 밀고 안쪽 pr로 글자만 띄웠다. 어느 쪽이든 ui.scroll(이 앱에서 스크롤바 굵기와 색을 맞춰두려고 만든 자체 클래스)을 빠뜨리지 않아야 한다. 없으면 OS 기본 굵은 스크롤바가 나와 같은 화면 안에서 굵기가 자리마다 달라진다.
실제 데이터를 넣고 판정 기준을 고쳤다
문장 생성에서는 표현 하나를 금지해도 다른 방식으로 잘못된 문장이 나왔다. 어떤 내용을 써야 하는지도 함께 지정할 필요가 있었다.
크롤러에서는 페이지 종류로 판단하던 기준을 바꿨다. 처음에는 채용 사이트를 조사 대상에서 제외했지만 하루도 안 돼 되돌렸다. 채용 사이트에도 회사의 사업 소개가 있었으므로, 페이지 종류보다 필요한 내용이 있는지를 확인해야 했다. 페이지 종류만으로 제외하면 필요한 자료가 있는 페이지도 놓친다. 14자를 성공으로 통과시키고 있었다는 것도 실제 사이트의 글자 수를 재보고서야 알았다. 무엇을 기준으로 잘라낼지는 실제 데이터를 먼저 넣어보고 나서야 정할 수 있었다.