수정 이력(1건, 최종 수정 2026년 09월 01일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- 일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635169)
- 최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635168)
이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.
小村 豪 (2026). 「.NET 8의 DLL을 형 있게 VBA에서 쓰는 방법 - COM 공개 + dscom으로 TLB를 생성」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635168 https://comcomponent.com/ko/blog/2026/03/16/007-dotnet8-dll-typed-vba-com-dscom-tlb/
- DOI(최신 버전)
- 10.5281/zenodo.21635168
- DOI(이 버전)
- 10.5281/zenodo.22217451
VBA에서 .NET 8의 처리를 호출하고 싶은 상황은 지금도 흔합니다. 특히 Excel이나 Access의 기존 자산은 그대로 남기면서, 무거운 처리, 문자열 처리, HTTP, 암호화, 업무 로직 같은 부분만 C#으로 옮기고 싶을 때입니다.
다만 CreateObject로 지연 바인딩에 치우치면 VBA 쪽은 Object투성이가 됩니다. IntelliSense는 약해지고, 메서드 이름의 오타는 실행 시까지 발견되지 않으며, 점점 문자열 의존의 수렁에 빠집니다.
flowchart TB
accTitle: 지연 바인딩의 수렁
accDescr: CreateObject로 지연 바인딩에 치우치면 VBA 쪽은 Object투성이가 되고, IntelliSense가 약해지며, 메서드 이름의 오타가 실행 시까지 발견되지 않는다는 것을 보여주는 그림.
lb1["CreateObject로 지연 바인딩"] --> lb2["VBA 쪽은 Object투성이"]
lb2 --> lb3["IntelliSense가 약해진다"]
lb2 --> lb4["typo가 실행 시까지 발견되지 않는다"]
그림 1: 지연 바인딩에 치우칠수록 문자열 의존의 수렁에 빠져든다.
그래서 이번에는 .NET 8의 DLL을 COM 공개하고, dscom으로 타입 라이브러리(TLB)를 생성하고, VBA에서 조기 바인딩으로 형 있게 이용하는 부분에 좁혀서 정리합니다.
.NET Framework + RegAsm의 옛날이야기, IDL을 손으로 써서 MIDL로 굳히는 이야기, Reg-Free COM 이야기는 이번에는 옆으로 치워 둡니다. 여기서는 .NET 8 / COM host / dscom / VBA early binding의 외길만 다룹니다.
또한 이 글에 나오는 코드는 빌드·검증할 수 있는 샘플 일체(COM 공개 라이브러리, TLB 생성·등록 스크립트, VBA 모듈, 단위 테스트)로 GitHub에 공개하고 있습니다.
dotnet8-dll-typed-vba-com-dscom-tlb - komurasoft-blog-samples (GitHub)
필요한 환경
| 항목 | 필요한 것 |
|---|---|
| OS | Windows. COM 등록을 하므로 regsvr32를 관리자 권한으로 실행할 수 있어야 합니다 |
| .NET SDK | .NET 8 SDK. EnableComHosting은 .NET 5 이후의 기능입니다 |
| Office | Excel 또는 Access. 32bit판인지 64bit판인지 먼저 확인합니다(3장) |
| TLB 생성 도구 | dscom. 64bit용과 32bit용은 입수 방법이 다릅니다(6장) |
| 클라이언트 PC | Office와 같은 bitness의 .NET 8 런타임(9장) |
순서에 들어가기 전에 자신의 환경 버전을 적어 두는 것을 강력히 권합니다. 나중에 「같은 순서인데 안 된다」가 되었을 때, 비교할 수 있는 정보가 여기에만 있기 때문입니다.
# .NET SDK와 런타임 목록(x64 / x86 중 어느 쪽이 들어 있는지도 알 수 있습니다)
dotnet --info
# Windows 빌드 번호
winver
Office의 버전과 비트는 Excel의 파일 > 계정 > Excel 정보에서 확인할 수 있습니다. 대화상자 제목 줄 끝에 32비트 또는 64비트라고 표시됩니다.
1. 먼저 결론
먼저 결론만 나열하면 흐름은 이렇습니다.
- .NET 8의 클래스 라이브러리를
EnableComHosting=true로 빌드한다 - COM에 보일 명시적인 인터페이스와 클래스를 만든다
- 클래스는
ClassInterfaceType.None으로 하고,AutoDual로 얼버무리지 않는다 - VBA에서 쓸 인터페이스는
InterfaceIsDual로 한다 - 빌드 후 생긴
*.dll에서dscom tlbexport로*.tlb를 만든다 regsvr32로*.comhost.dll을 등록한다dscom tlbregister로*.tlb를 등록한다- VBA에서 참조 설정을 추가하고,
Dim x As 라이브러리명.IYourInterface처럼 형 있게 쓴다
요컨대 COM의 입구는 .NET SDK가 만드는 *.comhost.dll, 형 정보는 dscom이 만드는 *.tlb, VBA는 그 TLB를 보고 조기 바인딩한다는 구성입니다.
flowchart TB
accTitle: 형 있게 쓰기까지의 외길
accDescr: EnableComHosting으로 빌드하고, dscom tlbexport로 TLB를 만들고, regsvr32로 comhost를, dscom tlbregister로 TLB를 등록하고, VBA의 참조 설정에서 형 있게 쓴다는 흐름을 보여주는 그림.
st1["EnableComHosting으로 빌드"] --> st2["dscom tlbexport로 TLB 생성"]
st2 --> st3["regsvr32로 comhost 등록"]
st3 --> st4["dscom tlbregister로 TLB 등록"]
st4 --> st5["VBA에서 참조 설정해서 형 있게 쓴다"]
그림 2: 빌드, TLB 생성, 두 개의 등록, 참조 설정 순으로 진행하면 형 있게 호출할 수 있다.
이 글의 지식 맵
.NET 8 클래스 라이브러리를 VBA에서 형 있게 이용하려면 COM의 형 정보인 타입 라이브러리가 필수이며, 이는 dscom이라는 도구가 .NET Framework에서 폐지된 tlbexp.exe나 RegAsm.exe의 후속으로 생성·등록합니다. .NET 쪽은 EnableComHosting으로 빌드한 COM host를 COM의 기동 입구로 삼으며, regsvr32에 의한 등록과 Office와의 bitness 일치가 전제가 됩니다. VBA 쪽은 타입 라이브러리를 참조 설정으로 읽어들임으로써 조기 바인딩을 쓸 수 있고, CreateObject에 의한 지연 바인딩보다 형에 안전하게 쓸 수 있습니다. 공개 후의 호환성은 IID나 CLSID의 처리와 ClassInterfaceType의 선택 방식에 좌우되며, ClassInterfaceType.None과 InterfaceIsDual, DispId의 조합이 VBA 참조의 손상을 피하는 정석입니다.
flowchart LR
accTitle: .NET 8 DLL을 VBA에서 형 있게 쓰는 방법
accDescr: VBA가 조기 바인딩으로 COM을 형 있게 쓰려면 타입 라이브러리가 필요하고, dscom이 그 TLB 생성과 등록을 담당하며, .NET 8 쪽은 COM host로 공개된다는 것, ClassInterfaceType과 IID·CLSID의 처리가 VBA 참조의 호환성에 어떻게 영향을 미치는지를 보여주는 그림
vba["VBA(Visual Basic for Applications)"]
dscom["dscom"]
type_library["타입 라이브러리(TLB)"]
com_early_binding["조기 바인딩(VBA)"]
com_late_binding["지연 바인딩(CreateObject)"]
tlbexp_regasm["tlbexp.exe / RegAsm.exe"]
comhost["COM host(*.comhost.dll)"]
regsvr32["regsvr32"]
dotnet[".NET(Core 이후)"]
com["COM(컴포넌트 오브젝트 모델)"]
iid["IID(인터페이스 식별자)"]
clsid["CLSID(Class ID)"]
vba_reference_break["VBA 참조·등록 손상"]
classinterfacetype_autodual["ClassInterfaceType.AutoDual"]
classinterfacetype_none["ClassInterfaceType.None"]
dispid_attribute["DispIdAttribute"]
interface_is_dual["InterfaceIsDual(듀얼 인터페이스)"]
hresult["HRESULT"]
dotnet_exception[".NET 예외"]
com_visible_attribute["ComVisibleAttribute"]
bitness_match_requirement["비트수 일치 요건"]
vba -.->|"전제로 한다"| type_library
com_early_binding -->|"전제로 한다"| type_library
vba -->|"이용한다"| com_early_binding
vba -->|"이용한다"| com_late_binding
com_late_binding -.->|"사용은 비권장"| vba
dscom -->|"구현을 담당한다"| type_library
dscom -->|"의 후속"| tlbexp_regasm
type_library -.->|"에서 구성할 수 있다"| dscom
comhost -->|"에서 구성할 수 있다"| regsvr32
comhost -->|"전제로 한다"| dotnet
comhost -->|"구현을 담당한다"| com
vba -.->|"이용한다"| com
com -->|"전제로 한다"| iid
com -->|"전제로 한다"| clsid
iid -.->|"원인이 될 수 있다"| vba_reference_break
clsid -.->|"원인이 될 수 있다"| vba_reference_break
classinterfacetype_autodual -.->|"원인이 될 수 있다"| vba_reference_break
classinterfacetype_autodual -->|"사용은 비권장"| vba
classinterfacetype_none -->|"권장되는 대응"| vba
dispid_attribute -->|"완화한다"| vba_reference_break
interface_is_dual -->|"권장되는 대응"| vba
com -->|"이용한다"| hresult
dotnet_exception -.->|"에서 확인할 수 있다"| hresult
dotnet -->|"에서 구성할 수 있다"| com_visible_attribute
comhost -->|"전제로 한다"| bitness_match_requirement
dscom -.->|"전제로 한다"| bitness_match_requirement
regsvr32 -->|"전제로 한다"| bitness_match_requirement
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 27건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. 이 구성의 전체상
먼저 무엇이 무엇의 역할인지를 그림 한 장으로 봅니다.
flowchart LR
VBA["VBA / Excel / Access"] -->|참조 설정한 TLB로 형 정보 취득| TLB["VbaTypedComSample.tlb"]
VBA -->|COM 호출| COMHOST["VbaTypedComSample.comhost.dll"]
COMHOST --> DOTNET["VbaTypedComSample.dll (.NET 8)"]
DOTNET --> RUNTIME[".NET 8 Runtime"]
그림 3: VBA는 TLB에서 형 정보를 얻고, comhost를 경유해 .NET 8의 구현 본체를 호출한다.
각각의 역할은 다음과 같습니다.
| 파일 | 역할 |
|---|---|
VbaTypedComSample.dll |
.NET 8의 구현 본체 |
VbaTypedComSample.comhost.dll |
COM에서 호출되는 입구 |
VbaTypedComSample.tlb |
VBA가 보는 형 정보 |
VbaTypedComSample.deps.json |
의존 관계 해결 정보 |
VbaTypedComSample.runtimeconfig.json |
.NET 런타임 기동 정보 |
여기서 중요한 것은 VBA가 형을 알기 위해 필요한 것은 TLB이고, COM의 기동 입구로 필요한 것은 comhost라는 점입니다.
.dll 하나만 건네고 끝, 이라고는 되지 않는 부분이 COM 세계의 순순하지 않은 점입니다.
3. 처음에 정할 것 - 32bit / 64bit를 맞춘다
여기를 놓치면 상당한 확률로 ActiveX 컴포넌트는 오브젝트를 작성할 수 없습니다. 쪽으로 굴러갑니다.
Office / VBA와 COM 서버의 bitness는 맞춰 주세요.
| 이용 측 | .NET 측 기준 | TLB 생성 | 등록 명령 |
|---|---|---|---|
| 64bit Office | x64 / win-x64 |
dscom |
C:\Windows\System32\regsvr32.exe |
| 32bit Office(64bit Windows 상) | x86 / win-x86 |
dscom32.exe |
C:\Windows\SysWOW64\regsvr32.exe |
.NET 5+ 이후의 COM host에서는 AnyCPU 그대로 두면 *.comhost.dll이 64bit 쪽으로 치우치기 쉬워, 32bit Office와 맞물리지 않는 경우가 있습니다. 그래서 Office에 맞춰 x86 / x64를 명시하는 편이 안전합니다.
flowchart TB
accTitle: AnyCPU 방치가 부르는 불일치
accDescr: AnyCPU 그대로 두면 comhost가 64bit 쪽으로 치우치기 쉬워 32bit Office와 맞물리지 않는 경우가 있으므로, Office에 맞춰 x86이나 x64를 명시하는 것이 안전함을 보여주는 그림.
b1["AnyCPU 그대로"] --> b2["comhost가 64bit 쪽으로 치우치기 쉽다"]
b2 --> b3["32bit Office와 맞물리지 않는다"]
b3 -.-> b4["Office에 맞춰 비트를 명시"]
그림 4: bitness의 어긋남은 「오브젝트를 작성할 수 없습니다」로 가는 지름길이 된다.
이 글의 코드는 64bit Office용을 예로 합니다. 32bit Office라면 뒤에 나오는 x64를 x86, win-x64를 win-x86으로 바꿔 읽어 주세요.
4. .NET 8 쪽을 만든다
여기서는 VBA에서 Add, Divide, Hello를 호출할 수 있는 최소 샘플로 합니다.
4.1 .csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableComHosting>true</EnableComHosting>
<PlatformTarget>x64</PlatformTarget>
<NETCoreSdkRuntimeIdentifier>win-x64</NETCoreSdkRuntimeIdentifier>
</PropertyGroup>
</Project>
포인트는 EnableComHosting입니다. 이것을 붙이면 빌드 시에 VbaTypedComSample.comhost.dll이 생성됩니다.
4.2 어셈블리 전체는 기본값으로 COM 비공개로 해 둔다
COM에 보일 형만 ComVisible(true)로 하고 싶으므로, 어셈블리 전체는 false로 해 두는 것이 편합니다.
using System.Runtime.InteropServices;
[assembly: ComVisible(false)]
4.3 공개할 인터페이스와 클래스를 작성한다
using System.Runtime.InteropServices;
namespace VbaTypedComSample;
[ComVisible(true)]
[Guid("2A1BBEDE-DE6E-4C34-AD60-2E9E0E33E999")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface ICalculator
{
[DispId(1)]
int Add(int x, int y);
[DispId(2)]
double Divide(double x, double y);
[DispId(3)]
string Hello(string name);
}
[ComVisible(true)]
[Guid("FAD1C752-0BB6-4DDD-889F-FE446350847A")]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(ICalculator))]
public class Calculator : ICalculator
{
public Calculator()
{
}
public int Add(int x, int y) => checked(x + y);
public double Divide(double x, double y)
{
if (y == 0)
{
throw new ArgumentOutOfRangeException(nameof(y), "0으로는 나눌 수 없습니다.");
}
return x / y;
}
public string Hello(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return "Hello";
}
return $"Hello, {name}";
}
}
이 코드에서 짚어 둘 점은 다음과 같습니다.
Guid는 인터페이스와 클래스에 각각 따로 부여한다ClassInterfaceType.None으로 해서 자동 생성 클래스 인터페이스에 의존하지 않는다- VBA에서 다루기 쉽도록
InterfaceIsDual로 한다 DispId를 붙여 두면 공개 후에 메서드 순서를 바꿨을 때의 사고를 줄이기 쉽다- COM에서
New되므로 public한 인수 없는 생성자를 준비한다
flowchart TB
accTitle: 공개할 형을 구성하는 방법
accDescr: 명시적인 인터페이스 ICalculator에 InterfaceIsDual과 DispId를 붙이고, 클래스 Calculator는 ClassInterfaceType.None으로 구현하며, Guid는 인터페이스와 클래스에 각각 따로 부여한다는 구성을 보여주는 그림.
if1["ICalculator〔명시 인터페이스〕"] --> d1["InterfaceIsDual과 DispId"]
cl1["Calculator〔클래스〕"] -->|"구현한다"| if1
cl1 --> d2["ClassInterfaceType.None"]
if1 -.-> g1["Guid는 각각 따로 부여"]
cl1 -.-> g1
그림 5: 형 있는 공개의 핵심은 명시 인터페이스와 None을 지정한 클래스의 조합이다.
5. 빌드한다
Release로 빌드합니다.
dotnet build -c Release
빌드 후 출력 폴더에는 적어도 다음과 같은 파일이 놓입니다.
bin/
Release/
net8.0-windows/
VbaTypedComSample.dll
VbaTypedComSample.comhost.dll
VbaTypedComSample.deps.json
VbaTypedComSample.runtimeconfig.json
배포나 등록에 쓰는 것은 이 폴더입니다. 나중에 배치 장소를 바꾸면 등록도 다시 해야 합니다.
6. dscom으로 TLB를 생성한다
6.1 dscom이란 무엇인가
dscom은 .NET 어셈블리에서 COM 타입 라이브러리(TLB)를 생성·등록하기 위한 오픈소스 명령줄 도구입니다. dSPACE사가 공개하고 있으며, 라이선스는 Apache-2.0입니다.
왜 필요한가 하면, .NET 5 이후에는 tlbexp.exe와 RegAsm.exe가 폐지되었기 때문입니다. .NET Framework 시대에는 이 둘로 TLB 생성과 어셈블리 등록을 할 수 있었지만, .NET 5+에는 그 후속 도구가 표준으로 들어 있지 않습니다. dscom은 그 공백을 메우기 위해 만들어진 도구입니다.
flowchart TB
accTitle: dscom이 메우는 공백
accDescr: .NET Framework 시대는 tlbexp.exe와 RegAsm.exe로 TLB 생성과 어셈블리 등록을 할 수 있었지만, .NET 5 이후에는 폐지되어 후속 도구가 표준에 없으므로 dscom이 그 공백을 메운다는 것을 보여주는 그림.
old1[".NET Framework 시대"] --> old2["tlbexp.exe와 RegAsm.exe"]
new1[".NET 5 이후"] --> new2["둘 다 폐지되고 후속 도구 없음"]
new2 --> ds1["dscom이 공백을 메운다"]
그림 6: TLB 생성 도구는 .NET 5 이후에는 dscom으로 대체되었다.
주요 서브커맨드는 이것만 기억해 두면 충분합니다.
| 서브커맨드 | 역할 |
|---|---|
tlbexport |
어셈블리에서 TLB를 내보낸다 |
tlbregister |
TLB를 시스템에 등록한다 |
tlbunregister |
TLB 등록을 해제한다 |
tlbdump |
TLB의 내용을 출력해 확인한다 |
tlbembed |
TLB를 파일에 임베드한다 |
tlbdump는 생성한 TLB에 의도한 형이 들어 있는지를 VBA를 열기 전에 확인하는 데 편리합니다.
6.2 64bit인 경우
64bit TLB만 만들 거라면 dotnet tool로 설치할 수 있습니다.
dotnet tool install --global dscom
다음으로 빌드한 어셈블리에서 TLB를 생성합니다.
dscom tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb
6.3 32bit Office용의 경우 - dscom32.exe는 어디서 입수하는가
여기가 32bit Office 대응에서 가장 막히는 부분입니다.
dotnet tool install로 설치되는 dscom은 AnyCPU 또는 64bit 어셈블리만 다룰 수 있고, 생성할 수 있는 것은 64bit TLB뿐입니다. 32bit TLB를 만들려면 별도의 실행 파일인 dscom32.exe가 필요하며, 이것은 NuGet이 아니라 GitHub의 릴리스 페이지에서 다운로드합니다.
- 입수처: https://github.com/dspace-group/dscom/releases
dscom.exe… AnyCPU 또는 64bit 어셈블리에서 64bit TLB를 만든다dscom32.exe… AnyCPU 또는 32bit 어셈블리에서 32bit TLB를 만든다
다운로드한 dscom32.exe는 이 글의 예시에서는 프로젝트 바로 아래의 tools 폴더에 두고 있습니다. 두는 위치는 자유지만, 빌드 출력물과 함께 배포하지 않도록 해 주세요. 개발 시 도구일 뿐 실행 시에는 필요 없습니다.
또 하나, 놓치기 쉬운 전제가 있습니다. dscom32.exe를 실행하려면 x86판 .NET 런타임이 설치되어 있어야 합니다. dscom이 hostfxr.dll을 로드하는 사정 때문이며, x64판만 설치된 환경에서는 동작하지 않습니다. dotnet --info 출력에 있는 목록에서 x86 런타임이 있는지 확인해 주세요.
flowchart TB
accTitle: 32bit TLB를 만들 준비
accDescr: dotnet tool로 설치되는 dscom은 64bit TLB만 만들 수 있으므로, 32bit TLB는 GitHub의 릴리스 페이지에서 입수한 dscom32.exe로 만들고, 실행하려면 x86판 .NET 런타임이 필요함을 보여주는 그림.
p1["32bit TLB가 필요하다"] --> p2["릴리스 페이지에서 dscom32.exe 입수"]
p2 --> p3["x86판 런타임 유무 확인"]
p3 --> p4["dscom32.exe로 tlbexport"]
p1 -.-> p5["dotnet tool판은 64bit TLB 전용"]
그림 7: 32bit 대응은 도구를 입수하는 경로부터 다르므로 여기서 막히기 쉽다.
.\tools\dscom32.exe tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb
덧붙여 dscom 측 문서에서도, AnyCPU 그대로 두면 *.comhost.dll이 64bit로 생성되므로, 32bit로 쓸 거라면 어셈블리 자체를 32bit로 컴파일하는 것이 권장되고 있습니다. 3장의 이야기와 같은 결론입니다.
빌드할 때마다 손으로 입력하는 것이 번거롭다면, dSPACE.Runtime.InteropServices.BuildTasks 패키지를 넣으면 컴파일 시에 TLB를 자동 생성할 수 있습니다.
7. COM host와 TLB를 등록한다
여기는 관리자 권한의 명령 프롬프트 / PowerShell에서 실행해 주세요.
7.1 64bit Office / 64bit COM의 경우
$out = Resolve-Path .\bin\Release\net8.0-windows
C:\Windows\System32\regsvr32.exe "$out\VbaTypedComSample.comhost.dll"
dscom tlbregister "$out\VbaTypedComSample.tlb"
7.2 32bit Office(64bit Windows 상)의 경우
$out = Resolve-Path .\bin\Release\net8.0-windows
C:\Windows\SysWOW64\regsvr32.exe "$out\VbaTypedComSample.comhost.dll"
.\tools\dscom32.exe tlbregister "$out\VbaTypedComSample.tlb"
여기서 하고 있는 것은 두 가지입니다.
regsvr32로*.comhost.dll을 COM 서버로 등록한다tlbregister로*.tlb를 타입 라이브러리로 등록한다
flowchart TB
accTitle: 등록은 두 갈래
accDescr: 관리자 권한으로 regsvr32가 comhost를 COM 서버로 등록하고, dscom tlbregister가 TLB를 타입 라이브러리로 등록한다는 두 개의 등록을 보여주는 그림.
adm["관리자 권한으로 실행"] --> r1["regsvr32로 comhost 등록"]
adm --> r2["tlbregister로 TLB 등록"]
r1 -.-> m1["COM 기동 입구의 등록"]
r2 -.-> m2["VBA가 보는 형 정보의 등록"]
그림 8: 기동 입구와 형 정보는 별개이므로, 등록도 둘이 한 쌍을 이룬다.
8. VBA에서 참조 설정해서 형 있게 쓴다
- Excel 또는 Access를 연다
Alt+F11로 VBA 편집기(VBE)를 연다. 리본에서 열려면개발 도구탭 >Visual Basic입니다.개발 도구탭이 보이지 않으면파일>옵션>리본 사용자 지정에서개발 도구에 체크를 넣습니다- VBE 메뉴에서
도구>참조 사용 가능한 참조목록은 알파벳순으로 나열됩니다. 등록이 성공했다면 그 안에 라이브러리 이름(기본값으로는 어셈블리 이름과 같은VbaTypedComSample)이 나타나므로, 왼쪽 체크박스를 체크하고확인- 목록에 보이지 않으면
찾아보기...버튼에서VbaTypedComSample.tlb를 직접 선택한다
목록에 나오지 않는 경우, 원인은 대개 3장의 bitness 불일치이거나 7장의 tlbregister가 통과하지 않은 것 중 하나입니다. 32bit Office에서는 64bit로 등록한 TLB가 보이지 않습니다.
flowchart TB
accTitle: 참조 설정에 나오지 않을 때의 원인 분리
accDescr: 참조 목록에 라이브러리가 나오지 않는 원인은 대개 bitness 불일치이거나 tlbregister가 통과하지 않은 것 중 하나이며, 32bit Office에서는 64bit로 등록한 TLB가 보이지 않음을 보여주는 그림.
q1["목록에 라이브러리가 안 나온다"] --> a1["3장의 bitness 불일치를 의심"]
q1 --> a2["7장의 tlbregister 실패를 의심"]
a1 -.-> nt1["32bit Office에서는 64bit TLB가 안 보인다"]
그림 9: 목록에 나오지 않는 원인은 거의 bitness나 등록 중 하나로 좁혀진다.
참조 설정이 들어갔는지는 보기 > 개체 브라우저(F2)를 열어, 왼쪽 위의 라이브러리 선택에서 VbaTypedComSample을 고를 수 있는지로 확인할 수 있습니다. 여기서 ICalculator와 Calculator, 그리고 Add / Divide / Hello가 보인다면 TLB는 올바르게 만들어진 것입니다.
Option Explicit
Public Sub UseCalculator()
Dim calc As VbaTypedComSample.ICalculator
Set calc = New VbaTypedComSample.Calculator
Debug.Print calc.Add(10, 20)
Debug.Print calc.Divide(10, 4)
Debug.Print calc.Hello("VBA")
End Sub
이 프로시저에 커서를 놓고 F5로 실행한 뒤 Ctrl + G로 직접 실행 창을 열면, 4장의 구현대로 3줄이 출력됩니다.
30
2.5
Hello, VBA
여기가 예상대로라면 참조 설정·COM 등록·런타임 기동·인수와 반환값의 마샬링까지 전부 통과한 것입니다. 반대로 여기서 값이 맞지 않는다면 .NET 쪽 구현을, 애초에 실행할 수 없다면 3장과 7장을 의심해 주세요.
flowchart TB
accTitle: 실행 결과에 따른 원인 분리
accDescr: 샘플의 실행 결과가 예상대로라면 참조 설정부터 마샬링까지 전부 통과한 것이고, 값이 맞지 않으면 .NET 쪽 구현을, 애초에 실행할 수 없으면 3장의 bitness와 7장의 등록을 의심한다는 원인 분리를 보여주는 그림.
r1["VBA 샘플을 실행"] --> q1{"결과는 어떤가"}
q1 -->|"예상대로"| ok1["경로는 전부 통과했다"]
q1 -->|"값이 맞지 않는다"| ng1[".NET 쪽 구현을 의심"]
q1 -->|"실행할 수 없다"| ng2["bitness와 등록을 의심"]
그림 10: 3줄의 출력만으로 어느 계층을 의심해야 할지 정해진다.
이것으로 VBA 쪽에는 이런 이점이 있습니다.
- IntelliSense가 동작한다
- 메서드 이름의 typo를 실행 전에 발견하기 쉽다
- Object Browser로 공개 API를 확인할 수 있다
Object로만 작성하는 것보다 읽기 쉽다
8.1 예외는 VBA 쪽에서는 COM 오류가 된다
예를 들어 Divide(10, 0)처럼 .NET 쪽에서 예외가 던져지면, VBA 쪽에서는 COM 오류로 보입니다.
Option Explicit
Public Sub UseCalculatorWithErrorHandling()
On Error GoTo EH
Dim calc As VbaTypedComSample.ICalculator
Set calc = New VbaTypedComSample.Calculator
Debug.Print calc.Divide(10, 0)
Exit Sub
EH:
Debug.Print Err.Number
Debug.Print Hex$(Err.Number)
Debug.Print Err.Description
End Sub
여기서 나오는 값을 읽는 법을 알아 두면 원인 분리가 빨라집니다.
| 항목 | 무엇이 들어가는가 |
|---|---|
Err.Number |
.NET 예외에 대응하는 HRESULT가 부호 있는 Long으로 들어갑니다. 10진수 그대로는 읽기 어려우므로 Hex$(Err.Number)로 16진수로 바꿉니다 |
Err.Description |
COM의 IErrorInfo를 통해 .NET 예외 메시지가 그대로 들어갑니다. 위 코드라면 0으로는 나눌 수 없습니다.를 포함한 문자열입니다 |
HRESULT 값은 예외 형마다 정해져 있습니다. ArgumentOutOfRangeException에 대응하는 것은 COR_E_ARGUMENTOUTOFRANGE이고, 값은 0x80131502입니다. 즉 Hex$(Err.Number)가 80131502로 되어 있으면, 예상대로 .NET 쪽의 ArgumentOutOfRangeException이 도착한 것이 됩니다.
주요한 것을 나열하면 이렇습니다.
| .NET 예외 | HRESULT 상수 | 값 |
|---|---|---|
ArgumentException |
COR_E_ARGUMENT |
0x80070057 |
ArgumentOutOfRangeException |
COR_E_ARGUMENTOUTOFRANGE |
0x80131502 |
InvalidOperationException |
COR_E_INVALIDOPERATION |
0x80131509 |
NotSupportedException |
COR_E_NOTSUPPORTED |
0x80131515 |
| 그 밖의 일반적인 예외 | COR_E_EXCEPTION |
0x80131500 |
VBA 쪽에서 예외 종류별로 처리를 나누고 싶다면, 이 HRESULT로 분기하게 됩니다. 다만 HRESULT로 분기하는 설계는 .NET 쪽 예외 형의 변경에 취약하므로, 업무적인 실패는 예외가 아니라 반환값이나 오류 코드로 돌려주는 편이 경계로서는 안정적입니다.
flowchart TB
accTitle: .NET 예외가 VBA에 도달하기까지
accDescr: .NET 쪽에서 던져진 예외는 COM 경계에서 HRESULT로 변환되고, VBA에서는 Err.Number에 그 HRESULT가 부호 있는 Long으로, Err.Description에 IErrorInfo를 통한 예외 메시지가 들어감을 보여주는 그림.
x1[".NET 쪽에서 예외가 throw된다"] --> x2["COM 경계에서 HRESULT로 변환"]
x2 --> x3["Err.Number에 부호 있는 값으로 들어간다"]
x2 --> x4["Err.Description에 메시지"]
x3 -.-> h1["Hex$로 16진수로 바꿔서 읽는다"]
그림 11: 예외는 COM 경계에서 HRESULT로 모습을 바꿔 VBA의 Err에 도달한다.
9. 배포할 때의 사고방식
배포 시에 중요한 것은 DLL 하나만 배포하는 것이 아니라 출력물 일체를 두는 것입니다.
VbaTypedComSample.dll
VbaTypedComSample.comhost.dll
VbaTypedComSample.deps.json
VbaTypedComSample.runtimeconfig.json
VbaTypedComSample.tlb
(필요하다면 의존 DLL 일체)
게다가 클라이언트 PC에는 대응하는 .NET 8 런타임이 필요합니다. COM host는 self-contained 배포가 아니라 기본적으로 framework-dependent한 운영이 됩니다.
구체적으로 넣을 것은 이렇습니다.
- 배포 페이지는 .NET 8 다운로드입니다
- 필요한 것은 SDK가 아니라 런타임입니다. 이 글의 샘플은 화면이 없는 클래스 라이브러리이므로
.NET Runtime으로 충분합니다. WPF나 Windows Forms의 형을 사용하는 경우는.NET Desktop Runtime이 필요합니다 - bitness는 Office에 맞춥니다. 64bit Office라면 x64, 32bit Office라면 x86 런타임입니다. 3장에서
x64/x86을 명시한 것과 같은 이유로, 여기도 맞지 않으면 실행되지 않습니다 - 도입되었는지는 클라이언트 PC에서
dotnet --list-runtimes를 실행해Microsoft.NETCore.App 8.x줄이 있는지 보면 알 수 있습니다
배포 자료에는 이 「필요한 런타임의 종류·버전·bitness」를 반드시 적어 두세요. 도입 절차에 적는 것을 잊으면, 현장에서 ActiveX 컴포넌트는 오브젝트를 작성할 수 없습니다.를 보고 bitness를 의심하느라 시간을 낭비하게 됩니다.
flowchart TB
accTitle: 배포 시에 갖출 것
accDescr: 배포는 DLL 단체가 아니라 출력물 일체를 두고, 클라이언트 PC에는 Office와 같은 bitness의 .NET 8 런타임을 설치하며, 필요한 런타임의 종류와 버전과 bitness를 배포 자료에 적어 둔다는 것을 보여주는 그림.
h1["출력물 일체를 배치"] --> u1["클라이언트에서 동작"]
h2["같은 bitness의 .NET 8 런타임"] --> u1
h3["배포 자료에 런타임 정보를 명기"] -.-> u1
그림 12: DLL 하나만으로는 동작하지 않고, 일체와 런타임이 갖춰져야 비로소 동작한다.
10. 빠지기 쉬운 함정
10.1 AnyCPU 그대로 방치하지 않는다
VBA / Office의 bitness와 COM host의 bitness가 어긋나면 상당히 찜찜한 방식으로 실패합니다.
- 64bit Office라면
x64/win-x64 - 32bit Office라면
x86/win-x86
10.2 ClassInterfaceType.AutoDual을 쓰지 않는다
얼핏 편해 보이지만, 공개 후에 멤버 순서나 구성을 건드리면 망가지기 쉽습니다.
VBA에서 형 있게 안정적으로 쓰고 싶다면, 명시 인터페이스를 정의하고 클래스는 ClassInterfaceType.None으로 해 두는 것이 정석입니다.
10.3 GUID를 경솔하게 재생성하지 않는다
COM에서는 GUID가 계약 그 자체입니다. IID나 CLSID를 공개 후에 경솔하게 바꾸면 기존 VBA 참조나 등록이 망가집니다.
10.4 공개된 인터페이스를 망가뜨리지 않는다
COM은 「나중에 메서드를 1개 더했을 뿐」이어도 평온하게 끝나지 않는 경우가 있습니다.
ICalculator는 남긴다- 변경이 크다면
ICalculator2를 새로 만든다 - 클래스는 양쪽을 구현해도 된다
flowchart TB
accTitle: 공개된 인터페이스를 지키는 방법
accDescr: 공개된 ICalculator는 그대로 남기고, 변경이 크다면 ICalculator2를 새로 만들어, 클래스는 양쪽을 구현해도 된다는 호환성 유지 방법을 보여주는 그림.
k1["공개된 ICalculator"] --> k2["그대로 남긴다"]
k3["큰 변경을 하고 싶다"] --> k4["ICalculator2를 새로 만든다"]
k2 --> k5["클래스는 양쪽을 구현할 수 있다"]
k4 --> k5
그림 13: 기존 계약은 남겨 둔 채 새 계약을 옆에 추가하는 것이 COM 방식이다.
10.5 형은 수수한 쪽으로 맞춘다
VBA에 보이는 경계에서는 너무 멋을 부리지 않는 편이 안전합니다.
궁합이 좋은 것은 우선 이 정도입니다.
intdoubleboolstringDateTimedecimalenum
10.6 Office를 연 채로 갱신하지 않는다
Excel이나 Access가 DLL을 붙잡은 채로 있어, 빌드나 재등록에서 번거로움이 생길 수 있습니다.
- Office를 닫는다
- 필요하다면 등록을 해제한다
- 다시 빌드한다
- 다시 한번 등록한다
등록 해제는 등록했을 때와 역순으로, 등록에 쓴 것과 같은 bitness의 명령으로 수행합니다. 관리자 권한이 필요한 것도 등록 시와 같습니다.
# 64bit Office / 64bit COM의 경우
$out = Resolve-Path .\bin\Release\net8.0-windows
dscom tlbunregister "$out\VbaTypedComSample.tlb"
C:\Windows\System32\regsvr32.exe /u "$out\VbaTypedComSample.comhost.dll"
# 32bit Office(64bit Windows 상)의 경우
$out = Resolve-Path .\bin\Release\net8.0-windows
.\tools\dscom32.exe tlbunregister "$out\VbaTypedComSample.tlb"
C:\Windows\SysWOW64\regsvr32.exe /u "$out\VbaTypedComSample.comhost.dll"
regsvr32의 /u가 등록 해제 옵션입니다. 등록할 때와 다른 regsvr32를 쓰면 해제할 수 없습니다(64bit로 등록한 것을 SysWOW64의 regsvr32로는 뗄 수 없습니다). 폴더째 이동·삭제하기 전에 해제해 두지 않으면, 레지스트리에 존재하지 않는 경로의 등록이 남습니다.
flowchart TB
accTitle: 갱신 전후의 등록 정리 방법
accDescr: 갱신 시에는 Office를 닫고, 필요하다면 등록했을 때와 역순이며 같은 bitness의 명령으로 등록을 해제한 뒤 다시 빌드하고, 다시 한번 등록한다는 흐름을 보여주는 그림.
u1["Office를 닫는다"] --> u2["필요하다면 역순으로 등록 해제"]
u2 --> u3["다시 빌드한다"]
u3 --> u4["다시 한번 등록한다"]
u2 -.-> u5["등록 시와 같은 bitness의 명령으로"]
그림 14: 붙잡힌 DLL과 잔류 등록을 피하기 위해 해제와 재등록을 한 쌍으로 수행한다.
11. 정리
.NET 8의 DLL을 형 있게 VBA에서 쓴다는 이야기는 COM 공개 + dscom으로 TLB 생성에 좁혀 버리면, 그렇게 무서운 절차는 아닙니다. .NET 8 쪽은 EnableComHosting=true로 하고 명시 인터페이스(클래스는 ClassInterfaceType.None, VBA용은 InterfaceIsDual)를 준비하며, dscom tlbexport로 TLB를 만들고, regsvr32로 *.comhost.dll을, dscom tlbregister로 *.tlb를 등록합니다. 나머지는 VBA에서 참조 설정을 넣고 조기 바인딩하기만 하면 됩니다.
헷갈릴 때는 COM host와 TLB를 나누어 생각하는 것이 요령입니다.
- 기동 입구는
*.comhost.dll - 형 정보는
*.tlb - 구현 본체는
*.dll
12. 참고 자료
- 이 글의 샘플 코드 일체(COM 공개 라이브러리, 스크립트, VBA, 테스트) - komurasoft-blog-samples (GitHub)
- Expose .NET components to COM - Microsoft Learn
- COM 상호운용을 위해 .NET 형을 한정하기 - Microsoft Learn
- ComInterfaceType 열거형 - Microsoft Learn
- ClassInterfaceType 열거형 - Microsoft Learn
- COM 호출 가능 래퍼 - Microsoft Learn
- DispIdAttribute 클래스 - Microsoft Learn
- dscom - NuGet Gallery
- dspace-group/dscom - GitHub(dscom 본체. 서브커맨드 목록과 32bit 대응 설명)
- dscom 릴리스 페이지(
dscom32.exe의 입수처) - HRESULT와 예외를 대응시키는 방법 - Microsoft Learn
- How to use the Regsvr32 tool and troubleshoot Regsvr32 error messages - Microsoft Support
- .NET 8 downloads
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
C#의 Excel 조작에서 EXCEL.EXE가 남는 문제 ── COM 참조 해제 패턴과 교체 판단
C#에서 Microsoft.Office.Interop.Excel로 Excel을 조작하면 EXCEL.EXE 프로세스가 남는 문제를, COM 참조 카운트와 RCW의 원리로 정리합니다. 2닷 규칙의 함정, Marshal.ReleaseComObject파...
Excel 매크로 VBA를 Power Automate로 이행하다 ── Office Scripts로 대체할 범위와 VBA로 남겨둘 범위
Excel VBA 매크로를 Power Automate로 이행할 수 있는지를 정리합니다. Office Scripts로 대체할 수 있는 범위와 VBA만이 할 수 있는 것, 커넥터의 제한값, 라이선스 요건, 재고 조사부터 시작하는 단계적 이행 방법까지...
DLL・COM 인터페이스의 하위 호환성 ── 어떤 변경이 호출 측을 망가뜨리는지의 판단표
DLL이나 COM 컴포넌트의 어떤 변경이 호출 측을 망가뜨리는지. 바이너리 호환・소스 호환・동작 호환의 3계층을 정리하고, 변경 내용별 판단표, COM 인터페이스 불변의 철칙, semver 운용까지를 실무 가이드로 정리합니다.
Arm판 Windows에서 업무 앱은 동작하는가 ── x64 에뮬레이션(Prism)과 네이티브 DLL·COM의 현실
「Arm판 Windows에서 업무 앱은 동작하는가」에 개발자·정보시스템 담당자를 위해 답합니다. x64 에뮬레이션(Prism)의 구조, 드라이버 등 동작하지 않는 계층, .NET의 AnyCPU와 P/Invoke 문제, Arm 대응 체크리스트까지 ...
Windows의 프로세스 간 통신을 어떻게 선택할까 ── 네임드 파이프 / TCP / gRPC / 공유 메모리 / COM 판단표
Windows 앱 사이의 연동 수단을 어떻게 선택할지 정리합니다. 네임드 파이프, 로컬 TCP, gRPC, 공유 메모리, 파일 연동, COM 각각의 강점과 함정을 판단표로 정리하고, UI+서비스 분리・32bit/64bit 브리지・권한 경계 같은 ...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
ActiveX 이관
COM / ActiveX / OCX 자산을 유지할지, 감쌀지, 교체할지의 단계적 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기존 자산 활용 & 이관 지원
COM / ActiveX / OCX 자산, 네이티브 코드, 32비트 의존성을 유지하면서 단계적인 이관 계획을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- .NET 8 DLL을 VBA에서 형 있게(조기 바인딩으로) 쓰려면 무엇이 필요합니까?
- .NET 8 클래스 라이브러리를 EnableComHosting=true로 빌드해서 *.comhost.dll을 생성하고, dscom tlbexport로 *.tlb를 만듭니다. 다음으로 regsvr32로 *.comhost.dll을, dscom tlbregister로 *.tlb를 등록하고, VBA의 참조 설정에서 그 TLB를 추가하면 Dim x As 라이브러리명.IYourInterface 형태로 형 있게 쓸 수 있습니다. 역할 분담으로 말하면, COM의 기동 입구가 *.comhost.dll, VBA가 보는 형 정보가 *.tlb, 구현 본체가 *.dll입니다.
- 「ActiveX 컴포넌트는 오브젝트를 작성할 수 없습니다」가 나오는 원인은 무엇입니까?
- 전형적인 원인은 Office/VBA와 COM 서버의 bitness 불일치입니다. 64bit Office라면 x64/win-x64로 빌드해서 System32의 regsvr32로 등록하고, 32bit Office(64bit Windows 상)라면 x86/win-x86으로 빌드해서 SysWOW64의 regsvr32로 등록하며, TLB 생성도 dscom32.exe를 씁니다. .NET 5+의 COM host에서는 AnyCPU 그대로 두면 *.comhost.dll이 64bit 쪽으로 치우치기 쉬워 32bit Office와 맞물리지 않는 경우가 있으므로, Office에 맞춰 x86/x64를 명시하는 것이 안전합니다.
- ClassInterfaceType.AutoDual을 쓰면 안 됩니까?
- 얼핏 편해 보이지만, 공개 후에 멤버 순서나 구성을 건드리면 망가지기 쉬우므로 피해야 합니다. VBA에서 형 있게 안정적으로 쓰고 싶다면, 명시적인 인터페이스를 정의하고 클래스는 ClassInterfaceType.None으로 하며, VBA에서 쓸 인터페이스는 InterfaceIsDual로 하는 것이 정석입니다. DispId를 붙여 두면 메서드 순서를 바꿀 때의 사고를 줄일 수 있습니다. 또한 COM에서는 GUID가 계약 그 자체이므로, IID나 CLSID를 공개 후에 경솔하게 재생성하면 기존 VBA 참조나 등록이 망가집니다.
- 배포할 때는 DLL 단체만 건네면 됩니까?
- DLL 단체로는 동작하지 않습니다. 구현 본체인 *.dll, *.comhost.dll, *.deps.json, *.runtimeconfig.json, *.tlb, 필요하다면 의존 DLL 일체를 함께 배치합니다. 게다가 클라이언트 PC에는 대응하는 .NET 8 런타임이 필요하며, COM host는 self-contained 배포가 아니라 기본적으로 framework-dependent한 운영이 됩니다. 배치 장소를 나중에 바꾸면 등록도 다시 해야 한다는 점에도 주의가 필요합니다.