Python + Selenium을 사용하여 이슈 작성을 자동화해 보았다
내가 입사하기 전, 티켓을 자동으로 올려주는 코드를 이미 누군가 만든 적이 있다고 들었다. 하지만 입사하고 거의 1년이 돼가는 지금까지 해당 프로그램을 사용하는 팀원들을 보지는 못했다. 적어도 서부의 경우는 그렇다. 동부에서는 사용하고 있는 것으로 알고 있다.
티켓을 올리는 게 시간이 걸리는 작업이다 보니 꼭 한번 만들어보고 싶었는데 시간이 좀처럼 없었다. 그러다 최근 한국에 휴일이 겹치면서 업무가 상대적으로 적었는데, 지금이 좋은 기회라고 생각해서 자동화 프로그램을 만들기로 했다.
기존 스크립트를 사용하는 사이드 프로젝트의 경우 Ruby를 사용했을 테지만, 팀원들과 공유를 위해서는 기본적으로 설치되어 있는 Python이 편할 것 같아서, 엄청 오랜만에 Python을 써봤다.
기존 프로그램의 경우, 내가 알기로는 아래와 같이 동작한다:
- GUI
- multithreading - 동시에 여러 티켓 등재
- headless - 선택 가능
- 티켓 작성 및 등재까지 자동화
내가 만든 프로그램의 경우는 아래와 같이 동작한다:
- TUI
- single-threaded - 티켓 하나씩 순차적으로 작성
- not headless - 무조건
- 티켓 작성까지만 자동화. 등재는 하지 않음.
우선 등재까지 하지 않은 이유는, 중복 티켓을 올리지 않기 위해서가 첫 번째이다. Selenium으로 submit
을 클릭했을 때, 어떠한 이유로 인해 서버에서의 반응 속도가 느리거나 혹은 프로그램의 이슈로 여러 번 클릭이 가해지면, 같은 티켓이 5개씩 등재될 수도 있다.
두 번째로는, 티켓을 올리기 전에 사람이 직접 간단하게나마 내용을 다시 한번 확인하기를 바랐다. 우리는 실수하는 동물이기에 확인 작업은 꼭 필요하다.
해당 프로그램을 만들고 나서 시험해 보기 위해 이슈를 부랴부랴 찾아 나섰다 ε=ε=┌( >_<)┘
잘 동작은 하는데 selenium 문제인지 컴퓨터 사양 문제인지 모르겠으나, 텍스트 기재할 때 시간이 좀 걸린다. 특히 이슈 내용! 해당 부분에서 시간이 좀 걸리다 보니, 티켓 하나 작성하는데 페이지 로딩 시간 포함 대략 2분 정도 걸리는 것 같다.
이슈 내용을 빈 문자열로 놓고 실행하면 80초 정도로 시간이 확 줄어든다. send_key
로 긴 내용의 텍스트를 보내버려서 그러는 건지, 정말 단순히 복붙하는 메소드는 없는지 찾아봐야겠다.
- 2025/05/08 - 17:34
send_keys
의 경우 단어나 문장보다는 사실 key stroke를 보내는 용도인 것 같다. 예를 들면 Ctrl + A
, DELETE
, 등등….자바스크립트라면 간단하게 해당 필드 선택해서 innerText
로 대입하면 되는데 이런 거 없나 했더니…! driver.execute_script
라는게 떡하니 있었다. 심지어 자바스크립트 실행이 가능하다.
기존 send_keys
를 사용하던 코드를 driver.execute_script(...)
로 바꿨더니, 티켓 하나 작성하는 데 대략 2분 걸리던 것이 지금은 1분밖에 안 걸린다. 티켓 5개로 시험해 봤더니 5분 20초 정도 걸렸다.
이 정도면 충분히 쓸만한 것 같다.
다만 올릴 이슈가 없어서 실제로 테스트를 해볼 수가 없다 (。•́︿•̀。) 일단 써봐야 팀원들과 공유하든 뭘 하든할 텐데 말이지.
- 2025/05/09 - 19:00
오늘 이슈 3개를 찾아서 자동화 프로그램으로 이슈를 작성해봤다.
일단 이슈가 작성되는 틈에 이슈에 올릴 영상을 편집하려 했는데 성능 문제가 있었다. 싱글 쓰레드임에도 불구하고 작업이 수행하는 동안 다른 일을 하는 행위는 자제해야 할 것 같다.
중간중간 텍스트 삽입이 안된 경우가 있었다. 이거는 이슈 3개 전부 동일하게 발생한 것으로 보아 내 코드의 문제인 것 같다.
대충 위 두 개를 제외하면 되게 편했다! 무엇보다 이슈를 잡았을 때, 바로바로 이슈 제목이나 description을 미리미리 작성해둘수도 있어서 관리하기 편했다.
- 2025/05/12 - 21:20
같은 플랫폼에서 일하는 팀원(사수)에게 프로젝트를 공유했고, 팀원이 사용하면서 이슈가 발생했다! 이슈 정보를 적는 부분 중 딱 한 부분에서, 값을 잘못 적으면 이슈가 발생했다. 해당 부분은 ternary로 코드를 리팩토링하여, 값이 존재하지 않거나 잘못된 경우 NULL
을 저장해서 selenium이 해당 부분에서는 아무런 동작을 하지 않도록 변경했다.
위 부분을 고치면서 ‘어느 부분에서 이슈가 발생했는지 알면 편하지 않을까?’ 와 같은 생각을 해서 logging
을 추가했다.
import logging
logging.basicConfig(level=logging.INFO, format='%(levelname)s - %(message)s'
logging.info('...')
logging.error('...')
logging.critical('...')
그리고 기존에 사용했던 implicitly_wait()
을 WebDriverWait()
으로 변경했다.
WebDriverWait(..).until(expected_conditions.presence_of_element_located((By.ID, '')))
마지막으로 selenium을 실행하기 전에 사용할 계정을 미리 준비하기 때문에 무조건 로그인에 성공할거라고 가정하고 코드를 구현했었는데, 생각보다 실패할 케이스가 많다는 것을 느꼈다. 계정정보 입력을 깜빡하는 경우도 있고, 오타가 발생하는 경우도 있었다. 그래서 성공/실패에 여부를 함수에서 반환하도록 변경했다. 약간 API 느낌나게 구성해봤다.
try:
driver.find_element(By.ID, '대충_로그인_에러_필드')
except NoSuchElementException:
return {'status': 200, 'data': 'Success'}
return {'status': 401, 'data': 'Unauthorized'}
- 2025/05/13 - 19:13
입력한 차종 정보를 이용해서 해당 차종의 고유 코드까지도 선택하도록 코드를 추가했는데 한 가지 이슈가 있었다. Select
에서 기본적으로 None
이 선택되어 있다보니, None
과 해당 차종의 코드 둘 다 선택이 되어 마지막에 submit이 되지 않았다.
찾아보니 선택해제를 위한 메소드가 존재했다.
driver.find_element(By.ID, "some-field").deselect_by_value('#')
- 2025/05/14 - 12:25
현재 내 프로그램의 구조는 작성하려는 이슈의 개수만큼 .py
파일을 만들어서 특정 폴더에 집어넣고 main.py
를 실행하면 런타임에 해당 파일들 module 형식으로 import해서 사용하는 방식이다.
프로그램을 executable로 한 번 만들어보려고 했는데 module부분에서 이슈가 발생하고 있다. 찾아보니 pyinstaller에서는 런타임에 모듈을 추가할 수 없는 모양이다. hiddenimport
가 있는 것 같기는 한데 이거는 이미 존재하는 파일이 대상이 아닌가 싶고,, 런타임에 파일을 동적으로 추가하는 방법은 아직 모르겠다.
시간을 투자하면 방법이야 찾을 수 있을 것 같기는 한데, 사실 executable이 그닥 끌리지는 않는다. 그냥 지금 이 상태로 터미널에서 명령어로 실행시키는게 훨씬 빠르기도 하고..
- 2025/05/17 - 18:50
현재 프로그램을 executable로 만드는데 성공! 잘 동작한다.
기존에 module_import()
를 사용하는 방식은 pyinstaller로 빌드한 프로그램이 런타임에 인식을 하지 못해서 동작하지 않았다. 동적으로 추가된 파일을 추가하는게 정말 불가능할까?
내가 방법을 모르는 것 뿐이었다😅 . 동적으로 추가된 .py
파일들을 인식시키기 위해서 아래의 조치를 취했다.
- pyinstaller에
--add-data
옵션을 사용하여 우선 이슈가 저장될 폴더만 포함시켰다 module_import
가 아닌importlib.util.spec_from_file_location
과importlib.util.module_from_spec(spec)
을 사용해서 동적으로 모듈을 불러올 수 있다
대충 아래와 같은 방식으로 동작한다.
def load_issue_modules():
modules = {}
issue_dir = os.path.join(os.path.dirname(__file__), '이슈')
for file in os.listdir(issue_dir):
if file.endswith(".py"):
module_name = file[:-3] # 'foo.py' -> 'foo'
module_path = os.path.join(issue_dir, file)
spec = importlib.util.spec_from_file_location(module_name, module_path)
if spec is not None:
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
modules[module_name] = module
return modules