일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- Next.js
- no router instance found
- 기술 낙서장
- 사내 이슈
- react-query 도입후기
- react-query
- react-query v5
- 캡스톤디자인 후기
- nextjs
- react-query&Next.js
- React.JS
- 결제페이지
- 사내 오류 해결
- MSW
- state 사용하기
- 더블엔씨
- 일상 생각
- state 관리
- 사내 오류 대응
- SW캡스톤디자인
- javascript
- router instance
- 리액트
- React
- 기술낙서장
- TypeScript
- ClientSide
- SSR
- JS변수
- node.js
- Today
- Total
코딩을 잘하고 싶은 코린이 동토니
playwright로 네이버 스마트스토어 자동 로그인 구현하기 본문
사내 프로젝트중 스마트스토어 자동 판매 프로그램이 있다.
해당 프로젝트의 경우 회사의 첫 BM이기도 했고 관련된 내용의 히스토리를 알고있는 분이 CTO님 제외 초창기 회사 개발팀 구성원분들 정도였는데 대부분의 초창기 개발팀 구성원 분들이 퇴사하고 나서 해당 프로젝트를 CTO님에게 인계받아 혼자 관리하게 되었다.
해당 프로젝트는 다음과 같이 구동되고 있다.
구동 방식
- 해당 프로그램을 위한 aws-ec2 instance를 생성한다.
- 생성된 instance를 접속해 vncserver를 시작해준다
- instance 내부에 생성된 뷰를 통해 크롬을 실행시키고 해당 프로그램을 실행시킨다.
이렇게 동작하는 프로그램에는 몇가지 문제점이 있었다.
문제점
- 네이버 스마트스토어 자동 로그인 시도시 간헐적으로 Captcha 인증이 발생해서 자동로그인 진행이 불가함
- 자동 로그인 1차 진행후 일정주기 (한달에 1번) 2차 이메일 인증이 발생하여 로그인을 하지 못함
- 메모리 과부화로 인해 해당 ec2 instance가 죽어버리는 현상
우선 해당 문제들중에서 3번을 제외한 자동로그인 관련된 문제부터 해결해보기로 했다.
우선 구현된 코드를 살펴보면 다음과 같다
const login = async (loginPage: Page) => {
//login started
await loginPage.goto(
'https://accounts.commerce.naver.com/login?url=https%3A%2F%2Fsell.smartstore.naver.com%2F%23%2Flogin-callback%3FreturnUrl%3Dhttps%253A%252F%252Fsell.smartstore.naver.com%252F%2523%252Fhome%252Fdashboard',
)
const loginButton = await loginPage.waitForSelector(
'button[class^=Login_btn_more][type=button]',
)
await loginButton.click()
}
const checkLogin = async (loginPage: Page) => {
await loginPage.goto('https://sell.smartstore.naver.com/#/home/dashboard')
await login(loginPage)
}
const main = async () => {
...check env
console.log('starting nlogin...')
const context = await chromium.launchPersistentContext(
process.env.USER_DATA_PATH as string,
options,
)
const loginPage = await context.newPage()
context.on('page', async (page) => {
await page.waitForLoadState()
try {
const id = await page.waitForSelector('input[name=id]')
const pw = await page.waitForSelector('input[name=pw]')
await fill(page, id, process.env.NAVER_USERNAME as string)
await fill(page, pw, process.env.NAVER_PASSWORD as string)
const loginKeepButton = await page.waitForSelector(
`//*[@id="login_keep_wrap"]/div[1]/label`,
)
await loginKeepButton.click()
const loginButton = await page.waitForSelector('//*[@id="log.login"]')
await loginButton.click()
const currentPopUpUrl = await page.url()
console.log('login success')
} catch (err) {
// ... do nth
}
})
await checkLogin(loginPage)
setInterval(async () => {
await checkLogin(loginPage)
}, LOGIN_PERIOD * 60 * 1000)
}
위 코드는
1. context를 직접 생성하여 해당 context를 통해 로그인페이지로 이동
2. 로그인 체크 및 아이디 비밀번호 입력 후 로그인 버튼 클릭
이렇게 구현되어있다
이제 이 코드에 captcha인증 팝업이 노출되는 경우 어떻게 해결할 것인지 추가를 해야한다.
Captcha 인증
captcha 인증의 경우 이미지 분석기를 통해 해당 인증을 피하는 방법을 고려해보았는데 네이버에서 제공하는 captcha의 경우 영수증을 기반으로한 문제를 내서 이미지 분석기를 통해 피하는 방법은 구현하기 너무 어려울 것 같았다.
방법을 찾아보는 와중 해당 captcha 팝업을 한번 종료하고 다시 로그인시도를 하면 captcha 인증 없이 로그인이 진행되는 점을 확인하였다.
그럼 이제 captcha 팝업 노출여부를 확인하고 재로그인 하는 코드를 추가해주면 된다.
// 로그인 이후 popUpUrl 확인
const main = async () => {
... check env & login
const currentPopUpUrl = await page.url()
if (currentPopUpUrl.includes('https://nid.naver.com/nidlogin.login')) {
console.log('captcha on')
captchaCount += 1
await captchaSlack(captchaCount) // captcha 3회 이상 발생 여부 검증
await page.close() // 팝업 종료
await checkLogin(loginPage)
}
... login
}
const captchaSlack = async (count: number) => {
if (count === 10) {
await transporter.sendMail(mailOptions, (err, info) => {
if (err) {
console.log(err, 'failed to send email')
}
console.log(info.envelope, 'email sent')
})
captchaCount = 0
}
if (count >= 3) {
await postToSlack(`nlogin captcha가 ${count}회 발생했습니다.`)
} else {
... do something
}
}
이런식으로 현재 페이지의 url을 검증하여 captcha 팝업이 노출되었는지 판단하고 captcha 노출시 창을 껏다 키는 형태로 구현을 하였다.
이 과정에서 captcha팝업이 3회이상 노출될 경우 지속적으로 captcha 팝업이 노출되는 문제를 확인하였고 이부분은 수동으로 로그인을 해주어야 하기때문에 슬랙채널로 해당 횟수를 전달 받을 수 있도록하였다.
이제 이메일 인증을 처리해보자
2차 이메일 인증
해당 문제의 경우에는 해결방법이 생각보다 쉬웠다. 처리과정은 이렇다
로그인 시도를 함 ⇒ 2차 이메일 인증 화면으로 이동 ⇒ 브라우저를 하나 더 생성하여 네이버 이메일 화면으로 이동 ⇒ 받은 이메일에서 가장 첫번째의 2차 이메일 인증 내용을 클릭 ⇒ 2차 이메일 인증 번호를 copy ⇒ 2차 이메일 인증 화면으로 다시 넘어와서 copy한 인증번호를 type⇒ 2차 이메일 인증 종료 ⇒ 정상 로그인
if (await isCertifyUrl(loginPage)) {
console.log('이메일 인증 시도하기')
await certifyEmail(context, loginPage)
}
// certifyEmail
const certifyEmail = async (context: BrowserContext, loginPage: Page) => {
const isCertify = await isCertifyUrl(loginPage)
if (!isCertify) {
return
}
const certifyButton = await loginPage.waitForSelector('span:text-is("인증")')
await certifyButton.click()
await loginPage.waitForTimeout(10 * 1000)
const confirmButton = await loginPage.waitForSelector(
'button:text-is("확인")',
)
await confirmButton.click()
const emailPage = await context.newPage()
await emailPage.goto('https://mail.naver.com')
const firstEmail = await emailPage
.locator(
'span.text:text-is("[네이버 커머스 ID] 이메일 인증번호를 안내드립니다.")',
)
.first()
await firstEmail.click()
const targetTd = await emailPage.locator('td >> nth=19')
const verifyNumber = await targetTd.textContent()
if (!verifyNumber) {
return
}
await emailPage.close()
const verifyNumberInput = await loginPage.waitForSelector(
'//*[@id="root"]/div/div[1]/div/div/div/ul/li[1]/div/div[3]/div/div[1]/div/div[1]/div/input',
)
await fill(loginPage, verifyNumberInput, verifyNumber.trim())
await loginPage.waitForTimeout(10 * 1000)
const verifyConfirmButton = await loginPage.waitForSelector(
'//*[@id="root"]/div/div[1]/div/div/div/div[2]/button/span',
)
await verifyConfirmButton.click()
await loginPage.waitForTimeout(10 * 1000)
const loginFinalButton = await loginPage.waitForSelector(
'//*[@id="root"]/div[1]/div/div/div/button[2]',
)
await loginFinalButton.click()
}
이런식으로 이메일 인증 페이지의 selector들을 하나씩 설정하여 차근차근 풀어주면 이메일 인증까지도 처리가 되었다.
메모리 과부화로 인한 프로그램 종료
이 문제가 가장 해결하기 난해한 문제였다.
playwright의 경우 e2e 테스트 용으로 만든 라이브러리이기 때문에 자동화 프로그램을 만들었을때의 메모리릭에 관한 부분은 고려하지 않았을 것이다. 아니나 다를까 약 26~27시간 정도 프로그램이 돌아가고 나면 메모리 부족으로 인해 프로그램이 강제 종료가 되었다.
메모리릭을 우리가 해결할 수 없었으므로 nlogin이 강제로 종료되었을때 자동으로 다시 프로그램을 실행시키는 방법이 필요했다.
첫 출발은 이러했다.
- 실행 주도권이 어디에 있는가? ⇒ 실행 주도권이 우리에게 있다
- 실행 주도권을 바탕으로 우리가 핸들링 할 수 있는 것들은 무엇이 있는가? ⇒ 실행 주도권이 우리에게 있다는것은 프로그램이 켜진 순간 해당 프로그램이 가진 pid를 캐치 할 수 있다.
- pid를 통해 할 수 있는것은? ⇒ 만약 해당 pid가 어떤 문제로 인해 종료가 되었다면 우리는 프로그램이 강제로 종료되었다는 것을 알 수 있다는 뜻이다
그럼 이제 해결방법은 알았으니 해결을 해야했다. 처음에는 script를 직접 짜려고 생각을 했지만 이미 이런 문제들을 겪고 있는 사람들이 꽤나 많았으므로 forever라는 node.js 전용 자동화 도구가 있었다.
forever ?
forever의 경우 forever 명령어로 프로그램을 실행시킬경우 알아서 해당 pid를 캐치하고 있다가 종료가 된 순간에 우리가 실행시킨 스크립트를 자동으로 다시 실행시켜준다. 딱 우리가 원하던 해결 방법이였다.
따라서 forever의 스크립트를 통해 이를 해결하였다
forever start -w -o ../nlogin_logs/out.log -e ../nlogin_logs/err.log -c "yarn serve" ./
위의 스크립트 실행을 통해서 프로그램이 지속적으로 실행이 되고 또한 언제 실행되고 언제 에러가 발생되는지 로그를 확인하기위해 상위 폴더에 nlogin_logs 라는 폴더를 하나 더 생성하여 해당 폴더에 log 파일들을 확인 할 수 있게끔 하였다.
'기술낙서장' 카테고리의 다른 글
사내 결제 페이지 개편하기 (1) | 2024.09.03 |
---|---|
사내 스마트 스토어 옵션 가격 책정 오류 수정하기 (1) | 2024.09.03 |
스마트 스토어 자동화 프로그램 429에러 처리하기 (0) | 2024.09.02 |
스마트 스토어 프로그램 health Check (1) | 2024.09.02 |