[Continue] Edit & Actions 기능 리뷰

hee.moon·2024년 9월 25일

Continue

목록 보기
1/2

Continue는 Copilot같은 IDE 통합 AI 코드 어시스턴트이다. Continue는 챗 모델을 유연하게 적용할 수 있고 (아직은) 무료라는 점에서 Copilot의 대안으로 평가받는 것 같다.

회사에서는 주로 Copilot의 도움을 받아 개발을 하고 있었는데, AI 코딩 도구를 더 잘 사용하고 싶어서 Continue 오픈소스 프로젝트에 참가하게 됐다. Continue에 익숙해지기 위해 회사에서 조금씩 사용해보고 있었는데 마침 기능 리뷰 시간이 마련되어 있어서 업무 중에 잠깐씩 기능에 집중해 보면서 블로그 글을 작성하게 되었다.


1. Edit 기능 리뷰

간단한 수정사항은 Continue의 Edit 기능으로 현재 파일을 벗어나지 않고 바로 적용할 수 있다. 회사에서 정적 코드 분석 도구로 Sonarqube를 사용하고 있는데, Sonarqube에서 나의 React 코드를 분석한 결과로 ‘Refactor this function to reduce its Cognitive Complexity from 27 to the 15 allowed.’와 같은 결과가 주로 표시되고 있다. 나는 Continue를 인지 복잡도(Cognitive Complexcity)를 개선하는 목적으로 유용하게 사용했다.

인지 복잡도는 코드가 얼마나 이해하기 어려운지를 측정하는 지표로, 코드의 가독성과 유지보수성을 평가하는 데 중요한 역할을 한다.

위 내용은 SummaryArea.tsx 파일의 인지 복잡도가 27이므로 15로 낮추라는 검사 결과이다. 이 문제를 Continue의 Edit 기능으로 해결해보자.

(Edit은 작은 Chat이라 간단한 문제에 적합하다. 왜냐면 복잡한 문제일수록 구체적으로 설명해야 하고 코드 변경사항이 많기 때문이다.)


위 내용은 내 코드가 어떻게 인지 복잡도 27점이 계산됐는지 알려준다. 나는 이 점수표를 먼저 확인한 후 간단하게 해결할 수 있는지(예를 들어 +5점짜리가 있으면 아예 파일을 분리해버린다) 판단한다. 12점이나 줄여야되는데 점수가 골고루 퍼져있다. 어떤 코드가 2점을 받았는지 코드 내용을 확인해보자.

  • SummaryArea.tsx
...

export default function SummaryArea({ certDetail, pathKey }: Readonly<Props>) {
  const { t } = useTranslation();
  const { pageHandler } = useOutletContext<OutletContextProps>();
  const { uid } = certDetail;

  if (pathKey === null) {
    return null;
  }

  let gridItems = [];

  ...

  switch (pathKey) {
    case PathKey.APPLICATIONS: {
      ...
      gridItems = [
        {
          name: t("appls.table.state"),
          value: STATUS_TEXT[Role.ISSUER][status]
        },
        { name: t("appls.table.requesterName"), value: requesterName },
        { name: t("appls.table.ens"), value: ens ?? null },
        { name: t("appls.table.registerCode"), value: registerCode },
        { name: t("appls.table.issueCode"), value: issueCode },
        { name: t("appls.table.createAt"), value: getFormatDate(createAt) }, // 🛑
        {
          name: t("appls.table.updateAt"),
          value: updateAt ? getFormatDate(updateAt) : null // 🛑
        },
        {
          name: t("appls.table.examinationAt"),
          value: examinationAt ? getFormatDate(examinationAt) : null // 🛑
        },
        { name: t("appls.table.examinationName"), value: examinationName }
      ];
      break;
    }
    case PathKey.CERTS: {
      ...
      gridItems = [
        {
          name: t("certs.table.state"),
          value: STATUS_TEXT[Role.REQUESTER][status]
        },
        { name: t("certs.table.ens"), value: ens ?? null },
        { name: t("certs.table.registerCode"), value: registerCode ?? null },
        {
          name: t("certs.table.submitAt"),
          value: submitAt ? getFormatDate(submitAt) : null // 🛑
        },
        {
          name: t("certs.table.updateAt"),
          value: updateAt ? getFormatDate(updateAt) : null // 🛑
        },
        {
          name: t("certs.table.examinationAt"),
          value: examinationAt ? getFormatDate(examinationAt) : null // 🛑
        },
        { name: t("certs.table.issueCode"), value: issueCode ?? null }
      ];
      break;
    }
    case PathKey.MY_QR: {
      ...
      gridItems = [
        {
          name: t("myQr.table.state"),
          value: STATUS_TEXT[Role.REQUESTER][status]
        },
        { name: t("myQr.table.ens"), value: ens ?? null },
        { name: t("myQr.table.registerCode"), value: registerCode ?? null },
        {
          name: t("myQr.table.submitAt"),
          value: submitAt ? getFormatDate(submitAt) : null // 🛑
        },
        { name: t("myQr.table.issueCode"), value: issueCode ?? null },
        {
          name: t("myQr.table.examinationAt"),
          value: examinationAt ? getFormatDate(examinationAt) : null // 🛑
        },
        {
          name: t("myQr.table.expiryAt"),
          value: expiryAt ? getFormatDate(expiryAt) : null // 🛑
        }
      ];
      break;
    }
  }

  return (
    <div className="w-full">
      ...
    </div>
  );
}

...

이 코드는 200줄 정도의 SummaryArea 컴포넌트이다. 이 중 붉은색 이모지로 표시한 부분이 +2점으로 계산된 코드이다. getFormatDate 함수는 타입이 String인 값을 리턴하기 때문에 분기 처리한 삼항연산자가 switch문 내부에서 실행되어 +2점으로 계산된 것으로 보인다. 이걸 고치면 18점을 낮출 수 있기 때문에 간단하게 해결할 수 있을 것 같다고 생각했다.



(참고) Sonarqube 인지 복잡도 계산 방식

인지 복잡도는 코드의 구조와 흐름을 기반으로 계산된다. SonarQube는 다음과 같은 요소들을 고려하여 인지 복잡도를 계산한다:

  1. 제어 흐름 구조:
    - 조건문 (ifelse ifelse)
    - 반복문 (forwhiledo-while)
    - 분기문 (switchcase)
    - 예외 처리 (trycatchfinally)
  2. 중첩 수준:
    - 중첩된 제어 구조는 인지 복잡도를 증가시킵니다. 예를 들어, 중첩된 if 문이나 반복문은 각각의 중첩 수준에 따라 복잡도가 증가합니다.
  3. 논리 연산자:
    - 복합 조건문에서 사용되는 논리 연산자 (&&||)는 인지 복잡도를 증가시킵니다.
  4. 재귀 호출:
    - 재귀 호출은 인지 복잡도를 증가시킵니다.
  5. 무조건적인 분기:
    - gotobreakcontinue와 같은 무조건적인 분기문은 인지 복잡도를 증가시킵니다.


1-1. 코드를 선택(하이라이트)하고 edit 프롬프트 실행하기

키보드나 마우스로 코드를 선택한 후 Cmd/Ctrl + i를 입력해 edit 입력창을 실행한다.

1-2. 프롬프트에 수정하고 싶은 내용 작성 후 Enter


1-3. Accept 또는 Reject 하기

하이라이트를 너무 작게 잡았는지 renderDateValue의 위치가 조금 이상(?)하지만 약간 수정만 하면 될 것 같다는 판단으로 Accept 처리를 했다. 테스트 후 프로젝트의 날짜 포맷을 관리하는 파일에 모듈로 옮겨놨다.

다시 Sonarqube를 실행한 후 해당 파일의 리포트를 확인하면, 인지 복잡도가 15 이하로 내려가서 이 파일의 Code smell이 사라진 것을 확인할 수 있다.


2. Actions 기능 리뷰

Actions 기능은 IDE 사이드바에 있는 채팅 영역에서 슬래시(/) 명령 등 다양한 트리거를 통해 복잡한 개발자 요구사항을 처리하기 위해 사용한다.


2-1. 채팅창에 코드 추가하기

채팅창에서 코드를 추가하려면 코드 영역을 선택한 후 Cmd/Ctrl + L을 사용한다. 내 VS Code에서는 Cmd/Ctrl + L에 다른 명령어가 할당되어 있어서 Shift + Cmd/Ctrl + L을 사용해줘야 했다.

그러면 사이드바에 선택된 코드가 추가되는 것을 확인할 수 있다.


2-2. 슬래시(/) 명령 둘러보기

채팅창에서 슬래시를 입력하면 아래 그림과 같이 드롭다운 리스트가 표시된다. 각 명령은 다음과 같은 기능을 제공한다:

  • /edit - 선택된 코드 수정하기
  • /comment - 선택된 코드에 대한 주석 작성하기
  • /share - 현재 채팅 세션을 마크다운 형식으로 내보내기
  • /cmd - Shell 명령어 생성하기
  • /commit - git 커밋 메시지 생성하기
  • /test - 하이라이트된 코드에 대한 유닛 테스트 작성하기

공식 문서를 찾아보니 /so라는 것도 있어서 문제해결에 유용할 것 같다. /so를 사용하면 스택오버플로우에서 검색할 수 있다.

Slash commands 문서 | Continue


.prompt 파일을 생성하여 직접 슬래시 명령을 만들 수도 있다. 예를 들어, 선택된 코드의 인지 복잡도를 낮추는 작업을 반복하기 때문에 /comp 명령어를 직접 만드는 것이다.

// .prompts/comp.prompt

<system>
You are an expert programmer
</system>

{{{ input }}}

Reduce cognitive complexity above selected code. 

{{ input }}은 선택한 코드를 의미한다. 실행해보면 아래와 같은 결과를 응답한다.


2-3. 터미널에서 발생한 에러 디버그

Cmd/Ctrl + Shift + R을 사용하면 현재 열려있는 터미널에서 발생한 에러에 대한 해결 방법을 제안해준다.

ERROR와 같은 문자가 없어도 에러라고 가정한 후 설명과 제안을 던져주는 것 같다.


3. Edit과 Actions 기능의 차이

코드 리팩토링을 위해 Continue의 Edit 기능과 Actions 기능을 모두 사용할 수 있다. 위에서 시도해본 인지 복잡도 개선 작업을 Actions의 슬래시 명령어로 처리했을 때 어떻게 응답하는지 살펴보자.

공통적으로 아래 내용을 사용한다.

  • 질문: "삼항연산자를 사용하지 않도록 renderDateValue 함수를 생성해 주세요."
  • 선택한 코드영역
// SummaryArea.tsx

{
  name: t("appls.table.updateAt"),
  value: updateAt ? getFormatDate(updateAt) : null
},
{
  name: t("appls.table.examinationAt"),
  value: examinationAt ? getFormatDate(examinationAt) : null
},

3-1. Edit 기능으로 리팩토링할 때 (단축키 Cmd/Ctrl + I)

실행 순서 | 코드 선택 -> Cmd/Ctrl + I -> 프롬프트에 질문('삼항연산자를 사용하지 않도록 renderDateValue 함수를 생성해 주세요.') 입력 -> Enter

앞서 살펴본대로 Continue가 현재 파일에서 코드를 수정한 후에 전후 차이를 보여주고 개발자가 Accept 또는 Reject를 선택하게 하는 방식이다.

실제로 LLM 프롬프트 로그를 확인해보자.
(확인 방법 | VS Code -> 패널에서 Output 선택 -> 드롭다운에서 'Continue - LLM Prompts/Completions' 선택)

코드 때문에 로그 길이가 길어서 형식 위주로 살펴보는 것이 좋을 것 같다(괄호를 사용해 주석을 달았다).

==========================================================================
==========================================================================
Settings:
contextLength: 128000
model: gpt-4o
maxTokens: 1024
raw: true
log: undefined

############################################

The user has requested a section of code in a file to be rewritten.

This is the prefix of the file:

(파일 내용 중 선택한 코드의 앞부분)


This is the suffix of the file:

(파일 내용 중 선택한 코드 포함 뒷부분)


This is the code to rewrite:

        {
          name: t("appls.table.updateAt"),
          value: updateAt ? getFormatDate(updateAt) : null
        },
        {
          name: t("appls.table.examinationAt"),
          value: examinationAt ? getFormatDate(examinationAt) : null
        },


The user's request is: "삼항연산자를 사용하지 않도록 renderDateValue 함수를 생성해 주세요."

Here is the rewritten code:

Continue의 Edit은 SummaryArea.tsx 파일 전체 내용 중 어떤 부분이 재작성되어야 하는지를 개발자 질문과 함께 요청한다. 하이라이트된 코드만 고려하는 것이 아니라 파일수준까지 고려하는 것 같다.


3-2. Actions 기능으로 리팩토링할 때 (단축키 Cmd/Ctrl + L)

실행 순서 | 코드 선택 -> Cmd/Ctrl + L -> 채팅창에 '/edit 삼항연산자를 사용하지 않도록 renderDateValue 함수를 생성해 주세요.' 입력 -> Enter

Enter를 치면 새로운 탭에서 병렬 편집기가 실행된다. Edit과 마찬가지로 Accept와 Reject 선택이 가능하다. 하지만 Actions는 Edit과 다르게 function 키워드를 사용했고 if문을 사용하지 않은 제안을 보내줬다. 즉 Edit과 응답 내용이 다르다.

Edit과 비교해보기 위해 LLM 프롬프트 로그를 확인해보자. 일단, Edit보다 로그가 더 길고 내용이 복잡하다.

==========================================================================
==========================================================================
Settings:
contextLength: 128000
model: gpt-4o
maxTokens: 4096
temperature: 0.5
log: undefined

############################################

<system>
You are an expert software developer. You give helpful and concise responses.

<user>
/test CertSection.tsx를 테스트하세요. (이번 질문이 아닌, 챗 세션에 있는 첫 번째 질문)

<assistant>


<user>
(이번 질문이 아닌, 챗 세션에 있는 두 번째 질문)

<assistant>
(위 질문에 대한 응답)

Here's the refactored code:
(코드 생략)

<user>
(이번 질문이 아닌, 챗 세션에 있는 세 번째 질문)

<assistant>
(위 질문에 대한 응답)

.
.
.

<user>
Take the file prefix and suffix into account, but only rewrite the code_to_edit as specified in the user_request. The code you write in modified_code_to_edit will replace the code between the code_to_edit tags. Do NOT preface your answer or write anything other than code. The </modified_code_to_edit> tag should be written to indicate the end of the modified code section. Do not ever use nested tags.
// 번역: 
// 파일 접두사와 접미사를 고려하되 
// user_request에 지정된 대로 code_to_edit만 다시 작성하세요. 
// Modified_code_to_edit에 작성한 코드는 code_to_edit 태그 사이의 코드를 대체합니다. 
// 답변 앞에 머리말을 붙이거나 코드 이외의 다른 내용을 작성하지 마세요. 
// </modified_code_to_edit> 태그는 수정된 코드 섹션의 끝을 나타내도록 작성되어야 합니다. 
// 중첩된 태그를 절대 사용하지 마세요.

Example:

<file_prefix>
class Database:
    def __init__(self):
        self._data = {{}}

    def get(self, key):
        return self._data[key]

</file_prefix>
<code_to_edit>
    def set(self, key, value):
        self._data[key] = value
</code_to_edit>
<file_suffix>

    def clear_all():
        self._data = {{}}
</file_suffix>
<user_request>
Raise an error if the key already exists.
</user_request>
<modified_code_to_edit>
    def set(self, key, value):
        if key in self._data:
            raise KeyError(f"Key {{key}} already exists")
        self._data[key] = value
</modified_code_to_edit>

Main task:

<file_prefix>
(SummaryArea.tsx 코드 선택 영역 앞부분)
</file_prefix>
<code_to_edit>
(SummaryArea.tsx 코드 선택 영역)
</code_to_edit>
<file_suffix>
(SummaryArea.tsx 코드 선택 영역 뒷부분)
</file_suffix>

<user_request>
/edit src/features/examine-appl/SummaryArea.tsx (93-100)
        {
          name: t("appls.table.updateAt"),
          value: updateAt ? getFormatDate(updateAt) : null
        },
        {
          name: t("appls.table.examinationAt"),
          value: examinationAt ? getFormatDate(examinationAt) : null
        },

삼항연산자를 사용하지 않도록 renderDateValue 함수를 생성해 주세요.
</user_request>
<modified_code_to_edit>


==========================================================================
==========================================================================
Completion:
const renderDateValue = (date: string | null) => {
  return date ? getFormatDate(date) : null;
};

{
  name: t("appls.table.updateAt"),
  value: renderDateValue(updateAt)
},
{
  name: t("appls.table.examinationAt"),
  value: renderDateValue(examinationAt)
},

Actions의 로그 내용을 살펴보면 Actions는 하이라이트된 파일 수준에 머무는 것이 아니라 이전 질문까지도 요청에 포함시키는 것을 알 수 있다.


차이를 정리해보면 아래와 같다.

항목EditActions
요청에 포함되는 내용- 하이라이트된 코드의 파일
- 하이라이트된 코드
- 챗 세션에서 작업했던 질문(과거 질문)
- 하이라이트된 코드의 파일
- 하이라이트된 코드
제안 위치현재 파일 내부새 탭의 병렬 편집기

또한 Edit과 Actions에서 각각 동일한 질문을 여러번 했을 때 항상 동일한 응답을 보내주진 않았다. 그래서 여기까지만 분석해봤을 때 Edit과 Actions의 응답 내용 자체만을 비교하는 것은 큰 의미가 없는 것 같다.


Continue 더 알아보기 | https://www.continue.dev

profile
Frontend Engineer

0개의 댓글