[작성자:] deverror_admin

  • [에러 해결] Python `str.replace()` 중첩 문자열 교체 충돌 및 성능 최적화 방법

    파이썬 개발자라면 문자열을 특정 패턴으로 교체해야 할 때 str.replace()re.sub() 같은 함수를 자주 사용합니다. 하지만 특정 상황, 즉 교체하려는 문자열 패턴 자체가 다른 교체의 대상 또는 결과가 되는 경우, 예기치 않은 오류가 발생하거나 성능 저하를 겪을 수 있습니다. 이번 문서에서는 이러한 ‘중첩 문자열 교체 충돌’ 문제를 명확히 분석하고, 효율적인 해결 방법을 제시합니다.

    1. 에러 발생 상황

    개발 과정에서 다음과 같은 문자열 교체 요구사항이 발생했다고 가정해봅시다.

    • "{{“를 "{"로, "}}""}"로 변경해야 합니다.
    • 동시에 "{""{{“로, "}""}}"로 변경해야 합니다.

    즉, {{...}} 형태의 플레이스홀더를 {...} 형태로 바꾸는 동시에, {...} 형태의 플레이스홀더를 {{...}} 형태로 바꾸는 작업입니다. 예를 들어, 아래와 같은 문자열이 주어졌을 때:

    x = "here is some {javascript}, but here is a {{pythonExpression}}"

    최종적으로는 다음과 같은 결과물을 얻고 싶습니다.

    "here is {{javascript}}, but here is a {pythonExpression}"

    2. 명확한 발생 원인

    이러한 문제는 파이썬의 str.replace() 함수가 문자열을 순차적으로 교체한다는 특성 때문에 발생합니다. 즉, 한 번의 .replace() 호출이 완료된 후에 다음 .replace() 호출이 실행됩니다.

    주어진 예시에서 "{""{{“로 바꾸는 작업과 "{{“를 "{"로 바꾸는 작업은 서로의 결과에 직접적으로 간섭(Interference)합니다.

    • 만약 먼저 "{""{{“로 교체하면, 원래 문자열에 있던 "{{“는 "{{""{"가 추가되어 "{{{"가 되어버릴 수 있습니다.
    • 반대로 "{{“를 "{"로 먼저 교체하면, 원래 "{"가 들어갈 자리에 "{"가 사라져 버리거나, 이후 교체 시 간섭을 일으킵니다.

    어떤 순서로 교체하더라도 다른 한쪽의 교체 작업에 영향을 주어, 개발자가 원하는 최종 결과물을 얻기 어렵게 만듭니다. 이는 곧 문자열 교체의 논리적 충돌로 이어지며, 복잡한 문자열 처리 로직에서 예상치 못한 버그의 주범이 됩니다.

    3. 해결 방법 및 코드 예시

    ❌ 잘못된 접근 방식 (직접 순차 교체 및 성능 문제)

    가장 직관적인 방법인 직접 순차 교체는 위에서 설명한 간섭 문제 때문에 원하는 결과를 얻기 어렵습니다. 다음 코드를 통해 실패하는 예시를 살펴보겠습니다.

    # 잘못된 접근 예시: 직접 순차 교체 시도
    x = "here is some {javascript}, but here is a {{pythonExpression}}"
    print("원본:", x)
    
    # 1단계: '{'를 '{{'로 교체 시도
    # 이 단계에서 "{{pythonExpression}}"이 "{{{pythonExpression}}"으로 변질될 수 있습니다.
    x_intermediate = x.replace("{", "{{").replace("}", "}}")
    print("1단계 후:", x_intermediate)
    
    # 2단계: '{{'를 '{'로 교체 시도
    # 1단계에서 변질된 문자열이 다시 영향을 받습니다.
    x_final_wrong = x_intermediate.replace("{{", "{").replace("}}", "}")
    print("최종 결과 (잘못됨):", x_final_wrong)
    # 예상되는 출력: here is {{javascript}}, but here is a {pythonExpression}
    # 실제 출력: here is {javascript}, but here is a {pythonExpression} (원치 않는 결과)

    또한, re 모듈의 re.sub()를 사용하여 여러 패턴을 동시에 교체하는 방법도 고려할 수 있습니다. 이는 논리적 간섭을 피할 수 있는 방법 중 하나이지만, 단순 문자열 교체에서는 str.replace() 체인보다 상대적으로 성능이 떨어질 수 있습니다. 실제 테스트 결과, re.sub() 방식이 str.replace()를 이용한 임시 구분자 방식보다 4배 이상 느린 것으로 나타났습니다.

    import re
    import datetime
    
    x_test = "here is {javascript}, but here is a pythonExpression {{var1}}"
    
    # re.sub()를 이용한 동시 교체 시도 (성능 비교 목적)
    d = {"{{": "{", "}}": "}", "{": "{{", "}": "}}"}
    start_re = datetime.datetime.now()
    for _ in range(1000001):
        x_re_sub = re.sub("|".join(re.escape(k) for k in d), lambda m: d[m.group(0)], x_test)
    finish_re = datetime.datetime.now()
    print("re.sub() 결과:", x_re_sub) # 문제의 복잡성으로 인해 정확한 결과를 보장하지 않을 수 있음
    print("Runtime (re.sub, 100만 회): ", finish_re - start_re) # 약 13초

    re.sub() 예시의 딕셔너리 정의는 원본 문제의 요구사항을 반영하지만, 이 방식은 복잡한 패턴 처리에는 유리하나, 단순 문자열 교체에는 성능 오버헤드가 발생하며, 특정 충돌 시나리오에서는 기대한 대로 작동하지 않을 수 있습니다.

    ✅ 올바른 해결책: 중간 임시 구분자 사용

    가장 효과적이고 성능이 우수한 해결책은 중간 임시 구분자(Intermediate Delimiter)를 사용하는 것입니다. 이 방법은 세 단계로 이루어집니다:

    1. 가장 먼저 교체되어야 하지만 다른 패턴과 겹치는 부분(예: "{{“, "}}")을 절대 충돌하지 않을 고유한 임시 문자열로 교체합니다 (예: "<<", ">>").
    2. 나머지 패턴(예: "{", "}")을 원하는 최종 형태로 교체합니다 (예: "{{", "}}"). 이 단계에서는 임시 문자열이 방패 역할을 하여 간섭을 막습니다.
    3. 임시 문자열(예: "<<", ">>")을 원래의 최종 형태(예: "{", "}")로 다시 교체합니다.

    다음은 이 방법을 적용한 파이썬 코드 예시입니다.

    import datetime
    
    x = "here is some {javascript}, but here is a {{pythonExpression}}"
    print("원본 문자열:", x)
    
    start_replace = datetime.datetime.now()
    for _ in range(1000001): # 성능 측정을 위해 100만 회 반복
        # 1. '{{'와 '}}'를 임시 구분자로 교체
        # '<<'와 '>>'는 원본 문자열에 존재하지 않는다고 가정합니다.
        temp_x = x.replace("{{", "<<").replace("}}", ">>")
    
        # 2. '{'와 '}'를 원하는 최종 형태인 '{{'와 '}}'로 교체
        final_x = temp_x.replace("{", "{{").replace("}", "}}")
    
        # 3. 임시 구분자를 원래 목표였던 '{'와 '}'로 다시 교체
        result_x = final_x.replace("<<", "{").replace(">>", "}")
    finish_replace = datetime.datetime.now()
    
    print("최종 결과 (올바름):", result_x)
    print("Runtime (str.replace() 체인, 100만 회): ", finish_replace - start_replace) # 약 3.7초

    위 코드 예시의 성능 측정 결과에서 볼 수 있듯이, str.replace() 체인을 사용한 중간 임시 구분자 방식은 re.sub() 방식보다 훨씬 빠르고 효율적입니다. 이는 str.replace()가 C 언어로 구현되어 있어 매우 빠른 반면, re.sub()는 정규표현식 파싱 및 매칭에 추가적인 오버헤드가 발생하기 때문입니다.

    4. 향후 예방을 위한 팁

    • 고유한 임시 구분자 선정: 임시 구분자로 사용할 문자열은 원본 문자열에 절대 나타나지 않을 법한 고유한 값을 선택해야 합니다. 예를 들어, GUID(Globally Unique Identifier)나 특수 문자열 조합(예: "__TEMP_DBL_BRACE_START__")을 사용하면 충돌 위험을 최소화할 수 있습니다.
    • 교체 순서 신중하게 고려: 중간 구분자를 사용하지 못하는 상황이라면, 항상 "가장 길고 복잡한 패턴"을 먼저 교체하고 "짧고 단순한 패턴"을 나중에 교체하는 원칙을 따르세요. 이는 교체 간 간섭을 줄이는 데 도움이 됩니다.
    • 성능과 가독성 균형: str.replace()는 단순 문자열 교체에 매우 빠르고 효율적입니다. 복잡한 패턴 매칭이 필요하지 않다면, re 모듈보다는 str.replace()를 적극적으로 활용하는 것이 좋습니다. 하지만 중간 구분자 방식이 코드를 다소 길게 만들 수 있으므로, 프로젝트의 가독성 기준과 성능 요구사항을 고려하여 최적의 방법을 선택해야 합니다.
    • 충분한 테스트 코드 작성: 특히 중첩되거나 복잡한 문자열 교체 로직의 경우, 다양한 엣지 케이스(Edge Cases)를 포함한 테스트 코드를 작성하여 변경 사항이 의도대로 작동하는지 항상 검증하는 것이 중요합니다.
  • [에러 해결] GCP Vertex AI Pipelines ModelGetOp 400 Request contains an invalid argument 원인과 해결 방법

    [에러 해결] GCP Vertex AI Pipelines ModelGetOp 400 Request contains an invalid argument 원인과 해결 방법

    Google Cloud Vertex AI Pipelines에서 ModelGetOp를 사용하여 Vertex AI Model Registry에 등록된 모델을 가져오려 할 때 400 Request contains an invalid argument 에러가 발생하는 문제에 직면할 수 있습니다. 이 가이드는 해당 에러의 원인을 분석하고 효과적인 해결 방법을 제시하여 여러분의 MLOps 파이프라인 구축을 돕습니다.

    에러 상황 소개

    여러분은 아마 다음과 같은 상황에서 이 에러를 마주했을 것입니다. Vertex AI Model Registry에 이미 학습된 모델이 등록되어 있고, 이를 Vertex AI Pipelines (Kubeflow Pipelines 기반) 내에서 배포하고자 할 때, ModelGetOp 컴포넌트가 예상치 못한 오류를 발생시킵니다.

    아래 파이프라인 코드와 유사하게 ModelGetOp에 모델의 전체 리소스 경로(Full Resource Name)를 직접 전달했을 때 문제가 발생합니다:

    @kfp.dsl.pipeline(
        name=DISPLAY_NAME,
        pipeline_root=BUCKET_URI,
        )
    def pipeline(PROJECT_ID: str):
       
        model_task = ModelGetOp(
            model_name='projects/80514224188/locations/us-central1/models/1800006515679555824',
            project=PROJECT_ID,
            location=GCP_REGION,
        )
        endpoint_op = EndpointCreateOp(
            project=PROJECT_ID,
            location=GCP_REGION,
            display_name=DISPLAY_NAME,
        )
    
        ModelDeployOp(
            model=model_task.outputs["model"],
            endpoint=endpoint_op.outputs["endpoint"],
            dedicated_resources_min_replica_count=1,
            dedicated_resources_max_replica_count=1,
            dedicated_resources_machine_type=MACHINE_TYPE,
        )
    

    파이프라인 실행 시 ModelGetOp 단계에서 400 Request contains an invalid argument 오류가 발생하며, 더 이상의 구체적인 로그는 제공되지 않아 디버깅에 어려움을 겪게 됩니다. 하지만 동일한 전체 리소스 경로를 사용하여 파이프라인 외부에서 aiplatform.Model('projects/...')와 같이 모델을 성공적으로 가져올 수 있다는 점에서 더욱 혼란스러울 수 있습니다.

    발생 원인

    이 에러의 핵심 원인은 ModelGetOp 컴포넌트의 model_name 파라미터가 예상하는 입력 형식에 대한 오해입니다.

    • Vertex AI SDK for Python (google.cloud.aiplatform)의 aiplatform.Model() 클래스는 모델의 전체 리소스 경로 (예: projects/<PROJECT_NUMBER>/locations/<LOCATION>/models/<MODEL_ID>)를 직접 파싱하여 사용할 수 있습니다.
    • 하지만 Kubeflow Pipelines의 ModelGetOp와 같은 Vertex AI SDK pre-built 컴포넌트들은 내부적으로 특정 형식의 인자를 기대합니다. ModelGetOpmodel_name 파라미터는 모델의 고유한 ID (숫자 문자열) 또는 표시 이름(Display Name)을 기대하며, projectlocation 파라미터와 함께 사용하여 해당 프로젝트 및 위치 내에서 모델을 식별합니다. 전체 리소스 경로를 직접 전달하면, 컴포넌트 내부에서 이 경로를 유효한 model_name으로 인식하지 못하고 잘못된 인자 오류(400)를 반환하게 됩니다.

    해결 방법 코드 예시

    ModelGetOp에 전체 리소스 경로 대신 모델의 고유한 숫자 ID를 전달해야 합니다. 전체 리소스 경로에서 모델 ID를 추출하는 간단한 방법은 다음과 같습니다.

    에러 발생 코드 (Original Code)

    # 에러 발생 코드
    model_task = ModelGetOp(
        model_name='projects/80514224188/locations/us-central1/models/1800006515679555824', # <-- 문제의 코드
        project=PROJECT_ID,
        location=GCP_REGION,
    )

    수정된 코드 (Corrected Code)

    # 모델의 전체 리소스 경로 (예시)
    FULL_MODEL_RESOURCE_NAME = 'projects/80514224188/locations/us-central1/models/1800006515679555824'
    
    # 전체 리소스 경로에서 모델 ID 추출
    # 마지막 '/' 이후의 문자열이 모델 ID입니다.
    MODEL_ID = FULL_MODEL_RESOURCE_NAME.split('/')[-1]
    
    # 수정된 ModelGetOp 호출
    model_task = ModelGetOp(
        model_name=MODEL_ID, # <-- 모델의 숫자 ID를 전달
        project=PROJECT_ID,
        location=GCP_REGION,
    )

    만약 model_name 파라미터에 모델의 표시 이름을 사용하고 싶다면, 해당 모델의 정확한 표시 이름을 전달하면 됩니다. 하지만 모델 ID를 사용하는 것이 더 명확하고 충돌 가능성이 적습니다.

    향후 예방을 위한 팁

    1. 공식 문서 확인: Vertex AI SDK의 각 KFP 컴포넌트(예: ModelGetOp, EndpointCreateOp 등)의 공식 문서를 항상 참조하여 각 파라미터가 기대하는 정확한 입력 형식을 확인하세요. SDK 컴포넌트는 일반적인 Vertex AI API 또는 SDK 클라이언트와는 다른 파라미터 명명 규칙이나 값 형식을 가질 수 있습니다.
    2. 파라미터 유형 이해: '리소스 이름'과 'ID' 또는 '표시 이름'을 구분하는 습관을 들이세요. 전체 리소스 이름은 API 호출이나 SDK 초기화에 자주 사용되지만, 특정 컴포넌트는 컨텍스트에 맞는 더 간결한 식별자를 요구할 수 있습니다.
    3. 디버깅 시 로그 활용: 400 Invalid Argument 에러는 광범위하게 발생할 수 있으므로, 가능하다면 컴포넌트의 입력 파라미터를 출력하는 임시 단계를 파이프라인에 추가하여 실제 전달되는 값이 무엇인지 확인하는 것이 좋습니다.
    4. 일관된 명명 규칙: 모델을 등록할 때 명확하고 일관된 표시 이름 또는 ID 명명 규칙을 사용하여 파이프라인에서 쉽게 참조할 수 있도록 합니다.

    이 가이드를 통해 GCP Vertex AI Pipelines에서 ModelGetOp 에러를 해결하고 안정적인 MLOps 파이프라인을 구축하는 데 도움이 되기를 바랍니다.

  • [에러 해결] Python NameError: name ‘generate_chk’ is not defined 원인과 해결 방법

    [에러 해결] Python NameError: name ‘generate_chk’ is not defined 원인과 해결 방법

    에러 상황 소개

    Python 스크립트 실행 중 NameError: name 'generate_chk' is not defined 오류가 발생했습니다. 이 오류는 Geometry Dash 레벨 업로드 프로그램을 파이썬으로 개발하는 과정에서 seed2 값을 생성하기 위해 generate_chk 함수를 호출했으나, 해당 함수가 정의되지 않아 발생하는 전형적인 문제였습니다.

    문제의 스크립트에서 발생한 에러 메시지는 다음과 같습니다:

    [***@archlinux usernm]$ python upload
    What is your username?levelnotinauto
    What is your Account ID?38627839
    What is your password?********
    What is the Level name?Check Desc
    How many coins are there?4848394
    How many stars do you want?392084092849
    Traceback (most recent call last):
      File "/home/***/scripts/usernm/upload", line 43, in <module>
        "seed2": generate_chk(key="41274", values=[generate_upload_seed(levelString)], salt="xI25fpAapCQg"), # This is talked about in the CHK encryption,
                 ^^^^^^^^^^^^
    NameError: name 'generate_chk' is not defined. Did you mean: 'generate_gjp2'?

    발생 원인

    NameError: name 'generate_chk' is not defined 오류는 파이썬 인터프리터가 generate_chk라는 이름의 함수를 찾을 수 없을 때 발생합니다. 문제의 스크립트에서는 requests, base64, hashlib 모듈을 임포트하고 있었지만, generate_chk 함수는 이들 표준 라이브러리에 포함되어 있지 않습니다. 또한, 사용자 본인이 해당 함수를 직접 정의하지도 않았습니다.

    사용자는 특정 Geometry Dash API 문서(현재는 아카이브됨)에서 generate_chk(key="41274", values=[generate_upload_seed(levelString)], salt="xI25fpAapCQg")와 같은 함수 호출 방식을 확인했으나, 실제 함수의 구현 코드를 누락한 채 사용했습니다. 이는 마치 특정 요리법에 "특별 소스 추가"라고만 적혀있고, 그 특별 소스를 만드는 방법은 알려주지 않은 상황과 같습니다. 즉, 함수의 호출 방식만 알고 실제 구현이 없는 상태에서 호출했기 때문에 발생한 오류입니다.

    특히 generate_chk 함수가 generate_upload_seed 함수의 반환 값을 인자로 받기 때문에, generate_upload_seed 함수 또한 정의되지 않았을 가능성이 높습니다.

    추가적으로, 원본 코드에서 generate_gjp2 함수는 정의되었으나, 그 반환 값인 hash를 전역 변수로 할당하지 않아 data 딕셔너리에서 "gjp": hash 부분도 NameError를 일으킬 수 있는 잠재적인 문제가 있었습니다.

    해결 방법 코드 예시

    이 문제를 해결하려면 generate_chkgenerate_upload_seed 함수의 실제 구현 코드를 스크립트 내에 정의해야 합니다. Geometry Dash API와 관련된 커뮤니티나 다른 오픈소스 프로젝트를 통해 해당 함수들의 구현을 찾거나, API 문서의 내용을 바탕으로 직접 구현해야 할 수도 있습니다.

    아래는 Geometry Dash API의 seed2 생성을 위한 generate_chkgenerate_upload_seed 함수의 예시 구현입니다. 실제 Geometry Dash API의 동작 방식에 따라 내부 로직이 달라질 수 있으므로, 참고하여 실제 API의 요구사항에 맞게 수정해야 합니다. (특히 XOR 연산 방식과 사용되는 상수 값들을 주의 깊게 확인해야 합니다.)

    에러 코드 (Before)

    # ... (생략) ...
    
    data = {
        # ...
        "gjp": hash, # 'hash'가 정의되지 않았을 수 있음
        # ...
        "seed2": generate_chk(key="41274", values=[generate_upload_seed(levelString)], salt="xI25fpAapCQg"), # NameError 발생 지점
        "secret": "Wmfd2893gb7"
    }
    
    # ... (생략) ...

    수정된 코드 예시 (After)

    generate_upload_seedgenerate_chk 함수 정의를 추가하고, generate_gjp2 함수의 호출을 명확히 합니다.

    import requests
    import base64
    import hashlib  # sha1() lives there
    
    user = input("What is your username?")
    accid = input("What is your Account ID?")
    passwd = input("What is your password?")
    lname = input ("What is the Level name?")
    coin = input("How many coins are there?")
    stars = input("How many stars do you want?")
    
    # --- Geometry Dash API 관련 유틸리티 함수 정의 시작 ---
    # 이 함수들은 Geometry Dash API의 특정 암호화 로직을 따르므로,
    # 공식 문서 또는 다른 구현을 참고하여 정확성을 확인해야 합니다.
    
    def generate_gjp2(password: str, salt: str = "mI29fmAnxgTs") -> str:
        password_with_salt = password + salt
        return hashlib.sha1(password_with_salt.encode()).hexdigest()
    
    def generate_upload_seed(level_string: str) -> str:
        """
        Geometry Dash API의 레벨 업로드 시 사용되는 seed 값을 생성하는 예시 함수.
        실제 구현은 levelString을 특정 방식으로 처리(예: Base64 인코딩, XOR 연산 등)할 수 있습니다.
        여기서는 예시를 위해 levelString의 길이를 반환합니다.
        실제 GD API는 더 복잡한 로직을 가질 수 있으니, 필요시 정확한 구현을 찾아야 합니다.
        """
        return str(len(level_string)) # placeholder: 실제 로직으로 교체 필요
    
    def generate_chk(key: str, values: list, salt: str) -> str:
        """
        Geometry Dash API의 "seed2" 값을 생성하는 예시 함수.
        일반적으로 key, values (generate_upload_seed의 결과), salt를 조합하여
        XOR 연산 및 SHA1 해싱을 통해 체크섬을 생성합니다.
        
        이 구현은 일반적인 Geometry Dash API 체크섬 패턴을 기반으로 한 예시입니다.
        정확한 알고리즘은 API 버전이나 엔드포인트에 따라 다를 수 있으므로,
        공식 문서 또는 역설계된 구현과 대조하여 검증하는 것이 필수적입니다.
        """
        if not values:
            raise ValueError("Values list cannot be empty for generate_chk.")
            
        main_value = str(values[0]) # values는 보통 단일 요소 리스트일 것
        
        # GD API 체크섬 로직의 일반적인 패턴 (XOR 연산 및 SHA1 해싱)
        
        # 1. key와 main_value를 조합
        # 예시 조합: seed + key
        combined_string_for_xor = main_value + key
        
        # 2. XOR 연산 적용 (GD API의 특정 방식)
        # 원본 문제의 seed2 salt 값을 XOR 키로 활용하는 경우가 많음
        xor_secret_key = "xI25fpAapCQg" # 원본 호출의 salt 값 재사용, 실제 GD API의 특정 값을 사용해야 함
        
        xor_result_chars = []
        for i, char_code in enumerate(combined_string_for_xor):
            xor_result_chars.append(chr(ord(char_code) ^ ord(xor_secret_key[i % len(xor_secret_key)])))
        xor_string = "".join(xor_result_chars)
            
        # 3. salt 추가 및 SHA1 해싱
        final_string_to_hash = xor_string + salt
        hashed_result = hashlib.sha1(final_string_to_hash.encode()).hexdigest()
        
        return hashed_result
    # --- Geometry Dash API 관련 유틸리티 함수 정의 끝 ---
    
    
    # gjp 해시 생성: 함수 호출 결과를 'hash' 변수에 할당
    hash = generate_gjp2(password=passwd)
    
    levelString = "H4sIAAAAAAAAC6WQwQ3DIAxFF3IlfxsIUU6ZIQP8AbJChy_GPSZqpF7-A4yfDOfhXcCiNMIqnVYrgYQl8rDwBTZCVbkQRI3oVHbiDU6F2jMF_lesl4q4kw2PJMbovxLBQxTpM3-I6q0oHmXjzx7N0240cu5w0UBNtESRkble8uSLHjh8nTubmYJZ2MvMrEITEN0gEJMxlLiMZ28frmj"
    
    data = {
        "gameVersion": 22,
        "accountID": accid,
        "gjp": hash, # 이제 'hash' 변수가 정확히 정의되어 있습니다.
        "userName": user,
        "levelID": 0,
        "levelName": lname,
        "levelDesc": "Q3Vyc2VkIGxldmVsIG55ZWghaGVoIGhlaCBhbnl3YXkgaXRzIGEgdmVyeSBicm9rZW4gbGV2ZWwgaSBzZW50IGEgcmVxdWVzdCB0byByb2Igc2VydmVyIHRocm91Z2ggcHl0aG9uIGxvbA",
        "levelVersion": "-47577473775738573",
        "levelLength": 48483845738573486574873869465947694,
        "audioTrack": 0,
        "auto": 0,
        "password": 314159,
        "original": 55610687,
        "twoPlayer": 0,
        "songID": 839583504693858468444987694,
        "objects": 1,
        "coins": coin,
        "requestedStars": stars,
        "unlisted": 0,
        "ldm": 0,
        "levelString": levelString,
        "seed2": generate_chk(key="41274", values=[generate_upload_seed(levelString)], salt="xI25fpAapCQg"), # 정의된 함수 사용
        "secret": "Wmfd2893gb7"
    }
    
    headers = {
        "User-Agent": ""
    }
    
    url = "http://www.boomlings.com/database/uploadGJLevel21.php"
    
    req = requests.post(url=url, data=data, headers=headers)
    print(req.text)

    향후 예방을 위한 팁

    • 외부 코드 사용 시 주의: API 문서나 예제 코드를 참조할 때는 호출 방식뿐만 아니라, 해당 함수나 클래스의 정의(구현)가 함께 제공되는지 반드시 확인해야 합니다. 누락된 부분이 있다면 직접 구현하거나 다른 자료를 찾아야 합니다.
    • 종속성 명확히 파악: 프로젝트에 사용되는 모든 함수, 클래스, 변수가 어디에서 오는지 (내부 정의, 특정 모듈 임포트 등) 명확히 이해하고 있어야 합니다. 파이썬의 NameError는 이러한 종속성이 제대로 충족되지 않았을 때 가장 흔히 발생합니다.
    • 아카이브된 문서 활용 시 주의: 아카이브되거나 오래된 문서는 정보가 불완전하거나 최신 API와 호환되지 않을 수 있습니다. 가능한 한 최신 공식 문서를 참조하고, 필요한 경우 관련 커뮤니티의 도움을 받는 것이 좋습니다.
    • 재사용 가능한 모듈화: 만약 generate_chk와 같은 유틸리티 함수를 직접 구현한다면, 이를 별도의 파일(예: gd_utils.py)로 분리하고 필요할 때 임포트하여 사용하는 것이 코드의 가독성과 재사용성을 높이는 좋은 방법입니다.
    • 단위 테스트: 핵심 로직을 담당하는 함수(예: generate_chk)에 대해서는 단위 테스트를 작성하여, 함수의 동작이 예상대로 이루어지는지 검증하는 습관을 들이면 좋습니다. 이는 특히 외부 API와의 인터랙션에서 중요한 데이터를 생성하는 함수에 유용합니다.
  • [에러 해결] Python NameError: name ‘generate_chk’ is not defined 원인과 해결 방법

    [에러 해결] Python NameError: name ‘generate_chk’ is not defined 원인과 해결 방법

    Python 개발 중 NameError: name '...' is not defined 에러는 흔히 마주치는 문제입니다. 특히 외부 API 문서나 오래된 코드 스니펫을 활용할 때 이러한 에러가 발생하기 쉽습니다. 이번 가이드에서는 Geometry Dash 레벨 업로드 프로그램을 파이썬으로 개발하던 중 발생한 NameError: name 'generate_chk' is not defined 에러의 원인을 분석하고, 실제 Geometry Dash API의 동작 방식에 기반한 해결책을 제시합니다.

    에러 상황 소개

    Geometry Dash 레벨 업로드 자동화 프로그램을 Python으로 작성하던 중, uploadGJLevel21 엔드포인트에 필요한 seed2 값을 생성하는 과정에서 다음과 같은 NameError가 발생했습니다. 문제는 generate_chk라는 함수를 호출하고 있었지만, 해당 함수가 프로그램 내 어디에도 정의되어 있지 않다는 것이었습니다.

    Traceback (most recent call last):
      File "/home/***/scripts/usernm/upload", line 43, in <module>
        "seed2": generate_chk(key="41274", values=[generate_upload_seed(levelString)], salt="xI25fpAapCQg"), # This is talked about in the CHK encryption,
                 ^^^^^^^^^^^^
    NameError: name 'generate_chk' is not defined. Did you mean: 'generate_gjp2'?

    이 에러는 data 딕셔너리의 "seed2" 키 값을 할당하는 라인에서 발생했습니다. 개발자는 requests, base64, hashlib 모듈에 이 함수가 있을 것이라고 추측했지만, 실제로는 존재하지 않았습니다.

    발생 원인

    NameError: name 'generate_chk' is not defined 에러의 핵심 원인은 이름 그대로 generate_chk 함수가 파이썬 인터프리터가 접근할 수 있는 어떤 스코프에도 정의되어 있지 않기 때문입니다. 파이썬은 변수나 함수를 사용하기 전에 반드시 선언(정의)되어 있어야 합니다.

    • 함수 미정의: 사용자가 코드 상에서 generate_chk 함수를 직접 정의하지 않았습니다.
    • 외부 라이브러리 오해: requests, base64, hashlib와 같은 표준 라이브러리나 흔히 사용되는 외부 라이브러리에는 generate_chk라는 이름의 함수가 기본적으로 포함되어 있지 않습니다. 이 함수는 Geometry Dash API의 특정 체크섬(CHK) 생성을 위한 커스텀 로직이며, 개발자가 직접 구현하거나 관련 API 래퍼 라이브러리에서 가져와야 합니다.
    • 문서의 불완전성 또는 오해: 참조했던 https://wyliemaster.github.io/gddocs 문서는 아카이브되어 더 이상 업데이트되지 않으며, 해당 문서가 특정 헬퍼 함수(generate_chk, generate_upload_seed 등)가 이미 정의되어 있음을 가정하고 코드를 제시했을 가능성이 높습니다.

    generate_chk 함수는 key, values, salt를 인자로 받아 seed2 값을 생성하는 역할을 합니다. Geometry Dash API의 seed2는 일반적으로 levelString의 아스키 값 합을 특정 키(예: 41274)와 XOR 연산한 후, 이를 문자열로 변환하고 salt를 붙여 SHA-1 해시하는 방식으로 구현됩니다.

    해결 방법 코드 예시

    이 에러를 해결하기 위해서는 Geometry Dash API의 seed2 생성 로직에 맞춰 generate_upload_seedgenerate_chk 함수를 직접 구현해야 합니다.

    에러 코드 (original snippet)

    # ... (생략) ...
    levelString = "H4sIAAAAAAAAC6WQwQ3DIAxFF3IlfxsIUU6ZIQP8AbJChy_GPSZqpF7-A4yfDOfhXcCiNMIqnVYrgYQl8rDwBTZCVbkQRI3oVHbiDU6F2jMF_lesl4q4kw2PJMbovxLBQxTpM3-I6q0oHmXjzx7N0240cu5w0UBNtESRkblevSLHjh8nTubmYJZ2MvMrEITEN0gEJMxlLiMZ28frmj"
    
    data = {
        # ... (생략) ...
        "levelString": levelString,
        "seed2": generate_chk(key="41274", values=[generate_upload_seed(levelString)], salt="xI25fpAapCQg"), # <-- 여기서 에러 발생
        "secret": "Wmfd2893gb7"
    }
    
    # ... (생략) ...
    

    수정된 코드

    generate_upload_seedgenerate_chk 함수를 정의하여 seed2 값을 올바르게 생성하도록 수정합니다. generate_upload_seedlevelString의 아스키 코드 합을 반환하고, generate_chk는 이 합과 제공된 key를 XOR 연산한 후 salt를 추가하여 SHA-1 해시를 계산합니다.

    import requests
    import base64
    import hashlib  # sha1() lives there
    
    user = input("What is your username?")
    accid = input("What is your Account ID?")
    passwd = input("What is your password?")
    lname = input ("What is the Level name?")
    coin = input("How many coins are there?")
    stars = input("How many stars do you want?")
    
    def generate_gjp2(password: str = passwd, salt: str = "mI29fmAnxgTs") -> str:
        password += salt
        hash = hashlib.sha1(password.encode()).hexdigest()
        return hash
    
    # --- 새로 추가된 함수 정의 시작 ---
    
    def generate_upload_seed(level_string: str) -> int:
        # Geometry Dash API의 'seed' 생성 방식에 따라 levelString의 아스키 코드 합을 반환합니다.
        # 이 값은 generate_chk 함수에서 추가적인 연산에 사용됩니다.
        return sum(ord(c) for c in level_string)
    
    def generate_chk(key: str, values: list, salt: str) -> str:
        # 'values'는 generate_upload_seed(levelString)의 결과(levelString의 아스키 합)를 포함합니다.
        # 'key'는 XOR 연산에 사용되며, 'salt'는 최종 해시 문자열에 추가됩니다.
        
        # values 리스트의 첫 번째 요소(levelString의 아스키 합)를 가져옵니다.
        sum_of_level_chars = values[0]
        
        # key를 정수형으로 변환합니다.
        key_int = int(key)
        
        # sum_of_level_chars와 key_int를 XOR 연산합니다.
        xor_result = sum_of_level_chars ^ key_int
        
        # XOR 결과와 salt를 연결하여 해시할 문자열을 만듭니다.
        string_to_hash = str(xor_result) + salt
        
        # SHA-1 해시를 계산하여 반환합니다.
        return hashlib.sha1(string_to_hash.encode()).hexdigest()
    
    # --- 새로 추가된 함수 정의 끝 ---
    
    levelString = "H4sIAAAAAAAAC6WQwQ3DIAxFF3IlfxsIUU6ZIQP8AbJChy_GPSZqpF7-A4yfDOfhXcCiNMIqnVYrgYQl8rDwBTZCVbkQRI3oVHbiDU6F2jMF_lesl4q4kw2PJMbovxLBQxTpM3-I6q0oHmXjzx7N0240cu5w0UBNtESRkble7SLHjh8nTubmYJZ2MvMrEITEN0gEJMxlLiMZ28frmj"
    
    data = {
        "gameVersion": 22,
        "accountID"": accid,
        "gjp": generate_gjp2(), # generate_gjp2 호출, passwd와 salt를 인자로 넘기도록 수정하거나 전역변수 사용
        "userName": user,
        "levelID": 0,
        "levelName": lname,
        "levelDesc": "Q3Vyc2VkIGxldmVsIG55ZWggaGVoIGhlaCBhbnl3YXkgaXRzIGEgdmVyeSBicm9rZW4gbGV2ZWwgaSBzZW50IGEgcmVxdWVzdCB0byByb2Igc2VydmVyIHRocm91Z2ggcHl0aG9uIGxvbA",
        "levelVersion": "-47577473775738573",
        "levelLength": 48483845738573486574873869465947694,
        "audioTrack": 0,
        "auto": 0,
        "password": 314159,
        "original": 55610687,
        "twoPlayer": 0,
        "songID": 839583504693858468444987694,
        "objects": 1,
        "coins": coin,
        "requestedStars": stars,
        "unlisted": 0,
        "ldm": 0,
        "levelString": levelString,
        "seed2": generate_chk(key="41274", values=[generate_upload_seed(levelString)], salt="xI25fpAapCQg"), # 이제 이 함수들이 정의되어 에러가 발생하지 않습니다.
        "secret": "Wmfd2893gb7"
    }
    
    headers = {
        "User-Agent": ""
    }
    
    url = "http://www.boomlings.com/database/uploadGJLevel21.php"
    
    req = requests.post(url=url, data=data, headers=headers)
    print(req.text)
    

    참고: 원래 코드의 "gjp": hash, 부분은 generate_gjp2() 함수를 호출하여 값을 할당해야 합니다. hash라는 변수가 직접 정의되지 않았고, generate_gjp2 함수가 선언되었음에도 호출되지 않았기 때문입니다. 위 수정된 코드에서는 "gjp": generate_gjp2(), 로 변경했습니다. 또한 generate_gjp2 함수가 전역 변수 passwd를 사용하도록 정의되어 있지만, 함수 내부에서 직접 인자로 받는 것이 더 좋은 코드 스타일입니다.

    향후 예방을 위한 팁

    • 함수 및 변수 정의 확인: 코드를 작성하거나 외부 코드를 사용할 때, 모든 함수와 변수가 사용 전에 정의되어 있는지 항상 확인하는 습관을 들여야 합니다. IDE의 경고 메시지를 주의 깊게 살펴보는 것이 큰 도움이 됩니다.
    • API 문서 최신화 및 교차 검증: 아카이브되었거나 오래된 API 문서를 참조할 때는 해당 API의 최신 문서나 다른 활성 커뮤니티(예: GitHub의 관련 프로젝트)를 통해 정보를 교차 검증하는 것이 중요합니다. API는 시간이 지남에 따라 변경될 수 있으며, 누락된 헬퍼 함수가 있을 수 있습니다.
    • 핵심 로직 이해: 단순히 함수 이름을 복사 붙여넣기하는 것을 넘어, 체크섬, 해시, 암호화 등 핵심 로직이 어떻게 작동하는지 이해하려고 노력해야 합니다. 이는 디버깅을 용이하게 하고, 에러 발생 시 스스로 해결할 수 있는 능력을 길러줍니다.
    • 모듈화 및 재사용 가능한 코드 작성: 특정 API와의 상호작용을 위한 헬퍼 함수들은 별도의 모듈로 분리하여 관리하면 코드의 가독성을 높이고 다른 프로젝트에서도 재사용하기 좋습니다.
    • 예외 처리 및 로깅: API 요청 시 발생할 수 있는 다양한 예외 상황(네트워크 에러, 응답 에러 등)에 대비하여 적절한 예외 처리(try-except)와 로깅을 구현하면 문제 발생 시 원인 파악에 큰 도움이 됩니다.
  • [에러 해결] Python NameError: name ‘generate_chk’ is not defined 원인과 해결 방법

    [에러 해결] Python NameError: name ‘generate_chk’ is not defined 원인과 해결 방법

    Python 개발 중 NameError: name 'generate_chk' is not defined 에러를 마주하셨나요? 특히 Geometry Dash API와 같은 외부 서비스와 연동할 때 종종 겪을 수 있는 문제입니다. 이 가이드는 해당 에러가 발생하는 원인을 분석하고, 명확한 코드 예시를 통해 해결 방법을 제시하며, 향후 유사한 문제를 예방하기 위한 팁을 제공합니다.

    에러 상황 소개

    사용자께서는 Python을 이용해 Geometry Dash 레벨을 업로드하는 프로그램을 개발하고 계셨습니다. 이 과정에서 seed2 값을 생성하기 위해 generate_chk 함수를 호출했는데, 아래와 같은 NameError가 발생했습니다.

    Traceback (most recent call last):
      File "/home/***/scripts/usernm/upload", line 43, in <module>
        "seed2": generate_chk(key="41274", values=[generate_upload_seed(levelString)], salt="xI25fpAapCQg"), # This is talked about in the CHK encryption,
                 ^^^^^^^^^^^^
    NameError: name 'generate_chk' is not defined. Did you mean: 'generate_gjp2'?
    

    이 에러는 스크립트 내에서 generate_chk라는 이름의 함수를 찾을 수 없을 때 발생합니다. 해당 함수는 Geometry Dash API 문서를 참조하여 사용하려 했으나, 참조 문서가 아카이브되어 정확한 구현 방식을 알기 어려운 상황이었습니다.

    발생 원인

    NameError: name 'generate_chk' is not defined 에러는 말 그대로 generate_chk라는 이름의 함수가 스크립트 어디에도 정의되어 있지 않기 때문에 발생합니다.

    • 원본 코드에서는 requests, base64, hashlib 모듈을 임포트했지만, 이 모듈들 안에 generate_chk 함수는 내장되어 있지 않습니다.
    • 사용자께서는 generate_gjp2 함수를 직접 정의하셨지만, generate_chkgenerate_upload_seed 함수는 정의하지 않으셨습니다.
    • Geometry Dash API와 같은 외부 서비스들은 고유한 인증 및 체크섬(checksum) 로직을 가집니다. generate_chk 함수는 이러한 특정 API의 커스텀 로직(예: XOR 암호화, SHA1 해싱, Base64 인코딩 등을 조합한 체크섬 생성)을 구현하기 위해 필요한 함수입니다. API 문서가 이 함수의 호출 시그니처만 제공하고 실제 구현 코드는 제공하지 않거나, 사용자가 직접 구현해야 하는 부분이었을 가능성이 큽니다.

    해결 방법 코드 예시

    이 에러를 해결하기 위해서는 generate_chk 함수와 그에 필요한 generate_upload_seed 함수를 직접 스크립트에 정의해주어야 합니다. Geometry Dash API의 CHK 생성 로직은 다소 복잡하며, 보통 XOR 암호화와 SHA1 해싱, 그리고 문자열 조작을 포함합니다. 다음은 NameError를 해결하기 위한 기본적인 구조와 함께, 일반적인 GD API CHK 생성 로직을 간략화하여 구현한 예시입니다. 실제 GD API와 완벽하게 호환되려면 Geometry Dash 커뮤니티나 다른 오픈소스 GD API 클라이언트의 구현을 참조하는 것이 좋습니다.

    에러 발생 코드 (일부)

    # ... (생략) ...
    
    data = {
        # ... (다른 데이터 필드) ...
        "levelString": levelString,
        "seed2": generate_chk(key="41274", values=[generate_upload_seed(levelString)], salt="xI25fpAapCQg"), # <-- 여기서 에러 발생
        "secret": "Wmfd2893gb7"
    }
    
    # ... (생략) ...
    

    수정된 코드 예시 (함수 정의 추가)

    다음은 generate_upload_seedgenerate_chk 함수를 추가하여 NameError를 해결하는 코드입니다. 실제 Geometry Dash의 CHK 로직을 반영하기 위해 필요한 라이브러리(예: zlib)를 임포트하고, 알려진 로직을 기반으로 함수를 구현합니다.

    import requests
    import base64
    import hashlib  # sha1() lives there
    import zlib     # Geometry Dash levelString 압축/해제에 사용될 수 있음 (필요시)
    
    user = input("What is your username?")
    accid = input("What is your Account ID?")
    passwd = input("What is your password?")
    lname = input ("What is the Level name?")
    coin = input("How many coins are there?")
    stars = input("How many stars do you want?")
    
    def generate_gjp2(password: str = passwd, salt: str = "mI29fmAnxgTs") -> str:
        password += salt
        hash_val = hashlib.sha1(password.encode()).hexdigest()
        return hash_val
    
    # --- NameError 해결을 위한 함수 정의 시작 ---
    
    def generate_upload_seed(level_string: str) -> str:
        # Geometry Dash API는 보통 levelString을 base64 디코딩 후 zlib 압축 해제합니다.
        # 여기서는 seed 값을 생성하기 위해 levelString 자체를 사용하거나,
        # Geometry Dash API의 특정 seed 생성 로직(예: 간단한 인코딩)을 따를 수 있습니다.
        # 정확한 로직은 GD API 버전에 따라 다를 수 있으므로, 관련 커뮤니티를 참고하세요.
        return level_string # 간단한 예시. 실제로는 더 복잡한 처리 필요.
    
    def generate_chk(key: str, values: list[str], salt: str) -> str:
        # Geometry Dash API의 CHK(Checksum) 생성 로직은 복잡하며,
        # key, values, salt를 조합하고 특정 XOR 연산과 SHA1 해싱을 거칩니다.
        # 다음은 NameError 해결을 위한 기본적인 구조와 GD API의 알려진 로직을
        # 부분적으로 반영한 예시입니다. 완벽한 호환을 위해서는 추가적인 조사가 필요합니다.
        
        # 1. values 리스트의 모든 항목을 문자열로 연결
        combined_values = "".join(values)
        
        # 2. CHK 생성에 사용되는 고유한 키 조합 (일반적으로 GD API에 고정된 값)
        #    원본 질문의 salt와 동일한 값 또는 다른 고정 키를 사용합니다.
        #    Geometry Dash API는 'secret' 파라미터와는 별개로 CHK 생성에
        #    특정 내부 'secret' 키를 사용하는 경우가 많습니다.
        #    여기서는 질문에 명시된 salt 값을 재활용합니다.
        secret_for_chk = salt # 또는 'xI25fpAapCQg'와 같은 고정 값
        
        # 3. 핵심 문자열 생성 (GD API의 특정 규칙에 따라 조합)
        #    일반적으로 key + combined_values + secret_for_chk 순서로 조합합니다.
        raw_string_for_hash = f"{key}{combined_values}{secret_for_chk}"
        
        # 4. SHA1 해싱
        hashed_value = hashlib.sha1(raw_string_for_hash.encode()).hexdigest()
        
        # 5. 해싱된 값에 특정 XOR 연산 적용 (Geometry Dash CHK의 특징)
        #    이 부분은 GD API 버전에 따라 다를 수 있으며, 특정 상수와 해싱 결과의
        #    각 문자를 XOR하는 방식이 흔합니다. (예: 0xBA, 0xCE, 0xAF, 0xA3 등)
        #    예시를 위해 알려진 GD CHK 로직 중 하나인 0xCAF3 상수 사용을 가정합니다.
        xor_constant = 0xCAF3 # 실제 Geometry Dash API에 사용되는 특정 상수
        
        final_chk_chars = []
        # 해싱된 문자열의 각 바이트에 대해 XOR 연산을 수행합니다.
        for char_code in hashed_value.encode('latin-1'): # 'latin-1'은 바이트-문자열 맵핑에 적합
            final_chk_chars.append(chr(char_code ^ xor_constant))
        
        # 6. 최종 CHK 문자열 반환
        return "".join(final_chk_chars)
    
    # --- NameError 해결을 위한 함수 정의 끝 ---
    
    levelString = "H4sIAAAAAAAAC6WQwQ3DIAxFF3IlfxsIUU6ZIQP8AbJChy_GPSZqpF7-A4yfDOfhXcCiNMIqnVYrgYQl8rDwBTZCVbkQRI3oVHbiDU6F2jMF_lesl4q4kw2PJMbovxLBQxTpM3-I6q0oHmXjzx7N0240cu5w0UBNtESRkblevSLHjh8nTubmYJZ2MvMrEITEN0gEJMxlLiMZ28frmj"
    
    data = {
        "gameVersion": 22,
        "accountID": accid,
        "gjp": generate_gjp2(password=passwd), # 'hash' 변수가 전역 범위에 없으므로 함수 호출로 변경
        "userName": user,
        "levelID": 0,
        "levelName": lname,
        "levelDesc": "Q3Vyc2VkIGxldmVsIG55ZWghaGVoIGhlaCBhbnl3YXkgaXRzIGEgdmVyeSBicm9rZW4gbGV2ZWwgaSBzZW50IGEgcmVxdWVzdCB0byByb2Igc2VydmVyIHRocm91Z2ggcHl0aG9uIGxvbA",
        "levelVersion": "-47577473775738573",
        "levelLength": 48483845738573486574873869465947694,
        "audioTrack": 0,
        "auto": 0,
        "password": 314159,
        "original": 55610687,
        "twoPlayer": 0,
        "songID": 839583504693858468444987694,
        "objects": 1,
        "coins": coin,
        "requestedStars": stars,
        "unlisted": 0,
        "ldm": 0,
        "levelString": levelString,
        "seed2": generate_chk(key="41274", values=[generate_upload_seed(levelString)], salt="xI25fpAapCQg"), # NameError 해결!
        "secret": "Wmfd2893gb7"
    }
    
    headers = {
        "User-Agent": ""
    }
    
    url = "http://www.boomlings.com/database/uploadGJLevel21.php"
    
    req = requests.post(url=url, data=data, headers=headers)
    print(req.text)
    

    참고:generate_chk 함수는 Geometry Dash API의 CHK 생성 로직을 간략화하여 구현한 예시입니다. 실제 Geometry Dash API는 버전별로 CHK 생성 로직이 미묘하게 다를 수 있으며, 더 복잡한 문자열 처리와 XOR 연산, 그리고 특정 상수 값을 사용합니다. 완벽하게 동작하는 구현을 원하시면, Geometry Dash 관련 오픈소스 프로젝트나 커뮤니티에서 제공하는 최신 CHK 생성 로직을 참고하시길 강력히 권장합니다.

    향후 예방을 위한 팁

    • 외부 API 문서 철저히 확인: 외부 API를 사용할 때는 공식 문서나 신뢰할 수 있는 개발 가이드를 통해 필요한 모든 함수와 그 구현 방식을 정확히 확인하세요. 특히 커스텀 암호화나 체크섬 로직은 구현 세부 사항까지 확인해야 합니다.
    • 지원 중단된 문서 사용 시 주의: 아카이브되거나 더 이상 지원되지 않는 문서는 내용이 최신 API와 맞지 않거나, 구현에 필요한 중요한 정보가 누락될 수 있으므로 주의하여 사용해야 합니다.
    • 사용 전 함수 정의 확인: 코드를 작성할 때, 어떤 함수를 호출하기 전에 해당 함수가 스크립트 내에 정의되어 있거나, 올바르게 임포트되었는지 항상 확인하는 습관을 들이세요. IDE나 린터(Linter)의 도움을 받는 것도 좋습니다.
    • 에러 메시지 적극 활용: NameError와 같은 에러 메시지는 문제가 된 정확한 ‘이름’을 알려줍니다. 또한 Python은 Did you mean: 'generate_gjp2'?와 같이 관련성이 있을 법한 이름을 힌트로 제공하기도 합니다. 이러한 힌트를 통해 문제가 된 이름이 정의되지 않았음을 인지하고, 혹시 오타가 있었는지 또는 필요한 함수 정의가 빠졌는지 파악하는 데 활용할 수 있습니다.
  • 안녕하세요!

    워드프레스에 오신 것을 환영합니다. 이것은 첫 글입니다. 바로 편집하거나 삭제한 다음 쓰기 시작하세요!