ใ€QA ์ž๋™ํ™”ใ€‘Selenium์œผ๋กœ ํšŒ์›๊ฐ€์ž… E2E ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ | ๋กœ๊ทธยท์Šคํฌ๋ฆฐ์ƒท ํฌํ•จ ์™„์ „ ํ•ด์„ค (Python)

ํ…Œ์ŠคํŠธ ์ž๋™ํ™”

๐Ÿ“Œ ์ด๋Ÿฐ ๋ถ„๊ป˜ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค

  • Selenium์„ ํ™œ์šฉํ•œ ์›น ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™”์— ๊ด€์‹ฌ ์žˆ๋Š” ๊ฐœ๋ฐœ์ž ๋ฐ QA ์—”์ง€๋‹ˆ์–ด
  • ํšŒ์›๊ฐ€์ž…ยทํผ ์ž…๋ ฅ ์ž๋™ํ™”๋ฅผ ๋ฐฐ์šฐ๊ณ  ์‹ถ์€ ๋ถ„
  • ๋กœ๊ทธ์ธยท๋กœ๊ทธ์•„์›ƒ E2E ํ…Œ์ŠคํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์‹ถ์€ ๋ถ„
  • ์Šคํฌ๋ฆฐ์ƒท ์บก์ฒ˜ ๋ฐ ๋กœ๊ทธ ๊ด€๋ฆฌ๋ฅผ ์ž๋™ํ™”ํ•˜๊ณ  ์‹ถ์€ ๋ถ„

โœ… ์ด ๊ธ€์„ ์ฝ์œผ๋ฉด ์–ป์„ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • Selenium์œผ๋กœ ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€์— ๊ฑธ์นœ ํผ ์กฐ์ž‘์„ ์ž๋™ํ™”ํ•˜๋Š” ๊ตฌํ˜„ ํŒจํ„ด์„ ์ตํž ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
  • XPath๋ฅผ ํ™œ์šฉํ•œ ์œ ์—ฐํ•œ ์š”์†Œ ํƒ์ƒ‰ ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
  • ๋กœ๊ทธ ์ถœ๋ ฅยท์Šคํฌ๋ฆฐ์ƒท ๊ด€๋ฆฌ์˜ ์‹ค๋ฌด์ ์ธ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•์„ ์Šต๋“ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
  • ํšŒ์›๊ฐ€์ž… โ†’ ๋กœ๊ทธ์•„์›ƒ โ†’ ๋กœ๊ทธ์ธ์˜ ์ „์ฒด E2E ํ๋ฆ„์„ ์ง์ ‘ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค

๐Ÿ‘จโ€๐Ÿ’ป ์ด ๊ธ€์˜ ์‹ ๋ขฐ์„ฑ์— ๋Œ€ํ•ด

์ด ๊ธ€์€ ์‹ค๋ฌด์—์„œ ์ž๋™ํ™” ํ…Œ์ŠคํŠธ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” QA ์—”์ง€๋‹ˆ์–ด๊ฐ€ ์‹ค์ œ๋กœ ๋™์ž‘์„ ํ™•์ธํ•œ ์ฝ”๋“œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
์†Œ์Šค์ฝ”๋“œ๋Š” ๋ชจ๋‘ GitHub์— ๊ณต๊ฐœ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค
(automated-testing-portfolio).
์‹ค๋ฌด์—์„œ ์Œ“์€ ๋…ธํ•˜์šฐ๋ฅผ ์•„๋‚Œ์—†์ด ๋‹ด์•˜์Šต๋‹ˆ๋‹ค.

“ํšŒ์›๊ฐ€์ž… ํผ ํ…Œ์ŠคํŠธ๋ฅผ ๋งค๋ฒˆ ์ˆ˜๋™์œผ๋กœ ํ•˜๋Š” ๊ฑด ์ด์ œ ํ•œ๊ณ„์•ผโ€ฆ” ์ด๋Ÿฐ ๊ณ ๋ฏผ์„ ํ•ด๋ณธ ์  ์žˆ์œผ์‹ ๊ฐ€์š”?
์ด ๊ธ€์—์„œ๋Š” Python ร— Selenium์„ ์‚ฌ์šฉํ•ด ์š”๋„๋ฐ”์‹œ ์นด๋ฉ”๋ผ ํšŒ์›๊ฐ€์ž… ํผ์„
์ž๋™์œผ๋กœ ์ž…๋ ฅยท์ œ์ถœยท๋กœ๊ทธ์ธ ํ™•์ธ๊นŒ์ง€ ์ˆ˜ํ–‰ํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ƒ์„ธํžˆ ํ•ด์„คํ•ฉ๋‹ˆ๋‹ค.
XPath์˜ ๋ณต์ˆ˜ ํŒจํ„ด์„ ํ™œ์šฉํ•œ ๊ฒฌ๊ณ ํ•œ ์š”์†Œ ํƒ์ƒ‰, ๋กœ๊ทธ์™€ ์Šคํฌ๋ฆฐ์ƒท์„ ํ†ตํ•œ ๋””๋ฒ„๊น… ํŽธ์˜์„ฑ ๋“ฑ
์‹ค๋ฌด์—์„œ ๋ฐ”๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” QA ์ž๋™ํ™” ์„ค๊ณ„ ์ฒ ํ•™์„ ๋น ์ง์—†์ด ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

๐ŸŽฏ ์ด ํ…Œ์ŠคํŠธ์˜ ๋ชฉ์ : ์‚ฌ์šฉ์ž๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ํšŒ์›๊ฐ€์ž…์„ ์™„๋ฃŒํ•  ์ˆ˜ ์žˆ๋Š”์ง€๋ฅผ E2E๋กœ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€์ž… ์™„๋ฃŒ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ดํ›„์˜ ๋กœ๊ทธ์ธยท๋กœ๊ทธ์•„์›ƒ๊นŒ์ง€ ์ผ๊ด€๋˜๊ฒŒ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

  1. ํ…Œ์ŠคํŠธ ๊ด€์ 
  2. ์ด ์Šคํฌ๋ฆฝํŠธ๋กœ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ
  3. ์ž๋™ํ™” ํ”Œ๋กœ์šฐ ์ „์ฒด ๊ฐœ์š”
  4. ์†Œ์Šค์ฝ”๋“œ ์ „๋ฌธ
  5. ์ฃผ์š” ๋ฉ”์„œ๋“œ ํ•ด์„ค
    1. โ‘  __init__() โ€” ํด๋ž˜์Šค ์ดˆ๊ธฐํ™” & ChromeDriver ์„ค์ •
    2. โ‘ก log() / take_screenshot() โ€” ๋กœ๊ทธ ๊ด€๋ฆฌ์™€ ์—๋น„๋˜์Šค ์ˆ˜์ง‘
    3. โ‘ข fill_basic_info() โ€” ์ด๋ฆ„ยทํ›„๋ฆฌ๊ฐ€๋‚˜ ์ž๋™ ์ž…๋ ฅ
    4. โ‘ฃ fill_address() โ€” ์šฐํŽธ๋ฒˆํ˜ธ ๊ฒ€์ƒ‰ + ์ฃผ์†Œ ์ž…๋ ฅ
    5. โ‘ค fill_contact_info() โ€” ์ „ํ™”๋ฒˆํ˜ธยท์ƒ๋…„์›”์ผยท์ด๋ฉ”์ผ ์ž…๋ ฅ
    6. โ‘ฅ accept_terms() โ€” ์ด์šฉ์•ฝ๊ด€ ์ฒดํฌ๋ฐ•์Šค
    7. โ‘ฆ click_next_button() / click_confirm_button() โ€” ํ™”๋ฉด ์ „ํ™˜ ๋ฒ„ํŠผ ํด๋ฆญ
    8. โ‘ง login() / logout() โ€” ๋กœ๊ทธ์ธ ํ™•์ธ๊ณผ ํ›„์ฒ˜๋ฆฌ
    9. โ‘จ reset_password() โ€” ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ํ”Œ๋กœ์šฐ
  6. ์‹คํ–‰ ๋กœ๊ทธ ์ƒ˜ํ”Œ
  7. ๋”์šฑ ๋ฐœ์ „์‹œํ‚ค๊ธฐ ์œ„ํ•œ ๊ฐœ์„  ์•„์ด๋””์–ด
  8. ํ™œ์šฉ ์žฅ๋ฉดยทํ™•์žฅ ์˜ˆ
  9. ์‹ค๋ฌด ์‘์šฉ: POM ์„ค๊ณ„ ร— CI/CD ์—ฐ๊ณ„
  10. ์ž์ฃผ ๊ฒช๋Š” ๋ฌธ์ œ & ํ•ด๊ฒฐ๋ฒ•
    1. โ‘  ์ฒดํฌ๋ฐ•์Šค๊ฐ€ ์ผ๋ฐ˜ ํด๋ฆญ์œผ๋กœ ๋ฐ˜์‘ํ•˜์ง€ ์•Š๋Š”๋‹ค
    2. โ‘ก ๋“œ๋กญ๋‹ค์šด๏ผˆselect ํƒœ๊ทธ๏ผ‰์„ ์ง์ ‘ ํด๋ฆญ์œผ๋กœ ์„ ํƒํ•  ์ˆ˜ ์—†๋‹ค
    3. โ‘ข ๋ฒ„ํŠผ์˜ XPath๊ฐ€ 1ํŒจํ„ด์ด๋ฉด ์‰ฝ๊ฒŒ ๋ง๊ฐ€์ง„๋‹ค
    4. โ‘ฃ ์šฐํŽธ๋ฒˆํ˜ธ ํ•„๋“œ ํŠน์ •์— ์• ๋ฅผ ๋จน์—ˆ๋‹ค
    5. โ‘ค ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์—ฌ๋ถ€ ํŒ์ •์ด ์–ด๋ ค์› ๋‹ค
  11. ์ •๋ฆฌ

ํ…Œ์ŠคํŠธ ๊ด€์ 

์ด ์Šคํฌ๋ฆฝํŠธ๋Š” ๋‹ค์Œ ํ…Œ์ŠคํŠธ ๊ด€์ ์„ ์ปค๋ฒ„ํ•ฉ๋‹ˆ๋‹ค. QA ์—”์ง€๋‹ˆ์–ด๋กœ์„œ “์ด ํ…Œ์ŠคํŠธ๊ฐ€ ๋ฌด์—‡์„ ๋ณด์žฅํ•˜๋Š”๊ฐ€”๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๋Š” ๊ฒƒ์ด ์‹ค๋ฌด์—์„œ๋Š” ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

๊ด€์  ๋‚ด์šฉ ์ด ์Šคํฌ๋ฆฝํŠธ์—์„œ์˜ ๋Œ€์‘
โœ… ์ •์ƒ ์ผ€์ด์Šค ๊ฐ€์ž… ์„ฑ๊ณตยท๋กœ๊ทธ์ธ ์„ฑ๊ณต ๊ฐ€์ž… โ†’ ๋กœ๊ทธ์•„์›ƒ โ†’ ๋กœ๊ทธ์ธ์„ ์ผ๊ด€๋˜๊ฒŒ ๊ฒ€์ฆ
โš ๏ธ ๋ฏธ์ž…๋ ฅ ์˜ค๋ฅ˜ ํ•„์ˆ˜ ํ•ญ๋ชฉ ๋ฏธ์ž…๋ ฅ ์‹œ ์˜ค๋ฅ˜ ํ‘œ์‹œ ํ™•์ธ ๊ฐ ๋‹จ๊ณ„ ์‹คํŒจ ์‹œ ๋กœ๊ทธ ์ถœ๋ ฅ + ์Šคํฌ๋ฆฐ์ƒท ์ €์žฅ
โš ๏ธ ์ž˜๋ชป๋œ ์ด๋ฉ”์ผ ํ˜•์‹ “@” ์—†์Œยท๋„๋ฉ”์ธ ์—†์Œ ๋“ฑ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ต์ฒดํ•˜์—ฌ ๋น„์ •์ƒ ์ผ€์ด์Šค ๊ฒ€์ฆ ๊ฐ€๋Šฅ
โš ๏ธ ๋น„๋ฐ€๋ฒˆํ˜ธ ์กฐ๊ฑด ์œ„๋ฐ˜ ๋ฌธ์ž ์ˆ˜ ๋ถ€์กฑยท์‚ฌ์šฉ ๋ถˆ๊ฐ€ ๋ฌธ์ž ๋“ฑ fill_password() ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜์—ฌ ํ™•์ธ ๊ฐ€๋Šฅ
๐Ÿ” ์ค‘๋ณต ๊ฐ€์ž… ๋™์ผ ์ด๋ฉ”์ผ๋กœ 2๋ฒˆ์งธ ๊ฐ€์ž… ์‹œ๋„ ์‹œ์˜ ๋™์ž‘ ๋™์ผํ•œ email ๊ฐ’์œผ๋กœ ์žฌ์‹คํ–‰ํ•˜์—ฌ ํ™•์ธ ๊ฐ€๋Šฅ

์ด ์Šคํฌ๋ฆฝํŠธ๋กœ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

ํ•˜๋‚˜์˜ ํด๋ž˜์Šค๋กœ ๊ฐ€์ž…๋ถ€ํ„ฐ ๋กœ๊ทธ์•„์›ƒ, ๋กœ๊ทธ์ธ๊นŒ์ง€ ์™„๊ฒฐ๋˜๋Š” ์™„์ „ ์ž๋™ํ™” ํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“

ํผ ์ž๋™ ์ž…๋ ฅ

์ด๋ฆ„ยทํ›„๋ฆฌ๊ฐ€๋‚˜ยท์ฃผ์†Œยท์ „ํ™”๋ฒˆํ˜ธยท์ƒ๋…„์›”์ผยท์ด๋ฉ”์ผ์„ ์ž๋™์œผ๋กœ ์ž…๋ ฅ

๐Ÿ“ฎ

์šฐํŽธ๋ฒˆํ˜ธ โ†’ ์ฃผ์†Œ ๊ฒ€์ƒ‰

์šฐํŽธ๋ฒˆํ˜ธ ์ž…๋ ฅ ํ›„ “์ฃผ์†Œ ๊ฒ€์ƒ‰” ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๊ณ  ํ›„๋ณด์—์„œ ์„ ํƒ

โœ…

์ด์šฉ์•ฝ๊ด€ ๋™์˜ ์ฒดํฌ๋ฐ•์Šค

ํด๋ฆญยท์ƒํƒœ ํ™•์ธยทJS ํด๋ฐฑ๊นŒ์ง€ ์ฒดํฌ๋ฐ•์Šค ์กฐ์ž‘์„ ์™„์ „ ์ž๋™ํ™”

๐Ÿ”

๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ (ํ™•์ธ ํ™”๋ฉด)

ํ™•์ธ ํ™”๋ฉด์—์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ํ™•์ธ์šฉ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž๋™ ์ž…๋ ฅ

๐Ÿ“ธ

์Šคํฌ๋ฆฐ์ƒท ์ž๋™ ์ €์žฅ

๊ฐ ๋‹จ๊ณ„์—์„œ ์ž๋™์œผ๋กœ PNG ์ €์žฅ. ๋””๋ฒ„๊น… ์‹œ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธ ๊ฐ€๋Šฅ

๐Ÿ“‹

๋กœ๊ทธ ํŒŒ์ผ ์ถœ๋ ฅ

ํƒ€์ž„์Šคํƒฌํ”„๊ฐ€ ํฌํ•จ๋œ ๋กœ๊ทธ๋ฅผ ํ…์ŠคํŠธ ํŒŒ์ผ๋กœ ์ €์žฅ. ์‹คํ–‰ ์ด๋ ฅ์„ ์ถ”์  ๊ฐ€๋Šฅ

์ž๋™ํ™” ํ”Œ๋กœ์šฐ ์ „์ฒด ๊ฐœ์š”

signup_complete() ๋ฉ”์„œ๋“œ๊ฐ€ ์ „์ฒด ๋‹จ๊ณ„๋ฅผ ํ†ต๊ด„ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๋‹จ๊ณ„๊ฐ€ ๋…๋ฆฝ๋œ ๋ฉ”์„œ๋“œ๋กœ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์–ด ๋””๋ฒ„๊น…๊ณผ ์žฌ์‚ฌ์šฉ์ด ์‰ฝ์Šต๋‹ˆ๋‹ค.

Step ์ฒ˜๋ฆฌ ๋‚ด์šฉ ํ˜ธ์ถœ ๋ฉ”์„œ๋“œ ์Šคํฌ๋ฆฐ์ƒท
1 ํ†ฑ ํŽ˜์ด์ง€ โ†’ ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€๋กœ ์ด๋™ navigate_to_register_page() 01_registration_page.png
2 ์ด๋ฆ„ยทํ›„๋ฆฌ๊ฐ€๋‚˜ ์ž…๋ ฅ fill_basic_info() โ€”
3 ์šฐํŽธ๋ฒˆํ˜ธยท์ฃผ์†Œ ์ž…๋ ฅ fill_address() โ€”
4 ์ „ํ™”๋ฒˆํ˜ธยท์ƒ๋…„์›”์ผยท์ด๋ฉ”์ผ ์ž…๋ ฅ fill_contact_info() โ€”
5 ์ด์šฉ์•ฝ๊ด€ ๋™์˜ ์ฒดํฌ accept_terms() 02_form_filled.png
6 “๋‹ค์Œ์œผ๋กœ” ๋ฒ„ํŠผ ํด๋ฆญ (์ž…๋ ฅ โ†’ ํ™•์ธ ํ™”๋ฉด) click_next_button() 03_confirmation_page.png
7 ํ™•์ธ ํ™”๋ฉด์—์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ fill_password() 04_password_filled.png
8 “๊ฐ€์ž…ํ•˜๊ธฐ” ๋ฒ„ํŠผ ํด๋ฆญ (ํ™•์ธ โ†’ ์™„๋ฃŒ) click_confirm_button() 05_registration_complete.png
9 ๋กœ๊ทธ์•„์›ƒ logout() 06_after_logout.png
10 ๊ฐ€์ž… ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ ํ™•์ธ login() 07_after_login.png
11 2๋ฒˆ์งธ ๋กœ๊ทธ์•„์›ƒ logout() 08_after_second_logout.png

์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ์ด๋ฆ„ยทํ›„๋ฆฌ๊ฐ€๋‚˜ยท์ฃผ์†Œยท์ „ํ™”๋ฒˆํ˜ธยท์ƒ๋…„์›”์ผยท์ด๋ฉ”์ผ์ด ๋ชจ๋‘ ์ž๋™์œผ๋กœ ์ž…๋ ฅ๋ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋Š” ๋”•์…”๋„ˆ๋ฆฌ ํ˜•์‹์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๋ฐ์ดํ„ฐ๋กœ ๊ต์ฒดํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค.

Selenium์œผ๋กœ ํšŒ์›๊ฐ€์ž… ํผ์— ์ž๋™ ์ž…๋ ฅํ•œ ํ™”๋ฉด. ์ด๋ฆ„ยท์ฃผ์†Œยท์ „ํ™”๋ฒˆํ˜ธยท์ƒ๋…„์›”์ผ์ด ๋ชจ๋‘ ์ž๋™ ์ž…๋ ฅ๋˜์–ด ์žˆ์Œ

โ–ฒ Selenium์ด ์ž๋™ ์ž…๋ ฅํ•œ ํšŒ์›๊ฐ€์ž… ํผ. ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๊ฐ€ ์ „์ฒด ํ•ญ๋ชฉ์— ์ •ํ™•ํ•˜๊ฒŒ ์ž…๋ ฅ๋˜์–ด ์žˆ์Œ

์†Œ์Šค์ฝ”๋“œ ์ „๋ฌธ

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import Select
from datetime import datetime
import os
import time

class YodobashiAutoSignup:
    def __init__(self, headless=False):
        self.top_url = "https://www.yodobashi.com/"
        self.register_url = "https://order.yodobashi.com/yc/member/register/index.html"
        options = webdriver.ChromeOptions()
        options.add_argument('--disable-blink-features=AutomationControlled')
        if not headless:
            options.add_argument('--start-maximized')
        else:
            options.add_argument('--headless')
        options.add_argument('--disable-popup-blocking')
        self.driver = webdriver.Chrome(options=options)
        self.wait = WebDriverWait(self.driver, 15)
        self.log_folder = os.path.join(
            os.path.expanduser("~"), "Desktop", "yodobashi_signup_logs"
        )
        self.setup_log_folder()
        self.log_file = os.path.join(
            self.log_folder,
            f"signup_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
        )

    def setup_log_folder(self):
        if not os.path.exists(self.log_folder):
            os.makedirs(self.log_folder)

    def log(self, message):
        timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        log_message = f"[{timestamp}] {message}"
        print(log_message)
        with open(self.log_file, 'a', encoding='utf-8') as f:
            f.write(log_message + '\n')

    def take_screenshot(self, name):
        screenshot_name = name.replace('(', '_').replace(')', '').replace(' ', '_')
        screenshot_path = os.path.join(
            self.log_folder,
            f"screenshot_{screenshot_name}_{datetime.now().strftime('%H%M%S')}.png"
        )
        self.driver.save_screenshot(screenshot_path)
        self.log(f"  ๐Ÿ“ท Screenshot: {os.path.basename(screenshot_path)}")
        return screenshot_path

    # ---- ์ดํ•˜ fill_basic_info / fill_address / fill_contact_info /
    #      fill_password / accept_terms / click_next_button /
    #      click_confirm_button / navigate_to_register_page /
    #      signup_complete / login / logout / reset_password /
    #      close ๋“ฑ์„ ๊ตฌํ˜„ ----

# ์‹คํ–‰ ์˜ˆ์‹œ
if __name__ == "__main__":
    user_data = {
        "last_name": "Yamada",
        "first_name": "Taro",
        "last_kana": "ใƒคใƒžใƒ€",
        "first_kana": "ใ‚ฟใƒญใ‚ฆ",
        "postal_code": "1500001",
        "address_detail": "1-1-1",
        "building": "Sample Building 101",
        "phone": "09012345678",
        "birth_year": "1990",
        "birth_month": "1",
        "birth_day": "1",
        "email": "test822211@example.com"
    }

    bot = YodobashiAutoSignup(headless=False)
    try:
        result = bot.signup_complete(user_data, password="hjerty12")
        print(f"Result: {result['message']}")
    finally:
        bot.close()

์ฃผ์š” ๋ฉ”์„œ๋“œ ํ•ด์„ค

์ด ํด๋ž˜์Šค๋Š” 10๊ฐœ ์ด์ƒ์˜ ๋ฉ”์„œ๋“œ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์œผ๋ฉฐ, ๊ฐ๊ฐ์ด ๋…๋ฆฝ๋œ ์—ญํ• ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๊ฐœ๋ณ„ ๋””๋ฒ„๊น…ยท์žฌ์‚ฌ์šฉยทํ™•์žฅ์ด ์‰ฌ์šด ์„ค๊ณ„๋ฅผ ์‹คํ˜„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

โ‘  __init__() โ€” ํด๋ž˜์Šค ์ดˆ๊ธฐํ™” & ChromeDriver ์„ค์ •

ํด๋ž˜์Šค ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ์‹œ ํ˜ธ์ถœ๋˜๋Š” ์ดˆ๊ธฐํ™” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. ์ธ์ˆ˜ headless=False๋ฅผ True๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ˆจ๊ธฐ๊ณ  ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด CI/CD ํ™˜๊ฒฝ์—์„œ์˜ ์‹คํ–‰์—๋„ ๋Œ€์‘ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ” ํฌ์ธํŠธ: --disable-blink-features=AutomationControlled ์˜ต์…˜์œผ๋กœ Selenium์˜ ์ž๋™ ์กฐ์ž‘์„ ๊ฐ์ง€ํ•˜๊ธฐ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

โš  ์ฃผ์˜: ๋กœ๊ทธ ํŒŒ์ผ๊ณผ ์Šคํฌ๋ฆฐ์ƒท์€ ๋ฐ์Šคํฌํ†ฑ์˜ yodobashi_signup_logs ํด๋”์— ์ž๋™ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. ์‹คํ–‰ํ•  ๋•Œ๋งˆ๋‹ค ํƒ€์ž„์Šคํƒฌํ”„๊ฐ€ ํฌํ•จ๋œ ์ƒˆ ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜๋ฏ€๋กœ ์‹คํ–‰ ์ด๋ ฅ์ด ๋ฎ์–ด์“ฐ์—ฌ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

โ‘ก log() / take_screenshot() โ€” ๋กœ๊ทธ ๊ด€๋ฆฌ์™€ ์—๋น„๋˜์Šค ์ˆ˜์ง‘

log()๋Š” ํƒ€์ž„์Šคํƒฌํ”„๊ฐ€ ํฌํ•จ๋œ ๋ฉ”์‹œ์ง€๋ฅผ ์ฝ˜์†”๊ณผ ํ…์ŠคํŠธ ํŒŒ์ผ ์–‘์ชฝ์— ๋™์‹œ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. take_screenshot()์€ ์ž„์˜์˜ ํƒ€์ด๋ฐ์— ํ™”๋ฉด์„ PNG๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด ๋‘ ๋ฉ”์„œ๋“œ๊ฐ€ ๋ชจ๋“  ๋‹จ๊ณ„์— ์‚ฝ์ž…๋˜์–ด ์žˆ์–ด, ํ…Œ์ŠคํŠธ ์‹คํŒจ ์‹œ ์–ด๋–ค ๋‹จ๊ณ„์—์„œ ๋ฌด์Šจ ์ผ์ด ์ผ์–ด๋‚ฌ๋Š”์ง€ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ’ก ์‹ค๋ฌด Tip: QA ํ˜„์žฅ์—์„œ๋Š” “ํ…Œ์ŠคํŠธ ์—๋น„๋˜์Šค(์ฆ์ )”์˜ ์ œ์ถœ์ด ์š”๊ตฌ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. ์Šคํฌ๋ฆฐ์ƒท์„ ์ž๋™ ์ˆ˜์ง‘ํ•ด ๋‘๋ฉด ํ…Œ์ŠคํŠธ ๋ณด๊ณ ์„œ ์ž‘์„ฑ ๊ณต์ˆ˜๋ฅผ ๋Œ€ํญ ์ ˆ๊ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โ‘ข fill_basic_info() โ€” ์ด๋ฆ„ยทํ›„๋ฆฌ๊ฐ€๋‚˜ ์ž๋™ ์ž…๋ ฅ

๋ชจ๋“  input[type="text"]๋ฅผ ์ผ๊ด„ ์ทจ๋“ํ•˜์—ฌ placeholder ์†์„ฑ๊ณผ name ์†์„ฑ์˜ ๋ฌธ์ž์—ด๋กœ ํ•„๋“œ๋ฅผ ํŠน์ •ํ•ฉ๋‹ˆ๋‹ค. ์†์„ฑ๊ฐ’์œผ๋กœ ํŒ๋‹จํ•˜๋Š” ์ ‘๊ทผ์„ ์ทจํ•จ์œผ๋กœ์จ ํผ์˜ ์ˆœ์„œ๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ ๊นจ์ง€์ง€ ์•Š๊ฒŒ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

โš  ํฌ์ธํŠธ: 'kana' not in name_attr.lower() ์กฐ๊ฑด์œผ๋กœ ๊ฐ€๋‚˜ ๊ณ„์—ด ํ•„๋“œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค.

โ‘ฃ fill_address() โ€” ์šฐํŽธ๋ฒˆํ˜ธ ๊ฒ€์ƒ‰ + ์ฃผ์†Œ ์ž…๋ ฅ

์ฃผ์†Œ ์ž…๋ ฅ์€ ์šฐํŽธ๋ฒˆํ˜ธ ๊ฒ€์ƒ‰ โ†’ ํ›„๋ณด ์„ ํƒ โ†’ ๋ฒˆ์ง€ ์ž…๋ ฅ์ด๋ผ๋Š” ๋ณต์ˆ˜ ๋‹จ๊ณ„๊ฐ€ ์–ฝํžŒ ์ฒ˜๋ฆฌ์ž…๋‹ˆ๋‹ค. 7์ž๋ฆฌ ์šฐํŽธ๋ฒˆํ˜ธ๋ฅผ ์•ž 3์ž๋ฆฌยท๋’ค 4์ž๋ฆฌ๋กœ ๋ถ„ํ• ํ•˜์—ฌ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค.

postal = user_data['postal_code']  # ์˜ˆ: '1500001'
postal1 = postal[:3]   # '150'
postal2 = postal[3:]   # '0001'

# name ์†์„ฑ์— 'zip' ๋˜๋Š” 'postal'์„ ํฌํ•จํ•˜๋Š” input์„ ์ถ”์ถœ
postal_inputs = []
for inp in self.driver.find_elements(By.XPATH, "//input[@type='text']"):
    name_attr = inp.get_attribute('name') or ''
    if 'zip' in name_attr.lower() or 'postal' in name_attr.lower():
        postal_inputs.append(inp)

๐Ÿ” ์„ค๊ณ„ ํฌ์ธํŠธ: ์™„์ „ ์ผ์น˜๋ณด๋‹ค ๋ถ€๋ถ„ ์ผ์น˜๊ฐ€ ๋ณ€๊ฒฝ์— ๊ฐ•ํ•˜๋‹ค๋Š” ์‹ค๋ฌด์ ์ธ ์‚ฌ๊ณ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

โ‘ค fill_contact_info() โ€” ์ „ํ™”๋ฒˆํ˜ธยท์ƒ๋…„์›”์ผยท์ด๋ฉ”์ผ ์ž…๋ ฅ

์ „ํ™”๋ฒˆํ˜ธ๋Š” 11์ž๋ฆฌ์™€ 10์ž๋ฆฌ ์–‘์ชฝ ํŒจํ„ด์— ๋Œ€์‘ํ•˜์—ฌ 3๋ถ„ํ• ๋กœ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค. ์ƒ๋…„์›”์ผ์˜ “์›””๊ณผ “์ผ”์€ <select> ํƒœ๊ทธ ๋“œ๋กญ๋‹ค์šด์œผ๋กœ, Select ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ select_by_value()๋กœ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก Tip: ๋“œ๋กญ๋‹ค์šด ์กฐ์ž‘์—๋Š” ๋ฐ˜๋“œ์‹œ Select(element)๋กœ ๋ž˜ํ•‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ง์ ‘ click()ํ•ด๋„ ์˜ต์…˜์ด ์„ ํƒ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ฃผ์˜ํ•˜์„ธ์š”.

โ‘ฅ accept_terms() โ€” ์ด์šฉ์•ฝ๊ด€ ์ฒดํฌ๋ฐ•์Šค

์ผ๋ฐ˜ ํด๋ฆญ โ†’ JS ํด๋ฆญ โ†’ JS๋กœ ์ง์ ‘ ํ”„๋กœํผํ‹ฐ ๋ณ€๊ฒฝ์˜ 3๋‹จ๊ณ„ ํด๋ฐฑ์œผ๋กœ ํ™•์‹คํ•˜๊ฒŒ ์ฒดํฌ๋ฅผ ๋„ฃ์Šต๋‹ˆ๋‹ค. ํด๋ฆญ ํ›„์— is_selected()๋กœ ์ƒํƒœ๋ฅผ ์žฌํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ๊ฒฌ๊ณ ํ•จ์˜ ํฌ์ธํŠธ์ž…๋‹ˆ๋‹ค.

๐Ÿ’ก Tip: arguments[0].click()๊ณผ arguments[0].checked = true๋Š” ๋ณ„๊ฐœ์ด๋ฏ€๋กœ ๋‘˜ ๋‹ค ์ค€๋น„ํ•ด ๋‘๋ฉด ๊ฑฐ์˜ ๋ชจ๋“  ์ผ€์ด์Šค์— ๋Œ€์‘ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

โ‘ฆ click_next_button() / click_confirm_button() โ€” ํ™”๋ฉด ์ „ํ™˜ ๋ฒ„ํŠผ ํด๋ฆญ

๋ฒ„ํŠผ ํ…์ŠคํŠธ๊ฐ€ ๋ณต์ˆ˜ ํŒจํ„ด์œผ๋กœ ์˜ˆ์ƒ๋˜๊ธฐ ๋•Œ๋ฌธ์— XPath ์…€๋ ‰ํ„ฐ๋ฅผ ๋ฐฐ์—ด๋กœ ๊ฐ€์ง€๊ณ  ์œ„์—์„œ๋ถ€ํ„ฐ ์ˆœ์„œ๋Œ€๋กœ ์‹œ๋„ํ•˜๋Š” ๋ฐฉ์‹์„ ์ฑ„์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

โš  ์„ค๊ณ„ ํฌ์ธํŠธ: ๋ณต์ˆ˜์˜ ์…€๋ ‰ํ„ฐ๋ฅผ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ถ™์—ฌ ์‹œ๋„ํ•˜๋Š” ํด๋ฐฑ ์„ค๊ณ„๋Š” ์‹ค๋ฌด Selenium ํ…Œ์ŠคํŠธ์—์„œ ๊ผญ ๋„์ž…ํ•˜๊ณ  ์‹ถ์€ ์ˆ˜๋ฒ•์ž…๋‹ˆ๋‹ค.

โ‘ง login() / logout() โ€” ๋กœ๊ทธ์ธ ํ™•์ธ๊ณผ ํ›„์ฒ˜๋ฆฌ

๊ฐ€์ž… ์™„๋ฃŒ ํ›„ ๋ฐ”๋กœ ๋กœ๊ทธ์•„์›ƒ โ†’ ๋กœ๊ทธ์ธ์„ ์‹คํ–‰ํ•จ์œผ๋กœ์จ ๊ฐ€์ž… ์™„๋ฃŒ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋กœ๊ทธ์ธ ๊ฐ€๋Šฅ ์ƒํƒœ๊นŒ์ง€ ์ผ๊ด€๋˜๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฉ”์„œ๋“œ ์š”์†Œ ํƒ์ƒ‰์˜ ๊ถ๋ฆฌ ์„ฑ๊ณต ํŒ์ • ๋กœ์ง
login() ๋ณต์ˆ˜ ์…€๋ ‰ํ„ฐ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ์‹œ๋„. logout ํฌํ•จ href๋Š” ์ œ์™ธ body ๋‚ด์— ์ธ์‚ฌ๋ง์ด ํฌํ•จ๋˜๋Š”์ง€ ํ™•์ธ
logout() ์‚ฌ์šฉ์ž ์ด๋ฆ„ ๋งํฌ ํด๋ฆญ์œผ๋กœ ๋“œ๋กญ๋‹ค์šด์„ ์—ด๊ณ  “๋กœ๊ทธ์•„์›ƒ” ์š”์†Œ๋ฅผ ๊ฒ€์ƒ‰ ๋กœ๊ทธ์•„์›ƒ ์š”์†Œ๋ฅผ ์ฐพ์ง€ ๋ชปํ•˜๋ฉด “์ด๋ฏธ ๋กœ๊ทธ์•„์›ƒ ์ƒํƒœ”๋กœ์„œ True ๋ฐ˜ํ™˜

โ‘จ reset_password() โ€” ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ํ”Œ๋กœ์šฐ

“๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žŠ์œผ์‹  ๋ถ„” ๋งํฌ๋ถ€ํ„ฐ ์‹œ์ž‘๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ํ”Œ๋กœ์šฐ๋ฅผ ์ž๋™ํ™”ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๊ธฐ๋Šฅ์˜ ๋™์ž‘ ํ™•์ธ ํ…Œ์ŠคํŠธ๋„ ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ” ํฌ์ธํŠธ: ์ด์ „ ๋‹จ๊ณ„์˜ ์ƒํƒœ์— ์˜์กดํ•˜์ง€ ์•Š๋Š” ๊ฒฌ๊ณ ํ•œ ํ…Œ์ŠคํŠธ ํ”Œ๋กœ์šฐ๋ฅผ ์‹คํ˜„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์‹คํ–‰ ๋กœ๊ทธ ์ƒ˜ํ”Œ

์‹คํ–‰ํ•˜๋ฉด ์ฝ˜์†”๊ณผ ํ…์ŠคํŠธ ํŒŒ์ผ์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋กœ๊ทธ๊ฐ€ ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค. ๊ฐ ๋‹จ๊ณ„์˜ ์‹คํ–‰ ์ƒํ™ฉ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

[2025-07-10 14:32:01] ======================================================================
[2025-07-10 14:32:01] Yodobashi Camera Member Registration Automation (Full Flow)
[2025-07-10 14:32:02] [Step 1] Navigating to member registration page...
[2025-07-10 14:32:05]   โœ“ Registration link clicked
[2025-07-10 14:32:08] [Step 2] Filling in basic information...
[2025-07-10 14:32:09]   โœ“ Last name: Yamada
[2025-07-10 14:32:09]   โœ“ First name: Taro
[2025-07-10 14:32:11] [Step 3] Filling in address information...
[2025-07-10 14:32:12]   โœ“ Postal code: 150-0001
[2025-07-10 14:32:14]   โœ“ Address search executed
[2025-07-10 14:32:18]   ๐Ÿ“ท Screenshot: screenshot_02_form_filled_143218.png
[2025-07-10 14:32:32] โœ“ Member registration complete!
[2025-07-10 14:32:45]   โœ“ Login successful!
[2025-07-10 14:32:50] Final result: Registration, logout, login, and logout all completed!

โš ๏ธ ์ด๋Ÿฐ ์—๋Ÿฌ๊ฐ€ ๋‚˜์˜ค๋Š” ๊ฒฝ์šฐ๋Š”๏ผŸ

======================================================================
๊ฒฐ๊ณผ: ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค
Email: test****@example.com
Password: ******
๋กœ๊ทธ ํŒŒ์ผ: C:\Users\HP\Desktop\yodobashi_signup_logs\signup_log_****.txt
======================================================================

์ด ์—๋Ÿฌ๊ฐ€ ๋‚˜์˜ค๋Š” ๊ฐ€์žฅ ํ”ํ•œ ์›์ธ์€ ์ด๋ฏธ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.

  • email ๊ฐ’์„ ๋‹ค๋ฅธ ์ฃผ์†Œ(์˜ˆ: test_99999@example.com)๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์žฌ์‹คํ–‰ํ•˜์„ธ์š”
  • ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ด๋ฉ”์ผ ์ฃผ์†Œ ํŒจํ„ด์„ ๋ฏธ๋ฆฌ ์ค€๋น„ํ•ด ๋‘๋ฉด ํŽธ๋ฆฌํ•ฉ๋‹ˆ๋‹ค
  • ๋™์ผํ•œ ์ฃผ์†Œ๋กœ ์žฌ๊ฐ€์ž…์„ ์‹œ๋„ํ•˜๋ฉด “์ค‘๋ณต ๊ฐ€์ž… ์—๋Ÿฌ”๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ๋‹จ๊ณ„๊นŒ์ง€ ์ง„ํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค

ํ•„์ž์˜ ํ™˜๊ฒฝ์—์„œ ์‹ค์ œ๋กœ ์‹คํ–‰ํ–ˆ์„ ๋•Œ์˜ ํ„ฐ๋ฏธ๋„ ์ถœ๋ ฅ์ž…๋‹ˆ๋‹ค. ๋‹จ๊ณ„ 1~3์ด ์ˆœ์„œ๋Œ€๋กœ ์‹คํ–‰๋˜์–ด ๊ฐ ์ž…๋ ฅ ํ•ญ๋ชฉ์ด โœ“๋กœ ํ™•์ธ๋˜๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Selenium์œผ๋กœ ํšŒ์›๊ฐ€์ž…์„ ์ž๋™ํ™”ํ•œ ํ„ฐ๋ฏธ๋„ ๋กœ๊ทธ. ๋‹จ๊ณ„ 1๋ถ€ํ„ฐ 3๊นŒ์ง€ ๊ฐ ์ž…๋ ฅ ํ•ญ๋ชฉ์ด ์ˆœ์„œ๋Œ€๋กœ ์‹คํ–‰๋˜๊ณ  ์žˆ์Œ

โ–ฒ ์‹ค์ œ ํ„ฐ๋ฏธ๋„ ์ถœ๋ ฅ. ๋‹จ๊ณ„1๏ผˆํŽ˜์ด์ง€ ์ด๋™๏ผ‰โ†’ ๋‹จ๊ณ„2๏ผˆ๊ธฐ๋ณธ ์ •๋ณด ์ž…๋ ฅ๏ผ‰โ†’ ๋‹จ๊ณ„3๏ผˆ์ฃผ์†Œ ์ž…๋ ฅ๏ผ‰์ด ์ˆœ์„œ๋Œ€๋กœ ์‹คํ–‰๋จ

๋”์šฑ ๋ฐœ์ „์‹œํ‚ค๊ธฐ ์œ„ํ•œ ๊ฐœ์„  ์•„์ด๋””์–ด

๐Ÿ”„ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ (DDT)

user_data๋ฅผ CSV๋‚˜ Excel์—์„œ ์ฝ์–ด์˜ค๋Š” ํ˜•์‹์œผ๋กœ ํ•˜๋ฉด ๋ณต์ˆ˜์˜ ์‚ฌ์šฉ์ž๋กœ ์ผ๊ด„ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

โšก ๋ณ‘๋ ฌ ์‹คํ–‰์œผ๋กœ ๊ณ ์†ํ™”

concurrent.futures.ThreadPoolExecutor๋‚˜ pytest-xdist๋ฅผ ์‚ฌ์šฉํ•œ ๋ณ‘๋ ฌ ์‹คํ–‰์œผ๋กœ ๋ณต์ˆ˜ ๊ณ„์ •์˜ ๊ฐ€์ž… ํ™•์ธ์„ ๋™์‹œ์— ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ›ก๏ธ ์žฌ์‹œ๋„ ๊ธฐ๋Šฅ ๊ฐ•ํ™”

๋„คํŠธ์›Œํฌ ์ง€์—ฐ ๋Œ€์ฑ…์œผ๋กœ tenacity ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•œ ์ž๋™ ์žฌ์‹œ๋„ ๊ธฐ๊ตฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๋”์šฑ ๊ฒฌ๊ณ ํ•ด์ง‘๋‹ˆ๋‹ค.

๐Ÿ“Š HTML ๋ฆฌํฌํŠธ ์ถœ๋ ฅ

pytest + allure-pytest๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ์Šคํฌ๋ฆฐ์ƒท์ด ํฌํ•จ๋œ HTML ํ…Œ์ŠคํŠธ ๋ฆฌํฌํŠธ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ™œ์šฉ ์žฅ๋ฉดยทํ™•์žฅ ์˜ˆ

๐Ÿš€ ์ด ์ฝ”๋“œ๊ฐ€ ๋น›๋‚˜๋Š” ์žฅ๋ฉด

๐Ÿงช ๋ฆฌ๊ทธ๋ ˆ์…˜ ํ…Œ์ŠคํŠธ

๋ฆด๋ฆฌ์Šค ์ „์— ํšŒ์›๊ฐ€์ž… ํ”Œ๋กœ์šฐ๊ฐ€ ๋ง๊ฐ€์ง€์ง€ ์•Š์•˜๋Š”์ง€ ์ž๋™์œผ๋กœ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. CI/CD์— ํ†ตํ•ฉํ•˜๋ฉด ๋งค ๋ฐฐํฌ ํ›„์— ์ž๋™ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“‹ ์ธ์ˆ˜ ํ…Œ์ŠคํŠธ ์ž๋™ํ™”

์ดํ•ด๊ด€๊ณ„์ž ๋ณด๊ณ ์šฉ์œผ๋กœ ์Šคํฌ๋ฆฐ์ƒท์ด ํฌํ•จ๋œ ์—๋น„๋˜์Šค๋ฅผ ์ž๋™ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์—๋น„๋˜์Šค ์ž‘์„ฑ ๊ณต์ˆ˜๋ฅผ ๋Œ€ํญ ์‚ญ๊ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”ง E2E ํ…Œ์ŠคํŠธ ํ•™์Šต ๊ต์žฌ

Selenium์˜ ์š”์†Œ ํƒ์ƒ‰ยท๋Œ€๊ธฐ ์ฒ˜๋ฆฌยท์Šคํฌ๋ฆฐ์ƒทยท๋กœ๊ทธ ์ถœ๋ ฅ ๋“ฑ ์‹ค๋ฌด์ ์ธ ํŒจํ„ด์ด ํ•œ๋ฐ ๋ชจ์ธ ํ•™์Šต ๊ต์žฌ๋กœ์„œ ์ตœ์ ์ž…๋‹ˆ๋‹ค.

๐Ÿ”„ ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ํ”Œ๋กœ์šฐ ํ™•์ธ

reset_password() ๋ฉ”์„œ๋“œ๋กœ “๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žŠ์œผ์‹  ๋ถ„” ํ”Œ๋กœ์šฐ์˜ ๋™์ž‘ ํ™•์ธ๋„ ์ž๋™ํ™”ํ•ฉ๋‹ˆ๋‹ค.

์‹ค๋ฌด ์‘์šฉ: POM ์„ค๊ณ„ ร— CI/CD ์—ฐ๊ณ„

๐Ÿ“ Page Object Model (POM)

ํŽ˜์ด์ง€๋ณ„๋กœ ํด๋ž˜์Šค๋ฅผ ๋‚˜๋ˆ„๊ณ  ์กฐ์ž‘ ๋กœ์ง๊ณผ ํ…Œ์ŠคํŠธ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์ด Selenium์˜ ์ •์„ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

pages/register_page.py โ† ์กฐ์ž‘ ๋กœ์ง
tests/test_register.py โ† ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค

โš™๏ธ CI/CD ํ†ตํ•ฉ

GitHub Actions์™€ ์กฐํ•ฉํ•˜๋ฉด ๋ฐฐํฌํ•  ๋•Œ๋งˆ๋‹ค ํšŒ์›๊ฐ€์ž… ํ”Œ๋กœ์šฐ์˜ ํ’ˆ์งˆ์„ ์ž๋™์œผ๋กœ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

push โ†’ GitHub Actions ์‹คํ–‰
โ†’ pytest ์‹คํ–‰ โ†’ ๋ฆฌํฌํŠธ ์ƒ์„ฑ

์ž์ฃผ ๊ฒช๋Š” ๋ฌธ์ œ & ํ•ด๊ฒฐ๋ฒ•

๊ตฌํ˜„ ์ค‘ ์‹ค์ œ๋กœ ๊ฒช์—ˆ๋˜ ๋ฌธ์ œ๋“ค์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ™์€ ๊ณณ์—์„œ ๋ง‰ํžˆ๋Š” ๋ถ„๋“ค๊ป˜ ๋„์›€์ด ๋˜๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.


โ‘  ์ฒดํฌ๋ฐ•์Šค๊ฐ€ ์ผ๋ฐ˜ ํด๋ฆญ์œผ๋กœ ๋ฐ˜์‘ํ•˜์ง€ ์•Š๋Š”๋‹ค

์ด์šฉ์•ฝ๊ด€ ์ฒดํฌ๋ฐ•์Šค์— click() ์„ ์‹คํ–‰ํ•ด๋„ ์ฒดํฌ๊ฐ€ ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์›์ธ์€ ์ฒดํฌ๋ฐ•์Šค ๋ณธ์ฒด๊ฐ€ visibility:hidden ์œผ๋กœ ์ˆจ๊ฒจ์ ธ ์žˆ๊ณ , ์‹ค์ œ๋กœ๋Š” ๋ผ๋ฒจ ์š”์†Œ๋ฅผ ํด๋ฆญํ•˜๋„๋ก ๊ตฌํ˜„๋˜์–ด ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

# โŒ ์ผ๋ฐ˜ ํด๋ฆญ์œผ๋กœ๋Š” ๋ฐ˜์‘ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ
checkbox = driver.find_element(By.ID, "js_i_checkPrivacyPolicyAgreement")
checkbox.click()

# โœ… JS ํด๋ฆญ โ†’ ํ”„๋กœํผํ‹ฐ ์ง์ ‘ ๋ณ€๊ฒฝ์˜ 3๋‹จ๊ณ„ ํด๋ฐฑ์œผ๋กœ ํ•ด๊ฒฐ
try:
    checkbox.click()
except:
    driver.execute_script("arguments[0].click();", checkbox)
    if not checkbox.is_selected():
        driver.execute_script("arguments[0].checked = true;", checkbox)

๐Ÿ’ก ํฌ์ธํŠธ๏ผš arguments[0].click()๏ผˆํด๋ฆญ ์ด๋ฒคํŠธ ๋ฐœํ™”๏ผ‰๊ณผ arguments[0].checked = true๏ผˆํ”„๋กœํผํ‹ฐ ์ง์ ‘ ๋ณ€๊ฒฝ๏ผ‰๋Š” ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ๋‘˜ ๋‹ค ๊ฐ–๊ณ  ์žˆ์œผ๋ฉด ๊ฑฐ์˜ ๋ชจ๋“  ๊ฒฝ์šฐ์— ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โ‘ก ๋“œ๋กญ๋‹ค์šด๏ผˆselect ํƒœ๊ทธ๏ผ‰์„ ์ง์ ‘ ํด๋ฆญ์œผ๋กœ ์„ ํƒํ•  ์ˆ˜ ์—†๋‹ค

์ƒ๋…„์›”์ผ์˜ ใ€Œ์›”ใ€ใ€Œ์ผใ€์ด ๋“œ๋กญ๋‹ค์šด์œผ๋กœ ๋˜์–ด ์žˆ์–ด์„œ ์ผ๋ฐ˜ click() ์œผ๋กœ๋Š” ์„ ํƒํ•  ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. Select ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฐ’์„ ์„ ํƒํ•  ์ˆ˜ ์—†๋‹ค๋Š” ๊ฒƒ์„ ๋ชฐ๋ผ์„œ ํ—ค๋งธ์Šต๋‹ˆ๋‹ค.

# โŒ ์ง์ ‘ ํด๋ฆญ์œผ๋กœ๋Š” ์„ ํƒํ•  ์ˆ˜ ์—†์Œ
month_element.click()

# โœ… Select ํด๋ž˜์Šค๋กœ ๊ฐ์‹ธ์„œ ์กฐ์ž‘
from selenium.webdriver.support.ui import Select
select = Select(month_element)
select.select_by_value("1")  # 1์›” ์„ ํƒ

๐Ÿ’ก ํฌ์ธํŠธ๏ผš <select> ํƒœ๊ทธ ์กฐ์ž‘์—๋Š” ๋ฐ˜๋“œ์‹œ Select ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ž„ํฌํŠธ๋ฅผ ์žŠ์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•˜์„ธ์š”.


โ‘ข ๋ฒ„ํŠผ์˜ XPath๊ฐ€ 1ํŒจํ„ด์ด๋ฉด ์‰ฝ๊ฒŒ ๋ง๊ฐ€์ง„๋‹ค

ใ€Œ๋‹ค์Œ์œผ๋กœ ์ง„ํ–‰ใ€ ๋ฒ„ํŠผ์„ XPath 1ํŒจํ„ด์œผ๋กœ ์ง€์ •ํ–ˆ๋”๋‹ˆ ์‚ฌ์ดํŠธ์˜ ๋ฏธ์„ธํ•œ ๋ณ€๊ฒฝ์œผ๋กœ ๋ฒ„ํŠผ์„ ์ฐพ์ง€ ๋ชปํ•˜๊ฒŒ ๋์Šต๋‹ˆ๋‹ค. ๋ฒ„ํŠผ ํ…์ŠคํŠธ๋Š” ใ€Œ๋‹ค์Œใ€ใ€Œ๋‹ค์Œ์œผ๋กœใ€ใ€Œ์ง„ํ–‰ใ€ ๋“ฑ ์—ฌ๋Ÿฌ ํŒจํ„ด์ด ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„ ํ•˜๋‚˜๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ–ˆ์Šต๋‹ˆ๋‹ค.

# โŒ 1ํŒจํ„ด๋งŒ์œผ๋กœ๋Š” ์‰ฝ๊ฒŒ ๋ง๊ฐ€์ง
button = driver.find_element(By.XPATH, "//button[text()='๋‹ค์Œ์œผ๋กœ ์ง„ํ–‰']")

# โœ… ์—ฌ๋Ÿฌ ํŒจํ„ด์„ ์ˆœ์„œ๋Œ€๋กœ ์‹œ๋„ํ•˜๋Š” ํด๋ฐฑ ์„ค๊ณ„
selectors = [
    "//button[contains(text(),'๋‹ค์Œ')]",
    "//input[@type='submit']",
    "//a[contains(text(),'๋‹ค์Œ')]",
]
for selector in selectors:
    elements = driver.find_elements(By.XPATH, selector)
    if elements:
        elements[0].click()
        break

๐Ÿ’ก ํฌ์ธํŠธ๏ผš ์—ฌ๋Ÿฌ ์…€๋ ‰ํ„ฐ๋ฅผ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ •ํ•ด ์ˆœ์„œ๋Œ€๋กœ ์‹œ๋„ํ•˜๋Š” ํด๋ฐฑ ์„ค๊ณ„๋Š” ์‹ค๋ฌด Selenium ํ…Œ์ŠคํŠธ์—์„œ ๊ผญ ๋„์ž…ํ•˜๊ณ  ์‹ถ์€ ๊ธฐ๋ฒ•์ž…๋‹ˆ๋‹ค.


โ‘ฃ ์šฐํŽธ๋ฒˆํ˜ธ ํ•„๋“œ ํŠน์ •์— ์• ๋ฅผ ๋จน์—ˆ๋‹ค

์šฐํŽธ๋ฒˆํ˜ธ ํ•„๋“œ๋ฅผ id ๋กœ ์ง์ ‘ ์ง€์ •ํ–ˆ๋”๋‹ˆ ์‚ฌ์ดํŠธ ๋ฆฌ๋‰ด์–ผ๋กœ id ๊ฐ€ ๋ฐ”๋€Œ์–ด ํ…Œ์ŠคํŠธ๊ฐ€ ๋ง๊ฐ€์กŒ์Šต๋‹ˆ๋‹ค. ์™„์ „ ์ผ์น˜๋ณด๋‹ค ๋ถ€๋ถ„ ์ผ์น˜๊ฐ€ ๋ณ€๊ฒฝ์— ๋” ๊ฐ•ํ•˜๋‹ค๋Š” ๊ฒƒ์„ ๋ฐฐ์› ์Šต๋‹ˆ๋‹ค.

# โŒ id์˜ ์™„์ „ ์ผ์น˜๋Š” ๋ณ€๊ฒฝ์— ์ทจ์•ฝ
driver.find_element(By.ID, "zipCode1")

# โœ… name ์†์„ฑ์˜ ๋ถ€๋ถ„ ์ผ์น˜๋กœ ์œ ์—ฐํ•˜๊ฒŒ ๋Œ€์‘
for inp in driver.find_elements(By.XPATH, "//input[@type='text']"):
    name_attr = inp.get_attribute('name') or ''
    if 'zip' in name_attr.lower() or 'postal' in name_attr.lower():
        postal_inputs.append(inp)

๐Ÿ’ก ํฌ์ธํŠธ๏ผš ID๋‚˜ ํด๋ž˜์Šค๋ช…์˜ ์™„์ „ ์ผ์น˜๋ณด๋‹ค ์†์„ฑ๊ฐ’์˜ ๋ถ€๋ถ„ ์ผ์น˜๋กœ ์ฐพ๋Š” ํŽธ์ด ์‚ฌ์ดํŠธ ๋ณ€๊ฒฝ์— ๊ฐ•ํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ ๋น„์šฉ์ด ์ค„์–ด๋“ญ๋‹ˆ๋‹ค.


โ‘ค ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์—ฌ๋ถ€ ํŒ์ •์ด ์–ด๋ ค์› ๋‹ค

๋กœ๊ทธ์ธ ํ›„ ใ€Œ์„ฑ๊ณตํ–ˆ๋Š”์ง€ ์—ฌ๋ถ€ใ€๋ฅผ ํŒ์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๊ณ ๋ฏผํ–ˆ์Šต๋‹ˆ๋‹ค. URL์ด ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๋„ ์žˆ์–ด, ํŽ˜์ด์ง€ ํƒ€์ดํ‹€์ด๋‚˜ URL๋งŒ์œผ๋กœ๋Š” ํŒ์ •ํ•  ์ˆ˜ ์—†๋Š” ์ƒํ™ฉ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

# โŒ URL๋งŒ์œผ๋กœ๋Š” ํŒ์ •ํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Œ
if "mypage" in driver.current_url:
    print("๋กœ๊ทธ์ธ ์„ฑ๊ณต")

# โœ… ํŽ˜์ด์ง€ ๋ณธ๋ฌธ์— ํฌํ•จ๋œ ํ…์ŠคํŠธ๋กœ ํŒ์ •
body_text = driver.find_element(By.TAG_NAME, "body").text
if "์•ˆ๋…•ํ•˜์„ธ์š”" in body_text or "๋‹˜" in body_text:
    print("๋กœ๊ทธ์ธ ์„ฑ๊ณต")

โš ๏ธ ์ฃผ์˜๏ผš ํŒ์ •์— ์‚ฌ์šฉํ•˜๋Š” ํ…์ŠคํŠธ๋Š” ์‚ฌ์ดํŠธ์— ๋”ฐ๋ผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ๋กœ๊ทธ์ธ ํ›„ ํŽ˜์ด์ง€๋ฅผ ํ™•์ธํ•ด์„œ ํ™•์‹คํžˆ ํ‘œ์‹œ๋˜๋Š” ํ…์ŠคํŠธ๋ฅผ ์„ ํƒํ•˜์„ธ์š”.

์ •๋ฆฌ

์ด ๊ธ€์—์„œ๋Š” ์š”๋„๋ฐ”์‹œ ์นด๋ฉ”๋ผ ํšŒ์›๊ฐ€์ž… ํผ์„ Python ร— Selenium์œผ๋กœ ์ž๋™ํ™”ํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ•ด์„คํ–ˆ์Šต๋‹ˆ๋‹ค.

์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ํ›„ ๋ฐ์Šคํฌํ†ฑ์˜ yodobashi_signup_logs ํด๋”๋ฅผ ์—ด๋ฉด ๋กœ๊ทธ ํŒŒ์ผ๊ณผ ์Šคํฌ๋ฆฐ์ƒท์ด ํƒ€์ž„์Šคํƒฌํ”„์™€ ํ•จ๊ป˜ ์ž๋™ ์ €์žฅ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ๊ทธ๋Œ€๋กœ ํ…Œ์ŠคํŠธ ์—๋น„๋˜์Šค๋กœ ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค.

yodobashi_signup_logs ํด๋”์˜ ๋‚ด์šฉ. ๋กœ๊ทธ ํŒŒ์ผ๊ณผ screenshot_00~03์˜ PNG ํŒŒ์ผ์ด ์ž๋™ ์ €์žฅ๋˜์–ด ์žˆ์Œ

โ–ฒ ์ž๋™ ์ €์žฅ๋œ ๋กœ๊ทธ ํด๋”์˜ ๋‚ด์šฉ. ๋กœ๊ทธ ํŒŒ์ผ + ์Šคํฌ๋ฆฐ์ƒท 4์žฅ์ด ํƒ€์ž„์Šคํƒฌํ”„์™€ ํ•จ๊ป˜ ์ •๋ฆฌ๋˜์–ด ์žˆ์Œ

๋ณต์ˆ˜ ์…€๋ ‰ํ„ฐ ํด๋ฐฑ ์„ค๊ณ„ XPath๋ฅผ ๋ณต์ˆ˜ ํŒจํ„ด์œผ๋กœ ์ค€๋น„ํ•˜์—ฌ ๊ฒฌ๊ณ ํ•œ ์š”์†Œ ํƒ์ƒ‰์„ ์‹คํ˜„
๋กœ๊ทธ + ์Šคํฌ๋ฆฐ์ƒท ์‹คํ–‰ ์ด๋ ฅ์„ ์ถ”์  ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜์—ฌ ๋””๋ฒ„๊น… ๊ณต์ˆ˜๋ฅผ ์ ˆ๊ฐ
๊ฐ€์ž… โ†’ ๋กœ๊ทธ์•„์›ƒ โ†’ ๋กœ๊ทธ์ธ ์ผ๊ด€ ํ”Œ๋กœ์šฐ ๊ฐ€์ž… ์™„๋ฃŒ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋กœ๊ทธ์ธ ๊ฐ€๋Šฅ ์ƒํƒœ๊นŒ์ง€ ๊ฒ€์ฆ
ํด๋ž˜์Šค ์„ค๊ณ„์— ์˜ํ•œ ์žฌ์‚ฌ์šฉ์„ฑ ๊ฐ ๊ธฐ๋Šฅ์ด ๋…๋ฆฝ๋œ ๋ฉ”์„œ๋“œ๋กœ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์–ด ํ™•์žฅ์ด ์‰ฌ์›€

QA ์—”์ง€๋‹ˆ์–ด๋กœ์„œ “๋งค๋ฒˆ ์ˆ˜๋™์œผ๋กœ ํผ์„ ์ฑ„์šฐ๋Š” ๊ฒƒ์€ ํ•œ๊ณ„”๋ผ๋Š” ๊ณผ์ œ์— ๋Œ€ํ•ด ์‹ค์ œ ํ˜„์žฅ์—์„œ ์“ธ ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์˜ ์ž๋™ํ™”๋ฅผ ๋ชฉํ‘œ๋กœ ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ผญ GitHub์˜ ์ฝ”๋“œ๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ์—ฌ๋Ÿฌ๋ถ„์˜ ํ”„๋กœ์ ํŠธ์— ๋งž๊ฒŒ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•ด ๋ณด์„ธ์š”!

ใ‚ฟใ‚คใƒˆใƒซใจURLใ‚’ใ‚ณใƒ”ใƒผใ—ใพใ—ใŸ