다음을 통해 공유


적합한 Visual Studio 확장성 모델 선택

세 가지 기본 확장성 모델인 VSSDK, Community Toolkit 및 VisualStudio.Extensibility를 사용하여 Visual Studio를 확장할 수 있습니다. 이 문서에서는 각각의 장단점을 다룹니다. 간단한 예제를 사용하여 모델 간의 아키텍처 및 코드 차이점을 강조 표시합니다.

VSSDK

VSSDK(또는 Visual Studio SDK)는 Visual Studio Marketplace대부분의 확장을 기반으로 하는 모델입니다. 이 모델은 Visual Studio 자체가 빌드되는 모델입니다. 가장 완전하고 강력하지만 올바르게 배우고 사용하는 것이 가장 복잡합니다. VSSDK를 사용하는 확장은 Visual Studio 자체와 동일한 프로세스에서 실행됩니다. Visual Studio와 동일한 프로세스에서 로드한다는 것은 액세스 위반, 무한 루프 또는 기타 문제가 있는 확장이 Visual Studio를 중단하거나 중단하여 고객 환경을 저하시킬 수 있음을 의미합니다. 또한 확장은 Visual Studio와 동일한 프로세스에서 실행되므로 .NET Framework를 사용하여 빌드할 수 있습니다. .NET 5 이상을 사용하는 라이브러리를 사용하거나 통합하려는 Extender는 VSSDK를 사용할 수 없습니다.

VSSDK의 API는 Visual Studio 자체가 변환되고 진화함에 따라 수년에 걸쳐 집계되었습니다. 단일 확장에서 레거시 각인의 COM 기반 API와 씨름하고, DTE기만적인 단순성을 통해 산들바람을 부리고, MEF 가져오기 및 내보내기를 땜질할 수 있습니다. 파일 시스템의 텍스트를 읽고 편집기 내에서 현재 활성 문서의 시작 부분에 삽입하는 확장을 작성하는 예제를 살펴보겠습니다. 다음 코드 조각은 VSSDK 기반 확장에서 명령이 호출될 때 처리할 코드를 보여 줍니다.

private void Execute(object sender, EventArgs e)
{
    var textManager = package.GetService<SVsTextManager, IVsTextManager>();
    textManager.GetActiveView(1, null, out IVsTextView activeTextView);

    if (activeTextView != null && activeTextView is IVsTextViewEx nativeView)
    {
        ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));

        IComponentModel2 compService = package.GetService<SComponentModel, IComponentModel2>();
        IVsEditorAdaptersFactoryService editorAdapter = compService.GetService<IVsEditorAdaptersFactoryService>();
        var wpfTextView = editorAdapter?.GetWpfTextView(activeTextView);

        if (frameValue is IVsWindowFrame frame && wpfTextView != null)
        {
            var fileText = File.ReadAllText(Path.Combine(Path.GetTempPath(), "test.txt"));
            wpfTextView.TextBuffer?.Insert(0, fileText);
        }
    }
}

또한 UI에 배치할 위치, 연결된 텍스트 등과 같은 명령 구성을 정의하는 파일을 제공해야 .vsct 합니다.

<Commands package="guidVSSDKPackage">
    <Groups>
        <Group guid="guidVSSDKPackageCmdSet" id="MyMenuGroup" priority="0x0600">
        <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS" />
        </Group>
    </Groups>

    <Buttons>
        <Button guid="guidVSSDKPackageCmdSet" id="InsertTextCommandId" priority="0x0100" type="Button">
        <Parent guid="guidVSSDKPackageCmdSet" id="MyMenuGroup" />
        <Icon guid="guidImages" id="bmpPic1" />
        <Strings>
            <ButtonText>Invoke InsertTextCommand (Unwrapped Community Toolkit)</ButtonText>
        </Strings>
        </Button>
        <Button guid="guidVSSDKPackageCmdSet" id="cmdidVssdkInsertTextCommand" priority="0x0100" type="Button">
        <Parent guid="guidVSSDKPackageCmdSet" id="MyMenuGroup" />
        <Icon guid="guidImages1" id="bmpPic1" />
        <Strings>
            <ButtonText>Invoke InsertTextCommand (VSSDK)</ButtonText>
        </Strings>
        </Button>
    </Buttons>

    <Bitmaps>
        <Bitmap guid="guidImages" href="Resources\InsertTextCommand.png" usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows, bmpPicStrikethrough" />
        <Bitmap guid="guidImages1" href="Resources\VssdkInsertTextCommand.png" usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows, bmpPicStrikethrough" />
    </Bitmaps>
</Commands>

샘플에서 볼 수 있듯이 코드는 직관적이지 않은 것처럼 보일 수 있으며 .NET에 익숙한 사람이 쉽게 선택할 가능성은 거의 없습니다. 많은 개념을 학습해야 하며 활성 편집기 텍스트에 액세스하기 위한 API 패턴은 구식입니다. 대부분의 확장기의 경우 VSSDK 확장은 온라인 원본에서 복사 및 붙여넣기로 구성되며, 이로 인해 디버깅 세션, 시행 착오 및 좌절이 발생할 수 있습니다. 대부분의 경우 VSSDK 확장은 확장 목표를 달성하는 가장 쉬운 방법이 아닐 수 있습니다(경우에 따라 유일한 선택임).

커뮤니티 도구 키트

커뮤니티 도구 키트는 더 쉬운 개발 환경을 위해 VSSDK를 래핑하는 Visual Studio용 오픈 소스 커뮤니티 기반 확장성 모델입니다. VSSDK를 기반으로 하기 때문에 VSSDK와 동일한 제한 사항이 적용됩니다(즉, .NET Framework만, Visual Studio의 나머지 부분으로부터 격리되지 않음). 커뮤니티 도구 키트를 사용하여 파일 시스템에 읽은 텍스트를 삽입하는 확장을 작성하는 동일한 예제를 계속 진행하면 명령 처리기에 대해 다음과 같이 확장이 작성됩니다.

protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
    DocumentView docView = await VS.Documents.GetActiveDocumentViewAsync();
    if (docView?.TextView == null) return;
    var fileText = File.ReadAllText(Path.Combine(Path.GetTempPath(), "test.txt"));
    docView.TextBuffer?.Insert(0, fileText);
}

결과 코드는 단순성과 직관적인 측면에서 VSSDK에서 훨씬 향상되었습니다. 줄 수가 크게 감소했을 뿐만 아니라 결과 코드도 적절해 보입니다. 차이점과 차이점을 SVsTextManager IVsTextManager이해할 필요가 없습니다. API는 더 많은 모양과 느낌 . 공통 작업의 우선 순위 지정과 함께 일반적인 명명 및 비동기 패턴을 수용하는 NET 친화적입니다. 그러나 커뮤니티 도구 키트는 여전히 기존 VSSDK 모델을 기반으로 하므로 기본 구조의 흔적이 뚫리고 있습니다. 예를 들어 .vsct 파일은 여전히 필요합니다. Community Toolkit은 API를 간소화하는 훌륭한 작업을 수행하지만 VSSDK의 제한에 바인딩되며 확장 구성을 더 간단하게 만들 수 있는 방법이 없습니다.

VisualStudio.Extensibility

VisualStudio.Extensibility는 확장이 기본 Visual Studio 프로세스 외부에서 실행되는 새로운 확장성 모델입니다. 이러한 기본적인 아키텍처 변화로 인해 이제 VSSDK 또는 Community Toolkit에서 사용할 수 없는 확장에 새로운 패턴과 기능을 사용할 수 있습니다. VisualStudio.Extensibility는 일관되고 사용하기 쉬운 완전히 새로운 API 집합을 제공하며, 확장이 .NET을 대상으로 하고, Visual Studio의 나머지 확장에서 발생하는 버그를 격리하고, 사용자가 Visual Studio를 다시 시작하지 않고 확장을 설치할 수 있도록 합니다. 그러나 새 모델은 새로운 기본 아키텍처를 기반으로 하므로 VSSDK 및 커뮤니티 도구 키트의 폭은 아직 없습니다. 이러한 격차를 해소하기 위해 프로세스에서 VisualStudio.Extensibility 확장을 실행하여 VSSDK API를 계속 사용할 수 있습니다. 그러나 이렇게 하면 확장이 .NET Framework를 기반으로 하는 Visual Studio와 동일한 프로세스를 공유하므로 .NET Framework만 대상으로 지정할 수 있습니다.

VisualStudio.Extensibility를 사용하여 파일에서 텍스트를 삽입하는 확장을 작성하는 동일한 예제를 계속 진행하면 명령 처리를 위해 확장이 다음과 같이 작성됩니다.

public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
    var activeTextView = await context.GetActiveTextViewAsync(cancellationToken);
    if (activeTextView is not null)
    {
        var editResult = await Extensibility.Editor().EditAsync(batch =>
        {
            var fileText = File.ReadAllText(Path.Combine(Path.GetTempPath(), "test.txt"));

            ITextDocumentEditor editor = activeTextView.Document.AsEditable(batch);
            editor.Insert(0, fileText);
        }, cancellationToken);
                
    }
}

배치, 텍스트 등에 대한 명령을 구성하려면 더 이상 파일을 제공할 .vsct 필요가 없습니다. 대신 코드를 통해 수행됩니다.

public override CommandConfiguration CommandConfiguration => new("%VisualStudio.Extensibility.Command1.DisplayName%")
{
    Icon = new(ImageMoniker.KnownValues.Extension, IconSettings.IconAndText),
    Placements = [CommandPlacement.KnownPlacements.ExtensionsMenu],
};

이 코드는 이해하고 따르기 쉽습니다. 대부분의 경우 명령 구성을 위해 IntelliSense의 도움을 받아 편집기를 통해서만 이 확장을 작성할 수 있습니다.

다양한 Visual Studio 확장성 모델 비교

샘플에서 VisualStudio.Extensibility를 사용하면 명령 처리기에서 Community Toolkit보다 더 많은 코드 줄이 있음을 알 수 있습니다. 커뮤니티 도구 키트는 VSSDK를 사용하여 확장을 빌드하는 데 유용한 래퍼입니다. 그러나 VisualStudio.Extensibility의 개발로 이어진 것이 바로 명확하지 않은 문제가 있습니다. 특히 커뮤니티 도구 키트에서 작성하고 이해하기 쉬운 코드가 발생하는 것처럼 보이는 경우 전환과 필요성을 이해하려면 예제를 검토하고 코드의 심층 계층에서 발생하는 작업을 비교해 보겠습니다.

이 샘플에서 코드를 신속하게 래프 해제하고 VSSDK 쪽에서 실제로 호출되는 항목을 확인할 수 있습니다. VSSDK에 필요한 세부 정보가 많기 때문에 명령 실행 코드 조각에만 집중할 예정이며 커뮤니티 도구 키트가 잘 숨겨야 합니다. 그러나 기본 코드를 살펴보면 여기서 단순함이 절충이 되는 이유를 이해할 수 있습니다. 단순성은 일부 기본 세부 정보를 숨기며, 이로 인해 예기치 않은 동작, 버그, 성능 문제 및 충돌까지 발생할 수 있습니다. 다음 코드 조각은 VSSDK 호출을 표시하기 위해 래핑 해제된 커뮤니티 도구 키트 코드를 보여 줍니다.

private void Execute(object sender, EventArgs e)
{
    package.JoinableTaskFactory.RunAsync(async delegate
    {
        var textManager = await package.GetServiceAsync<SVsTextManager, IVsTextManager>();
        textManager.GetActiveView(1, null, out IVsTextView activeTextView);

        if (activeTextView != null && activeTextView is IVsTextViewEx nativeView)
        {
            await package.JoinableTaskFactory.SwitchToMainThreadAsync();
            ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));

            IComponentModel2 compService = package.GetService<SComponentModel, IComponentModel2>();
            IVsEditorAdaptersFactoryService editorAdapter = compService.GetService<IVsEditorAdaptersFactoryService>();
            var wpfTextView = editorAdapter?.GetWpfTextView(activeTextView);

            if (frameValue is IVsWindowFrame frame && wpfTextView != null)
            {
                var fileText = File.ReadAllText(Path.Combine(Path.GetTempPath(), "test.txt"));
                wpfTextView.TextBuffer?.Insert(0, fileText);    
            }
        }
    });
}

여기서는 몇 가지 문제가 있으며 모두 스레딩 및 비동기 코드를 중심으로 진행됩니다. 각 항목에 대해 자세히 살펴보겠습니다.

비동기 API 및 비동기 코드 실행

가장 먼저 주의해야 할 점은 Community Toolkit의 ExecuteAsync 메서드가 VSSDK에서 래핑된 비동기 방화 및 잊어버리기 호출이라는 점입니다.

package.JoinableTaskFactory.RunAsync(async delegate
{
  …
});

VSSDK 자체는 핵심 API 관점에서 비동기 명령 실행을 지원하지 않습니다. 즉, 명령이 실행될 때 VSSDK는 백그라운드 스레드에서 명령 처리기 코드를 실행하고, 명령 처리기가 완료될 때까지 기다렸다가 실행 결과가 있는 원래 호출 컨텍스트로 사용자를 반환할 방법이 없습니다. 따라서 커뮤니티 도구 키트의 ExecuteAsync API가 구문적으로 비동기적이긴 하지만 비동기 실행은 사실이 아닙니다. 또한 비동기 실행 방법은 무시하므로 이전 호출이 먼저 완료되기를 기다리지 않고 ExecuteAsync를 반복해서 호출할 수 있습니다. 커뮤니티 도구 키트는 확장자가 일반적인 시나리오를 구현하는 방법을 검색할 수 있도록 돕는 측면에서 더 나은 환경을 제공하지만 궁극적으로 VSSDK의 근본적인 문제를 해결할 수는 없습니다. 이 경우 기본 VSSDK API는 비동기식이 아니며 Community Toolkit에서 제공하는 fire-and-forget 도우미 메서드는 비동기 생성 및 클라이언트 상태 작업을 제대로 처리할 수 없습니다. 디버그하기 어려운 몇 가지 문제가 숨겨질 수 있습니다.

UI 스레드 및 백그라운드 스레드

커뮤니티 도구 키트에서 래핑된 비동기 호출의 또 다른 오류는 코드 자체가 여전히 UI 스레드에서 실행되고, UI를 동결할 위험을 감수하지 않으려는 경우 백그라운드 스레드로 올바르게 전환하는 방법을 알아내는 확장 개발자에 있다는 것입니다. 커뮤니티 도구 키트가 VSSDK의 노이즈 및 추가 코드를 숨길 수 있는 만큼 Visual Studio에서 스레딩의 복잡성을 이해해야 합니다. VS 스레딩에서 배운 첫 번째 단원 중 하나는 백그라운드 스레드에서 모든 것을 실행할 수 없다는 것입니다. 즉, 모든 것이 스레드로부터 안전한 것은 아니며, 특히 COM 구성 요소로 이동하는 호출도 안전합니다. 따라서 위의 예제에서는 기본(UI) 스레드로 전환하라는 호출이 있음을 알 수 있습니다.

await package.JoinableTaskFactory.SwitchToMainThreadAsync();
ErrorHandler.ThrowOnFailure(nativeView.GetWindowFrame(out object frameValue));

물론 이 호출 후에 백그라운드 스레드로 다시 전환할 수 있습니다. 그러나 커뮤니티 도구 키트를 사용하는 extender인 경우 코드가 있는 스레드에 주의를 기울이고 UI를 동결할 위험이 있는지 확인해야 합니다. Visual Studio에서 스레딩은 제대로 하기 어려우며 교착 상태를 방지하기 위해 적절한 사용 JoinableTaskFactory 이 필요합니다. 스레딩을 올바르게 처리하는 코드를 작성하는 데 어려움을 겪는 것은 내부 Visual Studio 엔지니어에게도 지속적인 버그 소스였습니다. 반면 VisualStudio.Extensibility는 확장 프로세스를 실행하고 비동기 API 엔드투엔드에 의존하여 이 문제를 완전히 방지합니다.

단순 API와 간단한 개념

커뮤니티 도구 키트는 VSSDK의 많은 복잡성을 숨기므로 확장자에게 잘못된 단순성을 제공할 수 있습니다. 동일한 샘플 코드를 계속 살펴보겠습니다. extender가 Visual Studio 개발의 스레딩 요구 사항을 모르는 경우 해당 코드가 백그라운드 스레드에서 항상 실행된다고 가정할 수 있습니다. 텍스트에서 파일을 읽는 호출이 동기적이라는 사실에는 아무런 문제가 없습니다. 백그라운드 스레드에 있는 경우 해당 파일이 크면 UI가 고정되지 않습니다. 그러나 코드가 VSSDK로 래핑 해제되면 그렇지 않다는 것을 알게 됩니다. 따라서 커뮤니티 도구 키트의 API는 VSSDK와 연결되어 있으므로 이해하기 쉽고 쓰기가 더 응집력 있는 것처럼 보이지만 VSSDK 제한 사항이 적용됩니다. 단순성은 확장기가 이해하지 못하는 경우 더 많은 해를 끼칠 수 있는 중요한 개념에 대해 광택을 줄 수 있습니다. VisualStudio.Extensibility는 Out-of-process 모델 및 비동기 API를 기반으로 하여 주 스레드 종속성으로 인해 발생하는 많은 문제를 방지합니다. 프로세스가 부족하면 스레딩이 가장 간단해지지만 이러한 많은 이점은 In-Process에서 실행되는 확장으로도 이월됩니다. 예를 들어 VisualStudio.Extensibility 명령은 항상 백그라운드 스레드에서 실행됩니다. VSSDK API와 상호 작용하려면 스레딩 작동 방식에 대한 심층적인 지식이 필요하지만 적어도 이 예제와 같이 실수로 인한 중단 비용은 지불하지 않습니다.

비교 차트

이전 섹션에서 자세히 설명한 내용을 요약하기 위해 다음 표에서는 빠른 비교를 보여 드립니다.

VSSDK 커뮤니티 도구 키트 VisualStudio.Extensibility
런타임 지원 .NET Framework .NET Framework .NET
Visual Studio에서 격리
단순 API
비동기 실행 및 API
VS 시나리오 폭
다시 시작하지 않고 설치 가능
VS 2019 이하 지원

Visual Studio 확장성 요구 사항에 비교를 적용하는 데 도움이 되도록 몇 가지 샘플 시나리오와 사용할 모델에 대한 권장 사항은 다음과 같습니다.

  • Visual Studio 확장 개발을 접하는 경우 가장 쉬운 온보딩 환경을 통해 고품질 확장을 만들고 싶고 Visual Studio 2022 이상만 지원하면 됩니다.
    • 이 경우 VisualStudio.Extensibility를 사용하는 것이 좋습니다.
  • Visual Studio 2022 이상을 대상으로 하는 확장을 작성하려고 합니다. 그러나 VisualStudio.Extensibility는 필요한 모든 기능을 지원하지는 않습니다.
  • 기존 확장이 있고 최신 버전을 지원하도록 업데이트하려고 합니다. 확장에서 가능한 한 많은 버전의 Visual Studio를 지원하려고 합니다.
    • VisualStudio.Extensibility는 Visual Studio 2022 이상만 지원하므로 VSSDK 또는 커뮤니티 도구 키트가 이 경우에 가장 적합한 옵션입니다.
  • .NET을 활용하고 다시 시작하지 않고 설치하기 위해 VisualStudio.Extensibility로 마이그레이션하려는 기존 확장이 있습니다.
    • VisualStudio.Extensibility는 하위 수준의 Visual Studio 버전을 지원하지 않으므로 이 시나리오는 약간 더 미묘합니다.
      • 기존 확장이 Visual Studio 2022만 지원하고 필요한 모든 API가 있는 경우 VisualStudio.Extensibility를 사용하도록 확장을 다시 작성하는 것이 좋습니다. 그러나 확장에 VisualStudio.Extensibility에 아직 없는 API가 필요한 경우 계속 진행하여 프로세스에서 실행되는 VisualStudio.Extensibility 확장을 만들어 VSSDK API에 액세스할 수 있습니다. VisualStudio.Extensibility가 지원을 추가하고 확장이 프로세스 부족으로 이동되므로 초과 작업으로 VSSDK API 사용을 제거할 수 있습니다.
      • 확장에서 VisualStudio.Extensibility를 지원하지 않는 하위 수준의 Visual Studio 버전을 지원해야 하는 경우 코드베이스에서 일부 리팩터링을 수행하는 것이 좋습니다. Visual Studio 버전에서 공유할 수 있는 모든 공통 코드를 자체 라이브러리로 끌어오고 다른 확장성 모델을 대상으로 하는 별도의 VSIX 프로젝트를 만듭니다. 예를 들어 확장에서 Visual Studio 2019 및 Visual Studio 2022를 지원해야 하는 경우 솔루션에서 다음 프로젝트 구조를 채택할 수 있습니다.
        • MyExtension-VS2019(Visual Studio 2019를 대상으로 하는 VSSDK 기반 VSIX 컨테이너 프로젝트)
        • MyExtension-VS2022(Visual Studio 2022를 대상으로 하는 VSSDK+VisualStudio.Extensibility 기반 VSIX 컨테이너 프로젝트)
        • VSSDK-CommonCode(VSSDK를 통해 Visual Studio API를 호출하는 데 사용되는 일반적인 라이브러리입니다. 두 VSIX 프로젝트 모두 이 라이브러리를 참조하여 코드를 공유할 수 있습니다.)
        • MyExtension-BusinessLogic(확장의 비즈니스 논리와 관련된 모든 코드를 포함하는 공통 라이브러리입니다. 두 VSIX 프로젝트 모두 이 라이브러리를 참조하여 코드를 공유할 수 있습니다.)

다음 단계

확장기는 새 확장을 만들거나 기존 확장을 향상시킬 때 VisualStudio.Extensibility부터 시작하고 지원되지 않는 시나리오가 발생하는 경우 VSSDK 또는 커뮤니티 도구 키트를 사용하는 것이 좋습니다. 시작하려면 VisualStudio.Extensibility를 사용하여 이 섹션에 제시된 설명서를 살펴보세요. 샘플 또는 파일 문제에 대한 VSExtensibility GitHub 리포지토리를 참조할 수도 있습니다.