[플레이데이터 SK네트웍스 Family AI 캠프 21기] 5주차 회고 (단위 프로젝트1)

요한·2025년 10월 25일

🗓️ 5주차: 2025.10.20 ~ 2025.10.24

이번주는 MySQL, 웹 크롤링을 배워서 이를 기반으로 단위 프로젝트를 진행하였다. SQL 문법 SELECT부터 JOIN까지 실습, 웹 크롤링은 beautifulsoup, selenium 라이브러리를 활용하여 실습, 그리고 5명의 팀원들과 함께 역할분담을 하여 각자 맡은 일을 담당했다.

SQL 기본 문법

테이블 데이터 예제로 오라클에 있는 emp를 활용한다. 웹사이트에 sql 파일로 DB 테이블 내부에 데이터를 저장하고 원하는 데이터를 출력해본다. 가장 기본적인 구문은 SELECT, FROM, WHERE로 예를 들면 테이블을 FROM에서 가져오고 WHERE 조건에 맞게 원하는 컬럼을 SELECT에 작성한다.

select emp_id, emp_name "emp_name", hire_Date "입사일"
from emp;
where salary > 10000;

위 예제를 해석하자면 emp 테이블에 급여 1만 달러가 넘는 직원의 id, 이름, 입사일을 출력한다. select의 컬럼 바로 뒤에 별칭을 지정할 수 있어 보기 편하다.

DB를 관리하다 보면 여러 테이블을 접근해야 할 때가 많다. 이럴 때 바로 JOIN이라는 기능이 유용하다. source 테이블과 target 테이블간의 동일 key(메인, 외래키)가 있다면 INNER JOIN으로 합치며 한 쪽으로 테이블을 전부 사용하고 나머지 일부 합칠 수 있는 OUTER JOIN이 있다.

직원 ID가 200인 직원의 ID,이름,급여,업무,부서를 조회하는 쿼리는 아래와 같다. 여기서 테이블은 emp, job, dept를 사용해야 하며 emp와 job을 id가 같은 조건으로 합치고 job과 dept을 같은 id로 조건을 걸어 합친다. 그러면 where 조건으로 아이디가 200인 직원을 찾을 수 있다.

select e.emp_id, e.emp_name, e.salary, j.job_title, d.dept_name 
from emp e join job j on j.job_id=e.job_id 
  join dept d on e.dept_id = d.dept_id 
where e.emp_id=200; 
  

조금 더 어렵게 가볼까? 03~05년도 입사한 직원의 id, 이름, 업무, 연봉, 입사일, 메니저이름, 매니저입사일, 부서, 부서위치를 조회해본다. 직원 emp 기준으로 먼저 job 테이블을 left outer join을 하며 그 다음 dept(부서), 매니저 emp 테이블을 left join한다. 그 다음 where 절에서 03-05년도로 입사일을 비교하여 출력이 제대로 나오는 것을 확인한다.

select e.emp_id, e.emp_name 'employee', j.job_title, e.salary, e.hire_date, 
m.emp_name 'manager', m.hire_date 'mgr hired', d.dept_name, d.loc
from emp e left join job j on e.job_id = j.job_id 
		   left join dept d on e.dept_id = d.dept_id 
    	   left join emp m on e.mgr_id = m.emp_id
where year(e.hire_date) between 2003 and 2005;

web crawling

웹 크롤링은 웹 페이지의 원하는 데이터를 자동으로 추출하는 기술이다. 정보가 방대한 세상에서 자동화 기술을 이용해 데이터를 분석하여 시간과 비용을 줄여준다. 사용자가 웹 사이트를 전부 검색할 필요 없이 특정 항목만 추출하면 깔끔하게 정리가 된다. 가장 쉽게 알 수 있는 방법은 사용하고 있는 웹 브라우저에서 F12를 누른다.

웹 크롤링에서 제일 많이 사용하는 파이썬 라이브러리는 Beautifulsoup4이며 웹페이지를 파싱하기 위한 lxml과 함께 pip를 통해 다운로드 받는다.

pip install  beautifulsoup4  lxml

원하는 웹을 지정하여 tag의 이름을 text로 뽑아서 조회, find() 함수, CSS selector로 방법은 많다.

from bs4 import BeautifulSoup

with open("example.html", "rt", encoding='utf-8') as fr:
    html_doc = fr.read()

# find_all
soup = BeautifulSoup(html_doc, "lxml")
result = soup.find_all("div")
tag1 = result[0]
print("content:", tag1.text, tag1.get_text())
print("class속성값:", tag1.get("class"), tag1['class'])
  
# tag의 이름
print("content text:", result.text)
print("content text:", result.get_text())
print("attribue의 value:", result.get("class"))
print("attribue의 value:", result["class"])
  
# CSS selector
result2 = soup.select("a")         #  태그이름(a)    
result3 = soup.select("a[href='http://www.naver.com']")  
print(result, result2, result3)
  

홈페이지에 접속하려면 파이썬의 requests 모듈을 써야 한다. HTTP 프로토콜의 GET(클라이언트가 서버에게 요청)과 POST(클라이언트의 데이터를 서버로 전송)방식으로 URL을 넣으면 된다. get은 querystring의 요청파라미터나 dictionary의 name=value 형태로 파라미터로 보내는 방법이다. URL 뒤에 ?를 붙이며 이것이 querystring이다.

pip install requests
import requests

url = "https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=0&ie=utf8&query={}"
keyword = input("keyword:")
url = url.format(keyword)

res = requests.get(url)  # 요청 - 응답: 반환: 응답데이터

print(res.status_code)
if res.status_code == 200:
  print(type(res))
  print(type(res.text), len(res.text)) 
  print(res.text[:1000])
else:
  print("응답을 받지 못함.", res.status_code)

request로 연결하기 위해선 서버의 응답 결과를 확인해야 하는데 200번대는 성공이며 나머지 응답 상태 코드는 오류가 있거나 잘못된 방식이다.

단위 프로젝트: 국내 친환경 자동차 비교분석과 수소차 지원정보 서비스

파이썬, MySQL, 웹 크롤링을 기반으로 이틀 간 프로젝트를 진행하였다. 프로젝트 시작 전에 아이디어 공유와 업무 분장을 바탕으로 설계 및 구현하였다. 수소차를 선택한 이유가 기존 친환경 자동차 중 전기차는 최근 몇 년 사이 전국적으로 보편화가 된 반면에 수소차는 그에 미약하기에 수소차 관련 통계 자료를 기반으로 수소차 충전소 보급 확대 및 수소차 이용 독려를 하는 것이 목적이다.
업무는 데이터베이스, 프론트앤드(streamlit), 웹 크롤링, 백앤드, 데이터 전처리 이렇게 나누었다. 화면으로는 연도별 수소,전기차 증가 그래프, 지역별 수소차 충전소 현황, 수소차 FAQ, 그리고 수소차 충전소 조회 기능을 제공하는 것이 기본 기능이다. 웹 크롤링한 데이터를 MySQL 서버에 저장하고 그 csv 파일을 화면에 matplotlib 라이브러리를 활용하여 그래프를 도출했다.
내가 담당한 파트는 웹 크롤링이며 이후에 백앤드의 DB insert, fetch 업무도 지원해주었다. 처음에는 디버깅 하느라 시간이 걸려 크롤링 시작도 못했는데 1시간 단위로 시간이 지나 한 페이지, 그 다음 페이지 그리고 DB에 저장하기 위해 데이터를 다듬는 업무를 하여 원활히 진행할 수 있었다. 시간이 조금 더 있으면 팀 구성원들의 막히는 부분도 하나씩 검토해 보고 코드를 많이 고칠 수 있었는데하는 아쉬움은 있었다. 아직 산출물 작업과 코드 수정할 시간이 더 있으니 그 때보다 많이 개선될 것으로 본다.

📌 KEEP

  1. where 절을 활용하여 원하는 조건으로 컬럼을 출력하였다. '대략 이렇게 만들어야지' 라고 막연하게 생각하면 코드 생성이 힘들 수 있다. 주어진 예제를 최대한 많이 짜보면서 나만의 것으로 만드는 것이 중요하다.

    select emp_id, emp_name, job from emp where job in ('IT_PROG', 'ST_MAN');
    select emp_name from emp where emp_name like '__e%';
    select emp_id, emp_name, hire_date from emp where hire_date > '2004-05-31';
    select emp_name, hire_date from emp where job like '%MAN%'
     and dept_name='Shipping' and hire_date >= '2005-01-01';
    select emp_id, emp_name, salary from emp where salary > 5000 order by salary desc; # order by 3 해도 무관

    다음으로 group by, having을 이용하여 컬럼 값들을 집계하여 통계를 내본다. 컬럼 별로 계산 함수(sum,avg 등)을 사용하여 값을 저장하는 과정을 거치면 번거롭다.

    예를 들면 부서 별로 평균 급여와 직원 수를 조회한다고 하면 group by가 유용하다. 여기서 having을 사용하여 group by로 묶인 평균 급여를 5000 달러 이상 필터링해준다.

    select dept_name, avg(salary), count(*) 
     from emp 
     where dept_name is not null 
     group by dept_name 
     having avg(salary) >= 5000 order by 2;
  2. 웹 크롤링에서 제일 중요한 사항이 웹사이트에서 F12를 누르고 태그를 확인할 때 html인지 javascript로 짜여진 것인지 봐야 한다. javascript에서 AJAX 기법의 비동기적 요청은 requests만으로 크롤링이 힘들다. 게다가 requests는 로그인 요청 페이지로 넘어갈 때 크롤링이 제한 사항이 있다. 그래서 이러한 단점을 보완한 라이브러리가 selenium이며 이를 적절히 활용해야 한다. selenium이 구글 크롬 기반이라서 편하다는 장점은 있지만 로딩이 느리기에 단점이 없진 않다.

    아래의 예제: 네이버 홈페이지에서 검색 창에 '날씨 예보'를 입력하여 엔터 키를 누른다. 이후 웹 페이지에는 날씨와 관련된 내용들이 나타난다. selenium에서 알아두어야 할 사항이 implicit wait이다. 특정 조건이 만족하기 까지 기다리는 작업이다. 웹페이지를 열었는데 바로 작업해주면 크롤링이 어려우니 일종의 대기 모드를 걸어야 한다. 웹 사이트를 열지 않고 ChromeOptions()를 호출하여 실제와 동일하게 작동되도록 headless설정도 해준다.

    pip install selenium webdriver-manager
    from webdriver_manager.chrome import ChromeDriverManager
    from selenium.webdriver.chrome.service import Service
    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.common.keys import Keys
    import time
     
    option = webdriver.ChromeOptions()
    option.add_argument("--headless")
    service = Service(executable_path=ChromeDriverManager().install())
    browser = webdriver.Chrome(service=service,options=option)  
    
    browser.implicitly_wait(5)
    browser.maximize_window()
     
    browser.get("https://www.naver.com")
    time.sleep(2)
    browser.get_screenshot_as_file("naver_main.png")
     
    query_textfield = browser.find_element(By.ID, "query")
    query_textfield.send_keys("날씨 예보")
    query_textfield.send_keys(Keys.ENTER)
    browser.close()

🛑 Problem

  1. SQL 서브쿼리: SQL에는 괄호를 사용하여 서브쿼리를 만들 수 있다. DB의 양이 방대해지고 정교한 데이터를 다뤄야 하는 경우 쿼리의 문장 수는 복잡해진다. select, from, where, having에서 사용하며 아래와 같이 예시를 보면 알 수 있다.
    직원 아이디가 115인 직원과 같은 업무를 하는 부서에 속한 직원을 조회한다. 업무와 부서의 id와 where절에서 직원 ID가 115인 경우를 서브쿼리로 매칭시키면 되는데 예시문만 보면 바로 이해가 가긴 간다. 처음 SQL을 접하는 자에게는 섬뜻 어떻게 접근해야할지 모르는 경우가 많다. 쉽게 이야기하자면 Join을 하면 되는지 서브쿼리를 써야하는지 간혹 햇갈리는 경우가 있기 때문이다.

    select job_id, dept_id from emp 
    where (job_id, dept_id) = (select job_id, dept_id from emp where emp_id=115);

    단일행 서브쿼리면 이해가 빠를 수 있으나 join과 같이 사용해야 할 경우 복잡해진다. 부서 직원들의 평균이 전체 직원 평균 이상인 부서의 이름, 평균 급여를 조회하고 급여는 소숫점 2자리까지 나오고 통화표시와 단위 구분자를 출력해야 하는 경우다. 게다가 이런 경우는 group by와 having까지 묶어야 하고 concat을 써서 자릿수까지 맞춰줘야 한다.

    select dept_id, dept_name, concat('$',format(avg_salary,2)) 'avg' 
    from(
    	select d.dept_id, d.dept_name, avg(salary) 'avg_salary' 
    	from emp e left join dept d on e.dept_id = d.dept_id
    	group by d.dept_id, d.dept_name
    	having avg(salary) > (select avg(salary) from emp) 
    	order by 3 desc
    )tb;

    아무래도 테이블을 두 개 이상 만들어 하나씩 비교할 수밖에 없다. 평균 급여를 같이 보여주려면 부서 id, 부서명이 들어가야 하고 emp, dept를 조인하여 dept의 id, 부서명을 group by하여 having으로 평균 급여를 필터링 한다. 여기까지 from으로 묶어버리고 바깥 select절에서 아까 서브쿼리의 3가지 select 항목을 출력하면 되겠다.

  2. 프로젝트의 제일 중요한 점은 아이디어 설계 단계에서 짧은 시간에 구성하여 지체되지 않고 프로세스대로 흘러가는 것이다. 하지만 안건을 내면서 변경되는 사안이 많았고 구현 단계에서 설계의 문제가 있어 재설계 후 다시 구현해야 하는 번거로움이 있었다. 대부분 교육생들이 비전공자들이 많았고 전공자라고 하더라도 경험이 적었기에 발생하는 부분은 이해가 간다. 그렇다고 해서 너무 많은 시간을 주면 나태해지는 경우가 있기에 되도록이면 단시간에 해내도록 유도하려는 것 같다. 부트캠프에서 설계 방식이나 시스템적 이해도를 높이는 강의가 있었다면 이런 일은 줄어들 거라고 생각한다.

📜 Try

  1. 웹 크롤링을 하면서 향후 해보고 싶은 항목들이 있다. 수소차 관련 웹사이트에 페이지를 넘길 때 다음 버튼을 눌러 크롤링을 작업하는데 마지막 페이지에 도달했는데도 다음 버튼이 활성화되어 있다. 이걸 모르고 next만 계속 누르면 무한 루프에 빠지게 될 가능성이 높다. 페이지 수가 적어 1,2페이지만 보이도록 구현해놔서 여러 페이지가 있을 때 for 문을 적절히 돌려 작업해야 한다. 다음부터는 페이지 번호를 누르고 마지막 페이지 번호가 도달했을 때 그 때 next버튼을 한 번 눌러 작업하는 로직으로 수정해야 겠다.
  2. 프로젝트를 진행하면서 짧은 기간에 팀원들과 의사소통을 하면서 많은 것을 배웠다. 업무 프로세스가 진행이 되지 않을 때 피드백 받으면서 수정 및 보완했던 작업이 정말 많이 도움이 되었고 실수나 에러 사항이 있더라도 팀원들이 격려 및 이해해주었다. 학창시절에는 이러한 경험들이 많이 없어서 직장을 다닐 때에도 시행착오가 정말 많았다. DB 재설계 사항이 있어서 다시 구현해야 했고 시간이 너무나 촉박해 화면 구현도 제대로 하기 힘들었다. 하지만 이러한 경험들이 차기 프로젝트에 있어서 큰 교훈이 될 것이라고 믿으며 조금 더 적극적이고 팀원 간 피드백을 더 활발히 해야겠다는 생각이 들었다.

느낀점: MySQL을 실습하면서 이전에 몰랐던 함수나 기능들을 사용하면서 실무에 가까워진 느낌이 들었다. 웹 크롤링을 통해 데이터를 어떻게 수집하고 그와 관련된 기술을 배우면서 프로젝트에 적용하니 나의 것으로 만들 수 있어서 보람이 있었다. 😀

profile
요한의 개발일기

0개의 댓글