[{"data":1,"prerenderedAt":2509},["ShallowReactive",2],{"post-document:\u002Fpython\u002F2026\u002F08\u002F07\u002Fpython_2\u002F":3},{"id":4,"title":5,"body":6,"categories":2491,"date":2493,"description":2494,"extension":2495,"image":2496,"key_concepts":2496,"last_modified_at":2496,"legacyPath":2497,"meta":2498,"navigation":364,"part":2496,"path":2500,"published":364,"robots":2496,"seo":2501,"series":2496,"stem":2502,"strengths":2496,"summary":2496,"tags":2503,"tradeoffs":2496,"__hash__":2508},"posts\u002Fposts\u002FPython\u002F2026-08-07-python_2.md","Python 코드 품질: 디버깅부터 테스트와 자동화까지",{"type":7,"value":8,"toc":2458},"minimark",[9,18,21,40,43,54,57,63,66,72,75,80,85,88,91,108,111,117,124,129,131,135,140,281,288,293,299,302,308,311,318,320,324,329,332,377,380,386,390,393,396,399,402,407,437,440,450,452,456,466,517,520,526,529,532,535,544,547,550,559,562,571,574,577,616,619,625,629,634,651,654,657,659,663,670,713,716,722,725,731,734,744,746,750,756,776,785,788,791,800,803,809,812,815,820,826,829,856,859,865,868,870,874,880,887,929,935,938,944,947,964,967,971,974,999,1002,1005,1013,1015,1019,1022,1027,1029,1032,1038,1058,1061,1084,1087,1124,1127,1136,1139,1141,1145,1148,1151,1157,1160,1166,1169,1184,1191,1194,1206,1209,1221,1224,1236,1239,1252,1255,1267,1269,1273,1276,1284,1287,1290,1296,1299,1301,1307,1310,1313,1315,1319,1322,1329,1343,1345,1363,1365,1371,1377,1380,1385,1388,1390,1394,1397,1404,1418,1421,1437,1440,1457,1460,1474,1477,1483,1486,1510,1520,1523,1538,1541,1555,1558,1561,1564,1570,1573,1575,1579,1582,1596,1598,1610,1613,1627,1633,1636,1642,1645,1648,1650,1654,1660,1674,1677,1683,1686,1688,1859,1862,1874,1877,1892,1895,1908,1911,1914,1931,1933,1937,1942,1945,1948,1951,1957,1960,2199,2202,2208,2211,2213,2217,2220,2226,2229,2303,2305,2309,2312,2318,2321,2385,2387,2390,2393,2402,2405,2408,2411,2417,2420,2423,2428,2431,2436,2439,2442,2449,2454],[10,11,12,13,17],"p",{},"Python 코드를 작성할 때 중요한 것은 단순히 ",[14,15,16],"strong",{},"실행되는 코드","를 만드는 것이 아니다.",[10,19,20],{},"좋은 코드는 다음 조건을 만족해야 한다.",[22,23,24,28,31,34,37],"ul",{},[25,26,27],"li",{},"문제가 발생했을 때 원인을 빠르게 찾을 수 있어야 한다.",[25,29,30],{},"코드 변경 후 기존 기능이 깨지지 않았음을 확인할 수 있어야 한다.",[25,32,33],{},"팀원이 작성해도 일정한 코드 스타일을 유지해야 한다.",[25,35,36],{},"Git에 잘못된 코드가 들어가기 전에 문제를 발견할 수 있어야 한다.",[25,38,39],{},"서버나 CI 환경에서도 자동으로 품질을 검증할 수 있어야 한다.",[10,41,42],{},"이를 위해 사용되는 대표적인 도구가 다음과 같다.",[44,45,51],"pre",{"className":46,"code":48,"language":49,"meta":50},[47],"language-text","Debugger \u002F logging\n        ↓\n      pytest\n        ↓\n   pytest-cov\n        ↓\n      Ruff\n        ↓\n   pre-commit\n        ↓\n GitHub Actions\n","text","",[52,53,48],"code",{"__ignoreMap":50},[10,55,56],{},"결국 코드 품질 관리의 핵심은",[58,59,60],"blockquote",{},[10,61,62],{},"사람이 직접 확인하던 작업을 최대한 자동화하는 것",[10,64,65],{},"이라고 볼 수 있다.",[10,67,68],{},[69,70,71],"del",{},"코드 품질과 기능을 자동으로 검증해야 한다. 아직 프로젝트를 제대로 해본적이 없어서 크게 와닿지는 않는 걸",[73,74],"hr",{},[76,77,79],"h2",{"id":78},"_1-디버깅","1. 디버깅",[81,82,84],"h3",{"id":83},"버그란","버그란?",[10,86,87],{},"버그(Bug)는 프로그램이 의도한 대로 동작하지 않게 만드는 문제를 의미한다.",[10,89,90],{},"대표적으로 다음과 같은 문제가 있다.",[22,92,93,96,99,102,105],{},[25,94,95],{},"입력 데이터 오류",[25,97,98],{},"논리 오류",[25,100,101],{},"문법 오류",[25,103,104],{},"타입 오류",[25,106,107],{},"실행 중 발생하는 예외",[10,109,110],{},"일반적인 디버깅 과정은 다음과 같다.",[44,112,115],{"className":113,"code":114,"language":49,"meta":50},[47],"문제 재현\n   ↓\n원인 분석\n   ↓\n수정\n   ↓\n재실행\n   ↓\n정상 동작 확인\n",[52,116,114],{"__ignoreMap":50},[10,118,119,120,123],{},"중요한 것은 단순히 에러를 없애는 것이 아니라 ",[14,121,122],{},"왜 문제가 발생했는지 확인하는 것","이다.",[10,125,126],{},[69,127,128],{},"CODEX야 도와줘",[73,130],{},[76,132,134],{"id":133},"_2-자주-만나는-python-오류","2. 자주 만나는 Python 오류",[10,136,137],{},[69,138,139],{},"안 만나고 싶다.. 오류..",[141,142,143,159],"table",{},[144,145,146],"thead",{},[147,148,149,153,156],"tr",{},[150,151,152],"th",{},"오류",[150,154,155],{},"의미",[150,157,158],{},"예시",[160,161,162,176,191,206,221,236,251,266],"tbody",{},[147,163,164,170,173],{},[165,166,167],"td",{},[52,168,169],{},"SyntaxError",[165,171,172],{},"Python 문법 오류",[165,174,175],{},"괄호 또는 콜론 누락",[147,177,178,183,186],{},[165,179,180],{},[52,181,182],{},"NameError",[165,184,185],{},"정의되지 않은 변수 사용",[165,187,188],{},[52,189,190],{},"print(x)",[147,192,193,198,201],{},[165,194,195],{},[52,196,197],{},"TypeError",[165,199,200],{},"서로 맞지 않는 타입 연산",[165,202,203],{},[52,204,205],{},"\"2\" + 2",[147,207,208,213,216],{},[165,209,210],{},[52,211,212],{},"IndexError",[165,214,215],{},"리스트 범위를 벗어난 접근",[165,217,218],{},[52,219,220],{},"items[10]",[147,222,223,228,231],{},[165,224,225],{},[52,226,227],{},"KeyError",[165,229,230],{},"존재하지 않는 딕셔너리 키 접근",[165,232,233],{},[52,234,235],{},"data[\"name\"]",[147,237,238,243,246],{},[165,239,240],{},[52,241,242],{},"ValueError",[165,244,245],{},"값의 형식이 잘못됨",[165,247,248],{},[52,249,250],{},"int(\"hello\")",[147,252,253,258,261],{},[165,254,255],{},[52,256,257],{},"AttributeError",[165,259,260],{},"존재하지 않는 메서드나 속성 사용",[165,262,263],{},[52,264,265],{},"\"hello\".push()",[147,267,268,273,276],{},[165,269,270],{},[52,271,272],{},"ZeroDivisionError",[165,274,275],{},"0으로 나누기",[165,277,278],{},[52,279,280],{},"10 \u002F 0",[10,282,283,284,287],{},"에러가 발생하면 가장 먼저 ",[14,285,286],{},"Traceback의 마지막 줄","을 확인하는 것이 좋다.",[10,289,290],{},[69,291,292],{},"원래도 마지막 줄만 본다",[44,294,297],{"className":295,"code":296,"language":49,"meta":50},[47],"Traceback (most recent call last):\n  ...\nZeroDivisionError: division by zero\n",[52,298,296],{"__ignoreMap":50},[10,300,301],{},"마지막 줄에는 일반적으로",[44,303,306],{"className":304,"code":305,"language":49,"meta":50},[47],"예외 종류 : 문제 내용\n",[52,307,305],{"__ignoreMap":50},[10,309,310],{},"이 표시된다.",[10,312,313,314,317],{},"그 다음 위쪽으로 올라가면서 ",[14,315,316],{},"어느 파일의 몇 번째 줄에서 문제가 시작되었는지"," 추적한다.",[73,319],{},[76,321,323],{"id":322},"_3-print-디버깅","3. print() 디버깅",[10,325,326],{},[69,327,328],{},"개발자의 시간복잡도를 줄여준다. 알고리즘의 시간복잡도만 중요한게 아니라고",[10,330,331],{},"가장 간단한 디버깅 방법은 변수 값을 직접 출력하는 것이다.",[44,333,337],{"className":334,"code":335,"language":336,"meta":50,"style":50},"language-python shiki shiki-themes github-light github-dark","def add_numbers(a, b):\n    print(f\"DEBUG: a={a}, b={b}\")\n    return a + b\n\n\nprint(add_numbers(3, 5))\n","python",[52,338,339,347,353,359,366,371],{"__ignoreMap":50},[340,341,344],"span",{"class":342,"line":343},"line",1,[340,345,346],{},"def add_numbers(a, b):\n",[340,348,350],{"class":342,"line":349},2,[340,351,352],{},"    print(f\"DEBUG: a={a}, b={b}\")\n",[340,354,356],{"class":342,"line":355},3,[340,357,358],{},"    return a + b\n",[340,360,362],{"class":342,"line":361},4,[340,363,365],{"emptyLinePlaceholder":364},true,"\n",[340,367,369],{"class":342,"line":368},5,[340,370,365],{"emptyLinePlaceholder":364},[340,372,374],{"class":342,"line":373},6,[340,375,376],{},"print(add_numbers(3, 5))\n",[10,378,379],{},"출력:",[44,381,384],{"className":382,"code":383,"language":49,"meta":50},[47],"DEBUG: a=3, b=5\n8\n",[52,385,383],{"__ignoreMap":50},[81,387,389],{"id":388},"왜-사용하는가","왜 사용하는가?",[10,391,392],{},"프로그램이 실제로 어떤 값을 가지고 실행되는지 빠르게 확인할 수 있다.",[10,394,395],{},"특히 작은 프로그램에서는 매우 간단하고 효과적이다!",[81,397,398],{"id":398},"한계",[10,400,401],{},"프로그램이 커지면 다음과 같은 문제가 생긴다.",[10,403,404],{},[69,405,406],{},"다 읽어낼 자신 있으면 써보던가 ㅋ",[44,408,410],{"className":334,"code":409,"language":336,"meta":50,"style":50},"print(data)\nprint(\"여기 실행됨\")\nprint(result)\nprint(user)\nprint(response)\n",[52,411,412,417,422,427,432],{"__ignoreMap":50},[340,413,414],{"class":342,"line":343},[340,415,416],{},"print(data)\n",[340,418,419],{"class":342,"line":349},[340,420,421],{},"print(\"여기 실행됨\")\n",[340,423,424],{"class":342,"line":355},[340,425,426],{},"print(result)\n",[340,428,429],{"class":342,"line":361},[340,430,431],{},"print(user)\n",[340,433,434],{"class":342,"line":368},[340,435,436],{},"print(response)\n",[10,438,439],{},"어떤 출력이 실제 결과이고 어떤 출력이 디버깅용인지 구분하기 어려워진다.",[10,441,442,443,446,447],{},"따라서 간단한 확인은 ",[52,444,445],{},"print()","로 할 수 있지만 규모가 커지면 ",[14,448,449],{},"Debugger와 logging을 사용하는 것이 좋다.",[73,451],{},[76,453,455],{"id":454},"_4-logging","4. logging",[10,457,458,459,461,462,465],{},"실무 프로그램에서는 ",[52,460,445],{}," 대신 ",[52,463,464],{},"logging","을 많이 사용한다.",[44,467,469],{"className":334,"code":468,"language":336,"meta":50,"style":50},"import logging\n\nlogging.basicConfig(level=logging.DEBUG)\n\nlogging.debug(\"디버깅 메시지\")\nlogging.info(\"프로그램 실행\")\nlogging.warning(\"주의가 필요한 상태\")\nlogging.error(\"오류 발생\")\nlogging.critical(\"심각한 오류 발생\")\n",[52,470,471,476,480,485,489,494,499,505,511],{"__ignoreMap":50},[340,472,473],{"class":342,"line":343},[340,474,475],{},"import logging\n",[340,477,478],{"class":342,"line":349},[340,479,365],{"emptyLinePlaceholder":364},[340,481,482],{"class":342,"line":355},[340,483,484],{},"logging.basicConfig(level=logging.DEBUG)\n",[340,486,487],{"class":342,"line":361},[340,488,365],{"emptyLinePlaceholder":364},[340,490,491],{"class":342,"line":368},[340,492,493],{},"logging.debug(\"디버깅 메시지\")\n",[340,495,496],{"class":342,"line":373},[340,497,498],{},"logging.info(\"프로그램 실행\")\n",[340,500,502],{"class":342,"line":501},7,[340,503,504],{},"logging.warning(\"주의가 필요한 상태\")\n",[340,506,508],{"class":342,"line":507},8,[340,509,510],{},"logging.error(\"오류 발생\")\n",[340,512,514],{"class":342,"line":513},9,[340,515,516],{},"logging.critical(\"심각한 오류 발생\")\n",[10,518,519],{},"로그 레벨은 다음과 같이 구분된다.",[44,521,524],{"className":522,"code":523,"language":49,"meta":50},[47],"DEBUG\n  ↓\nINFO\n  ↓\nWARNING\n  ↓\nERROR\n  ↓\nCRITICAL\n",[52,525,523],{"__ignoreMap":50},[81,527,389],{"id":528},"왜-사용하는가-1",[10,530,531],{},"환경에 따라 필요한 로그만 출력할 수 있기 때문이다.",[10,533,534],{},"개발 환경에서는",[44,536,538],{"className":334,"code":537,"language":336,"meta":50,"style":50},"logging.DEBUG\n",[52,539,540],{"__ignoreMap":50},[340,541,542],{"class":342,"line":343},[340,543,537],{},[10,545,546],{},"까지 확인하고,",[10,548,549],{},"운영 환경에서는",[44,551,553],{"className":334,"code":552,"language":336,"meta":50,"style":50},"logging.INFO\n",[52,554,555],{"__ignoreMap":50},[340,556,557],{"class":342,"line":343},[340,558,552],{},[10,560,561],{},"또는",[44,563,565],{"className":334,"code":564,"language":336,"meta":50,"style":50},"logging.WARNING\n",[52,566,567],{"__ignoreMap":50},[340,568,569],{"class":342,"line":343},[340,570,564],{},[10,572,573],{},"이상만 기록하는 방식으로 사용할 수 있다.",[10,575,576],{},"파일에 저장하는 것도 가능하다.",[44,578,580],{"className":334,"code":579,"language":336,"meta":50,"style":50},"logging.basicConfig(\n    filename=\"app.log\",\n    level=logging.INFO,\n    format=\"%(asctime)s - %(levelname)s - %(message)s\",\n)\n\nlogging.info(\"프로그램 시작\")\n",[52,581,582,587,592,597,602,607,611],{"__ignoreMap":50},[340,583,584],{"class":342,"line":343},[340,585,586],{},"logging.basicConfig(\n",[340,588,589],{"class":342,"line":349},[340,590,591],{},"    filename=\"app.log\",\n",[340,593,594],{"class":342,"line":355},[340,595,596],{},"    level=logging.INFO,\n",[340,598,599],{"class":342,"line":361},[340,600,601],{},"    format=\"%(asctime)s - %(levelname)s - %(message)s\",\n",[340,603,604],{"class":342,"line":368},[340,605,606],{},")\n",[340,608,609],{"class":342,"line":373},[340,610,365],{"emptyLinePlaceholder":364},[340,612,613],{"class":342,"line":501},[340,614,615],{},"logging.info(\"프로그램 시작\")\n",[10,617,618],{},"결과:",[44,620,623],{"className":621,"code":622,"language":49,"meta":50},[47],"2026-08-07 09:00:01 - INFO - 프로그램 시작\n",[52,624,622],{"__ignoreMap":50},[626,627,628],"h4",{"id":628},"효과",[10,630,631,633],{},[52,632,464],{},"을 사용하면",[22,635,636,639,642,645,648],{},[25,637,638],{},"오류 발생 시간",[25,640,641],{},"프로그램 실행 흐름",[25,643,644],{},"API 요청 결과",[25,646,647],{},"처리한 데이터 수",[25,649,650],{},"비정상 상황",[10,652,653],{},"등을 기록할 수 있다.",[10,655,656],{},"운영 중 문제가 발생했을 때 원인을 추적하기 훨씬 쉬워진다.",[73,658],{},[76,660,662],{"id":661},"_5-traceback","5. traceback",[10,664,665,666,669],{},"예외가 발생한 위치와 호출 흐름을 자세히 확인하고 싶다면 ",[52,667,668],{},"traceback","을 사용할 수 있다.",[44,671,673],{"className":334,"code":672,"language":336,"meta":50,"style":50},"import traceback\n\ntry:\n    result = 10 \u002F 0\n\nexcept Exception as error:\n    print(\"에러 발생:\", error)\n    traceback.print_exc()\n",[52,674,675,680,684,689,694,698,703,708],{"__ignoreMap":50},[340,676,677],{"class":342,"line":343},[340,678,679],{},"import traceback\n",[340,681,682],{"class":342,"line":349},[340,683,365],{"emptyLinePlaceholder":364},[340,685,686],{"class":342,"line":355},[340,687,688],{},"try:\n",[340,690,691],{"class":342,"line":361},[340,692,693],{},"    result = 10 \u002F 0\n",[340,695,696],{"class":342,"line":368},[340,697,365],{"emptyLinePlaceholder":364},[340,699,700],{"class":342,"line":373},[340,701,702],{},"except Exception as error:\n",[340,704,705],{"class":342,"line":501},[340,706,707],{},"    print(\"에러 발생:\", error)\n",[340,709,710],{"class":342,"line":507},[340,711,712],{},"    traceback.print_exc()\n",[10,714,715],{},"단순히",[44,717,720],{"className":718,"code":719,"language":49,"meta":50},[47],"division by zero\n",[52,721,719],{"__ignoreMap":50},[10,723,724],{},"만 보는 것이 아니라,",[44,726,729],{"className":727,"code":728,"language":49,"meta":50},[47],"어떤 함수\n→ 어떤 파일\n→ 몇 번째 줄\n→ 어떤 예외\n",[52,730,728],{"__ignoreMap":50},[10,732,733],{},"에서 문제가 발생했는지 확인할 수 있다.",[10,735,736,737,739,740,743],{},"다만 대부분의 경우 처리되지 않은 예외는 Python이 기본적으로 traceback을 출력하기 때문에, ",[52,738,668],{}," 모듈은 ",[14,741,742],{},"예외를 잡은 뒤에도 전체 호출 정보를 기록해야 할 때"," 특히 유용하다.",[73,745],{},[76,747,749],{"id":748},"_6-assert","6. assert",[10,751,752,755],{},[52,753,754],{},"assert","는 개발 중 “이 조건은 반드시 참이어야 한다”라는 가정을 검사할 때 사용한다.",[44,757,759],{"className":334,"code":758,"language":336,"meta":50,"style":50},"def divide(a, b):\n    assert b != 0, \"b는 0이 될 수 없습니다.\"\n    return a \u002F b\n",[52,760,761,766,771],{"__ignoreMap":50},[340,762,763],{"class":342,"line":343},[340,764,765],{},"def divide(a, b):\n",[340,767,768],{"class":342,"line":349},[340,769,770],{},"    assert b != 0, \"b는 0이 될 수 없습니다.\"\n",[340,772,773],{"class":342,"line":355},[340,774,775],{},"    return a \u002F b\n",[44,777,779],{"className":334,"code":778,"language":336,"meta":50,"style":50},"divide(10, 2)\n",[52,780,781],{"__ignoreMap":50},[340,782,783],{"class":342,"line":343},[340,784,778],{},[10,786,787],{},"는 정상 실행된다.",[10,789,790],{},"하지만",[44,792,794],{"className":334,"code":793,"language":336,"meta":50,"style":50},"divide(10, 0)\n",[52,795,796],{"__ignoreMap":50},[340,797,798],{"class":342,"line":343},[340,799,793],{},[10,801,802],{},"을 실행하면",[44,804,807],{"className":805,"code":806,"language":49,"meta":50},[47],"AssertionError: b는 0이 될 수 없습니다.\n",[52,808,806],{"__ignoreMap":50},[10,810,811],{},"가 발생한다.",[81,813,814],{"id":814},"주의",[10,816,817,819],{},[52,818,754],{},"를 사용자 입력 검증이나 중요한 비즈니스 로직에 사용하는 것은 적절하지 않다.",[10,821,822,823,825],{},"Python은 최적화 옵션으로 실행할 경우 ",[52,824,754],{},"가 제거될 수 있기 때문이다.",[10,827,828],{},"따라서 실제 입력 검증은 다음과 같이 작성하는 것이 좋다.",[44,830,832],{"className":334,"code":831,"language":336,"meta":50,"style":50},"def divide(a, b):\n    if b == 0:\n        raise ValueError(\"b는 0이 될 수 없습니다.\")\n\n    return a \u002F b\n",[52,833,834,838,843,848,852],{"__ignoreMap":50},[340,835,836],{"class":342,"line":343},[340,837,765],{},[340,839,840],{"class":342,"line":349},[340,841,842],{},"    if b == 0:\n",[340,844,845],{"class":342,"line":355},[340,846,847],{},"        raise ValueError(\"b는 0이 될 수 없습니다.\")\n",[340,849,850],{"class":342,"line":361},[340,851,365],{"emptyLinePlaceholder":364},[340,853,854],{"class":342,"line":368},[340,855,775],{},[10,857,858],{},"정리하면",[44,860,863],{"className":861,"code":862,"language":49,"meta":50},[47],"assert\n→ 개발자가 가정한 내부 상태 검사\n\nif + raise\n→ 실제 프로그램 입력과 예외 검증\n",[52,864,862],{"__ignoreMap":50},[10,866,867],{},"으로 생각하면 된다.",[73,869],{},[76,871,873],{"id":872},"_7-vs-code-debugger","7. VS Code Debugger",[10,875,876,877,879],{},"프로그램이 커지면 ",[52,878,445],{},"를 계속 추가하는 것보다 Debugger를 사용하는 것이 효율적이다.",[10,881,882,883,886],{},"VS Code에서는 코드 왼쪽에 ",[14,884,885],{},"Breakpoint","를 설정할 수 있다.",[44,888,890],{"className":334,"code":889,"language":336,"meta":50,"style":50},"def calculate_total(prices):\n    total = sum(prices)\n\n    return total\n\n\nprices = [1000, 2000, 3000]\nresult = calculate_total(prices)\n",[52,891,892,897,902,906,911,915,919,924],{"__ignoreMap":50},[340,893,894],{"class":342,"line":343},[340,895,896],{},"def calculate_total(prices):\n",[340,898,899],{"class":342,"line":349},[340,900,901],{},"    total = sum(prices)\n",[340,903,904],{"class":342,"line":355},[340,905,365],{"emptyLinePlaceholder":364},[340,907,908],{"class":342,"line":361},[340,909,910],{},"    return total\n",[340,912,913],{"class":342,"line":368},[340,914,365],{"emptyLinePlaceholder":364},[340,916,917],{"class":342,"line":373},[340,918,365],{"emptyLinePlaceholder":364},[340,920,921],{"class":342,"line":501},[340,922,923],{},"prices = [1000, 2000, 3000]\n",[340,925,926],{"class":342,"line":507},[340,927,928],{},"result = calculate_total(prices)\n",[10,930,931,934],{},[52,932,933],{},"total = sum(prices)"," 부분에 Breakpoint를 걸면 실행이 해당 지점에서 멈춘다.",[10,936,937],{},"이때 다음 값을 직접 확인할 수 있다.",[44,939,942],{"className":940,"code":941,"language":49,"meta":50},[47],"prices\ntotal\nresult\n",[52,943,941],{"__ignoreMap":50},[10,945,946],{},"뿐만 아니라",[22,948,949,952,955,958,961],{},[25,950,951],{},"변수 타입",[25,953,954],{},"객체 내부 값",[25,956,957],{},"리스트 내용",[25,959,960],{},"DataFrame",[25,962,963],{},"함수 호출 흐름",[10,965,966],{},"등도 확인할 수 있다.",[81,968,970],{"id":969},"데이터-분석에서-특히-유용한-이유","데이터 분석에서 특히 유용한 이유",[10,972,973],{},"예를 들어 Pandas DataFrame을 다룰 때마다",[44,975,977],{"className":334,"code":976,"language":336,"meta":50,"style":50},"print(df)\nprint(df.columns)\nprint(df.dtypes)\nprint(df.shape)\n",[52,978,979,984,989,994],{"__ignoreMap":50},[340,980,981],{"class":342,"line":343},[340,982,983],{},"print(df)\n",[340,985,986],{"class":342,"line":349},[340,987,988],{},"print(df.columns)\n",[340,990,991],{"class":342,"line":355},[340,992,993],{},"print(df.dtypes)\n",[340,995,996],{"class":342,"line":361},[340,997,998],{},"print(df.shape)\n",[10,1000,1001],{},"를 작성하는 것보다 Breakpoint에서 객체를 직접 확인하는 것이 훨씬 편하다.",[10,1003,1004],{},"즉,",[58,1006,1007],{},[10,1008,1009,1010,1012],{},"복잡한 프로그램일수록 ",[52,1011,445],{},"보다 Debugger가 효율적이다.",[73,1014],{},[76,1016,1018],{"id":1017},"_8-pytest","8. pytest",[10,1020,1021],{},"코드를 수정할 때 가장 무서운 문제는",[58,1023,1024],{},[10,1025,1026],{},"새로운 기능을 고쳤는데 기존 기능이 망가지는 것",[10,1028,123],{},[10,1030,1031],{},"이를 방지하기 위해 자동화 테스트를 작성한다.",[10,1033,1034,1035,123],{},"Python에서 가장 많이 사용하는 테스트 도구 중 하나가 ",[52,1036,1037],{},"pytest",[44,1039,1043],{"className":1040,"code":1041,"language":1042,"meta":50,"style":50},"language-bash shiki shiki-themes github-light github-dark","pip install pytest\n","bash",[52,1044,1045],{"__ignoreMap":50},[340,1046,1047,1051,1055],{"class":342,"line":343},[340,1048,1050],{"class":1049},"sScJk","pip",[340,1052,1054],{"class":1053},"sZZnC"," install",[340,1056,1057],{"class":1053}," pytest\n",[10,1059,1060],{},"예를 들어 다음 함수가 있다고 하자.",[44,1062,1064],{"className":334,"code":1063,"language":336,"meta":50,"style":50},"# src\u002Fcalculator.py\n\ndef add(a: int, b: int) -> int:\n    return a + b\n",[52,1065,1066,1071,1075,1080],{"__ignoreMap":50},[340,1067,1068],{"class":342,"line":343},[340,1069,1070],{},"# src\u002Fcalculator.py\n",[340,1072,1073],{"class":342,"line":349},[340,1074,365],{"emptyLinePlaceholder":364},[340,1076,1077],{"class":342,"line":355},[340,1078,1079],{},"def add(a: int, b: int) -> int:\n",[340,1081,1082],{"class":342,"line":361},[340,1083,358],{},[10,1085,1086],{},"테스트 코드는 다음과 같이 작성한다.",[44,1088,1090],{"className":334,"code":1089,"language":336,"meta":50,"style":50},"# tests\u002Ftest_calculator.py\n\nfrom src.calculator import add\n\n\ndef test_add():\n    assert add(1, 2) == 3\n",[52,1091,1092,1097,1101,1106,1110,1114,1119],{"__ignoreMap":50},[340,1093,1094],{"class":342,"line":343},[340,1095,1096],{},"# tests\u002Ftest_calculator.py\n",[340,1098,1099],{"class":342,"line":349},[340,1100,365],{"emptyLinePlaceholder":364},[340,1102,1103],{"class":342,"line":355},[340,1104,1105],{},"from src.calculator import add\n",[340,1107,1108],{"class":342,"line":361},[340,1109,365],{"emptyLinePlaceholder":364},[340,1111,1112],{"class":342,"line":368},[340,1113,365],{"emptyLinePlaceholder":364},[340,1115,1116],{"class":342,"line":373},[340,1117,1118],{},"def test_add():\n",[340,1120,1121],{"class":342,"line":501},[340,1122,1123],{},"    assert add(1, 2) == 3\n",[10,1125,1126],{},"실행:",[44,1128,1130],{"className":1040,"code":1129,"language":1042,"meta":50,"style":50},"pytest\n",[52,1131,1132],{"__ignoreMap":50},[340,1133,1134],{"class":342,"line":343},[340,1135,1129],{"class":1049},[10,1137,1138],{},"pytest가 테스트 파일을 자동으로 찾아 실행한다.",[73,1140],{},[76,1142,1144],{"id":1143},"_9-pytest-테스트-탐색-규칙","9. pytest 테스트 탐색 규칙",[10,1146,1147],{},"pytest는 기본적으로 다음 이름을 테스트로 인식한다.",[626,1149,1150],{"id":1150},"파일",[44,1152,1155],{"className":1153,"code":1154,"language":49,"meta":50},[47],"test_*.py\n*_test.py\n",[52,1156,1154],{"__ignoreMap":50},[10,1158,1159],{},"예:",[44,1161,1164],{"className":1162,"code":1163,"language":49,"meta":50},[47],"test_user.py\nuser_test.py\n",[52,1165,1163],{"__ignoreMap":50},[626,1167,1168],{"id":1168},"함수",[44,1170,1172],{"className":334,"code":1171,"language":336,"meta":50,"style":50},"def test_login():\n    ...\n",[52,1173,1174,1179],{"__ignoreMap":50},[340,1175,1176],{"class":342,"line":343},[340,1177,1178],{},"def test_login():\n",[340,1180,1181],{"class":342,"line":349},[340,1182,1183],{},"    ...\n",[10,1185,1186,1187,1190],{},"즉 함수 이름이 ",[52,1188,1189],{},"test_","로 시작해야 한다.",[10,1192,1193],{},"특정 테스트만 실행할 수도 있다.",[44,1195,1197],{"className":1040,"code":1196,"language":1042,"meta":50,"style":50},"pytest tests\u002F\n",[52,1198,1199],{"__ignoreMap":50},[340,1200,1201,1203],{"class":342,"line":343},[340,1202,1037],{"class":1049},[340,1204,1205],{"class":1053}," tests\u002F\n",[10,1207,1208],{},"특정 파일:",[44,1210,1212],{"className":1040,"code":1211,"language":1042,"meta":50,"style":50},"pytest tests\u002Ftest_calculator.py\n",[52,1213,1214],{"__ignoreMap":50},[340,1215,1216,1218],{"class":342,"line":343},[340,1217,1037],{"class":1049},[340,1219,1220],{"class":1053}," tests\u002Ftest_calculator.py\n",[10,1222,1223],{},"특정 함수:",[44,1225,1227],{"className":1040,"code":1226,"language":1042,"meta":50,"style":50},"pytest tests\u002Ftest_calculator.py::test_add\n",[52,1228,1229],{"__ignoreMap":50},[340,1230,1231,1233],{"class":342,"line":343},[340,1232,1037],{"class":1049},[340,1234,1235],{"class":1053}," tests\u002Ftest_calculator.py::test_add\n",[10,1237,1238],{},"상세 결과:",[44,1240,1242],{"className":1040,"code":1241,"language":1042,"meta":50,"style":50},"pytest -v\n",[52,1243,1244],{"__ignoreMap":50},[340,1245,1246,1248],{"class":342,"line":343},[340,1247,1037],{"class":1049},[340,1249,1251],{"class":1250},"sj4cs"," -v\n",[10,1253,1254],{},"첫 번째 실패에서 중단:",[44,1256,1258],{"className":1040,"code":1257,"language":1042,"meta":50,"style":50},"pytest -x\n",[52,1259,1260],{"__ignoreMap":50},[340,1261,1262,1264],{"class":342,"line":343},[340,1263,1037],{"class":1049},[340,1265,1266],{"class":1250}," -x\n",[73,1268],{},[76,1270,1272],{"id":1271},"_10-pytest를-사용하는-효과","10. pytest를 사용하는 효과",[10,1274,1275],{},"테스트를 작성해두면 코드 수정 후",[44,1277,1278],{"className":1040,"code":1129,"language":1042,"meta":50,"style":50},[52,1279,1280],{"__ignoreMap":50},[340,1281,1282],{"class":342,"line":343},[340,1283,1129],{"class":1049},[10,1285,1286],{},"한 번으로 기존 기능이 정상인지 확인할 수 있다.",[10,1288,1289],{},"예를 들어 다음 기능이 있다고 하자.",[44,1291,1294],{"className":1292,"code":1293,"language":49,"meta":50},[47],"데이터 로딩\n데이터 검증\n데이터 변환\nCSV 저장\nAPI 호출\n",[52,1295,1293],{"__ignoreMap":50},[10,1297,1298],{},"각 기능에 테스트를 만들어 두면 프로그램을 수정할 때마다 전체 기능을 사람이 직접 실행해볼 필요가 줄어든다.",[10,1300,1004],{},[44,1302,1305],{"className":1303,"code":1304,"language":49,"meta":50},[47],"코드 수정\n   ↓\npytest 실행\n   ↓\n기존 기능 정상 여부 확인\n",[52,1306,1304],{"__ignoreMap":50},[10,1308,1309],{},"이 가능해진다.",[10,1311,1312],{},"이것이 자동화 테스트의 가장 큰 장점이다.",[73,1314],{},[76,1316,1318],{"id":1317},"_11-pytest-cov","11. pytest-cov",[10,1320,1321],{},"테스트가 존재한다고 해서 모든 코드가 테스트되고 있다는 의미는 아니다.",[10,1323,1324,1325,1328],{},"어떤 코드가 실제 테스트를 거쳤는지 확인하기 위해 ",[14,1326,1327],{},"Coverage","를 사용한다.",[44,1330,1332],{"className":1040,"code":1331,"language":1042,"meta":50,"style":50},"pip install pytest-cov\n",[52,1333,1334],{"__ignoreMap":50},[340,1335,1336,1338,1340],{"class":342,"line":343},[340,1337,1050],{"class":1049},[340,1339,1054],{"class":1053},[340,1341,1342],{"class":1053}," pytest-cov\n",[10,1344,1126],{},[44,1346,1348],{"className":1040,"code":1347,"language":1042,"meta":50,"style":50},"pytest tests\u002F --cov=src --cov-report=term-missing\n",[52,1349,1350],{"__ignoreMap":50},[340,1351,1352,1354,1357,1360],{"class":342,"line":343},[340,1353,1037],{"class":1049},[340,1355,1356],{"class":1053}," tests\u002F",[340,1358,1359],{"class":1250}," --cov=src",[340,1361,1362],{"class":1250}," --cov-report=term-missing\n",[10,1364,1159],{},[44,1366,1369],{"className":1367,"code":1368,"language":49,"meta":50},[47],"Name                 Stmts   Miss   Cover\n-----------------------------------------\nsrc\u002Fclean.py            25      3     88%\nsrc\u002Ffeatures.py         38      0    100%\n-----------------------------------------\nTOTAL                   63      3     95%\n",[52,1370,1368],{"__ignoreMap":50},[10,1372,1373,1376],{},[52,1374,1375],{},"Miss","는 테스트되지 않은 코드 수를 의미한다.",[10,1378,1379],{},"따라서 Coverage는 단순히 숫자를 높이는 도구라기보다",[58,1381,1382],{},[10,1383,1384],{},"어떤 코드 경로에 테스트가 부족한지 찾는 도구",[10,1386,1387],{},"로 사용하는 것이 좋다.",[73,1389],{},[76,1391,1393],{"id":1392},"_12-ruff-이걸를-잘-기억하자","12. Ruff (이걸를 잘 기억하자!)",[10,1395,1396],{},"Python 프로젝트에서는 코드 스타일과 잠재적인 오류를 자동으로 검사하는 Linter를 사용한다.",[10,1398,1399,1400,1403],{},"최근 Python 프로젝트에서는 ",[14,1401,1402],{},"Ruff","가 많이 사용된다.",[44,1405,1407],{"className":1040,"code":1406,"language":1042,"meta":50,"style":50},"pip install ruff\n",[52,1408,1409],{"__ignoreMap":50},[340,1410,1411,1413,1415],{"class":342,"line":343},[340,1412,1050],{"class":1049},[340,1414,1054],{"class":1053},[340,1416,1417],{"class":1053}," ruff\n",[10,1419,1420],{},"검사:",[44,1422,1424],{"className":1040,"code":1423,"language":1042,"meta":50,"style":50},"ruff check .\n",[52,1425,1426],{"__ignoreMap":50},[340,1427,1428,1431,1434],{"class":342,"line":343},[340,1429,1430],{"class":1049},"ruff",[340,1432,1433],{"class":1053}," check",[340,1435,1436],{"class":1053}," .\n",[10,1438,1439],{},"자동 수정:",[44,1441,1443],{"className":1040,"code":1442,"language":1042,"meta":50,"style":50},"ruff check . --fix\n",[52,1444,1445],{"__ignoreMap":50},[340,1446,1447,1449,1451,1454],{"class":342,"line":343},[340,1448,1430],{"class":1049},[340,1450,1433],{"class":1053},[340,1452,1453],{"class":1053}," .",[340,1455,1456],{"class":1250}," --fix\n",[10,1458,1459],{},"포매팅:",[44,1461,1463],{"className":1040,"code":1462,"language":1042,"meta":50,"style":50},"ruff format .\n",[52,1464,1465],{"__ignoreMap":50},[340,1466,1467,1469,1472],{"class":342,"line":343},[340,1468,1430],{"class":1049},[340,1470,1471],{"class":1053}," format",[340,1473,1436],{"class":1053},[10,1475,1476],{},"Ruff는 다음과 같은 작업을 하나의 도구에서 처리할 수 있다.",[44,1478,1481],{"className":1479,"code":1480,"language":49,"meta":50},[47],"Lint\nImport 정리\n일부 오류 자동 수정\nPython 코드 Formatting\n",[52,1482,1480],{"__ignoreMap":50},[10,1484,1485],{},"예를 들어 다음 코드가 있다고 하자.",[44,1487,1489],{"className":334,"code":1488,"language":336,"meta":50,"style":50},"import os\nimport sys\n\nx=10\n",[52,1490,1491,1496,1501,1505],{"__ignoreMap":50},[340,1492,1493],{"class":342,"line":343},[340,1494,1495],{},"import os\n",[340,1497,1498],{"class":342,"line":349},[340,1499,1500],{},"import sys\n",[340,1502,1503],{"class":342,"line":355},[340,1504,365],{"emptyLinePlaceholder":364},[340,1506,1507],{"class":342,"line":361},[340,1508,1509],{},"x=10\n",[10,1511,1512,1515,1516,1519],{},[52,1513,1514],{},"os",", ",[52,1517,1518],{},"sys","를 사용하지 않는다면 Ruff가 불필요한 import를 탐지할 수 있다.",[10,1521,1522],{},"Formatting 역시",[44,1524,1526],{"className":334,"code":1525,"language":336,"meta":50,"style":50},"def add(a,b):\n return a+b\n",[52,1527,1528,1533],{"__ignoreMap":50},[340,1529,1530],{"class":342,"line":343},[340,1531,1532],{},"def add(a,b):\n",[340,1534,1535],{"class":342,"line":349},[340,1536,1537],{}," return a+b\n",[10,1539,1540],{},"같은 코드를",[44,1542,1544],{"className":334,"code":1543,"language":336,"meta":50,"style":50},"def add(a, b):\n    return a + b\n",[52,1545,1546,1551],{"__ignoreMap":50},[340,1547,1548],{"class":342,"line":343},[340,1549,1550],{},"def add(a, b):\n",[340,1552,1553],{"class":342,"line":349},[340,1554,358],{},[10,1556,1557],{},"처럼 일관된 형태로 정리할 수 있다.",[81,1559,628],{"id":1560},"효과-1",[10,1562,1563],{},"사람이 직접",[44,1565,1568],{"className":1566,"code":1567,"language":49,"meta":50},[47],"공백이 이상하다\nimport 순서가 이상하다\n사용하지 않는 변수가 있다\n스타일이 다르다\n",[52,1569,1567],{"__ignoreMap":50},[10,1571,1572],{},"같은 문제를 찾을 필요가 줄어든다.",[73,1574],{},[76,1576,1578],{"id":1577},"_13-black은-언제-사용하는가","13. Black은 언제 사용하는가?",[10,1580,1581],{},"Black은 대표적인 Python 코드 Formatter다.",[44,1583,1585],{"className":1040,"code":1584,"language":1042,"meta":50,"style":50},"pip install black\n",[52,1586,1587],{"__ignoreMap":50},[340,1588,1589,1591,1593],{"class":342,"line":343},[340,1590,1050],{"class":1049},[340,1592,1054],{"class":1053},[340,1594,1595],{"class":1053}," black\n",[10,1597,1126],{},[44,1599,1601],{"className":1040,"code":1600,"language":1042,"meta":50,"style":50},"black .\n",[52,1602,1603],{"__ignoreMap":50},[340,1604,1605,1608],{"class":342,"line":343},[340,1606,1607],{"class":1049},"black",[340,1609,1436],{"class":1053},[10,1611,1612],{},"검사만 할 경우:",[44,1614,1616],{"className":1040,"code":1615,"language":1042,"meta":50,"style":50},"black --check .\n",[52,1617,1618],{"__ignoreMap":50},[340,1619,1620,1622,1625],{"class":342,"line":343},[340,1621,1607],{"class":1049},[340,1623,1624],{"class":1250}," --check",[340,1626,1436],{"class":1053},[10,1628,1629,1630,123],{},"Black의 가장 큰 특징은 설정 자유도를 줄이는 대신 ",[14,1631,1632],{},"누가 실행해도 거의 동일한 형태의 코드가 나온다는 것",[10,1634,1635],{},"다만 Ruff 자체에도 Formatter가 있기 때문에 새로운 프로젝트라면",[44,1637,1640],{"className":1638,"code":1639,"language":49,"meta":50},[47],"Ruff lint\n+\nRuff format\n",[52,1641,1639],{"__ignoreMap":50},[10,1643,1644],{},"구성만 사용하는 것도 가능하다.",[10,1646,1647],{},"기존 프로젝트에서 이미 Black을 사용하고 있다면 굳이 변경할 필요는 없다.",[73,1649],{},[76,1651,1653],{"id":1652},"_14-pre-commit","14. pre-commit",[10,1655,1656,1657,669],{},"문제가 있는 코드가 Git에 커밋되기 전에 자동으로 검사하고 싶다면 ",[52,1658,1659],{},"pre-commit",[44,1661,1663],{"className":1040,"code":1662,"language":1042,"meta":50,"style":50},"pip install pre-commit\n",[52,1664,1665],{"__ignoreMap":50},[340,1666,1667,1669,1671],{"class":342,"line":343},[340,1668,1050],{"class":1049},[340,1670,1054],{"class":1053},[340,1672,1673],{"class":1053}," pre-commit\n",[10,1675,1676],{},"프로젝트 루트에",[44,1678,1681],{"className":1679,"code":1680,"language":49,"meta":50},[47],".pre-commit-config.yaml\n",[52,1682,1680],{"__ignoreMap":50},[10,1684,1685],{},"파일을 작성한다.",[10,1687,1159],{},[44,1689,1693],{"className":1690,"code":1691,"language":1692,"meta":50,"style":50},"language-yaml shiki shiki-themes github-light github-dark","repos:\n  - repo: https:\u002F\u002Fgithub.com\u002Fastral-sh\u002Fruff-pre-commit\n    rev: v0.12.0\n    hooks:\n      - id: ruff-check\n        args: [--fix]\n\n      - id: ruff-format\n\n  - repo: https:\u002F\u002Fgithub.com\u002Fpre-commit\u002Fpre-commit-hooks\n    rev: v5.0.0\n    hooks:\n      - id: check-yaml\n      - id: check-merge-conflict\n      - id: check-added-large-files\n      - id: debug-statements\n","yaml",[52,1694,1695,1705,1719,1729,1736,1749,1763,1767,1778,1782,1794,1804,1811,1823,1835,1847],{"__ignoreMap":50},[340,1696,1697,1701],{"class":342,"line":343},[340,1698,1700],{"class":1699},"s9eBZ","repos",[340,1702,1704],{"class":1703},"sVt8B",":\n",[340,1706,1707,1710,1713,1716],{"class":342,"line":349},[340,1708,1709],{"class":1703},"  - ",[340,1711,1712],{"class":1699},"repo",[340,1714,1715],{"class":1703},": ",[340,1717,1718],{"class":1053},"https:\u002F\u002Fgithub.com\u002Fastral-sh\u002Fruff-pre-commit\n",[340,1720,1721,1724,1726],{"class":342,"line":355},[340,1722,1723],{"class":1699},"    rev",[340,1725,1715],{"class":1703},[340,1727,1728],{"class":1053},"v0.12.0\n",[340,1730,1731,1734],{"class":342,"line":361},[340,1732,1733],{"class":1699},"    hooks",[340,1735,1704],{"class":1703},[340,1737,1738,1741,1744,1746],{"class":342,"line":368},[340,1739,1740],{"class":1703},"      - ",[340,1742,1743],{"class":1699},"id",[340,1745,1715],{"class":1703},[340,1747,1748],{"class":1053},"ruff-check\n",[340,1750,1751,1754,1757,1760],{"class":342,"line":373},[340,1752,1753],{"class":1699},"        args",[340,1755,1756],{"class":1703},": [",[340,1758,1759],{"class":1053},"--fix",[340,1761,1762],{"class":1703},"]\n",[340,1764,1765],{"class":342,"line":501},[340,1766,365],{"emptyLinePlaceholder":364},[340,1768,1769,1771,1773,1775],{"class":342,"line":507},[340,1770,1740],{"class":1703},[340,1772,1743],{"class":1699},[340,1774,1715],{"class":1703},[340,1776,1777],{"class":1053},"ruff-format\n",[340,1779,1780],{"class":342,"line":513},[340,1781,365],{"emptyLinePlaceholder":364},[340,1783,1785,1787,1789,1791],{"class":342,"line":1784},10,[340,1786,1709],{"class":1703},[340,1788,1712],{"class":1699},[340,1790,1715],{"class":1703},[340,1792,1793],{"class":1053},"https:\u002F\u002Fgithub.com\u002Fpre-commit\u002Fpre-commit-hooks\n",[340,1795,1797,1799,1801],{"class":342,"line":1796},11,[340,1798,1723],{"class":1699},[340,1800,1715],{"class":1703},[340,1802,1803],{"class":1053},"v5.0.0\n",[340,1805,1807,1809],{"class":342,"line":1806},12,[340,1808,1733],{"class":1699},[340,1810,1704],{"class":1703},[340,1812,1814,1816,1818,1820],{"class":342,"line":1813},13,[340,1815,1740],{"class":1703},[340,1817,1743],{"class":1699},[340,1819,1715],{"class":1703},[340,1821,1822],{"class":1053},"check-yaml\n",[340,1824,1826,1828,1830,1832],{"class":342,"line":1825},14,[340,1827,1740],{"class":1703},[340,1829,1743],{"class":1699},[340,1831,1715],{"class":1703},[340,1833,1834],{"class":1053},"check-merge-conflict\n",[340,1836,1838,1840,1842,1844],{"class":342,"line":1837},15,[340,1839,1740],{"class":1703},[340,1841,1743],{"class":1699},[340,1843,1715],{"class":1703},[340,1845,1846],{"class":1053},"check-added-large-files\n",[340,1848,1850,1852,1854,1856],{"class":342,"line":1849},16,[340,1851,1740],{"class":1703},[340,1853,1743],{"class":1699},[340,1855,1715],{"class":1703},[340,1857,1858],{"class":1053},"debug-statements\n",[10,1860,1861],{},"설치:",[44,1863,1865],{"className":1040,"code":1864,"language":1042,"meta":50,"style":50},"pre-commit install\n",[52,1866,1867],{"__ignoreMap":50},[340,1868,1869,1871],{"class":342,"line":343},[340,1870,1659],{"class":1049},[340,1872,1873],{"class":1053}," install\n",[10,1875,1876],{},"전체 파일 검사:",[44,1878,1880],{"className":1040,"code":1879,"language":1042,"meta":50,"style":50},"pre-commit run --all-files\n",[52,1881,1882],{"__ignoreMap":50},[340,1883,1884,1886,1889],{"class":342,"line":343},[340,1885,1659],{"class":1049},[340,1887,1888],{"class":1053}," run",[340,1890,1891],{"class":1250}," --all-files\n",[10,1893,1894],{},"이후",[44,1896,1898],{"className":1040,"code":1897,"language":1042,"meta":50,"style":50},"git commit\n",[52,1899,1900],{"__ignoreMap":50},[340,1901,1902,1905],{"class":342,"line":343},[340,1903,1904],{"class":1049},"git",[340,1906,1907],{"class":1053}," commit\n",[10,1909,1910],{},"을 실행하면 커밋 전에 자동 검사가 실행된다.",[10,1912,1913],{},"검사를 통과하지 못하면 커밋이 중단된다.",[10,1915,1916,1917,1920,1921,1515,1924,1927,1928,1930],{},"참고로 ",[52,1918,1919],{},"debug-statements","는 ",[52,1922,1923],{},"breakpoint()",[52,1925,1926],{},"pdb"," 같은 디버거 코드가 남아 있는지 검사하는 Hook이며 일반적인 모든 ",[52,1929,445],{},"를 차단하는 기능은 아니다.",[73,1932],{},[76,1934,1936],{"id":1935},"_15-github-actions-ci","15. GitHub Actions CI",[10,1938,1939,1941],{},[52,1940,1659],{},"은 개발자의 컴퓨터에서 실행된다.",[10,1943,1944],{},"하지만 개발자가 검사를 실행하지 않거나 설정이 다른 경우도 있다.",[10,1946,1947],{},"따라서 GitHub에서도 다시 테스트를 실행하는 것이 좋다.",[10,1949,1950],{},"이것이 **CI(Continuous Integration)**다.",[44,1952,1955],{"className":1953,"code":1954,"language":49,"meta":50},[47],"Developer\n   ↓\ngit push\n   ↓\nGitHub\n   ↓\nRuff\n   ↓\npytest\n   ↓\n통과 \u002F 실패\n",[52,1956,1954],{"__ignoreMap":50},[10,1958,1959],{},"간단한 GitHub Actions 예시는 다음과 같다.",[44,1961,1963],{"className":1690,"code":1962,"language":1692,"meta":50,"style":50},"name: Python CI\n\non:\n  push:\n  pull_request:\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions\u002Fcheckout@v4\n\n      - name: Set up Python\n        uses: actions\u002Fsetup-python@v5\n        with:\n          python-version: \"3.11\"\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n\n      - name: Ruff\n        run: |\n          ruff check .\n          ruff format --check .\n\n      - name: Pytest\n        run: pytest\n",[52,1964,1965,1975,1979,1986,1993,2000,2004,2011,2018,2028,2032,2039,2051,2055,2066,2076,2083,2094,2099,2111,2123,2129,2135,2140,2152,2161,2167,2173,2178,2190],{"__ignoreMap":50},[340,1966,1967,1970,1972],{"class":342,"line":343},[340,1968,1969],{"class":1699},"name",[340,1971,1715],{"class":1703},[340,1973,1974],{"class":1053},"Python CI\n",[340,1976,1977],{"class":342,"line":349},[340,1978,365],{"emptyLinePlaceholder":364},[340,1980,1981,1984],{"class":342,"line":355},[340,1982,1983],{"class":1250},"on",[340,1985,1704],{"class":1703},[340,1987,1988,1991],{"class":342,"line":361},[340,1989,1990],{"class":1699},"  push",[340,1992,1704],{"class":1703},[340,1994,1995,1998],{"class":342,"line":368},[340,1996,1997],{"class":1699},"  pull_request",[340,1999,1704],{"class":1703},[340,2001,2002],{"class":342,"line":373},[340,2003,365],{"emptyLinePlaceholder":364},[340,2005,2006,2009],{"class":342,"line":501},[340,2007,2008],{"class":1699},"jobs",[340,2010,1704],{"class":1703},[340,2012,2013,2016],{"class":342,"line":507},[340,2014,2015],{"class":1699},"  test",[340,2017,1704],{"class":1703},[340,2019,2020,2023,2025],{"class":342,"line":513},[340,2021,2022],{"class":1699},"    runs-on",[340,2024,1715],{"class":1703},[340,2026,2027],{"class":1053},"ubuntu-latest\n",[340,2029,2030],{"class":342,"line":1784},[340,2031,365],{"emptyLinePlaceholder":364},[340,2033,2034,2037],{"class":342,"line":1796},[340,2035,2036],{"class":1699},"    steps",[340,2038,1704],{"class":1703},[340,2040,2041,2043,2046,2048],{"class":342,"line":1806},[340,2042,1740],{"class":1703},[340,2044,2045],{"class":1699},"uses",[340,2047,1715],{"class":1703},[340,2049,2050],{"class":1053},"actions\u002Fcheckout@v4\n",[340,2052,2053],{"class":342,"line":1813},[340,2054,365],{"emptyLinePlaceholder":364},[340,2056,2057,2059,2061,2063],{"class":342,"line":1825},[340,2058,1740],{"class":1703},[340,2060,1969],{"class":1699},[340,2062,1715],{"class":1703},[340,2064,2065],{"class":1053},"Set up Python\n",[340,2067,2068,2071,2073],{"class":342,"line":1837},[340,2069,2070],{"class":1699},"        uses",[340,2072,1715],{"class":1703},[340,2074,2075],{"class":1053},"actions\u002Fsetup-python@v5\n",[340,2077,2078,2081],{"class":342,"line":1849},[340,2079,2080],{"class":1699},"        with",[340,2082,1704],{"class":1703},[340,2084,2086,2089,2091],{"class":342,"line":2085},17,[340,2087,2088],{"class":1699},"          python-version",[340,2090,1715],{"class":1703},[340,2092,2093],{"class":1053},"\"3.11\"\n",[340,2095,2097],{"class":342,"line":2096},18,[340,2098,365],{"emptyLinePlaceholder":364},[340,2100,2102,2104,2106,2108],{"class":342,"line":2101},19,[340,2103,1740],{"class":1703},[340,2105,1969],{"class":1699},[340,2107,1715],{"class":1703},[340,2109,2110],{"class":1053},"Install dependencies\n",[340,2112,2114,2117,2119],{"class":342,"line":2113},20,[340,2115,2116],{"class":1699},"        run",[340,2118,1715],{"class":1703},[340,2120,2122],{"class":2121},"szBVR","|\n",[340,2124,2126],{"class":342,"line":2125},21,[340,2127,2128],{"class":1053},"          python -m pip install --upgrade pip\n",[340,2130,2132],{"class":342,"line":2131},22,[340,2133,2134],{"class":1053},"          pip install -r requirements.txt\n",[340,2136,2138],{"class":342,"line":2137},23,[340,2139,365],{"emptyLinePlaceholder":364},[340,2141,2143,2145,2147,2149],{"class":342,"line":2142},24,[340,2144,1740],{"class":1703},[340,2146,1969],{"class":1699},[340,2148,1715],{"class":1703},[340,2150,2151],{"class":1053},"Ruff\n",[340,2153,2155,2157,2159],{"class":342,"line":2154},25,[340,2156,2116],{"class":1699},[340,2158,1715],{"class":1703},[340,2160,2122],{"class":2121},[340,2162,2164],{"class":342,"line":2163},26,[340,2165,2166],{"class":1053},"          ruff check .\n",[340,2168,2170],{"class":342,"line":2169},27,[340,2171,2172],{"class":1053},"          ruff format --check .\n",[340,2174,2176],{"class":342,"line":2175},28,[340,2177,365],{"emptyLinePlaceholder":364},[340,2179,2181,2183,2185,2187],{"class":342,"line":2180},29,[340,2182,1740],{"class":1703},[340,2184,1969],{"class":1699},[340,2186,1715],{"class":1703},[340,2188,2189],{"class":1053},"Pytest\n",[340,2191,2193,2195,2197],{"class":342,"line":2192},30,[340,2194,2116],{"class":1699},[340,2196,1715],{"class":1703},[340,2198,1129],{"class":1053},[10,2200,2201],{},"GitHub에 코드를 Push하거나 Pull Request를 만들 때마다 자동으로",[44,2203,2206],{"className":2204,"code":2205,"language":49,"meta":50},[47],"Lint\nFormatting\nTest\n",[52,2207,2205],{"__ignoreMap":50},[10,2209,2210],{},"를 검사할 수 있다.",[73,2212],{},[76,2214,2216],{"id":2215},"_16-추천-프로젝트-구조","16. 추천 프로젝트 구조",[10,2218,2219],{},"작은 프로젝트라도 코드를 다음처럼 분리해두면 관리하기 편하다.",[44,2221,2224],{"className":2222,"code":2223,"language":49,"meta":50},[47],"my_project\u002F\n│\n├── src\u002F\n│   └── mymodule\u002F\n│       ├── __init__.py\n│       └── core.py\n│\n├── tests\u002F\n│   └── test_core.py\n│\n├── .github\u002F\n│   └── workflows\u002F\n│       └── ci.yml\n│\n├── .pre-commit-config.yaml\n├── pyproject.toml\n├── requirements.txt\n└── README.md\n",[52,2225,2223],{"__ignoreMap":50},[10,2227,2228],{},"역할을 나누면 다음과 같다.",[141,2230,2231,2241],{},[144,2232,2233],{},[147,2234,2235,2238],{},[150,2236,2237],{},"경로",[150,2239,2240],{},"역할",[160,2242,2243,2253,2263,2273,2283,2293],{},[147,2244,2245,2250],{},[165,2246,2247],{},[52,2248,2249],{},"src\u002F",[165,2251,2252],{},"실제 프로그램 코드",[147,2254,2255,2260],{},[165,2256,2257],{},[52,2258,2259],{},"tests\u002F",[165,2261,2262],{},"테스트 코드",[147,2264,2265,2270],{},[165,2266,2267],{},[52,2268,2269],{},".github\u002Fworkflows\u002F",[165,2271,2272],{},"GitHub Actions",[147,2274,2275,2280],{},[165,2276,2277],{},[52,2278,2279],{},"pyproject.toml",[165,2281,2282],{},"Python 도구 설정",[147,2284,2285,2290],{},[165,2286,2287],{},[52,2288,2289],{},".pre-commit-config.yaml",[165,2291,2292],{},"커밋 전 자동 검사",[147,2294,2295,2300],{},[165,2296,2297],{},[52,2298,2299],{},"requirements.txt",[165,2301,2302],{},"의존성 관리",[73,2304],{},[76,2306,2308],{"id":2307},"_17-실제-개발-흐름","17. 실제 개발 흐름",[10,2310,2311],{},"코드 품질 도구를 각각 따로 외우기보다는 하나의 개발 과정으로 이해하는 것이 좋다.",[44,2313,2316],{"className":2314,"code":2315,"language":49,"meta":50},[47],"1. 코드 작성\n        ↓\n2. VS Code Debugger로 문제 확인\n        ↓\n3. pytest로 기능 검증\n        ↓\n4. Ruff로 코드 검사 및 Formatting\n        ↓\n5. pre-commit 자동 검사\n        ↓\n6. git commit\n        ↓\n7. git push\n        ↓\n8. GitHub Actions에서 다시 검증\n",[52,2317,2315],{"__ignoreMap":50},[10,2319,2320],{},"각 도구의 역할도 명확하다.",[141,2322,2323,2332],{},[144,2324,2325],{},[147,2326,2327,2330],{},[150,2328,2329],{},"도구",[150,2331,2240],{},[160,2333,2334,2342,2349,2356,2364,2371,2378],{},[147,2335,2336,2339],{},[165,2337,2338],{},"Debugger",[165,2340,2341],{},"실행 중 문제 원인 분석",[147,2343,2344,2346],{},[165,2345,464],{},[165,2347,2348],{},"프로그램 실행 기록",[147,2350,2351,2353],{},[165,2352,1037],{},[165,2354,2355],{},"기능이 정상인지 검증",[147,2357,2358,2361],{},[165,2359,2360],{},"pytest-cov",[165,2362,2363],{},"테스트하지 않은 코드 확인",[147,2365,2366,2368],{},[165,2367,1402],{},[165,2369,2370],{},"코드 스타일 및 오류 검사",[147,2372,2373,2375],{},[165,2374,1659],{},[165,2376,2377],{},"잘못된 코드의 커밋 방지",[147,2379,2380,2382],{},[165,2381,2272],{},[165,2383,2384],{},"서버에서 최종 자동 검증",[73,2386],{},[76,2388,2389],{"id":2389},"정리",[10,2391,2392],{},"처음 Python을 배울 때는",[44,2394,2396],{"className":334,"code":2395,"language":336,"meta":50,"style":50},"print()\n",[52,2397,2398],{"__ignoreMap":50},[340,2399,2400],{"class":342,"line":343},[340,2401,2395],{},[10,2403,2404],{},"만으로도 대부분의 문제를 해결할 수 있다.",[10,2406,2407],{},"하지만 프로젝트 규모가 커지고 다른 사람과 코드를 공유하기 시작하면 코드 품질을 사람이 직접 관리하기 어렵다.",[10,2409,2410],{},"따라서 점차 다음 구조로 발전시키는 것이 좋다.",[44,2412,2415],{"className":2413,"code":2414,"language":49,"meta":50},[47],"print\n ↓\nDebugger \u002F logging\n ↓\npytest\n ↓\nRuff\n ↓\npre-commit\n ↓\nCI\n",[52,2416,2414],{"__ignoreMap":50},[10,2418,2419],{},"특히 데이터 분석이나 머신러닝 코드도 예외가 아니다.",[10,2421,2422],{},"좋은 코드의 기준은 단순히",[58,2424,2425],{},[10,2426,2427],{},"지금 실행되는가?",[10,2429,2430],{},"에서 끝나는 것이 아니라,",[58,2432,2433],{},[10,2434,2435],{},"나중에 수정해도 정상적으로 동작하는지 검증할 수 있는가?",[10,2437,2438],{},"까지 포함한다.",[10,2440,2441],{},"결국 테스트와 코드 품질 자동화의 목적은 하나",[58,2443,2444],{},[10,2445,2446],{},[14,2447,2448],{},"코드를 변경해도 신뢰할 수 있는 개발 환경을 만드는 것!.",[10,2450,2451],{},[69,2452,2453],{},"Devops, MLops애서 더 소개해야지",[2455,2456,2457],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":50,"searchDepth":349,"depth":349,"links":2459},[2460,2463,2464,2468,2471,2472,2475,2478,2479,2480,2481,2482,2485,2486,2487,2488,2489,2490],{"id":78,"depth":349,"text":79,"children":2461},[2462],{"id":83,"depth":355,"text":84},{"id":133,"depth":349,"text":134},{"id":322,"depth":349,"text":323,"children":2465},[2466,2467],{"id":388,"depth":355,"text":389},{"id":398,"depth":355,"text":398},{"id":454,"depth":349,"text":455,"children":2469},[2470],{"id":528,"depth":355,"text":389},{"id":661,"depth":349,"text":662},{"id":748,"depth":349,"text":749,"children":2473},[2474],{"id":814,"depth":355,"text":814},{"id":872,"depth":349,"text":873,"children":2476},[2477],{"id":969,"depth":355,"text":970},{"id":1017,"depth":349,"text":1018},{"id":1143,"depth":349,"text":1144},{"id":1271,"depth":349,"text":1272},{"id":1317,"depth":349,"text":1318},{"id":1392,"depth":349,"text":1393,"children":2483},[2484],{"id":1560,"depth":355,"text":628},{"id":1577,"depth":349,"text":1578},{"id":1652,"depth":349,"text":1653},{"id":1935,"depth":349,"text":1936},{"id":2215,"depth":349,"text":2216},{"id":2307,"depth":349,"text":2308},{"id":2389,"depth":349,"text":2389},[2492],"Python","2026-08-07 09:00:00 +0900","Python 디버깅, 로깅, pytest, Ruff, pre-commit과 GitHub Actions를 이용한 품질 관리 흐름을 정리한다.","md",null,"\u002Fpython\u002F2026\u002F08\u002F07\u002Fpython_2\u002F",{"layout":2499},"post","\u002Fposts\u002Fpython\u002F2026-08-07-python_2",{"title":5,"description":2494},"posts\u002FPython\u002F2026-08-07-python_2",[2492,2504,2505,1402,2506,2507],"Debugging","Pytest","Pre-commit","CI","e5tcDhNDa9Tlj4PJpaZTDz-tJ01U_iDLPY_a7eBxoRY",1788744790136]