Search
๐Ÿ’ก

Command

Created
2026/01/22 10:40
Tags
2026/01/22 12:06

Context

Auto-complete ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ํƒ‘์žฌํ•œ Popover๊ฐ€ ํ•„์š”ํ–ˆ๋‹ค. shadcn์—์„œ๋Š” Command๋ผ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•ด ์ด ๊ธ€์„ ์ž‘์„ฑํ•œ๋‹ค.

Agenda

โ€ข
Command์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์ดํ•ดํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•จ

Contents

Auto-complete ์ปดํฌ๋„ŒํŠธ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•˜๋Š” ๊ฒƒ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์˜ต์…˜์—์„œ ์ฐพ๊ณ  ์„ ํƒ ์‹œ ๊ทธ ๊ฐ’์„ ํ• ๋‹นํ•œ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ Auto-complete๋Š” Input ํ˜•ํƒœ๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์–ด์•ผํ•˜๋Š”๋ฐ, ์ด ๊ฒฝ์šฐ ๋ช‡๋ช‡ ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค. ์‚ฌ์šฉ์ž์˜ ์š”๊ตฌ์‚ฌํ•ญ์ด ๋ฐœ์ƒํ•œ์ ์ด ์žˆ์—ˆ๋Š”๋ฐ, ์–ด๋А ๊ฐ’์„ ์ž…๋ ฅํ•œ ๋’ค ์„ ํƒ ํ›„ ๋‹ค์‹œ ์žฌ์ž…๋ ฅ ํ•  ๋•Œ ์ด ๋•Œ ์ƒˆ๋กœ์šด ๊ฐ’์„ ์„ ํƒํ•˜์ง€ ์•Š์œผ๋ฉด ์ด์ „ ๊ฐ’์œผ๋กœ ๋‚จ์•„์žˆ๊ฒŒ ํ•ด๋‹ฌ๋ผ๋Š” ๊ฒƒ์ด์—ˆ๋‹ค. ์ƒํƒœ ๊ฐ’์„ ์ €์žฅํ•ด๋‘๋Š” ๊ฒƒ์œผ๋กœ ํฐ ์–ด๋ ค์›€์ด ์žˆ์ง€๋Š” ์•Š์ง€๋งŒ ํ†ต์ผ์„ฑ์ด ๊นจ์ง€๋Š” ๋А๋‚Œ์ด๋‹ค. shadcn์—์„œ ๋ ˆํผ๋Ÿฐ์Šค๋ฅผ ์ฐพ๋˜ ์ค‘ Command๋ผ๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ์–ด ์ด ๊ฒƒ์— ๋Œ€ํ•ด ๋ถ„์„ํ•ด๋ณธ๋‹ค.
Command
const Command = ({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) => { return ( <CommandPrimitive data-slot='command' className={cn( 'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md', className, )} {...props} /> ); };
TypeScript
๋ณต์‚ฌ
Command ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. ๋‹ค๋ฅธ shadcn/ui ์ปดํฌ๋„ŒํŠธ์™€ ๊ฐ™๊ฒŒ ์–ด๋А ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ์‹ธ๋Š” Wrapper ํ˜•์‹์œผ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ, ์ด Command๋Š” radix-ui๊ฐ€ ์•„๋‹Œ cmdk๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. ๊ธฐ๋ณธ ์Šคํƒ€์ผ๋ง์œผ๋กœ flex display์— ๊ทธ ์•ˆ์˜ ํ•ญ๋ชฉ๋“ค์„ ์„ธ๋กœ๋กœ ๋ฐฐ์น˜ํ•œ๋‹ค. ์ด Command ์ปดํฌ๋„ŒํŠธ๋Š” ์•„๋ž˜์„œ ๊ธฐ์ˆ ํ•  ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ์‹ธ๋Š” ๋˜๋‹ค๋ฅธ Wrapper์ด๋‹ค.
CommandDialog
const CommandDialog = ({ title = 'Command Palette', description = 'Search for a command to run...', children, className, showCloseButton = true, ...props }: React.ComponentProps<typeof Dialog> & { title?: string; description?: string; className?: string; showCloseButton?: boolean; }) => { return ( <Dialog {...props}> <DialogHeader className='sr-only'> <DialogTitle>{title}</DialogTitle> <DialogDescription>{description}</DialogDescription> </DialogHeader> <DialogContent className={cn('overflow-hidden p-0', className)} showCloseButton={showCloseButton} > <Command className='**:[[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5'> {children} </Command> </DialogContent> </Dialog> ); };
TypeScript
๋ณต์‚ฌ
Command์— ๋ Œ๋”๋ง ๋  ํ•ญ๋ชฉ์„ ๊ฐ์‹ธ๋Š” ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. shadcn/ui์˜ Dialog ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํ…œํ”Œ๋ฆฟ์„ ๊ตฌ์„ฑํ•œ๋‹ค.
CommandInput
const CommandInput = ({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Input>) => { return ( <div data-slot='command-input-wrapper' className='flex h-10 items-center gap-2 border-b px-3' > <SearchIcon className='size-4 shrink-0 opacity-50' /> <CommandPrimitive.Input data-slot='command-input' className={cn( 'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50', className, )} {...props} /> </div> ); };
TypeScript
๋ณต์‚ฌ
CommandInput ์ปดํฌ๋„ŒํŠธ๋Š” Command ์•ˆ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๊ฒ€์ƒ‰ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. ๋ณดํ†ต ์ด ์•„๋ž˜์— CommandList๊ฐ€ ๋ฐฐ์น˜ ๋˜๋ฉฐ ์ž…๋ ฅํ•œ ๊ฒƒ์„ ๊ธฐ๋ฐ˜์œผ๋กœ CommandList๊ฐ€ ํ•„ํ„ฐ๋ง ๋˜์–ด ํ‘œ์‹œ ๋œ๋‹ค.
์›๋ž˜ CommandInput ๋‚ด๋ถ€์˜ wrapper์ธ div์˜ className์€ ๋‹ค์Œ๊ณผ ๊ฐ™์•˜๋‹ค.
className='flex h-9 items-center gap-2 border-b px-3'
JavaScript
๋ณต์‚ฌ
๊ทธ๋Ÿฐ๋ฐ CommandPrimitive.Input์˜ height์ด h-10์œผ๋กœ ๋˜์–ด์žˆ์–ด์„œ h-10์œผ๋กœ ์ˆ˜์ •ํ•˜์˜€๋‹ค.
CommandList
const CommandList = ({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) => { return ( <CommandPrimitive.List data-slot='command-list' className={cn( 'max-h-75 scroll-py-1 overflow-x-hidden overflow-y-auto', className, )} {...props} /> ); };
TypeScript
๋ณต์‚ฌ
Command ํ•ญ๋ชฉ์ด ํ‘œ์‹œ ๋˜๋Š” ์˜์—ญ์ด๋‹ค. CommandItem์ด ํ‘œ์‹œ๋˜๋Š” ๊ณณ์ด๋‹ค.
CommandEmpty
const CommandEmpty = ({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) => { return ( <CommandPrimitive.Empty data-slot='command-empty' className='py-6 text-center text-sm' {...props} /> ); };
TypeScript
๋ณต์‚ฌ
CommandList์— ๋Œ€์ƒ์ด ์—†์„ ๋•Œ ๋…ธ์ถœ ๋˜๋Š” ํ•ญ๋ชฉ์ด๋‹ค.
CommandGroup
const CommandGroup = ({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Group>) => { return ( <CommandPrimitive.Group data-slot='command-group' className={cn( 'text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium', className, )} {...props} /> ); };
TypeScript
๋ณต์‚ฌ
Command์— ํ‘œ์‹œ ๋˜๋Š” List๋Š” Group์„ ์ง€์–ด์„œ ํ‘œ์‹œ ๋  ์ˆ˜ ์žˆ๋‹ค. ์ด Group ๋‹จ์œ„์˜ Wrapper๋ฅผ ๋‹ด๋‹นํ•˜๋Š” CommandGroup ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. CommandGroup์˜ heading ํŒŒ๋ผ๋ฏธํ„ฐ์— ๊ฐ’์„ ๋ถ€์—ฌํ•ด์„œ ํ•ด๋‹น ๊ทธ๋ฃน์˜ ํƒ€์ดํ‹€์„ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋‹ค.
CommandSeparator
const CommandSeparator = ({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Separator>) => { return ( <CommandPrimitive.Separator data-slot='command-separator' className={cn('bg-border -mx-1 h-px', className)} {...props} /> ); };
TypeScript
๋ณต์‚ฌ
๊ทธ๋ฃน๊ณผ ๊ทธ๋ฃน๊ฐ„ ๋ถ„๋ฆฌ์„ ์„ ํ‘œ์‹œ ํ•ด์ฃผ๋Š” ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. -mx-1์€ ์–‘์ชฝ์œผ๋กœ ์ด ๋ถ„๋ฆฌ์„ ์„ ๋Š˜๋ ค์ฃผ๋Š” ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜๋Š”๋ฐ ์™œ๋ƒํ•˜๋ฉด ๋งŒ์•ฝ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์— ํŒจ๋”ฉ์ด ์žˆ์œผ๋ฉด ๋ถ„๋ฆฌ์„ ์ด ๋๊นŒ์ง€ ๊ฐ€์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
CommandItem
const CommandItem = ({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) => { return ( <CommandPrimitive.Item data-slot='command-item' className={cn( "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} {...props} /> ); };
TypeScript
๋ณต์‚ฌ
CommandList์— ํ‘œ์‹œ ๋˜๋Š” Item ์ปดํฌ๋„ŒํŠธ์ด๋‹ค.
CommandShortcut
const CommandShortcut = ({ className, ...props }: React.ComponentProps<'span'>) => { return ( <span data-slot='command-shortcut' className={cn( 'text-muted-foreground ml-auto text-xs tracking-widest', className, )} {...props} /> ); };
TypeScript
๋ณต์‚ฌ
Command ๋ฉ”๋‰ด์—์„œ ๋‹จ์ถ•ํ‚ค ํ‘œ์‹œ์šฉ ์ปดํฌ๋„ŒํŠธ์ด๋‹ค. ml-auto๋ฅผ ํ†ตํ•ด ์˜ค๋ฅธ์ชฝ ๋์— ๋ฐฐ์น˜ํ•˜๋ฉฐ tracking-widest๋กœ ๋„“์€ ์ž๊ฐ„์„ ์ ์šฉํ–ˆ๋‹ค.