수정 이력(7건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 기사 맨 앞에 「이 기사의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대응해 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하세요.
- 사용자 단계를 작업 스케줄러로 시작하는 절차가, 바로 앞 절차를 실행하지 않으면 동작하지 않는 서술이었기 때문에, 스크립트 배치부터 독립해서 읽을 수 있게 고쳤습니다. 아울러 WinGet Configuration과 함께 쓸 때 앱 목록이 이중으로 관리되는 점을 올바르게 다시 설명했습니다.
- 키팅 스크립트의 3부 구성을 그림으로 그리고, 긴 코드의 어디에 무엇이 있는지를 대응표로 추가했습니다. 아울러 사용자 단계 시작 방법 3가지 비교와 실제 등록 절차, 설정 JSON의 전체 키 목록, WinGet Configuration과 자체 스크립트의 역할 분담을 추가했습니다.
- 본문의 관련 기사 링크 문구가 링크 대상의 현재 제목과 어긋나 있던 부분을 실제 제목에 맞췄습니다. 본문 내용은 바꾸지 않았습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22175094)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「winget + PowerShell로 PC 키팅을 자동화한다 ── 절차서를 실행 가능하게 만들기」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/winget-powershell-pc-kitting/
- DOI(등록된 아카이브)
- 10.5281/zenodo.22175094
- DOI(마지막 등록 버전)
- 10.5281/zenodo.22175095
신입 사원이 들어올 때마다, 또는 PC를 교체할 때마다 담당자가 절차서를 보면서 한 대씩 설정한다 ── 중소기업 IT에서는 아직도 이것이 흔한 장면입니다. 문제는 시간만이 아닙니다. 수작업은 재현성이 없기 때문에 「이 단말만 설정이 다르다」는 문제가 나중에 커집니다. 절차서는 갱신되지 않은 채 낡아지고, 담당자가 바뀌면 세부 사항이 사라집니다.
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로 사일런트 실행하는 것이 현실적입니다.
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 20건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
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를 받으므로, machine scope 설치 프로그램이 준비되어 있는지를 미리 조사할 수 있습니다.5
# machine scope 설치 프로그램이 있는지 확인한다
winget show --id Google.Chrome -e --scope machine
# 비교를 위해 user scope 쪽도 본다
winget show --id Google.Chrome -e --scope user
지정한 scope에 해당하는 설치 프로그램이 없으면 그 취지의 메시지가 됩니다. 키팅 설정 파일에 "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 절차적 스크립트와 비교한 이점은 세 가지입니다.
- 이미 원하는 상태라면 아무것도 하지 않는다(멱등)
- 앱 도입과 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이 나설 차례입니다. 모두 「이미 실행됐으면 아무것도 하지 않는」 형태로 쓰는 것이 핵심입니다.
이 장은 길어서 먼저 전체 모습을 보입니다. 등장하는 파일은 세 개뿐이며, 「무엇을 넣을지」를 정하는 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 경로의 함정」도 참조하세요.
사용자 단계를 어떻게 시작할 것인가. 두 단계의 핵심은 이 비승격 스크립트를 「이용자 로그온 시, 그 이용자 자신의 권한으로」 돌리는 장치입니다. 여기가 이어지지 않으면 관리자 단계만 돌아가 「드라이브도 프린터도 설정되지 않은 단말」이 됩니다. 방법은 세 가지입니다.
| 방법 | 실행 시점 | 맞는 환경 | 주의점 |
|---|---|---|---|
| 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 옵션으로 설치 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로 넣는 역할 분담이 소규모 조직에서는 가장 현실적입니다.