신입사원이 입사할 때마다, 혹은 PC를 교체할 때마다 담당자가 절차서를 보면서 한 대씩 설정한다 ── 중소기업의 정보시스템 부서에서는 여전히 이것이 표준적인 풍경입니다. 문제는 시간만이 아닙니다. 수작업은 재현성이 없기 때문에, “이 단말만 설정이 다르다”는 문제가 나중에 발목을 잡습니다. 절차서는 갱신되지 않은 채 낡아가고, 담당자가 바뀌면 세부 사항이 사라집니다.
Windows에는 표준으로 패키지 관리자 winget이 들어 있어, 앱 도입을 한 줄로 작성할 수 있습니다. 나아가 WinGet Configuration을 사용하면 앱과 설정을 선언적(절차를 나열하는 것이 아니라 “최종적으로 이렇게 되어 있으면 좋겠다”는 상태를 적는 방식)인 YAML 파일 하나로 표현할 수 있습니다. 그리고 winget이 다루지 않는 영역(프린터, 네트워크 드라이브, 사내 표준 레지스트리 설정 등)은 PowerShell로 보완할 수 있습니다.
이 글에서는 키팅 절차서를 “실행 가능한 파일”로 바꾸는 방법을, 실제로 운영할 수 있는 수준으로 정리합니다.
1. 먼저 결론
- 앱 도입은 winget에 맡깁니다.
winget install은 조용한 설치를 표준으로 지원합니다.1 - 기존 PC의 구성은
winget export로 추출할 수 있습니다. 다만 winget 관리하의 패키지에 한정되며, 설정은 포함되지 않습니다.2 - 선언적으로 하려면 WinGet Configuration(
winget configure)입니다. PowerShell DSC 기반으로, 앱과 설정을 YAML 하나에 작성할 수 있습니다. Windows 10 1809 이상 + winget 1.6 이상이 요건입니다.3 - winget으로 부족한 부분은 PowerShell로 보완합니다. 프린터, 공유 드라이브, 레지스트리, Windows 기능, 로컬 계정 등입니다.
- PowerShell에서 다루려면
Microsoft.WinGet.Client모듈이 있습니다.Install-WinGetPackage등의 cmdlet을 사용할 수 있습니다.4 - 시스템 컨텍스트(SYSTEM 계정 등, 로그온 중인 사용자와는 별도인 계정에서의 실행)에는 주의가 필요합니다. Microsoft도 향후 개발 항목으로 들고 있는 단계이며, 실제 실행 계정에서의 검증이 필수입니다.3
- 멱등성(몇 번 실행해도 같은 결과가 되는 것)을 반드시 확보합니다. 키팅은 도중에 실패하는 것이 당연하며, 몇 번이든 다시 실행할 수 있어야 합니다.
- 사내 독자 앱은 무리하게 winget에 태우지 않고, PowerShell로 사일런트 실행하는 것이 현실적입니다.
2. winget의 기본 ── 무인 설치 작성법
먼저 대화 없이 확실하게 설치하기 위한 정형 문구를 짚어 둡니다.1
# ID를 정확히 지정해서 설치한다(-e 는 완전 일치, --id 는 ID 지정)
# --silent : UI를 표시하지 않음
# --accept-package-agreements : 패키지 사용 조건에 동의
# --accept-source-agreements : 소스 사용 조건에 동의
# --scope machine : 전체 사용자용으로 설치(대응 패키지만)
# 줄바꿈 백틱은 줄 끝에 둔다. 뒤에 주석을 쓰면 줄바꿈으로 인식되지 않는다
winget install --id Google.Chrome -e --silent `
--accept-package-agreements --accept-source-agreements --scope machine
# ID를 조회한다
winget search "Visual Studio Code"
winget show --id Microsoft.VisualStudioCode
--accept-*를 빠뜨리면 무인 실행이 동의 대기 상태로 멈춥니다. 이것이 키팅 자동화에서 가장 먼저 걸리는 부분입니다.
--scope machine은 모든 패키지에서 사용할 수 있는 것은 아니며, 사용자 단위로만 설치할 수 있는 앱도 있습니다. 그 경우에는 최초 로그온 시 사용자 컨텍스트에서 실행하는 구성으로 합니다.
대응 여부는 설치하기 전에 확인할 수 있습니다. winget show는 --scope를 받아들이므로, 머신 스코프의 설치 프로그램이 준비되어 있는지 사전에 조사할 수 있습니다.5
# 머신 스코프의 설치 프로그램이 있는지 확인한다
winget show --id Google.Chrome -e --scope machine
# 비교를 위해 사용자 스코프 쪽도 확인해 둔다
winget show --id Google.Chrome -e --scope user
지정한 스코프에 해당하는 설치 프로그램이 없으면 그 취지의 메시지가 표시됩니다. 키팅 설정 파일에 "scope": "machine"이라고 적기 전에, 대상 패키지를 이 명령으로 한 번씩 확인해 두면 무인 실행 도중에서야 실패를 발견하는 사태를 피할 수 있습니다.
3. 기존 PC의 구성 추출하기 ── export / import
이미 정비된 “표준 PC”가 있다면, 그 구성을 파일로 만들 수 있습니다.2
# 표준 PC에서 설치된 패키지 목록을 JSON으로 내보낸다
winget export --output D:\kitting\apps.json --include-versions
# 새 PC에서 복원한다
winget import --import-file D:\kitting\apps.json `
--accept-package-agreements --accept-source-agreements --ignore-unavailable
출력되는 JSON은 다음과 같은 구조입니다(값은 예시입니다).2
{
"CreationDate": "2026-07-25T10:40:00.000-00:00",
"Sources": [
{
"Packages": [
{ "PackageIdentifier": "Google.Chrome", "Version": "126.0.6478.127" },
{ "PackageIdentifier": "Microsoft.VisualStudioCode", "Version": "1.101.2" },
{ "PackageIdentifier": "7zip.7zip", "Version": "24.09" }
],
"SourceDetails": {
"Argument": "https://cdn.winget.microsoft.com/cache",
"Identifier": "Microsoft.Winget.Source_8wekyb3d8bbwe",
"Name": "winget",
"Type": "Microsoft.PreIndexed.Package"
}
}
],
"WinGetVersion": "1.9.25180"
}
보시는 대로 내용은 패키지 식별자와 버전의 목록입니다(--include-versions를 붙이지 않으면 Version이 들어가지 않고, import는 최신판을 설치합니다).2 즉 winget import가 하는 일은 “이 식별자의 패키지를 설치한다”뿐이며, 앱의 설정도, winget 이외로 설치한 앱도 여기에는 한 글자도 들어 있지 않습니다. JSON을 직접 열어 보면 이 제약을 구체적으로 알 수 있습니다.
제약을 이해해 두는 것이 중요합니다.
| 할 수 있는 것 | 할 수 없는 것 |
|---|---|
| winget 관리하 패키지 목록의 재현 | 앱 내부 설정의 이관 |
버전 고정(--include-versions) |
winget 이외로 설치한 앱·사내 독자 앱 |
존재하지 않는 패키지의 건너뛰기(--ignore-unavailable) |
라이선스 인증, 로그인 상태 |
즉 winget import는 출발점일 뿐, 키팅 전체가 아닙니다. 나머지를 어떻게 채울지가 본론입니다.
4. 선언적으로 작성하기 ── WinGet Configuration
winget configure는 “최종적으로 이랬으면 좋겠다”는 상태를 YAML로 선언하고, PowerShell DSC를 통해 적용하는 구조입니다.3 절차적인 스크립트와 비교했을 때의 장점은 3가지입니다.
- 이미 원하는 상태라면 아무것도 하지 않는다(멱등)
- 앱 도입과 Windows·앱 설정을 파일 하나로 표현할 수 있다
- 도중에 실패해도 같은 파일을 다시 실행하면 된다
# kitting.winget ── 표준 단말이 갖춰야 할 상태를 선언한다
properties:
configurationVersion: 0.2.0
resources:
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: chrome
directives:
description: Google Chrome을 도입한다
allowPrerelease: true
settings:
id: Google.Chrome
source: winget
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: vscode
directives:
description: Visual Studio Code를 도입한다
settings:
id: Microsoft.VisualStudioCode
source: winget
- resource: Microsoft.Windows.Developer/DeveloperMode
id: devmode
directives:
description: 개발자 모드를 활성화한다(개발용 단말만)
allowPrerelease: true
settings:
Ensure: Present
# 적용 전에 내용을 확인한다(무엇이 실행되는지 표시)
winget configure show --file D:\kitting\kitting.winget
# 적용한다
winget configure --file D:\kitting\kitting.winget --accept-configuration-agreements
요건은 Windows 10 버전 1809(빌드 17763) 이상 또는 Windows 11, winget 1.6.2631 이상입니다.3 오래된 단말이 남아 있는 환경에서는 다음 장의 PowerShell을 통한 구성 쪽이 확실합니다.
4.1. directives 읽는 법 ── allowPrerelease는 앱에 관한 이야기가 아니다
YAML을 다른 샘플에서 이어붙이면 반드시 혼란이 생기는 것이 directives입니다. 여기에 적는 것은 리소스 취급에 관한 지시이지, 설치할 앱의 지정(그것은 settings 쪽)이 아닙니다.
description… 실행 시 표시되는 설명문.winget configure show의 출력이 그대로 읽을 수 있는 내용이 되므로 반드시 작성합니다allowPrerelease… DSC 리소스를 제공하는 PowerShell 모듈의 프리릴리스 버전 사용을 허가하는 지시입니다. “앱의 프리릴리스 버전(베타 버전)을 설치한다”는 의미가 아닙니다.
이 구분이 중요합니다. allowPrerelease는 모듈 단위의 이야기이므로, 같은 resource:를 사용하는 항목들 사이에서 값이 어긋나 있는 것은 대체로 샘플을 이어붙인 데서 온 불일치입니다. 위 예시에서는 Microsoft.WinGet.DSC/WinGetPackage 두 건 중 chrome에만 붙어 있지만, 앱 사정에 따라 나눌 이유는 없으므로 같은 리소스 종류라면 통일해 주세요. 판단 기준은 “그 리소스를 제공하는 모듈에 안정판이 공개되어 있는가” 하나뿐이며, 안정판이 있다면 제외하고, 아직 프리릴리스밖에 없는 모듈을 사용할 때만 붙입니다.
4.2. WinGet Configuration과 자체 PowerShell의 역할 분담
“WinGet Configuration이 멱등한데 왜 다음 장에서 자체 멱등 코드를 작성하는가”는 당연한 의문입니다. 답은 다룰 수 있는 범위가 다르기 때문이며, 대체 관계가 아닙니다. 요건별로 정리합니다.
| 요건 | WinGet Configuration | 자체 PowerShell(5장) | 실무에서의 선택 |
|---|---|---|---|
| winget 공개 앱의 도입 | ◎ 표준 WinGetPackage 리소스 |
○ 작성할 수 있지만 스스로 멱등하게 만들어야 함 | Configuration |
| Windows 설정(개발자 모드 등) | ○ 대응하는 DSC 리소스가 있으면 | ○ | 리소스가 있다면 Configuration |
| 임의의 레지스트리 값(사내 표준 설정) | △ 대응 리소스에 따라 다름 | ◎ 무엇이든 작성 가능 | PowerShell |
| 공유 폴더의 사내 앱(MSI/EXE) | △ 표준 리소스 범위 밖 | ◎ | PowerShell |
| 네트워크 드라이브·공유 프린터 | × 사용자 단위·로그온 시 처리 | ◎ | PowerShell(비승격 단계) |
| 부서별·기종별 차이 | △ YAML을 나눠야 함 | ◎ 설정 JSON 교체만으로 해결 | PowerShell |
| 대상 OS가 오래됨(1809 미만·winget 1.6 미만) | × 요건을 충족하지 못함 | ◎ | PowerShell |
| 종료 코드로 배포 도구에 재부팅 필요 여부를 반환 | △ 제어하기 어려움 | ◎ 스스로 결정 가능 | PowerShell |
결론적으로는 “winget의 영역에 들어가는 것은 Configuration으로, 들어가지 않는 것은 PowerShell로”입니다. 다만 두 가지를 함께 사용한다면, 앱 목록은 어느 한쪽에만 두세요. Configuration의 YAML에 패키지를 적고 5장의 kitting.config.json의 wingetPackages에도 같은 것을 적으면, 한쪽을 수정했을 때 다른 쪽이 오래된 채로 남게 됩니다. Configuration을 병용하는 구성으로 한다면, PowerShell 쪽에서는 앱 도입 처리를 제외하고 JSON에는 레지스트리 값이나 공유 프린터 등 Configuration이 다룰 수 없는 항목만을 담는 것이 실무적입니다. 반대로 Configuration만으로 전부를 조달하려고 대응 리소스를 찾아 헤매는 것은, 투자 대비 효과가 낮은 작업인 경우가 많습니다.
5. PowerShell로 보완하기 ── winget이 하지 않는 부분
실무 키팅에서 수고의 대부분을 차지하는 것은 사실 앱 도입 이외의 부분입니다. 여기서 PowerShell이 활약합니다. 모두 “이미 실행되어 있다면 아무것도 하지 않는” 형태로 작성하는 것이 요점입니다.
이 장은 길기 때문에, 먼저 전체 그림을 보여드립니다. 등장하는 것은 파일 3개뿐이며, “무엇을 설치할지”를 정하는 JSON을 하나 두고, 이를 스크립트 두 개가 각기 다른 권한으로 읽는 구성입니다.
flowchart TB
CFG["kitting.config.json<br/>이 단말이 갖춰야 할 상태 정의"]
subgraph ADM["관리자 단계 - 승격, 단말당 1회"]
A1["Invoke-KsKitting.ps1"]
A2["폴더 / HKLM 레지스트리 /<br/>Windows 기능 / winget 패키지 /<br/>사내 앱 - MSI·EXE"]
A3["설정 JSON을 ProgramData 아래로 배포"]
A1 --> A2
A1 --> A3
end
subgraph USR["사용자 단계 - 비승격, 이용자 로그온마다"]
U1["Invoke-KsUserKitting.ps1"]
U2["HKCU 레지스트리 /<br/>네트워크 드라이브 /<br/>공유 프린터"]
U1 --> U2
end
CFG --> A1
A3 -->|"배포된 설정을 다시 읽기"| U1
그림 1: 키팅의 3단 구성. 같은 정의를 권한이 다른 두 단계가 읽는다
나누는 원칙은 하나뿐입니다. 머신 전체에 효과가 미치는 설정은 관리자 단계, 이용자별로 만들어지는 설정은 사용자 단계. 이 둘을 섞으면, 뒤에서 다루듯 “할당했어야 할 드라이브가 보이지 않는다”는 형태로 반드시 사고가 납니다.
이 뒤에 나오는 코드는 길기 때문에, 어디에 무엇이 적혀 있는지를 먼저 보여드립니다. 관리자 스크립트 안은 --- N. --- 주석으로 구분해 두었으므로, 필요한 곳만 읽으셔도 됩니다.
| 구분 | 하는 일 | 이 부분을 읽어야 할 사람 |
|---|---|---|
| 서두(전처리) | 설정 JSON 읽기, C:\ProgramData로의 배포, Start-Transcript에 의한 로그 시작 |
전원 |
--- 1. 표준 폴더 --- |
폴더 생성. 멱등 작성법의 가장 간단한 예 | 멱등 작성법을 알고 싶은 사람 |
--- 2. 레지스트리 설정 --- |
HKLM에 쓰기. 값뿐 아니라 타입도 비교하는 것이 요점 | 사내 표준 설정을 배포하고 싶은 사람 |
--- 3. Windows 기능 --- |
기능 활성화와 재부팅 필요 여부를 파악하는 방법 | .NET Framework 3.5 등이 필요한 사람 |
--- 4. winget 패키지 --- |
설치 여부 판정과, 종료 코드를 신뢰하지 않는 작성법 | winget 부분만 보고 싶은 사람 |
--- 5. 사내 앱 --- |
공유 폴더의 MSI/EXE를 사일런트 실행. 버전 비교와 종료 코드 처리 | 사내 앱을 배포하고 싶은 사람 |
| 말미(종료 처리) | 3010 / 1641 / 1 / 0의 구분 반환 | 배포 도구(Intune 등)와 연결하는 사람 |
이 스크립트는 kitting.config.json을 읽어들입니다. 설정 파일의 완전한 예시는 이 글의 샘플 코드(글 말미의 zip)에 kitting.config.json으로 동봉되어 있지만, 구조만 먼저 보여드립니다.
{
"folders": [ "C:\\Work", "C:\\KsTools" ],
"registry": [
{ "key": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Edge",
"name": "HomepageLocation", "value": "https://intra.example.co.jp/", "type": "String" }
],
"userRegistry": [
{ "key": "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced",
"name": "HideFileExt", "value": 0, "type": "DWord" }
],
"windowsFeatures": [ "NetFx3" ],
"wingetPackages": [ { "id": "Google.Chrome", "scope": "machine" } ],
"internalApps": [
{ "displayName": "KsApp 업무 시스템 클라이언트",
"installer": "\\\\fileserver\\deploy\\KsApp\\KsAppSetup.msi",
"arguments": "/quiet /norestart INSTALLDIR=\"C:\\Program Files\\KsApp\"",
"version": "3.2.0" }
],
"drives": [ { "letter": "S", "path": "\\\\fileserver\\share" } ],
"printers": [ { "name": "\\\\printserver\\MFP-1F", "connection": "\\\\printserver\\MFP-1F" } ]
}
각 키의 역할과 어느 단계가 읽는지를 표로 정리합니다. 설계 의도는 이 표에 전부 드러나 있습니다.
| 키 | 내용 | 읽는 단계 | 보충 |
|---|---|---|---|
folders |
생성할 표준 폴더 경로 | 관리자(승격) | 존재하면 아무것도 하지 않음 |
registry |
HKLM에 쓰는 사내 표준 설정 | 관리자(승격) | 정책 계열(Edge 홈페이지 등). 모든 사용자에게 적용됨 |
userRegistry |
HKCU에 쓰는 이용자별 설정 | 사용자(비승격) | 확장자 표시 등. HKLM에 써도 효과가 없음 |
windowsFeatures |
활성화할 Windows 옵션 기능 | 관리자(승격) | 재부팅이 필요한 경우가 있음 |
wingetPackages |
winget으로 설치할 패키지의 id와 scope |
관리자(승격) | scope는 사전에 winget show --scope로 확인(2장) |
internalApps |
공유 폴더의 사내 앱(MSI/EXE) | 관리자(승격) | displayName은 “프로그램 및 기능”의 표시 이름과 완전히 일치시켜야 함 |
drives |
네트워크 드라이브 할당 | 사용자(비승격) | 로그온 세션 단위. 승격 쪽에서 만들면 이용자에게 보이지 않음 |
printers |
공유 프린터 연결 | 사용자(비승격) | 위와 동일. 사용자별 연결이 만들어짐 |
단계 열이 두 종류로 나뉘어 있는 것이 이 설정 파일의 설계 그 자체입니다. 새 항목을 추가할 때는, 먼저 “이것은 머신의 설정인가, 이용자의 설정인가”를 정한 다음 어느 키에 넣을지를 선택하세요.
registry와 userRegistry를 나누는 것은 적용 대상이 다르기 때문입니다. 탐색기의 “확장자 표시”(HideFileExt) 같은 설정은 HKLM의 정책이 아니라 이용자별 HKCU를 참조합니다. 관리자 단계에서 HKLM에 써도 표시는 바뀌지 않습니다. 이런 사용자 단위 설정은 userRegistry에 넣고, 후술하는 비승격 단계에서 적용합니다.
internalApps의 displayName은 “프로그램 및 기능”에 표시되는 이름과 완전히 일치시켜야 합니다. version을 지정하면 그 버전 이상이 설치되어 있는지까지 확인합니다(생략 시에는 이름 일치만으로 판정합니다).
앱 도입 자체는 앞 장의 winget import나 WinGet Configuration으로도 할 수 있지만, 이 스크립트에도 wingetPackages의 도입을 포함시켰습니다. 설정 파일을 하나로 모아 두면 “이 단말에 무엇을 설치할지”의 정의가 한곳에 모이고, 실행도 한 번으로 끝나기 때문입니다. 반대로 설정 파일에 적은 항목을 스크립트가 읽지 않은 채 방치하면, 설치되지 않은 단말을 “키팅 성공”으로 기록해 버립니다.
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[string] $ConfigPath = "$PSScriptRoot\kitting.config.json"
)
$ErrorActionPreference = 'Stop'
# 5.1의 Get-Content는 BOM이 없으면 ANSI 코드 페이지로 읽는다.
# UTF-8 JSON을 일본어 환경에서 읽으면 깨지므로 명시적으로 지정한다
$config = Get-Content $ConfigPath -Raw -Encoding utf8 | ConvertFrom-Json
$rebootRequired = $false
$rebootInitiated = $false
$log = "C:\ProgramData\KsKitting\kitting_$(Get-Date -f yyyyMMdd_HHmmss).log"
$null = New-Item -Path (Split-Path $log) -ItemType Directory -Force
# 후술하는 사용자 단위 단계는 별도 프로세스로 동작하므로, 설정을
# 모든 사용자가 읽을 수 있는 위치로 배포해 둔다.
# 배포물을 이 위치에 두고 그대로 실행하면 복사 원본과 대상이 같은 파일이 된다.
# Copy-Item은 자기 자신으로의 복사를 오류로 처리하므로, $ErrorActionPreference = 'Stop'
# 아래에서는 키팅이 시작되기 전에 스크립트째 멈춰 버린다
$sharedConfig = 'C:\ProgramData\KsKitting\kitting.config.json'
$sourceFull = (Resolve-Path $ConfigPath).ProviderPath
$sharedFull = if (Test-Path $sharedConfig) { (Resolve-Path $sharedConfig).ProviderPath } else { '' }
if (-not [string]::Equals($sourceFull, $sharedFull, 'OrdinalIgnoreCase')) {
Copy-Item $ConfigPath $sharedConfig -Force
}
Start-Transcript -Path $log -Append
try {
# --- 1. 표준 폴더 ------------------------------------------------
foreach ($dir in $config.folders) {
if (-not (Test-Path $dir)) {
$null = New-Item -Path $dir -ItemType Directory
Write-Verbose "생성: $dir"
}
}
# --- 2. 사내 표준 레지스트리 설정 --------------------------------------
foreach ($reg in $config.registry) {
if (-not (Test-Path $reg.key)) { $null = New-Item -Path $reg.key -Force }
$item = Get-Item -Path $reg.key
$exists = $item.GetValueNames() -contains $reg.name
# 값뿐 아니라 "타입"도 비교한다. REG_SZ의 "1"과 DWORD의 1은
# PowerShell의 비교에서는 같다고 판정되어, 실제로는 적용되지 않은 설정을
# "적용 완료"로 오판할 수 있다
$sameValue = $exists -and $item.GetValue($reg.name) -eq $reg.value
$sameKind = $exists -and $item.GetValueKind($reg.name).ToString() -eq $reg.type
if (-not ($sameValue -and $sameKind)) {
Set-ItemProperty -Path $reg.key -Name $reg.name -Value $reg.value -Type $reg.type
Write-Verbose "설정: $($reg.key)\$($reg.name) = $($reg.value) ($($reg.type))"
}
}
# --- 3. Windows 기능 --------------------------------------------------
foreach ($feature in $config.windowsFeatures) {
$state = Get-WindowsOptionalFeature -Online -FeatureName $feature
if ($state.State -ne 'Enabled') {
# -NoRestart로 재부팅을 억제한 경우, 필요 여부는 반환값의 RestartNeeded에 나타난다.
# 이를 파악해 두지 않으면 "재부팅이 필요한데 0으로 종료"하게 된다
$result = Enable-WindowsOptionalFeature -Online -FeatureName $feature -NoRestart
if ($result.RestartNeeded) { $rebootRequired = $true }
}
}
# --- 4. winget 패키지 ----------------------------------------------
# winget의 종료 코드는 "이미 설치됨"에서도 0 이외가 될 수 있고,
# 반대로 0이어도 실제로는 설치되지 않은 경우가 있다. 성공 여부는 상태 조회로 판단한다.
# --scope를 붙이지 않으면 관리자 자신의 사용자 단위 설치를 잡아내어
# "machine으로 설치됨"으로 오판할 수 있다
function Test-KsWingetPackage {
param([string] $Id, [string] $Scope)
$arguments = @('list', '--id', $Id, '--exact', '--accept-source-agreements')
if ($Scope) { $arguments += @('--scope', $Scope) }
$null = winget @arguments 2>&1
return ($LASTEXITCODE -eq 0)
}
foreach ($package in $config.wingetPackages) {
if (Test-KsWingetPackage -Id $package.id -Scope $package.scope) { continue }
# 줄바꿈 백틱은 줄 끝에 둔다. 뒤에 주석을 쓰면 줄바꿈으로 인식되지 않는다
winget install --id $package.id -e --silent `
--accept-package-agreements --accept-source-agreements `
--scope $package.scope
$wingetExit = $LASTEXITCODE
# 여기서 경고만 표시하고 진행하면, 필수 앱이 설치되지 않은 단말을
# 배포 도구가 "키팅 성공"으로 기록해 버린다
if (-not (Test-KsWingetPackage -Id $package.id -Scope $package.scope)) {
throw "$($package.id) 도입에 실패했습니다 (winget ExitCode=$wingetExit)"
}
}
# --- 5. 사내 앱(공유 폴더의 설치 프로그램을 사일런트 실행) ----
# 설치된 목록은 32bit/64bit 양쪽 뷰를 명시해서 연다.
# 32bit PowerShell이 64bit Windows에서 동작하면(Intune 구성에 따라 발생할 수 있음),
# WOW64 리디렉션으로 HKLM:\SOFTWARE\...가 32bit 뷰를 가리켜
# 64bit 앱을 "미설치"로 오판해 매번 다시 설치해 버린다
$installedApps = foreach ($view in 'Registry64', 'Registry32') {
$baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine', $view)
try {
$uninstall = $baseKey.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
if (-not $uninstall) { continue }
try {
foreach ($name in $uninstall.GetSubKeyNames()) {
$appKey = $uninstall.OpenSubKey($name)
if (-not $appKey) { continue }
try {
$displayName = $appKey.GetValue('DisplayName')
if ($displayName) {
[pscustomobject]@{
DisplayName = [string] $displayName
DisplayVersion = [string] $appKey.GetValue('DisplayVersion')
}
}
}
finally { $appKey.Dispose() }
}
}
finally { $uninstall.Dispose() }
}
finally { $baseKey.Dispose() }
}
foreach ($app in $config.internalApps) {
$installed = $installedApps | Where-Object DisplayName -eq $app.displayName
# DisplayName의 일치만으로 "설치됨"이라고 판단하지 않는다. 오래된 버전이 설치된 채로
# 새 버전이 설치되지 않아, 재실행해도 설정 파일의 상태로 수렴하지 않게 된다.
# 설정에 version이 있으면 그 버전 이상이 설치되어 있는지까지 확인한다
$upToDate = if ($app.version) {
$wanted = [version] $app.version
[bool]($installed | Where-Object {
$cur = $_.DisplayVersion -as [version] # 버전 형식이 아닌 표기는 대상에서 제외
$cur -and $cur -ge $wanted
})
} else {
[bool] $installed
}
if ($upToDate) { continue }
# Start-Process의 -ArgumentList는 배열을 공백으로 연결해 한 줄의 명령줄로 만들기 때문에,
# 공백을 포함한 값은 설정 파일 쪽에서 따옴표로 감싸 두어야 한다
# 예: '/quiet /norestart INSTALLDIR="C:\Program Files\KsApp"'
# .msi는 실행 파일이 아니므로, 그대로 Start-Process에 전달하면
# "유효한 애플리케이션이 아닙니다"로 실패한다. msiexec를 경유해서 실행한다
if ([System.IO.Path]::GetExtension($app.installer) -eq '.msi') {
$msiArgs = @('/i', "`"$($app.installer)`"") + $app.arguments
$proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList $msiArgs -Wait -PassThru -NoNewWindow
}
else {
$proc = Start-Process -FilePath $app.installer -ArgumentList $app.arguments -Wait -PassThru -NoNewWindow
}
# Windows Installer의 종료 코드는 "0만이 성공"이 아니다.
# 3010과 1641은 모두 성공이며, 재부팅 취급만 다르다
switch ($proc.ExitCode) {
0 { } # 성공
3010 { $rebootRequired = $true } # 성공. 재부팅 필요(ERROR_SUCCESS_REBOOT_REQUIRED)
1641 { $rebootInitiated = $true } # 성공. 설치 프로그램이 재부팅을 시작함
default {
throw "$($app.displayName) 설치에 실패했습니다 (ExitCode=$($proc.ExitCode))"
}
}
# 재부팅이 시작되었다면, 이후의 설치는 실행해도 중단된다
if ($rebootInitiated) { break }
}
if ($rebootInitiated) {
# 1641은 "성공. 다만 재부팅을 이미 시작함". 0을 반환하면 배포 도구는
# "완료되었는데 멋대로 재부팅했다"고 취급하므로, 그대로 전달한다
Write-Host '설치 프로그램이 재부팅을 시작했습니다. 재부팅 후 다시 실행해 주세요' -ForegroundColor Yellow
exit 1641
}
if ($rebootRequired) {
# 3010을 그대로 반환하면 Intune이나 배포 도구 쪽이 "성공. 재부팅 필요"로 해석해
# 재부팅 스케줄링과 보고를 수행할 수 있다. 여기서 0을 반환하면 재부팅이 잊혀진다
Write-Host '키팅 완료(재부팅 필요)' -ForegroundColor Yellow
exit 3010
}
Write-Host '키팅 완료' -ForegroundColor Green
exit 0
}
catch {
Write-Warning "실패: $($_.Exception.Message)"
Write-Warning $_.InvocationInfo.PositionMessage
exit 1
}
finally {
Stop-Transcript
}
이 관리자 스크립트에서 네트워크 드라이브 할당을 의도적으로 제외했다는 점에 주의하세요. 드라이브 문자 할당은 로그온 세션 단위의 설정이므로, 관리자로 승격한 세션에서 생성해도 UAC 아래에서는 이용자의 일반 탐색기에서는 보이지 않습니다. Intune 등에서 시스템 권한으로 실행한 경우에는 애초에 SYSTEM의 세션에 할당되어 버려, 이용자와는 무관합니다. 사용자 고유의 설정은 그 사용자의 로그온 시 비승격으로 실행하는 것이 정답입니다.
# 사용자 단위 설정(비승격으로, 이용자의 로그온 시 실행한다. 등록 방법은 후술)
# 관리자 스크립트와는 별도 프로세스이므로 $config는 이어받지 않는다.
# 관리자 단계에서 ProgramData로 배포해 둔 설정을 다시 읽는다
$configPath = 'C:\ProgramData\KsKitting\kitting.config.json'
if (-not (Test-Path $configPath)) {
Write-Warning "설정 파일을 찾을 수 없습니다: $configPath"
exit 1
}
$config = Get-Content $configPath -Raw -Encoding utf8 | ConvertFrom-Json
# 사용자 단위 레지스트리 설정(HideFileExt 등. HKLM에 써도 효과가 없음)
foreach ($reg in $config.userRegistry) {
if (-not (Test-Path $reg.key)) { $null = New-Item -Path $reg.key -Force }
$item = Get-Item -Path $reg.key
$exists = $item.GetValueNames() -contains $reg.name
$same = $exists -and
$item.GetValue($reg.name) -eq $reg.value -and
$item.GetValueKind($reg.name).ToString() -eq $reg.type
if (-not $same) {
Set-ItemProperty -Path $reg.key -Name $reg.name -Value $reg.value -Type $reg.type
}
}
# 네트워크 드라이브
foreach ($drive in $config.drives) {
$local = "$($drive.letter):"
$existing = Get-SmbMapping -LocalPath $local -ErrorAction SilentlyContinue
if ($existing) {
# "할당이 있는가"만으로 판단하면, 공유 이전 등으로 설정을 바꿔도
# 오래된 할당이 남은 채 "성공"으로 보고되어 버린다. 연결 대상까지 비교한다
if ($existing.RemotePath.TrimEnd('\') -eq $drive.path.TrimEnd('\')) { continue }
Remove-SmbMapping -LocalPath $local -Force
}
New-SmbMapping -LocalPath $local -RemotePath $drive.path -Persistent $true
}
# 공유 프린터 연결도 사용자 단위다. 관리자나 SYSTEM으로 실행하면
# 그 계정에만 연결이 만들어지고, 이용자에게는 보이지 않는다
foreach ($printer in $config.printers) {
if (-not (Get-Printer -Name $printer.name -ErrorAction SilentlyContinue)) {
Add-Printer -ConnectionName $printer.connection
}
}
공유 프린터 연결(Add-Printer -ConnectionName)도 같은 취급입니다. 이는 사용자별 연결을 만드는 조작이므로, 관리자 스크립트나 Intune의 SYSTEM 실행으로 수행해도, 나중에 로그온하는 직원에게는 보이지 않습니다. 전체 단말 공통으로 두고 싶다면, 프린터를 머신 단위로 배포하는 구조(인쇄 서버의 정책 배포 등)를 사용하거나, 이 비승격 단계에서 실행하세요.
같은 이유로, 사용자 프로필 아래의 파일 배치, HKCU로의 쓰기, 사용자용 바로가기 생성도 이 비승격 단계에 모읍니다. “머신 전체의 설정은 관리자로 한 번, 사용자 고유의 설정은 로그온마다 비승격으로”라는 이단 구성이 키팅 스크립트의 기본형입니다. UNC 경로와 드라이브 할당에 관한 주의점은 “네트워크 공유·UNC 경로의 함정“도 참조하세요.
사용자 단계를 어떻게 실행할 것인가. 이단 구성의 핵심은 이 비승격 스크립트를 “이용자의 로그온 시, 그 이용자 본인의 권한으로” 실행시키는 장치입니다. 이 부분이 연결되어 있지 않으면 관리자 단계만 동작해 “드라이브도 프린터도 설정되지 않은 단말”이 만들어집니다. 방법은 3가지가 있습니다.
| 방법 | 실행 시점 | 적합한 환경 | 주의점 |
|---|---|---|---|
| HKLM의 Run 키 | 모든 사용자의 로그온마다 | 도메인 미참가. Intune·배포 도구에서 스크립트 하나로 완결하고 싶음 | 매번 실행되므로 멱등이 전제. 창을 표시하지 않는 지정이 필요 |
| 작업 스케줄러(로그온 시 트리거) | 모든 사용자의 로그온마다 | 실행 결과 이력을 남기고 싶음. 지연 실행하고 싶음 | “최고 권한으로 실행”을 꺼야 함(켜면 승격되어 의미가 없어짐) |
| 로그온 스크립트(그룹 정책) | 모든 사용자의 로그온마다 | 도메인 참가 완료 | 기존 GPO 운영에 태울 수 있지만 단독 단말에서는 다루기 어려움 |
방법 1: HKLM의 Run 키에 등록한다(관리자 단계의 마지막에 실행합니다)
# 사용자 단계 스크립트를 모든 사용자가 읽을 수 있는 위치에 배치한다
$userScript = 'C:\ProgramData\KsKitting\Invoke-KsUserKitting.ps1'
Copy-Item "$PSScriptRoot\Invoke-KsUserKitting.ps1" $userScript -Force
# HKLM의 Run은 로그온한 이용자의 권한(비승격)으로 실행된다.
# HKCU에 쓰려고 해도, 관리자/SYSTEM으로 동작하는 이상 쓰이는 것은
# "관리자 자신의 HKCU"이며, 이제부터 사용할 직원의 HKCU가 아니다
$runKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
$command = 'powershell.exe -NoProfile -ExecutionPolicy Bypass ' +
"-WindowStyle Hidden -File `"$userScript`""
Set-ItemProperty -Path $runKey -Name 'KsUserKitting' -Value $command
방법 2: 작업 스케줄러에 등록한다(이력을 남기고 싶은 경우)
방법 1을 건너뛰고 여기서부터 읽는 경우를 대비해, 스크립트 배치부터 다시 작성합니다.
# 모든 사용자가 읽을 수 있는 위치에 배치한다(방법 1과 동일)
$userScript = 'C:\ProgramData\KsKitting\Invoke-KsUserKitting.ps1'
New-Item -ItemType Directory -Path (Split-Path $userScript) -Force | Out-Null
Copy-Item -Path '.\Invoke-KsUserKitting.ps1' -Destination $userScript -Force
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$userScript`""
$trigger = New-ScheduledTaskTrigger -AtLogOn
# BUILTIN\Users(S-1-5-32-545)를 지정하면 로그온한 본인의 권한으로 실행된다.
# RunLevel Limited가 "승격하지 않음"의 지정이다. 여기를 Highest로 하면
# 승격된 세션에서 실행되어 드라이브 할당이 이용자에게 보이지 않게 된다
$principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545' -RunLevel Limited
Register-ScheduledTask -TaskName 'KsUserKitting' `
-Action $action -Trigger $trigger -Principal $principal -Force
방법 3: 그룹 정책의 로그온 스크립트
설정 위치는 사용자 구성 > 정책 > Windows 설정 > 스크립트(로그온/로그오프) > 로그온입니다(단독 단말의 gpedit.msc에서는 정책 계층이 없어 사용자 구성 > Windows 설정 > 스크립트(로그온/로그오프)). 대화상자의 “PowerShell 스크립트” 탭에 추가하세요. “스크립트” 탭에 .ps1을 직접 적으면 실행 파일로 취급되어 의도한 동작이 되지 않습니다. 로컬 정책의 경우, 스크립트 실체는 %SystemRoot%\System32\GroupPolicy\User\Scripts\Logon에 배치됩니다.
어느 방법이든 로그온할 때마다 실행된다는 점은 동일합니다. 사용자 단계 스크립트를 멱등하게(현재 상태를 확인한 뒤 변경하는 방식으로) 작성해 둔 것은 이 때문이기도 합니다. “한 번만 실행하고 싶다”면 HKCU 아래에 완료 마커를 적어 두고 맨 앞에서 확인하는 형태로 만드세요.
설계상의 요점을 짚어 둡니다.
- 승격이 필요한 설정과 사용자 단위 설정을 나눈다. 앞서 설명한 대로 섞으면 “할당했어야 할 드라이브가 보이지 않는” 사고로 이어집니다
- 설정은 JSON으로 외부화한다. 부서별·기종별 차이를 스크립트를 바꾸지 않고 표현할 수 있습니다
- 각 처리 전에 현재 상태를 확인한다. 이로써 몇 번을 실행해도 안전해집니다
Start-Transcript로 증적을 남긴다. “이 단말에서 무엇을 했는지”를 나중에 추적할 수 있습니다(“PowerShell의 출력 스트림과 로그 설계”)- 종료 코드를 반환한다. Intune이나 배포 도구에서 성공 여부를 판정할 수 있습니다. Windows Installer에서는 0만이 성공이 아니라는 점에 주의하세요. 3010(ERROR_SUCCESS_REBOOT_REQUIRED, 재부팅 필요)과 1641(ERROR_SUCCESS_REBOOT_INITIATED, 재부팅 이미 시작됨)은 모두 성공입니다.6 이들을
default로 떨어뜨려 실패로 취급하면, 성공한 설치가 배포 도구 상에서는 빨간색으로 기록됩니다. 성공으로 취급한 뒤, 마지막에 그 코드 그대로 호출자에게 반환하는 것이 요점입니다. 0으로 반환하면 배포 도구 쪽이 재부팅 필요성을 알 수단을 잃습니다(“PowerShell의 오류 처리와 재실행 설계”) - 설치 프로그램 인자의 따옴표에 주의한다.
Start-Process -ArgumentList는 배열을 공백으로 연결할 뿐이므로, 인자의 구분은 유지되지 않습니다. 공백을 포함한 경로는 설정 파일 쪽에서 따옴표를 붙이거나,ProcessStartInfo.ArgumentList(PowerShell 7)를 사용하세요(“PowerShell에서 외부 exe를 올바르게 호출하기”)
6. PowerShell 모듈에서 winget 사용하기
명령줄 출력을 문자열로 분석하는 것보다, PowerShell 모듈을 사용하는 쪽이 견고합니다. Microsoft.WinGet.Client에는 Find-WinGetPackage / Install-WinGetPackage / Get-WinGetPackage / Update-WinGetPackage 등의 cmdlet이 있습니다.4
Install-Module -Name Microsoft.WinGet.Client -Scope AllUsers -Force
# 이미 설치되어 있는지 확인한 뒤 설치한다(멱등)
foreach ($id in 'Google.Chrome', 'Microsoft.VisualStudioCode', '7zip.7zip') {
if (Get-WinGetPackage -Id $id -ErrorAction SilentlyContinue) {
Write-Verbose "설치됨: $id"
continue
}
Install-WinGetPackage -Id $id -Mode Silent -Scope System
}
객체로서 결과가 반환되므로, 성공 여부 판정이나 목록 대조를 그대로 작성할 수 있는 것이 장점입니다.
7. 무인 실행·시스템 컨텍스트 주의사항
키팅을 완전 자동화하려고 하면 반드시 “어느 계정으로 실행할 것인가”라는 문제에 부딪힙니다.
- winget은 사용자 컨텍스트에서의 실행을 전제로 하는 부분이 있습니다. 시스템 컨텍스트에서의 실행은 Microsoft가 향후 기능으로 들고 있는 단계입니다3
- 승격이 필요한 처리와 사용자 고유의 처리는 나눕니다. 머신 전체의 설정은 관리자 권한으로, 사용자 프로필 아래의 설정은 최초 로그온 시 실행하는 구성이 다루기 쉬워집니다
- 반드시 실제 실행 계정으로 검증하세요. “수중의 관리자 계정에서는 동작했는데 배포하니 동작하지 않는다”는 이 분야에서 가장 흔한 실패입니다(“작업 스케줄러의 작업이 실행되지 않는다”)
8. 실무의 정석(판단표)
| 할 일 | 수단 | 보충 |
|---|---|---|
| 시판·OSS 제품의 도입 | winget install / configure | --silent --accept-*는 필수1 |
| 기존 표준기 구성 추출 | winget export |
설정은 포함되지 않음. 출발점으로 사용2 |
| 앱 + Windows 설정을 선언적으로 | winget configure(YAML) |
Win10 1809 이상 + winget 1.6 이상3 |
| 레지스트리·Windows 기능·사내 앱 | PowerShell(관리자) | 상태 확인 후 변경(멱등) |
| 공유 드라이브·공유 프린터·사용자 고유 설정 | PowerShell(로그온 시·비승격) | 승격·SYSTEM으로 만들면 이용자에게 보이지 않음 |
| 사내 독자 앱 | PowerShell + 사일런트 설치 프로그램 | 전용 리포지토리 구축은 소규모에는 과합니다 |
| PowerShell에서 제어하고 싶음 | Microsoft.WinGet.Client |
출력의 문자열 분석이 필요 없어짐4 |
| 무인 실행 | 실행 계정으로 검증 | 시스템 컨텍스트는 제약 있음3 |
| 실시 기록 | Start-Transcript + 종료 코드 |
“이 단말에 무엇을 했는지”를 남김 |
| 재부팅이 필요한 경우 | 종료 코드 3010 / 1641을 반환 | 둘 다 성공. 0으로 반환하면 배포 도구가 재부팅을 인식할 수 없음6 |
9. 정리
- 키팅 절차서는 실행 가능한 파일로 바꿀 수 있습니다. 앱 도입은 winget, 그 외 설정은 PowerShell이라는 역할 분담이 현실적입니다.
winget install에서는--silent와--accept-package-agreements--accept-source-agreements를 반드시 붙입니다. 빠뜨리면 무인 실행이 멈춥니다.winget export는 기존 표준기에서 구성을 추출할 수 있지만, 설정이나 winget 관리 밖의 앱은 포함되지 않습니다.- WinGet Configuration(
winget configure)을 사용하면 앱과 설정을 선언적인 파일 하나로 모을 수 있고, 재실행에 강한 구성이 됩니다. - PowerShell 쪽 처리는 반드시 “현재 상태를 확인한 뒤 변경한다”는 방식으로 작성해 멱등성을 확보하세요.
- 머신 전체의 설정은 관리자 권한으로, 네트워크 드라이브나 공유 프린터 등 사용자 고유의 설정은 로그온 시 비승격으로, 실행 단계를 나누세요. 승격 세션이나 SYSTEM으로 만든 연결은 이용자에게 보이지 않습니다.
- 무인 실행에서는 실행 계정의 검증이 가장 중요합니다. 시스템 컨텍스트에서의 winget 실행에는 제약이 있다는 것을 전제로 설계하세요.
샘플 코드 다운로드
이 글에서 다룬 코드는 그대로 동작시킬 수 있는 형태로 정리해 배포하고 있습니다. 관리자 단계·사용자 단계·설정 파일의 완전한 예시가 들어 있습니다.
이 글의 샘플은 Windows나 테넌트에 의존하기 때문에 실행 검증은 하지 않았습니다. 구문 분석과 PSScriptAnalyzer를 통한 정적 분석까지는 모든 파일에 대해 실시했지만, 동작은 반드시 여러분의 검증 환경에서 확인해 주세요.
# 구문 분석 + 정적 분석(Windows가 아니어도 실행 가능)
./Invoke-SampleTests.ps1
설정값(경로, 서버명, 테넌트 ID 등)은 예시입니다. 그대로 프로덕션 환경에서 실행하지 말고 자사 환경에 맞게 바꿔서 사용하세요.
관련 글
- PowerShell에서 외부 exe를 올바르게 호출하기 ── 인자의 따옴표・종료 코드・문자 깨짐의 함정
- PowerShell의 에러 처리와 재실행 설계 ── try/catch가 먹히지 않는 함정부터 exit code·재시도의 정석까지
- Write-Host를 그만두다 ── PowerShell의 출력 스트림과 로그 설계
- 작업 스케줄러 작업이 실행되지 않는다・0x1로 끝난다 ── 원인 분리와 안전한 운영 설계
- 키오스크 모드로 업무 단말을 지키다 ── Assigned Access・Shell Launcher 선택법과 운영 설계
- Windows 10 지원 종료 이후의 현실적 해법 ── ESU·LTSC·교체 판단표
관련 상담 영역
합동회사 코무라소프트에서는 PC 키팅이나 사내 표준 환경의 자동화, 절차서로 사람에게 의존하고 있는 운영의 실행 가능화, 배포 스크립트 설계 지원을 다루고 있습니다.
참고 링크
-
Microsoft Learn, install 명령(winget). –id / -e를 통한 대상 지정, –silent를 통한 무인 설치, –accept-package-agreements / –accept-source-agreements를 통한 사용 조건 동의, –scope를 통한 설치 범위(user / machine) 지정에 대해. 아울러 Use WinGet to install and manage applications의 명령 목록에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, export 명령(winget). 설치된 패키지 목록을 JSON으로 내보낼 수 있다는 점, –include-versions를 통한 버전 기록, import 명령을 통한 복원과 –ignore-unavailable의 동작, 내보내기 대상이 winget 관리하 패키지에 한정된다는 점, 그리고 출력 JSON의 계층(Sources / Packages / PackageIdentifier / Version, Version은 선택 사항)에 대해. JSON 구조는 packages.schema.2.0.json에도 정의되어 있으며, 최상위가 WinGetVersion·CreationDate·Sources이고, Sources의 각 요소가 SourceDetails(Name / Identifier / Argument / Type)와 Packages를 가지며, Packages의 각 요소가 PackageIdentifier를 필수로 하고 Version 등을 선택 사항으로 가진다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, WinGet Configuration. WinGet Configuration이 YAML로 원하는 상태를 선언하고 PowerShell DSC를 사용해 적용하는 구조라는 점, 무인 설치에 이용할 수 있다는 점, Windows 10 버전 1809(빌드 17763) 이상 또는 Windows 11과 WinGet v1.6.2631 이상이 필요하다는 점, 관리자 셸에서 실행한 경우의 UAC 취급, 시스템 컨텍스트에서의 실행이 향후 개발 항목으로 올라와 있다는 점에 대해. 아울러 configure 명령의 show / –accept-configuration-agreements에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
GitHub, microsoft/winget-cli ─ Microsoft.WinGet.Client PowerShell 모듈. PowerShell Gallery에서 Microsoft.WinGet.Client 모듈을 설치할 수 있다는 점, Find-WinGetPackage / Get-WinGetPackage / Install-WinGetPackage / Update-WinGetPackage / Uninstall-WinGetPackage 등의 cmdlet이 제공되며 결과를 객체로 다룰 수 있다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, show 명령(winget). 지정한 애플리케이션의 상세 정보(메타데이터와 설치 프로그램 정보)를 표시하는 명령이라는 점, –scope 옵션으로 설치 범위(user / machine)를 선택할 수 있다는 점, 표시되는 설치 프로그램 정보가 지정한 인자와 WinGet의 판단에 기반한다는 점에 대해. ↩
-
Microsoft Learn, Windows Installer 오류 코드. ERROR_SUCCESS_REBOOT_REQUIRED(3010)가 “변경 사항을 적용하려면 재부팅이 필요함. 설치 자체는 성공”이라는 점, ERROR_SUCCESS_REBOOT_INITIATED(1641)가 “설치 프로그램이 재부팅을 시작함. 성공을 나타내는 코드”라는 점에 대해. ↩ ↩2
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
PowerShell 스크립트의 인수 설계와 모듈화 ── 「작동하는 스크립트」에서 「남에게 넘길 수 있는 스크립트」로
PowerShell 스크립트를 다른 사람에게 넘길 수 있는 품질로 끌어올리는 절차를 정리합니다. param 블록과 [CmdletBinding()], 입력 검증, 파이프라인 입력, -WhatIf 대응, .psm1 모듈화, 사내 공유와 Git 관리의...
PowerShell 모듈의 사내 배포와 업데이트 ── PSResourceGet과 사내 리포지토리
공유 폴더의 ps1을 복사해 돌려쓰는 운영에서 벗어나는 방법을 정리합니다. 모듈 매니페스트 작성법, 버저닝, PSResourceGet을 이용한 사내 리포지토리 구축과 배포·업데이트, 서명과의 조합까지 해설합니다.
PowerShell로 REST API와 연동하기 ── Invoke-RestMethod의 실무
PowerShell에서 사내 API나 SaaS의 REST API를 호출하는 실무를 정리합니다. 인증 헤더를 넘기는 방법, 한국어(다국어) JSON의 문자 깨짐 대책, 4xx/5xx 에러 처리, 429 재시도, 페이징, 프록시와 TLS의 함정까지 ...
PowerShell 스크립트가 느릴 때 살펴볼 곳 ── 배열・파이프라인・대조의 급소
PowerShell 스크립트가 느려지는 대표적인 원인을 정리합니다. 배열의 +=가 O(n^2)이 되는 이유, 파이프라인과 foreach의 차이, 대조의 해시테이블화, 파일 I/O 개선, 그리고 올바른 측정법까지 실무 관점에서 설명합니다.
Write-Host를 그만두다 ── PowerShell의 출력 스트림과 로그 설계
PowerShell 6가지 출력 스트림의 구분 사용법, Write-Host가 안고 있는 문제와 올바른 쓰임새, 함수의 반환값이 오염되는 원인, -Verbose나 -InformationVariable을 통한 호출 측 제어, 구조화 로그를 남기는 방...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- winget import로 키팅을 전부 자동화할 수 있나요?
- 앱 설치까지는 자동화할 수 있지만, 그것만으로는 충분하지 않습니다. winget import가 재현하는 것은 패키지 목록일 뿐이며, 앱 내부 설정, 프린터 추가, 네트워크 드라이브 할당, 전원 설정, 레지스트리를 통한 사내 표준 설정 등은 대상이 아닙니다. 또한 winget 이외의 방법으로 설치한 앱이나 사내 독자 업무 앱은 export에 포함되지 않습니다. 실무에서는 앱 도입은 winget에 맡기고, 나머지 설정은 PowerShell 스크립트로 보완하는 이단 구성이 현실적입니다.
- winget과 WinGet Configuration(winget configure)은 어떻게 다른가요?
- winget의 install/import는 '이 순서로 이것을 설치한다'는 절차적인 지시이지만, WinGet Configuration은 '최종적으로 이 상태이기를 바란다'는 선언을 YAML 파일에 적는 방식입니다. 내부적으로 PowerShell DSC를 사용하며, 앱 도입뿐 아니라 Windows 설정이나 앱 구성까지 파일 하나로 표현할 수 있습니다. 이미 원하는 상태라면 아무것도 하지 않으므로, 도중에 실패해도 같은 파일을 다시 실행하면 되고, 키팅을 다시 진행하는 데 강한 것이 장점입니다. Windows 10 1809 이상과 winget 1.6 이상이 필요합니다.
- 작업 스케줄러나 Intune에서 SYSTEM 권한으로 winget을 실행해도 괜찮나요?
- 주의가 필요합니다. winget은 사용자 컨텍스트에서의 실행을 전제로 하는 부분이 있으며, 시스템 컨텍스트에서의 실행은 Microsoft가 향후 개발 항목으로 들고 있는 단계입니다. 실무에서는 전체 사용자용으로 설치하는 --scope machine을 사용하거나, PowerShell 모듈(Microsoft.WinGet.Client)을 경유해 실행하거나, 최초 로그온 시 사용자 컨텍스트에서 실행하는 등의 우회책을 취합니다. 어느 쪽이든 실제로 사용할 실행 계정으로 반드시 검증하세요.
- 키팅 스크립트는 몇 번을 실행해도 안전해야 하나요?
- 예. 멱등성(몇 번 실행해도 같은 결과가 되는 것)은 필수라고 생각해야 합니다. 키팅은 도중에 실패하는 일이 일상적으로 있으며, 그때마다 처음부터 다시 시작할 수 있어야 합니다. 폴더 생성은 Test-Path로 존재를 확인한 뒤, 레지스트리 설정은 현재 값을 확인한 뒤, 앱 도입은 이미 설치되어 있는지 확인한 뒤 실행하도록 작성해 두면, 실패한 지점부터 다시 시작할 수 있습니다. WinGet Configuration은 이 사고방식이 처음부터 내장되어 있습니다.
- 사내 독자 업무 앱도 winget으로 배포할 수 있나요?
- 사내용 프라이빗 리포지토리(REST API 소스)를 준비하면 가능하지만, 이를 위한 서버를 구축·유지하는 수고가 듭니다. 몇 개 정도의 앱이라면, PowerShell 스크립트로 공유 폴더의 설치 프로그램을 사일런트 실행하는 쪽이 간단합니다. 시판·OSS 제품은 winget에 맡기고 사내 앱은 PowerShell로 설치한다는 구분이 소규모 조직에서는 가장 현실적입니다.