KT 에이블스쿨 잡페어에서 TmaxBI라는 부스를 방문하게 되면서 Tmax라는 회사에 관심이 생겼다. 이 회사에서 에이블스쿨 수료생을 대상으로 어떤 직무를 채용할지 알 수 없기 때문에, 다양한 직무의 Job Description(JD)을 미리 파악해두면 차후 지원서를 효과적으로 작성할 수 있을 것이라 생각했다.
티맥스 - 채용 홈페이지
티맥스 채용 홈페이지입니다.
tmaxcareers.ninehire.site
상시 채용 페이지를 확인해보니, 예상보다 너무 많은 채용 공고가 올라와있어 내용을 하나씩 정리하는건 비효율적이라고 판단했다. 효율적 정보 수집을 위해 Python으로 웹크롤링을 시도했으나, 간단하게 끝날 것이라는 예상과 달리 생각보다 시간이 많이 소요되었다. 그 과정에서 사용한 코드를 하나씩 살펴보면서 원인과 결과를 정리했다.
채용공고 웹크롤링
import requests
from bs4 import BeautifulSoup
# 대상 URL
url = "https://tmaxcareers.ninehire.site/recruit"
# 페이지 요청
response = requests.get(url)
# 요청 성공 여부 확인
if response.status_code == 200:
# HTML 파싱
soup = BeautifulSoup(response.text, 'html.parser')
# 모든 'a' 태그 찾기
links = soup.find_all('a')
for link in links:
href = link.get('href') # 'href' 속성을 가져옵니다
if href:
print(f"Link: {href}")
else:
print(f"Failed to retrieve the page. Status code: {response.status_code}")
먼저 requests와 bs4로 페이지가 문제없이 출력되는지 확인했다. 만약 html 선택자로 파싱이 안된다면, 네트워크 정보를 하나씩 추가하면서 연결에 성공할 때까지 작업을 반복해야하기 때문이다. 다행히, response 200으로 연결 자체에는 문제가 없음을 확인했다.


다음으로, 사이트의 구조를 확인했다. 계열사별로 다양한 채용공고가 기재되어있으며, 개별 공고를 누르면 직무별로 담당 업무 및 필요한 역량을 확인할 수 있다. 여기서 개별 채용 공고의 URL이 job_posting/고유ID 구조임을 확인했다.
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
import re
import time
# 웹 드라이버 설정
driver = webdriver.Chrome(service=Service())
# 대상 URL
url = "https://tmaxcareers.ninehire.site/recruit"
driver.get(url)
# 페이지 로딩 대기
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.TAG_NAME, "body"))
)
# 스크롤을 통해 페이지의 모든 콘텐츠 로드
last_height = driver.execute_script("return document.body.scrollHeight")
while True:
# 페이지 끝까지 스크롤
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# 스크롤 후 로딩 대기
time.sleep(2)
# 새로운 페이지 높이 계산
new_height = driver.execute_script("return document.body.scrollHeight")
if new_height == last_height:
break
last_height = new_height
# 페이지 소스 가져오기
page_content = driver.page_source
# 정규 표현식으로 'job_posting'이 포함된 링크 추출
pattern = r'"(https://tmaxcareers\.ninehire\.site/job_posting/[^"]+)"'
matches = re.findall(pattern, page_content)
for match in matches:
print(f"Link: {match}")
# 브라우저 종료
driver.quit()
구조를 확인했으니, 이제 개별 채용공고 링크를 가져와서 파싱을 진행하면 끝이라고 생각했다. Webdriver로 페이지를 끝까지 내려서 모든 페이지 소스를 가져오고, job_posting이 포함된 링크를 추출했다.


일부 링크는 정상적으로 현재 채용 공고를 출력했지만, 이미 접수가 중단된 채용공고들이 대다수였다. 단순히 URL 구조만으로는 현재 진행중인 채용 공고 정보를 가져올 수 없었다.

새로운 방법으로, 채용공고의 HTML 태그 구조를 확인했다. 그 결과, div 태그 중 "fuGTMM" 으로 끝나는 class 안에 해당 페이지의 a태그가 전부 들어있음을 확인했다. 해당 태그를 webdriver로 가져왔다.
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 웹 드라이버 설정
driver = webdriver.Chrome(service=Service())
# 대상 URL
url = "https://tmaxcareers.ninehire.site/recruit"
driver.get(url)
# 페이지 로딩 대기
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "JobPostingsDropdownTypeLayoutJobPostings__Content-sc-b886b4af-0"))
)
# 특정 클래스 이름을 가진 모든 div 요소 찾기
divs = driver.find_elements(By.CLASS_NAME, "JobPostingsDropdownTypeLayoutJobPostings__Content-sc-b886b4af-0.fuGTMM")
# 각 div 요소 안의 a 태그 href 추출
for div in divs:
links = div.find_elements(By.TAG_NAME, "a")
for link in links:
href = link.get_attribute('href')
print(f"Link: {href}")
# 브라우저 종료
driver.quit()

그 결과, 현재 진행되고 있는 채용공고 링크들을 수집할 수 있었다. 사진에 있는 링크는 1페이지에 기재된 공고이고, 모든 페이지의 공고를 가져 오기 위해 try-except 문을 활용했다.
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 웹 드라이버 설정
driver = webdriver.Chrome(service=Service())
# 대상 URL
url = "https://tmaxcareers.ninehire.site/recruit"
driver.get(url)
# 페이지 로딩 대기
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "JobPostingsDropdownTypeLayoutJobPostings__Content-sc-b886b4af-0"))
)
# 페이지 수를 설정
max_pages = 10
for page in range(1, max_pages + 1):
# 페이지 번호 출력
print(f"Processing page {page}...")
# 특정 클래스 이름을 가진 모든 div 요소 찾기
divs = driver.find_elements(By.CLASS_NAME, "JobPostingsDropdownTypeLayoutJobPostings__Content-sc-b886b4af-0.fuGTMM")
# 각 div 요소 안의 a 태그 href 추출
for div in divs:
links = div.find_elements(By.TAG_NAME, "a")
for link in links:
href = link.get_attribute('href')
print(f"Link: {href}")
# 다음 페이지로 이동
if page < max_pages:
try:
next_page = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, f"//li[@title='{page + 1}']/a"))
)
next_page.click()
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "JobPostingsDropdownTypeLayoutJobPostings__Content-sc-b886b4af-0"))
)
except Exception as e:
print(f"Failed to navigate to page {page + 1}: {e}")
break
# 브라우저 종료
driver.quit()
모든 페이지를 확인한 결과, 189개의 채용공고 링크를 확보했다. 이제 개별 링크에서 어떤 정보를 가져올지 확인해보자.


링크를 확인하고 기업명, 직무, 세부 직무, 경력 사항, 고용 형태, 근무지, 채용 방식, 담당 업무, 필수 요건, 우대 요건 이상 10개의 column으로 구성된 테이블을 제작했다.
# 각 링크를 방문하여 채용 공고 내용 추출
job_data = []
for job_url in job_links:
driver = webdriver.Chrome(service=Service())
driver.get(job_url)
# 페이지 로딩 대기
try:
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "PostingLayout1Header__TitleWrapper-sc-128164c6-2"))
)
try:
# 기업명, 직무, 세부 직무, 경력, 고용 형태, 근무지, 채용 방식 추출
label_container = driver.find_element(By.CLASS_NAME, "LabelGroup__Container-sc-d40b1697-0.hLfIyn")
labels = label_container.find_elements(By.TAG_NAME, "span")
label_texts = [label.text for label in labels]
if len(label_texts) == 7:
company, job_title, detailed_job, experience, employment_type, location, hiring_method = label_texts
elif len(label_texts) == 6:
company, job_title, experience, employment_type, location, hiring_method = label_texts
detailed_job = ""
else:
continue
# 담당 업무, 필수 요건, 우대 요건
content_container = driver.find_element(By.CLASS_NAME, "PostingHTMLContent__Content-sc-a46df660-0.dMdmJ")
sections = content_container.find_elements(By.XPATH, ".//h3")
tasks = []
requirements = []
preferences = []
for section in sections:
title = section.text
try:
ul_element = section.find_element(By.XPATH, "following-sibling::ul")
li_elements = ul_element.find_elements(By.TAG_NAME, "li")
items = [li.text for li in li_elements]
if title == "✔ 담당 업무":
tasks.extend(items)
elif title == "✔ 필수 요건":
requirements.extend(items)
elif title == "✔ 우대 요건":
preferences.extend(items)
except Exception as e:
print(f"Failed to process section '{title}' at {job_url}: {e}")
continue
# 데이터 저장
job_data.append({
'Company': company,
'Job Title': job_title,
'Detailed Job': detailed_job,
'Experience': experience,
'Employment Type': employment_type,
'Location': location,
'Hiring Method': hiring_method,
'Tasks': '\n'.join(tasks),
'Requirements': '\n'.join(requirements),
'Preferences': '\n'.join(preferences),
})
except Exception as e:
print(f"Failed to process job post at {job_url}: {e}")
except Exception as e:
print(f"Failed to load job post at {job_url}: {e}")
finally:
# 브라우저 종료
driver.quit()
# 데이터프레임 생성
df = pd.DataFrame(job_data)
# 데이터프레임 파일로 저장
df.to_excel('job_postings.xlsx', index=False)
print("Data extraction complete and saved to job_postings.xlsx")

크롤링 결과, 전체 189개의 공고 중 30% 정도만 내용을 가져올 수 있었다. 70%의 공고를 살펴보니 계열사별로 공고 내부의 html 구조가 달랐고, 제목을 h3태그로 지정한 공고와 h1태그로 지정한 공고도 있어서 제대로 파싱이 되지 않았다. 세부적으로 발견한 사항은 다음과 같다.
- li 태그 안에 텍스트가 직접 제시된 경우
- li > p > span 태그 구조
- 기타 예외 처리
문제 사항을 반영해서 수정한 결과, 전체 공고 중 70%는 데이터 프레임으로 저장에 성공했다. 실패한 30%의 대부분은 R&D 직무이었는데, 해당 직무는 세부 직무가 비어있는 공고들이다. 다른 공고들과 달리 태그 구조가 달라서 파싱에 실패한 것으로 추측된다.
TmaxBI 채용공고 분석
데이터의 수가 많진 않지만, 어떤 역량을 강조하는지 궁금해서 TmaxBI의 공고 24가지로 TF-IDF 분석을 시도했다. 이 방법은 각 단어가 채용공고에서 얼마나 중요한지를 평가하는 방법이다. 여러 JD에서 자주 등장하는 단어를 다른 JD에서는 얼마나 자주 언급되지 않는지를 고려하여 어떤 역량이 두드러지는지를 분석할 수 있다. 일반적인 채용공고에 4년제 대학 졸업자, 해외여행 결격자와 같은 역량이 반복되기 때문에도 TF-IDF가 적합한 방법이라 판단했다.
# df는 TmaxBI의 채용공고들로 구성된 테이블
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
df['combined'] = df['Requirements'].fillna('') + ' ' + df['Preferences'].fillna('')
# Text preprocessing function
def preprocess_text(text):
# Remove special characters and digits
text = re.sub(r'[^가-힣\s]', '', text)
return text
# Apply preprocessing
df['cleaned_text'] = df['combined'].apply(preprocess_text)
# TF-IDF 벡터라이저 초기화
tfidf_vectorizer = TfidfVectorizer(max_features=100) # 상위 100개의 중요 단어를 추출합니다.
tfidf_matrix = tfidf_vectorizer.fit_transform(combined_text)
# TF-IDF 결과를 DataFrame으로 변환
tfidf_df = pd.DataFrame(tfidf_matrix.toarray(), columns=tfidf_vectorizer.get_feature_names_out())
# 결과 출력 (단어와 각 문서에서의 TF-IDF 값)
print(tfidf_df)
# 각 문서에서 가장 중요한 단어들 추출
for i, row in tfidf_df.iterrows():
print(f"Document {i+1}:")
print(row.sort_values(ascending=False).head(10)) # 상위 10개 단어 출력
TF-IDF 분석 결과는 개별 채용 공고에서 특정 단어가 해당 문서 내에서 얼마나 중요한지를 나타낸다. 값이 높을수록 해당 단어가 그 문서에서 중요한 단어로 간주된다. 분석 결과는 다음과 같다.
문서별로 높은 TF-IDF 값을 가진 단어들:
- Document 2: '경력이', '기획', '업무', '서비스'
- Document 6: '테스트', '수행', '자동화'
- Document 14, 15, 16: '경험이', '프로젝트', 'pm', 'it'
- Document 18: '기획', '역량', '데이터분석'
- Document 19: '디자인', '협업', '경력'
- Document 23, 24: '클라우드', '운영', '커뮤니케이션', 'db', '개발'
공통된 역량으로 '있으신', '보유하신', '경험' 등과 같은 단어가 여러 문서에서 반복적으로 등장하고 있다. 이는 대부분의 채용 공고에서 관련 경험과 보유 역량을 중요하게 다루고 있음을 나타낸다.
몇몇 문서에서는 TF-IDF 값이 모두 0으로 나와 있다. 이는 해당 문서에서 의미 있는 단어들이 포함되지 않거나, 단어가 매우 일반적이어서 모든 문서에 걸쳐 균일하게 분포되기 때문에 TF-IDF 값이 낮게 나오게 되는 경우이다. 4년제 대학 졸업자, 해외여행 결격자와 같은 역량으로 추정된다.
TmaxBI에서는 기획, 프로젝트 관리, IT 및 클라우드 관련 역량, 데이터 분석, 테스트 자동화 등을 중점 역량으로 다루고 있으며, 경험 및 보유 역량에 대한 강조가 거의 모든 공고에서 나타나고 있음을 확인할 수 있다.
'Data Science' 카테고리의 다른 글
| [API] 어디까지 가능한 걸까? (1) | 2025.09.20 |
|---|---|
| [API] API의 정의와 원리 (1) | 2025.09.20 |
| [에이블스쿨] DNN과 활성화 함수 (0) | 2024.04.10 |
| [에이블스쿨] 딥러닝이란? (0) | 2024.04.06 |
| [에이블스쿨] SVM (2) | 2024.03.24 |