์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ ํŒŒ์ด์ฌ์œผ๋กœ ๋งŒ๋“œ๋Š” CLI ๋„๊ตฌ: Click ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์™„์ „ ์ •๋ณต

๐Ÿš€ ํŒŒ์ด์ฌ์œผ๋กœ ๋งŒ๋“œ๋Š” CLI ๋„๊ตฌ: Click ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์™„์ „ ์ •๋ณต

๋ช…๋ น์ค„ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์šฐ์•„ํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๋งˆ๋ฒ• ๊ฐ™์€ ๋„๊ตฌ โœจ

## ๐ŸŽฏ CLI ๋„๊ตฌ, ์™œ ๋งŒ๋“ค์–ด์•ผ ํ• ๊นŒ? ์—ฌ๋Ÿฌ๋ถ„, ํ˜น์‹œ ํ„ฐ๋ฏธ๋„ ์ฐฝ์—์„œ git commit์ด๋‚˜ npm install ๊ฐ™์€ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•ด๋ณธ ์  ์žˆ๋‚˜์š”? ์ด๋Ÿฐ ๋ช…๋ น์–ด๋“ค์ด ๋ฐ”๋กœ CLI(Command Line Interface) ๋„๊ตฌ์˜ˆ์š”! ๐Ÿ˜Š

๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๋ฐ˜๋ณต์ ์ธ ์ž‘์—…์„ ์ž๋™ํ™”ํ•˜๊ฑฐ๋‚˜, ํŒ€์›๋“ค๊ณผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ์„ ๋•Œ๊ฐ€ ์žˆ์ฃ . ์˜ˆ๋ฅผ ๋“ค์–ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—…, ๋กœ๊ทธ ๋ถ„์„, ํŒŒ์ผ ๋ณ€ํ™˜ ๊ฐ™์€ ์ž‘์—…๋“ค์„ ๋งค๋ฒˆ ์ˆ˜๋™์œผ๋กœ ํ•˜๊ธฐ์—” ๋„ˆ๋ฌด ๊ท€์ฐฎ์ž–์•„์š”? ์ด๋Ÿด ๋•Œ CLI ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค๋ฉด ์ •๋ง ํŽธ๋ฆฌํ•ฉ๋‹ˆ๋‹ค! ๐ŸŽ‰

ํŒŒ์ด์ฌ์—๋Š” CLI ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ์—ฌ๋Ÿฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์žˆ์–ด์š”. argparse๋Š” ๊ธฐ๋ณธ ๋‚ด์žฅ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ง€๋งŒ ์ฝ”๋“œ๊ฐ€ ๊ธธ์–ด์ง€๊ณ  ๋ณต์žกํ•ด์š”. docopt์€ ๋…ํŠนํ•œ ์ ‘๊ทผ ๋ฐฉ์‹์ด์ง€๋งŒ ์œ ์—ฐ์„ฑ์ด ๋–จ์–ด์ง€์ฃ . ๊ทธ๋Ÿฐ๋ฐ Click ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์ •๋ง ์ง๊ด€์ ์ด๊ณ  ์šฐ์•„ํ•˜๊ฒŒ CLI ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค! ๐Ÿ’Ž
๐Ÿค” Click์ด ํŠน๋ณ„ํ•œ ์ด์œ  โ€ข ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๊ธฐ๋ฐ˜์˜ ๊น”๋”ํ•œ ๋ฌธ๋ฒ•
โ€ข ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜๋Š” ๋„์›€๋ง ๋ฉ”์‹œ์ง€
โ€ข ํƒ€์ž… ๊ฒ€์ฆ๊ณผ ๋ณ€ํ™˜ ์ž๋™ ์ฒ˜๋ฆฌ
โ€ข ์ค‘์ฒฉ ๋ช…๋ น์–ด ๊ตฌ์กฐ ์ง€์›
โ€ข ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฌ์šด ๊ตฌ์กฐ
โ€ข Flask ๊ฐœ๋ฐœ์ž๊ฐ€ ๋งŒ๋“  ์‹ ๋ขฐ์„ฑ
ํ„ฐ๋ฏธ๋„ $ python app.py ๋ช…๋ น์–ด ์‹คํ–‰ ์˜ต์…˜ ํŒŒ์‹ฑ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋กœ์ง ์‹คํ–‰ Click ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ @click.command() @click.option() ์ž๋™ ๋„์›€๋ง ์ƒ์„ฑ ๊ฒฐ๊ณผ ์ถœ๋ ฅ โœ“ ์ž‘์—… ์™„๋ฃŒ! ์ฒ˜๋ฆฌ ์‹œ๊ฐ„: 0.5์ดˆ ํŒŒ์ผ 3๊ฐœ ์ƒ์„ฑ๋จ ## ๐Ÿ“ฆ Click ์„ค์น˜ํ•˜๊ธฐ Click์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋จผ์ € ์„ค์น˜๊ฐ€ ํ•„์š”ํ•ด์š”. ์ •๋ง ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค! ๐Ÿ˜„
pip install click
๋งŒ์•ฝ ํŠน์ • ๋ฒ„์ „์„ ์„ค์น˜ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋ ‡๊ฒŒ ํ•  ์ˆ˜ ์žˆ์–ด์š”:
pip install click==8.1.7
๐Ÿ’ก ๊ฐ€์ƒํ™˜๊ฒฝ ์‚ฌ์šฉ ํŒ ํ”„๋กœ์ ํŠธ๋งˆ๋‹ค ๋…๋ฆฝ์ ์ธ ํ™˜๊ฒฝ์„ ์œ ์ง€ํ•˜๋ ค๋ฉด ๊ฐ€์ƒํ™˜๊ฒฝ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„์š”!

python -m venv venv
source venv/bin/activate (Mac/Linux)
venv\Scripts\activate (Windows)
์„ค์น˜๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋ ค๋ฉด:
python -c "import click; print(click.__version__)"
๋ฒ„์ „ ์ •๋ณด๊ฐ€ ์ถœ๋ ฅ๋˜๋ฉด ์„ฑ๊ณต์ž…๋‹ˆ๋‹ค! ๐ŸŽŠ
## ๐ŸŒŸ ์ฒซ ๋ฒˆ์งธ Click ํ”„๋กœ๊ทธ๋žจ ๋งŒ๋“ค๊ธฐ ์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ Click์„ ์‚ฌ์šฉํ•ด๋ณผ๊นŒ์š”? ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ "Hello World" ํ”„๋กœ๊ทธ๋žจ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด๋ด…์‹œ๋‹ค! ๐Ÿ‘‹
import click

@click.command()
def hello():
    """๊ฐ„๋‹จํ•œ ์ธ์‚ฌ ํ”„๋กœ๊ทธ๋žจ"""
    click.echo('์•ˆ๋…•ํ•˜์„ธ์š”! Click์˜ ์„ธ๊ณ„์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค! ๐ŸŽ‰')

if __name__ == '__main__':
    hello()
์ด ์ฝ”๋“œ๋ฅผ hello.py๋กœ ์ €์žฅํ•˜๊ณ  ์‹คํ–‰ํ•ด๋ณด์„ธ์š”:
python hello.py
๐Ÿ” ์ฝ”๋“œ ๋ถ„์„ @click.command(): ์ด ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๊ฐ€ ์ผ๋ฐ˜ ํ•จ์ˆ˜๋ฅผ CLI ๋ช…๋ น์–ด๋กœ ๋ณ€ํ™˜ํ•ด์ค˜์š”!

click.echo(): print() ๋Œ€์‹  ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ์œ ๋‹ˆ์ฝ”๋“œ ์ฒ˜๋ฆฌ๊ฐ€ ๋” ์•ˆ์ •์ ์ด์—์š”.

๋…์ŠคํŠธ๋ง("""..."""): ์ž๋™์œผ๋กœ ๋„์›€๋ง ๋ฉ”์‹œ์ง€๊ฐ€ ๋ฉ๋‹ˆ๋‹ค!
๋„์›€๋ง์„ ๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด --help ์˜ต์…˜์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”:
python hello.py --help
์งœ์ž”! ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋œ ๋„์›€๋ง์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค. ์ด๊ฒŒ Click์˜ ๋งˆ๋ฒ•์ด์—์š”! โœจ
## ๐ŸŽ›๏ธ ์˜ต์…˜๊ณผ ์ธ์ž ๋‹ค๋ฃจ๊ธฐ CLI ๋„๊ตฌ์˜ ํ•ต์‹ฌ์€ ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ ์ž…๋ ฅ์„ ๋ฐ›๋Š” ๊ฑฐ์ฃ . Click์—์„œ๋Š” ์˜ต์…˜(Option)๊ณผ ์ธ์ž(Argument) ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹์œผ๋กœ ์ž…๋ ฅ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์š”! ๐ŸŽฏ ### ์˜ต์…˜(Option) ์‚ฌ์šฉํ•˜๊ธฐ ์˜ต์…˜์€ --name์ด๋‚˜ -n ๊ฐ™์€ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉ๋˜๋Š” ์„ ํƒ์  ๋งค๊ฐœ๋ณ€์ˆ˜์˜ˆ์š”:
import click

@click.command()
@click.option('--name', default='์นœ๊ตฌ', help='์ธ์‚ฌํ•  ์‚ฌ๋žŒ์˜ ์ด๋ฆ„')
@click.option('--count', default=1, help='์ธ์‚ฌ ๋ฐ˜๋ณต ํšŸ์ˆ˜')
def greet(name, count):
    """์ด๋ฆ„์„ ๋ฐ›์•„์„œ ์ธ์‚ฌํ•˜๋Š” ํ”„๋กœ๊ทธ๋žจ"""
    for _ in range(count):
        click.echo(f'์•ˆ๋…•ํ•˜์„ธ์š”, {name}๋‹˜! ๐Ÿ˜Š')

if __name__ == '__main__':
    greet()
์ด์ œ ๋‹ค์–‘ํ•œ ๋ฐฉ์‹์œผ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์š”:
# ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ
python greet.py

# ์ด๋ฆ„ ์ง€์ •
python greet.py --name ์ฒ ์ˆ˜

# ๋ฐ˜๋ณต ํšŸ์ˆ˜ ์ง€์ •
python greet.py --name ์˜ํฌ --count 3

# ์งง์€ ์˜ต์…˜ ์‚ฌ์šฉ (์ •์˜ํ•˜๋ฉด)
python greet.py -n ๋ฏผ์ˆ˜ -c 2
๐Ÿ’ก ์งง์€ ์˜ต์…˜ ์ถ”๊ฐ€ํ•˜๊ธฐ @click.option('--name', '-n', default='์นœ๊ตฌ')

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด --name๊ณผ -n ๋‘˜ ๋‹ค ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”!
### ์ธ์ž(Argument) ์‚ฌ์šฉํ•˜๊ธฐ ์ธ์ž๋Š” ์˜ต์…˜๊ณผ ๋‹ฌ๋ฆฌ ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’์ด์—์š”. ๋ช…๋ น์–ด ๋ฐ”๋กœ ๋’ค์— ์ž…๋ ฅํ•˜์ฃ :
import click

@click.command()
@click.argument('filename')
def process_file(filename):
    """ํŒŒ์ผ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํ”„๋กœ๊ทธ๋žจ"""
    click.echo(f'๐Ÿ“ {filename} ํŒŒ์ผ์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค...')
    # ์‹ค์ œ ํŒŒ์ผ ์ฒ˜๋ฆฌ ๋กœ์ง
    click.echo('โœ… ์ฒ˜๋ฆฌ ์™„๋ฃŒ!')

if __name__ == '__main__':
    process_file()
์‹คํ–‰ ๋ฐฉ๋ฒ•:
python process.py data.txt
โš ๏ธ ์˜ต์…˜ vs ์ธ์ž, ์–ธ์ œ ๋ญ˜ ์“ธ๊นŒ? ์ธ์ž(Argument): ํ•„์ˆ˜ ์ž…๋ ฅ๊ฐ’, ๋ช…๋ น์–ด์˜ ํ•ต์‹ฌ ๋Œ€์ƒ
์˜ˆ) ํŒŒ์ผ๋ช…, URL, ์‚ฌ์šฉ์ž ID

์˜ต์…˜(Option): ์„ ํƒ์  ์„ค์ •, ๋™์ž‘ ๋ฐฉ์‹ ์กฐ์ •
์˜ˆ) ์ถœ๋ ฅ ํ˜•์‹, ์ƒ์„ธ ์ •๋ณด ํ‘œ์‹œ, ๋””๋ฒ„๊ทธ ๋ชจ๋“œ
### ํƒ€์ž… ์ง€์ •ํ•˜๊ธฐ Click์€ ์ž๋™์œผ๋กœ ํƒ€์ž…์„ ๋ณ€ํ™˜ํ•ด์ค˜์š”! ์ •๋ง ํŽธ๋ฆฌํ•˜์ฃ ? ๐Ÿ˜
import click

@click.command()
@click.option('--age', type=int, help='๋‚˜์ด')
@click.option('--height', type=float, help='ํ‚ค (cm)')
@click.option('--active', type=bool, help='ํ™œ์„ฑ ์ƒํƒœ')
def user_info(age, height, active):
    """์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ถœ๋ ฅํ•˜๋Š” ํ”„๋กœ๊ทธ๋žจ"""
    click.echo(f'๋‚˜์ด: {age}์„ธ (ํƒ€์ž…: {type(age).__name__})')
    click.echo(f'ํ‚ค: {height}cm (ํƒ€์ž…: {type(height).__name__})')
    click.echo(f'ํ™œ์„ฑ: {active} (ํƒ€์ž…: {type(active).__name__})')

if __name__ == '__main__':
    user_info()
์ž˜๋ชป๋œ ํƒ€์ž…์„ ์ž…๋ ฅํ•˜๋ฉด Click์ด ์ž๋™์œผ๋กœ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค˜์š”:
python user.py --age ์Šค๋ฌผ๋‹ค์„ฏ
# Error: Invalid value for '--age': '์Šค๋ฌผ๋‹ค์„ฏ' is not a valid integer.
ํƒ€์ž… ์„ค๋ช… ์˜ˆ์‹œ
str ๋ฌธ์ž์—ด (๊ธฐ๋ณธ๊ฐ’) ์ด๋ฆ„, ๋ฉ”์‹œ์ง€
int ์ •์ˆ˜ ๋‚˜์ด, ๊ฐœ์ˆ˜
float ์‹ค์ˆ˜ ํ‚ค, ๋ฌด๊ฒŒ
bool ๋ถˆ๋ฆฌ์–ธ ํ™œ์„ฑํ™” ์—ฌ๋ถ€
click.File() ํŒŒ์ผ ๊ฐ์ฒด ์ž…์ถœ๋ ฅ ํŒŒ์ผ
click.Path() ํŒŒ์ผ/๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ ์ €์žฅ ์œ„์น˜
click.Choice() ์„ ํƒ์ง€ ์ค‘ ํ•˜๋‚˜ ์ถœ๋ ฅ ํ˜•์‹
## ๐ŸŽจ ๊ณ ๊ธ‰ ์˜ต์…˜ ๊ธฐ๋Šฅ๋“ค Click์€ ์ •๋ง ๋‹ค์–‘ํ•œ ์˜ต์…˜ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์š”. ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณผ๊นŒ์š”? ๐Ÿš€ ### ํ”Œ๋ž˜๊ทธ(Flag) ์˜ต์…˜ ๋ถˆ๋ฆฌ์–ธ ๊ฐ’์„ ๋‹ค๋ฃจ๋Š” ํŠน๋ณ„ํ•œ ๋ฐฉ์‹์ด์—์š”:
import click

@click.command()
@click.option('--verbose', '-v', is_flag=True, help='์ƒ์„ธ ์ •๋ณด ์ถœ๋ ฅ')
@click.option('--quiet', '-q', is_flag=True, help='์ตœ์†Œ ์ •๋ณด๋งŒ ์ถœ๋ ฅ')
def process(verbose, quiet):
    """์ฒ˜๋ฆฌ ํ”„๋กœ๊ทธ๋žจ"""
    if verbose:
        click.echo('๐Ÿ” ์ƒ์„ธ ๋ชจ๋“œ: ๋ชจ๋“  ์ •๋ณด๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.')
        click.echo('๋‹จ๊ณ„ 1: ์ดˆ๊ธฐํ™” ์ค‘...')
        click.echo('๋‹จ๊ณ„ 2: ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ...')
        click.echo('๋‹จ๊ณ„ 3: ์ฒ˜๋ฆฌ ์ค‘...')
    elif quiet:
        click.echo('์™„๋ฃŒ')
    else:
        click.echo('โœ… ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')

if __name__ == '__main__':
    process()
์‚ฌ์šฉ ์˜ˆ์‹œ:
python process.py          # ์ผ๋ฐ˜ ๋ชจ๋“œ
python process.py -v       # ์ƒ์„ธ ๋ชจ๋“œ
python process.py --quiet  # ์กฐ์šฉํ•œ ๋ชจ๋“œ
### ์„ ํƒ์ง€(Choice) ์˜ต์…˜ ๋ฏธ๋ฆฌ ์ •์˜๋œ ๊ฐ’ ์ค‘์—์„œ๋งŒ ์„ ํƒํ•˜๋„๋ก ์ œํ•œํ•  ์ˆ˜ ์žˆ์–ด์š”:
import click

@click.command()
@click.option('--format', 
              type=click.Choice(['json', 'xml', 'csv', 'yaml'], case_sensitive=False),
              default='json',
              help='์ถœ๋ ฅ ํ˜•์‹')
def export(format):
    """๋ฐ์ดํ„ฐ๋ฅผ ๋‚ด๋ณด๋‚ด๋Š” ํ”„๋กœ๊ทธ๋žจ"""
    click.echo(f'๐Ÿ“ค {format.upper()} ํ˜•์‹์œผ๋กœ ๋‚ด๋ณด๋‚ด๋Š” ์ค‘...')
    
    if format == 'json':
        click.echo('{"status": "success", "data": [...]}')
    elif format == 'xml':
        click.echo('<response><status>success</status></response>')
    elif format == 'csv':
        click.echo('id,name,value\n1,test,100')
    else:
        click.echo('status: success\ndata: []')

if __name__ == '__main__':
    export()
์ž˜๋ชป๋œ ์„ ํƒ์ง€๋ฅผ ์ž…๋ ฅํ•˜๋ฉด:
python export.py --format txt
# Error: Invalid value for '--format': 'txt' is not one of 'json', 'xml', 'csv', 'yaml'.
### ๋‹ค์ค‘ ๊ฐ’ ์˜ต์…˜ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๊ฐ’์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์š”:
import click

@click.command()
@click.option('--tag', multiple=True, help='ํƒœ๊ทธ ์ถ”๊ฐ€ (์—ฌ๋Ÿฌ ๋ฒˆ ์‚ฌ์šฉ ๊ฐ€๋Šฅ)')
def create_post(tag):
    """๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ”„๋กœ๊ทธ๋žจ"""
    click.echo('๐Ÿ“ ์ƒˆ ํฌ์ŠคํŠธ ์ƒ์„ฑ ์ค‘...')
    
    if tag:
        click.echo(f'๐Ÿท๏ธ  ํƒœ๊ทธ: {", ".join(tag)}')
    else:
        click.echo('ํƒœ๊ทธ๊ฐ€ ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.')

if __name__ == '__main__':
    create_post()
์‚ฌ์šฉ ์˜ˆ์‹œ:
python post.py --tag python --tag cli --tag tutorial
# ์ถœ๋ ฅ: ๐Ÿท๏ธ  ํƒœ๊ทธ: python, cli, tutorial
### ํ”„๋กฌํ”„ํŠธ(Prompt) ์˜ต์…˜ ๊ฐ’์ด ์ œ๊ณต๋˜์ง€ ์•Š์œผ๋ฉด ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฌผ์–ด๋ด์š”:
import click

@click.command()
@click.option('--username', prompt='์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”', help='์‚ฌ์šฉ์ž ์ด๋ฆ„')
@click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True, help='๋น„๋ฐ€๋ฒˆํ˜ธ')
def login(username, password):
    """๋กœ๊ทธ์ธ ํ”„๋กœ๊ทธ๋žจ"""
    click.echo(f'๐Ÿ‘ค {username}๋‹˜, ๋กœ๊ทธ์ธ ์‹œ๋„ ์ค‘...')
    # ์‹ค์ œ ์ธ์ฆ ๋กœ์ง
    click.echo('โœ… ๋กœ๊ทธ์ธ ์„ฑ๊ณต!')

if __name__ == '__main__':
    login()
์‹คํ–‰ํ•˜๋ฉด ์ด๋ ‡๊ฒŒ ๋™์ž‘ํ•ด์š”:
python login.py
์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”: ์ฒ ์ˆ˜
Password: ****
Repeat for confirmation: ****
๐Ÿ‘ค ์ฒ ์ˆ˜๋‹˜, ๋กœ๊ทธ์ธ ์‹œ๋„ ์ค‘...
โœ… ๋กœ๊ทธ์ธ ์„ฑ๊ณต!
๐Ÿ’ก ํ”„๋กฌํ”„ํŠธ ํ™œ์šฉ ํŒ hide_input=True: ์ž…๋ ฅ ๋‚ด์šฉ์„ ์ˆจ๊น€ (๋น„๋ฐ€๋ฒˆํ˜ธ์šฉ)
confirmation_prompt=True: ๋‘ ๋ฒˆ ์ž…๋ ฅ๋ฐ›์•„ ํ™•์ธ
default: ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • (Enter๋งŒ ๋ˆŒ๋Ÿฌ๋„ ๋จ)
### ๋ฒ”์œ„ ์ œํ•œ ์ˆซ์ž ์ž…๋ ฅ์˜ ๋ฒ”์œ„๋ฅผ ์ œํ•œํ•  ์ˆ˜ ์žˆ์–ด์š”:
import click

@click.command()
@click.option('--volume', type=click.IntRange(0, 100), default=50, help='๋ณผ๋ฅจ (0-100)')
@click.option('--quality', type=click.FloatRange(0.0, 1.0), default=0.8, help='ํ’ˆ์งˆ (0.0-1.0)')
def settings(volume, quality):
    """์„ค์ • ํ”„๋กœ๊ทธ๋žจ"""
    click.echo(f'๐Ÿ”Š ๋ณผ๋ฅจ: {volume}%')
    click.echo(f'โญ ํ’ˆ์งˆ: {quality * 100:.0f}%')

if __name__ == '__main__':
    settings()
๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด:
python settings.py --volume 150
# Error: Invalid value for '--volume': 150 is not in the range 0<=x<=100.
Click ์˜ต์…˜ ํƒ€์ž… ๊ฐ€์ด๋“œ ๊ธฐ๋ณธ ํƒ€์ž… str, int, float bool ํŒŒ์ผ ํƒ€์ž… click.File() click.Path() ์ œํ•œ ํƒ€์ž… click.Choice() click.IntRange() ํŠน์ˆ˜ ๊ธฐ๋Šฅ is_flag multiple 1 ์˜ต์…˜ ์ •์˜ @click.option() 2 ํƒ€์ž… ์ง€์ • type=int 3 ๊ฒ€์ฆ ์ž๋™ ์ฒดํฌ 4 ๋ณ€ํ™˜ ํƒ€์ž… ๋ณ€ํ™˜ 5 ํ•จ์ˆ˜ ์ „๋‹ฌ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ โœจ ๋ชจ๋“  ๊ณผ์ •์ด ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค! โœจ
## ๐Ÿ—๏ธ ๋ช…๋ น์–ด ๊ทธ๋ฃน ๋งŒ๋“ค๊ธฐ ์‹ค์ œ CLI ๋„๊ตฌ๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํ•˜์œ„ ๋ช…๋ น์–ด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์ฃ . git์„ ์ƒ๊ฐํ•ด๋ณด์„ธ์š”: git commit, git push, git pull ๊ฐ™์€ ๊ฒƒ๋“ค์ด์š”! Click์—์„œ๋Š” ์ด๋Ÿฐ ๊ตฌ์กฐ๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์š”. ๐ŸŽช ### ๊ธฐ๋ณธ ๊ทธ๋ฃน ๊ตฌ์กฐ
import click

@click.group()
def cli():
    """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ ๋„๊ตฌ"""
    pass

@cli.command()
def init():
    """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™”"""
    click.echo('๐Ÿ—„๏ธ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค...')
    click.echo('โœ… ์ดˆ๊ธฐํ™” ์™„๋ฃŒ!')

@cli.command()
@click.option('--table', required=True, help='๋ฐฑ์—…ํ•  ํ…Œ์ด๋ธ” ์ด๋ฆ„')
def backup(table):
    """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฑ์—…"""
    click.echo(f'๐Ÿ’พ {table} ํ…Œ์ด๋ธ”์„ ๋ฐฑ์—…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค...')
    click.echo('โœ… ๋ฐฑ์—… ์™„๋ฃŒ!')

@cli.command()
def status():
    """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒํƒœ ํ™•์ธ"""
    click.echo('๐Ÿ“Š ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒํƒœ:')
    click.echo('  - ์—ฐ๊ฒฐ: ์ •์ƒ')
    click.echo('  - ํ…Œ์ด๋ธ” ์ˆ˜: 15')
    click.echo('  - ์ €์žฅ ๊ณต๊ฐ„: 2.3GB')

if __name__ == '__main__':
    cli()
์ด์ œ ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”:
python db.py --help        # ์ „์ฒด ๋„์›€๋ง
python db.py init          # ์ดˆ๊ธฐํ™”
python db.py backup --table users  # ๋ฐฑ์—…
python db.py status        # ์ƒํƒœ ํ™•์ธ
๐ŸŽฏ ๋ช…๋ น์–ด ๊ทธ๋ฃน์˜ ์žฅ์  โ€ข ๊ด€๋ จ ๊ธฐ๋Šฅ์„ ๋…ผ๋ฆฌ์ ์œผ๋กœ ๋ฌถ์„ ์ˆ˜ ์žˆ์–ด์š”
โ€ข ๊ฐ ๋ช…๋ น์–ด๋งˆ๋‹ค ๋…๋ฆฝ์ ์ธ ์˜ต์…˜ ์„ค์ • ๊ฐ€๋Šฅ
โ€ข ์ฝ”๋“œ ๊ตฌ์กฐ๊ฐ€ ๊น”๋”ํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์‰ฌ์›Œ์š”
โ€ข ๋Œ€๊ทœ๋ชจ CLI ๋„๊ตฌ ๊ฐœ๋ฐœ์— ํ•„์ˆ˜์ ์ด์—์š”
### ์ค‘์ฒฉ๋œ ๊ทธ๋ฃน ๊ทธ๋ฃน ์•ˆ์— ๋˜ ๋‹ค๋ฅธ ๊ทธ๋ฃน์„ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์–ด์š”:
import click

@click.group()
def cli():
    """ํด๋ผ์šฐ๋“œ ๊ด€๋ฆฌ ๋„๊ตฌ"""
    pass

@cli.group()
def server():
    """์„œ๋ฒ„ ๊ด€๋ฆฌ"""
    pass

@server.command()
@click.option('--name', required=True, help='์„œ๋ฒ„ ์ด๋ฆ„')
def start(name):
    """์„œ๋ฒ„ ์‹œ์ž‘"""
    click.echo(f'๐Ÿš€ {name} ์„œ๋ฒ„๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค...')

@server.command()
@click.option('--name', required=True, help='์„œ๋ฒ„ ์ด๋ฆ„')
def stop(name):
    """์„œ๋ฒ„ ์ค‘์ง€"""
    click.echo(f'๐Ÿ›‘ {name} ์„œ๋ฒ„๋ฅผ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค...')

@cli.group()
def database():
    """๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ"""
    pass

@database.command()
def migrate():
    """๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰"""
    click.echo('๐Ÿ”„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค...')

if __name__ == '__main__':
    cli()
์‚ฌ์šฉ ์˜ˆ์‹œ:
python cloud.py server start --name web-01
python cloud.py server stop --name web-01
python cloud.py database migrate
### ์ปจํ…์ŠคํŠธ(Context) ํ™œ์šฉํ•˜๊ธฐ ๋ช…๋ น์–ด ๊ฐ„์— ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์ปจํ…์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•ด์š”:
import click

@click.group()
@click.option('--debug/--no-debug', default=False, help='๋””๋ฒ„๊ทธ ๋ชจ๋“œ')
@click.pass_context
def cli(ctx, debug):
    """์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ด€๋ฆฌ ๋„๊ตฌ"""
    ctx.ensure_object(dict)
    ctx.obj['DEBUG'] = debug
    
    if debug:
        click.echo('๐Ÿ› ๋””๋ฒ„๊ทธ ๋ชจ๋“œ๊ฐ€ ํ™œ์„ฑํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')

@cli.command()
@click.pass_context
def deploy(ctx):
    """์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฐฐํฌ"""
    if ctx.obj['DEBUG']:
        click.echo('[DEBUG] ๋ฐฐํฌ ํ”„๋กœ์„ธ์Šค ์‹œ์ž‘...')
        click.echo('[DEBUG] ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํ™•์ธ ์ค‘...')
        click.echo('[DEBUG] ๋นŒ๋“œ ํŒŒ์ผ ๊ฒ€์ฆ ์ค‘...')
    
    click.echo('๐Ÿš€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋ฐฐํฌํ•ฉ๋‹ˆ๋‹ค...')
    click.echo('โœ… ๋ฐฐํฌ ์™„๋ฃŒ!')

@cli.command()
@click.pass_context
def test(ctx):
    """ํ…Œ์ŠคํŠธ ์‹คํ–‰"""
    if ctx.obj['DEBUG']:
        click.echo('[DEBUG] ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ • ์ค‘...')
        click.echo('[DEBUG] ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ๋กœ๋”ฉ...')
    
    click.echo('๐Ÿงช ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค...')
    click.echo('โœ… ๋ชจ๋“  ํ…Œ์ŠคํŠธ ํ†ต๊ณผ!')

if __name__ == '__main__':
    cli()
์‹คํ–‰:
python app.py --debug deploy
# ๋””๋ฒ„๊ทธ ์ •๋ณด์™€ ํ•จ๊ป˜ ๋ฐฐํฌ ์‹คํ–‰

python app.py test
# ์ผ๋ฐ˜ ๋ชจ๋“œ๋กœ ํ…Œ์ŠคํŠธ ์‹คํ–‰
๐Ÿ’ก ์ปจํ…์ŠคํŠธ ํ™œ์šฉ ํŒ @click.pass_context: ์ปจํ…์ŠคํŠธ ๊ฐ์ฒด๋ฅผ ํ•จ์ˆ˜์— ์ „๋‹ฌ
ctx.obj: ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ๋”•์…”๋„ˆ๋ฆฌ
ctx.parent: ๋ถ€๋ชจ ๋ช…๋ น์–ด์˜ ์ปจํ…์ŠคํŠธ ์ ‘๊ทผ

์„ค์ •, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ, ๋กœ๊ฑฐ ๋“ฑ์„ ๊ณต์œ ํ•  ๋•Œ ์œ ์šฉํ•ด์š”!
## ๐Ÿ“‚ ํŒŒ์ผ๊ณผ ๊ฒฝ๋กœ ๋‹ค๋ฃจ๊ธฐ CLI ๋„๊ตฌ์—์„œ ํŒŒ์ผ์„ ๋‹ค๋ฃจ๋Š” ๊ฑด ์ •๋ง ํ”ํ•œ ์ผ์ด์ฃ . Click์€ ํŒŒ์ผ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ํŠน๋ณ„ํ•œ ํƒ€์ž…๋“ค์„ ์ œ๊ณตํ•ด์š”! ๐Ÿ“ ### File ํƒ€์ž… ์‚ฌ์šฉํ•˜๊ธฐ
import click

@click.command()
@click.option('--input', type=click.File('r'), help='์ž…๋ ฅ ํŒŒ์ผ')
@click.option('--output', type=click.File('w'), default='-', help='์ถœ๋ ฅ ํŒŒ์ผ (๊ธฐ๋ณธ: ํ‘œ์ค€์ถœ๋ ฅ)')
def convert(input, output):
    """ํŒŒ์ผ ๋ณ€ํ™˜ ๋„๊ตฌ"""
    if input:
        click.echo('๐Ÿ“– ํŒŒ์ผ์„ ์ฝ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค...')
        content = input.read()
        
        # ๊ฐ„๋‹จํ•œ ๋ณ€ํ™˜ ์˜ˆ์‹œ: ๋Œ€๋ฌธ์ž๋กœ ๋ณ€ํ™˜
        converted = content.upper()
        
        output.write(converted)
        click.echo('โœ… ๋ณ€ํ™˜ ์™„๋ฃŒ!')
    else:
        click.echo('์ž…๋ ฅ ํŒŒ์ผ์ด ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.')

if __name__ == '__main__':
    convert()
์‚ฌ์šฉ ์˜ˆ์‹œ:
# ํŒŒ์ผ์—์„œ ์ฝ์–ด์„œ ํŒŒ์ผ๋กœ ์“ฐ๊ธฐ
python convert.py --input data.txt --output result.txt

# ํŒŒ์ผ์—์„œ ์ฝ์–ด์„œ ํ™”๋ฉด์— ์ถœ๋ ฅ
python convert.py --input data.txt

# ํ‘œ์ค€ ์ž…๋ ฅ์—์„œ ์ฝ๊ธฐ
echo "hello world" | python convert.py --output result.txt
๐Ÿ“ File ๋ชจ๋“œ ์˜ต์…˜ 'r': ์ฝ๊ธฐ ๋ชจ๋“œ (ํ…์ŠคํŠธ)
'w': ์“ฐ๊ธฐ ๋ชจ๋“œ (ํ…์ŠคํŠธ)
'a': ์ถ”๊ฐ€ ๋ชจ๋“œ (ํ…์ŠคํŠธ)
'rb': ์ฝ๊ธฐ ๋ชจ๋“œ (๋ฐ”์ด๋„ˆ๋ฆฌ)
'wb': ์“ฐ๊ธฐ ๋ชจ๋“œ (๋ฐ”์ด๋„ˆ๋ฆฌ)

'-'๋ฅผ ํŒŒ์ผ๋ช…์œผ๋กœ ์‚ฌ์šฉํ•˜๋ฉด ํ‘œ์ค€ ์ž…์ถœ๋ ฅ์„ ์˜๋ฏธํ•ด์š”!
### Path ํƒ€์ž… ์‚ฌ์šฉํ•˜๊ธฐ ํŒŒ์ผ์ด๋‚˜ ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ๋ฅผ ๊ฒ€์ฆํ•˜๊ณ  ์‹ถ์„ ๋•Œ:
import click
import os

@click.command()
@click.option('--source', 
              type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True),
              required=True,
              help='์›๋ณธ ํŒŒ์ผ')
@click.option('--dest',
              type=click.Path(file_okay=True, dir_okay=False, writable=True),
              required=True,
              help='๋Œ€์ƒ ํŒŒ์ผ')
def copy_file(source, dest):
    """ํŒŒ์ผ ๋ณต์‚ฌ ๋„๊ตฌ"""
    click.echo(f'๐Ÿ“‹ {source} โ†’ {dest}')
    
    # ์‹ค์ œ ๋ณต์‚ฌ ๋กœ์ง
    with open(source, 'rb') as src:
        with open(dest, 'wb') as dst:
            dst.write(src.read())
    
    size = os.path.getsize(dest)
    click.echo(f'โœ… ๋ณต์‚ฌ ์™„๋ฃŒ! ({size:,} bytes)')

if __name__ == '__main__':
    copy_file()
์˜ต์…˜ ์„ค๋ช…
exists=True ๊ฒฝ๋กœ๊ฐ€ ์กด์žฌํ•ด์•ผ ํ•จ
file_okay=True ํŒŒ์ผ ํ—ˆ์šฉ
dir_okay=True ๋””๋ ‰ํ† ๋ฆฌ ํ—ˆ์šฉ
readable=True ์ฝ๊ธฐ ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ
writable=True ์“ฐ๊ธฐ ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ
resolve_path=True ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋กœ ๋ณ€ํ™˜
### ๋””๋ ‰ํ† ๋ฆฌ ์ž‘์—…
import click
import os

@click.command()
@click.option('--dir',
              type=click.Path(exists=True, file_okay=False, dir_okay=True),
              required=True,
              help='์Šค์บ”ํ•  ๋””๋ ‰ํ† ๋ฆฌ')
@click.option('--ext', default='.txt', help='ํŒŒ์ผ ํ™•์žฅ์ž')
def scan(dir, ext):
    """๋””๋ ‰ํ† ๋ฆฌ ์Šค์บ” ๋„๊ตฌ"""
    click.echo(f'๐Ÿ” {dir} ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์Šค์บ”ํ•ฉ๋‹ˆ๋‹ค...\n')
    
    files = []
    for root, dirs, filenames in os.walk(dir):
        for filename in filenames:
            if filename.endswith(ext):
                filepath = os.path.join(root, filename)
                size = os.path.getsize(filepath)
                files.append((filepath, size))
    
    if files:
        click.echo(f'๐Ÿ“ {ext} ํŒŒ์ผ {len(files)}๊ฐœ ๋ฐœ๊ฒฌ:\n')
        total_size = 0
        for filepath, size in files:
            click.echo(f'  โ€ข {filepath} ({size:,} bytes)')
            total_size += size
        
        click.echo(f'\n๐Ÿ“Š ์ด ํฌ๊ธฐ: {total_size:,} bytes')
    else:
        click.echo(f'โŒ {ext} ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.')

if __name__ == '__main__':
    scan()
## ๐ŸŽจ ์ถœ๋ ฅ ์Šคํƒ€์ผ๋ง ํ„ฐ๋ฏธ๋„ ์ถœ๋ ฅ์„ ์˜ˆ์˜๊ฒŒ ๊พธ๋ฏธ๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํ›จ์”ฌ ์ข‹์•„์ ธ์š”! Click์€ ์ƒ‰์ƒ๊ณผ ์Šคํƒ€์ผ์„ ์‰ฝ๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. ๐ŸŒˆ ### ์ƒ‰์ƒ ์‚ฌ์šฉํ•˜๊ธฐ
import click

@click.command()
def colorful():
    """๋‹ค์–‘ํ•œ ์ƒ‰์ƒ ์ถœ๋ ฅ"""
    click.echo(click.style('โœ… ์„ฑ๊ณต ๋ฉ”์‹œ์ง€', fg='green', bold=True))
    click.echo(click.style('โš ๏ธ  ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€', fg='yellow', bold=True))
    click.echo(click.style('โŒ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€', fg='red', bold=True))
    click.echo(click.style('โ„น๏ธ  ์ •๋ณด ๋ฉ”์‹œ์ง€', fg='blue'))
    click.echo(click.style('๐Ÿ” ๋””๋ฒ„๊ทธ ๋ฉ”์‹œ์ง€', fg='cyan', dim=True))
    
    # ๋ฐฐ๊ฒฝ์ƒ‰๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
    click.echo(click.style('์ค‘์š”ํ•œ ์•Œ๋ฆผ!', fg='white', bg='red', bold=True))
    
    # ์—ฌ๋Ÿฌ ์Šคํƒ€์ผ ์กฐํ•ฉ
    click.echo(click.style('๋ฐ‘์ค„ + ๊ตต๊ฒŒ', underline=True, bold=True))
    click.echo(click.style('๊นœ๋นก์ž„ ํšจ๊ณผ', blink=True))

if __name__ == '__main__':
    colorful()
๐ŸŽจ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ƒ‰์ƒ ์ „๊ฒฝ์ƒ‰(fg): black, red, green, yellow, blue, magenta, cyan, white
๋ฐฐ๊ฒฝ์ƒ‰(bg): ์ „๊ฒฝ์ƒ‰๊ณผ ๋™์ผ

์Šคํƒ€์ผ:
โ€ข bold=True: ๊ตต๊ฒŒ
โ€ข dim=True: ํ๋ฆฌ๊ฒŒ
โ€ข underline=True: ๋ฐ‘์ค„
โ€ข blink=True: ๊นœ๋นก์ž„
โ€ข reverse=True: ๋ฐ˜์ „
### ์ง„ํ–‰ ํ‘œ์‹œ์ค„(Progress Bar) ๊ธด ์ž‘์—…์„ ํ•  ๋•Œ ์ง„ํ–‰ ์ƒํ™ฉ์„ ๋ณด์—ฌ์ฃผ๋ฉด ์ข‹์ฃ :
import click
import time

@click.command()
@click.option('--count', default=100, help='์ฒ˜๋ฆฌํ•  ํ•ญ๋ชฉ ์ˆ˜')
def process(count):
    """์ง„ํ–‰ ํ‘œ์‹œ์ค„ ์˜ˆ์ œ"""
    click.echo('๐Ÿš€ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค...\n')
    
    with click.progressbar(range(count), label='์ฒ˜๋ฆฌ ์ค‘') as bar:
        for item in bar:
            # ์‹ค์ œ ์ž‘์—… ์‹œ๋ฎฌ๋ ˆ์ด์…˜
            time.sleep(0.02)
    
    click.echo('\nโœ… ๋ชจ๋“  ์ž‘์—…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!')

if __name__ == '__main__':
    process()
๋” ์„ธ๋ฐ€ํ•œ ์ œ์–ด๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด:
import click
import time

@click.command()
def download():
    """ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜"""
    files = ['file1.zip', 'file2.zip', 'file3.zip', 'file4.zip', 'file5.zip']
    
    with click.progressbar(
        files,
        label='๋‹ค์šด๋กœ๋“œ ์ค‘',
        fill_char='โ–ˆ',
        empty_char='โ–‘',
        bar_template='%(label)s [%(bar)s] %(info)s',
        show_percent=True,
        show_eta=True
    ) as bar:
        for file in bar:
            # ๋‹ค์šด๋กœ๋“œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
            time.sleep(0.5)
            click.echo(f'\n  โœ“ {file} ์™„๋ฃŒ')

if __name__ == '__main__':
    download()
### ํ™•์ธ ํ”„๋กฌํ”„ํŠธ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ™•์ธ์„ ๋ฐ›๊ณ  ์‹ถ์„ ๋•Œ:
import click

@click.command()
@click.option('--force', is_flag=True, help='ํ™•์ธ ์—†์ด ๊ฐ•์ œ ์‹คํ–‰')
def delete_all(force):
    """๋ชจ๋“  ๋ฐ์ดํ„ฐ ์‚ญ์ œ"""
    if not force:
        if not click.confirm('โš ๏ธ  ์ •๋ง๋กœ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?'):
            click.echo('โŒ ์ž‘์—…์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')
            return
    
    click.echo('๐Ÿ—‘๏ธ  ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค...')
    # ์‹ค์ œ ์‚ญ์ œ ๋กœ์ง
    click.echo('โœ… ์‚ญ์ œ ์™„๋ฃŒ!')

if __name__ == '__main__':
    delete_all()
### ํŽ˜์ด์ง•(Paging) ๊ธด ์ถœ๋ ฅ์„ ํŽ˜์ด์ง€ ๋‹จ์œ„๋กœ ๋ณด์—ฌ์ฃผ๊ธฐ:
import click

@click.command()
def show_logs():
    """๋กœ๊ทธ ๋ณด๊ธฐ"""
    logs = []
    for i in range(100):
        logs.append(f'[2024-01-{i%30+1:02d}] ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ #{i+1}: ์‹œ์Šคํ…œ ์ •์ƒ ์ž‘๋™ ์ค‘')
    
    # ํŽ˜์ด์ €๋ฅผ ํ†ตํ•ด ์ถœ๋ ฅ
    click.echo_via_pager('\n'.join(logs))

if __name__ == '__main__':
    show_logs()
๐Ÿ’ก ์ถœ๋ ฅ ์Šคํƒ€์ผ๋ง ํŒ โ€ข ์ƒ‰์ƒ์€ ์ ์ ˆํžˆ ์‚ฌ์šฉํ•˜์„ธ์š”. ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ์˜คํžˆ๋ ค ๊ฐ€๋…์„ฑ์ด ๋–จ์–ด์ ธ์š”
โ€ข ์„ฑ๊ณต์€ ๋…น์ƒ‰, ๊ฒฝ๊ณ ๋Š” ๋…ธ๋ž€์ƒ‰, ์—๋Ÿฌ๋Š” ๋นจ๊ฐ„์ƒ‰์ด ์ผ๋ฐ˜์ ์ธ ์ปจ๋ฒค์…˜์ด์—์š”
โ€ข ์ง„ํ–‰ ํ‘œ์‹œ์ค„์€ 5์ดˆ ์ด์ƒ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์— ์‚ฌ์šฉํ•˜์„ธ์š”
โ€ข ์ค‘์š”ํ•œ ์ž‘์—… ์ „์—๋Š” ๋ฐ˜๋“œ์‹œ ํ™•์ธ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋„ฃ์œผ์„ธ์š”
ํ„ฐ๋ฏธ๋„ - Click CLI ๋„๊ตฌ $ python app.py process --verbose โ„น๏ธ ์ž‘์—…์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค... ์ฒ˜๋ฆฌ ์ค‘... 70% โœ… ํŒŒ์ผ ์ฒ˜๋ฆฌ ์™„๋ฃŒ: data1.txt โœ… ํŒŒ์ผ ์ฒ˜๋ฆฌ ์™„๋ฃŒ: data2.txt โš ๏ธ ๊ฒฝ๊ณ : data3.txt ์ผ๋ถ€ ์†์ƒ๋จ โœ… ํŒŒ์ผ ์ฒ˜๋ฆฌ ์™„๋ฃŒ: data4.txt โœจ ๋ชจ๋“  ์ž‘์—…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!
## ๐Ÿงช ํ…Œ์ŠคํŠธํ•˜๊ธฐ CLI ๋„๊ตฌ๋„ ํ…Œ์ŠคํŠธ๊ฐ€ ์ค‘์š”ํ•ด์š”! Click์€ ํ…Œ์ŠคํŠธ๋ฅผ ์‰ฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ”ฌ ### CliRunner ์‚ฌ์šฉํ•˜๊ธฐ
import click
from click.testing import CliRunner

# ํ…Œ์ŠคํŠธํ•  CLI ๋ช…๋ น์–ด
@click.command()
@click.option('--name', default='World', help='์ด๋ฆ„')
@click.option('--count', default=1, type=int, help='๋ฐ˜๋ณต ํšŸ์ˆ˜')
def hello(name, count):
    """์ธ์‚ฌ ํ”„๋กœ๊ทธ๋žจ"""
    for _ in range(count):
        click.echo(f'Hello, {name}!')

# ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
def test_hello_default():
    """๊ธฐ๋ณธ๊ฐ’ ํ…Œ์ŠคํŠธ"""
    runner = CliRunner()
    result = runner.invoke(hello)
    
    assert result.exit_code == 0
    assert 'Hello, World!' in result.output

def test_hello_with_name():
    """์ด๋ฆ„ ์ง€์ • ํ…Œ์ŠคํŠธ"""
    runner = CliRunner()
    result = runner.invoke(hello, ['--name', '์ฒ ์ˆ˜'])
    
    assert result.exit_code == 0
    assert 'Hello, ์ฒ ์ˆ˜!' in result.output

def test_hello_with_count():
    """๋ฐ˜๋ณต ํšŸ์ˆ˜ ํ…Œ์ŠคํŠธ"""
    runner = CliRunner()
    result = runner.invoke(hello, ['--name', '์˜ํฌ', '--count', '3'])
    
    assert result.exit_code == 0
    assert result.output.count('Hello, ์˜ํฌ!') == 3

if __name__ == '__main__':
    # ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ ์‹คํ–‰
    test_hello_default()
    test_hello_with_name()
    test_hello_with_count()
    print('โœ… ๋ชจ๋“  ํ…Œ์ŠคํŠธ ํ†ต๊ณผ!')
### pytest์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” pytest๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„์š”:
# test_cli.py
import pytest
from click.testing import CliRunner
from myapp import cli

@pytest.fixture
def runner():
    """CliRunner ํ”ฝ์Šค์ฒ˜"""
    return CliRunner()

def test_basic_command(runner):
    """๊ธฐ๋ณธ ๋ช…๋ น์–ด ํ…Œ์ŠคํŠธ"""
    result = runner.invoke(cli, ['status'])
    assert result.exit_code == 0
    assert '์ •์ƒ' in result.output

def test_with_options(runner):
    """์˜ต์…˜ ํฌํ•จ ํ…Œ์ŠคํŠธ"""
    result = runner.invoke(cli, ['process', '--input', 'test.txt'])
    assert result.exit_code == 0

def test_error_handling(runner):
    """์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ"""
    result = runner.invoke(cli, ['process'])  # ํ•„์ˆ˜ ์˜ต์…˜ ๋ˆ„๋ฝ
    assert result.exit_code != 0
    assert 'Error' in result.output

def test_file_operations(runner):
    """ํŒŒ์ผ ์ž‘์—… ํ…Œ์ŠคํŠธ"""
    with runner.isolated_filesystem():
        # ์ž„์‹œ ํŒŒ์ผ ์ƒ์„ฑ
        with open('test.txt', 'w') as f:
            f.write('test content')
        
        # ๋ช…๋ น์–ด ์‹คํ–‰
        result = runner.invoke(cli, ['convert', '--input', 'test.txt'])
        assert result.exit_code == 0
        
        # ๊ฒฐ๊ณผ ํ™•์ธ
        assert os.path.exists('output.txt')
๐Ÿงช CliRunner์˜ ์ฃผ์š” ๊ธฐ๋Šฅ invoke(): ๋ช…๋ น์–ด ์‹คํ–‰
isolated_filesystem(): ์ž„์‹œ ํŒŒ์ผ์‹œ์Šคํ…œ ์ƒ์„ฑ
result.exit_code: ์ข…๋ฃŒ ์ฝ”๋“œ ํ™•์ธ
result.output: ์ถœ๋ ฅ ๋‚ด์šฉ ํ™•์ธ
result.exception: ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ ํ™•์ธ
### ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํ…Œ์ŠคํŠธ
import click
from click.testing import CliRunner

@click.command()
@click.option('--api-key', envvar='API_KEY', help='API ํ‚ค')
def api_call(api_key):
    """API ํ˜ธ์ถœ"""
    if not api_key:
        click.echo('โŒ API ํ‚ค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.')
        raise click.Abort()
    
    click.echo(f'โœ… API ํ‚ค๋กœ ์š”์ฒญ: {api_key[:4]}****')

def test_with_env_var():
    """ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํ…Œ์ŠคํŠธ"""
    runner = CliRunner()
    
    # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •
    result = runner.invoke(api_call, env={'API_KEY': 'test1234567890'})
    
    assert result.exit_code == 0
    assert 'test****' in result.output

def test_without_env_var():
    """ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์—†์„ ๋•Œ ํ…Œ์ŠคํŠธ"""
    runner = CliRunner()
    result = runner.invoke(api_call)
    
    assert result.exit_code != 0
    assert 'API ํ‚ค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค' in result.output
## ๐ŸŽ ์‹ค์ „ ์˜ˆ์ œ: ์™„์ „ํ•œ CLI ๋„๊ตฌ ๋งŒ๋“ค๊ธฐ ์ด์ œ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ชจ๋‘ ํ™œ์šฉํ•ด์„œ ์‹ค์šฉ์ ์ธ CLI ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊นŒ์š”? ํŒŒ์ผ ๊ด€๋ฆฌ ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค! ๐Ÿ› ๏ธ
import click
import os
import shutil
from pathlib import Path
from datetime import datetime

@click.group()
@click.version_option(version='1.0.0')
def cli():
    """
    ๐Ÿ“ ํŒŒ์ผ ๊ด€๋ฆฌ ๋„๊ตฌ v1.0.0
    
    ํŒŒ์ผ๊ณผ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์‰ฝ๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” CLI ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.
    """
    pass

@cli.command()
@click.argument('path', type=click.Path(exists=True))
@click.option('--recursive', '-r', is_flag=True, help='ํ•˜์œ„ ๋””๋ ‰ํ† ๋ฆฌ ํฌํ•จ')
@click.option('--human-readable', '-h', is_flag=True, help='์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•์‹')
def size(path, recursive, human_readable):
    """๋””๋ ‰ํ† ๋ฆฌ ํฌ๊ธฐ ๊ณ„์‚ฐ"""
    click.echo(f'๐Ÿ“Š {path} ํฌ๊ธฐ ๊ณ„์‚ฐ ์ค‘...\n')
    
    total_size = 0
    file_count = 0
    
    if os.path.isfile(path):
        total_size = os.path.getsize(path)
        file_count = 1
    else:
        if recursive:
            for root, dirs, files in os.walk(path):
                for file in files:
                    filepath = os.path.join(root, file)
                    try:
                        total_size += os.path.getsize(filepath)
                        file_count += 1
                    except OSError:
                        pass
        else:
            for item in os.listdir(path):
                itempath = os.path.join(path, item)
                if os.path.isfile(itempath):
                    total_size += os.path.getsize(itempath)
                    file_count += 1
    
    if human_readable:
        size_str = format_size(total_size)
    else:
        size_str = f'{total_size:,} bytes'
    
    click.echo(f'๐Ÿ“ ํŒŒ์ผ ์ˆ˜: {file_count:,}๊ฐœ')
    click.echo(f'๐Ÿ’พ ์ด ํฌ๊ธฐ: {size_str}')

def format_size(bytes):
    """๋ฐ”์ดํŠธ๋ฅผ ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜"""
    for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
        if bytes < 1024.0:
            return f'{bytes:.2f} {unit}'
        bytes /= 1024.0
    return f'{bytes:.2f} PB'

@cli.command()
@click.argument('source', type=click.Path(exists=True))
@click.argument('dest', type=click.Path())
@click.option('--move', is_flag=True, help='๋ณต์‚ฌ ๋Œ€์‹  ์ด๋™')
@click.option('--force', '-f', is_flag=True, help='๋ฎ์–ด์“ฐ๊ธฐ ํ™•์ธ ์•ˆ ํ•จ')
def copy(source, dest, move, force):
    """ํŒŒ์ผ ๋˜๋Š” ๋””๋ ‰ํ† ๋ฆฌ ๋ณต์‚ฌ/์ด๋™"""
    action = '์ด๋™' if move else '๋ณต์‚ฌ'
    
    if os.path.exists(dest) and not force:
        if not click.confirm(f'โš ๏ธ  {dest}๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๋ฎ์–ด์“ฐ์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?'):
            click.echo('โŒ ์ž‘์—…์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.')
            return
    
    click.echo(f'๐Ÿ“‹ {action} ์ค‘: {source} โ†’ {dest}')
    
    try:
        if os.path.isfile(source):
            if move:
                shutil.move(source, dest)
            else:
                shutil.copy2(source, dest)
        else:
            if move:
                shutil.move(source, dest)
            else:
                shutil.copytree(source, dest, dirs_exist_ok=True)
        
        click.echo(click.style(f'โœ… {action} ์™„๋ฃŒ!', fg='green', bold=True))
    except Exception as e:
        click.echo(click.style(f'โŒ ์—๋Ÿฌ: {str(e)}', fg='red', bold=True))

@cli.command()
@click.argument('path', type=click.Path(exists=True))
@click.option('--ext', multiple=True, help='๊ฒ€์ƒ‰ํ•  ํ™•์žฅ์ž (์˜ˆ: .txt .py)')
@click.option('--name', help='ํŒŒ์ผ๋ช… ํŒจํ„ด')
@click.option('--size-min', type=int, help='์ตœ์†Œ ํฌ๊ธฐ (bytes)')
@click.option('--size-max', type=int, help='์ตœ๋Œ€ ํฌ๊ธฐ (bytes)')
def find(path, ext, name, size_min, size_max):
    """ํŒŒ์ผ ๊ฒ€์ƒ‰"""
    click.echo(f'๐Ÿ” {path}์—์„œ ํŒŒ์ผ ๊ฒ€์ƒ‰ ์ค‘...\n')
    
    found_files = []
    
    for root, dirs, files in os.walk(path):
        for file in files:
            filepath = os.path.join(root, file)
            
            # ํ™•์žฅ์ž ํ•„ํ„ฐ
            if ext and not any(file.endswith(e) for e in ext):
                continue
            
            # ์ด๋ฆ„ ํŒจํ„ด ํ•„ํ„ฐ
            if name and name.lower() not in file.lower():
                continue
            
            # ํฌ๊ธฐ ํ•„ํ„ฐ
            try:
                file_size = os.path.getsize(filepath)
                if size_min and file_size < size_min:
                    continue
                if size_max and file_size > size_max:
                    continue
            except OSError:
                continue
            
            found_files.append((filepath, file_size))
    
    if found_files:
        click.echo(f'๐Ÿ“ {len(found_files)}๊ฐœ ํŒŒ์ผ ๋ฐœ๊ฒฌ:\n')
        
        for filepath, size in sorted(found_files):
            size_str = format_size(size)
            click.echo(f'  โ€ข {filepath} ({size_str})')
        
        total_size = sum(size for _, size in found_files)
        click.echo(f'\n๐Ÿ’พ ์ด ํฌ๊ธฐ: {format_size(total_size)}')
    else:
        click.echo('โŒ ์กฐ๊ฑด์— ๋งž๋Š” ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.')

@cli.command()
@click.argument('path', type=click.Path(exists=True))
@click.option('--days', type=int, default=30, help='๋ณด๊ด€ ๊ธฐ๊ฐ„ (์ผ)')
@click.option('--dry-run', is_flag=True, help='์‹ค์ œ ์‚ญ์ œ ์—†์ด ์‹œ๋ฎฌ๋ ˆ์ด์…˜')
def cleanup(path, days, dry_run):
    """์˜ค๋ž˜๋œ ํŒŒ์ผ ์ •๋ฆฌ"""
    from datetime import datetime, timedelta
    
    cutoff_date = datetime.now() - timedelta(days=days)
    
    click.echo(f'๐Ÿ—‘๏ธ  {days}์ผ ์ด์ƒ ๋œ ํŒŒ์ผ ์ •๋ฆฌ ์ค‘...')
    if dry_run:
        click.echo(click.style('(์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ชจ๋“œ)', fg='yellow'))
    click.echo()
    
    deleted_files = []
    total_size = 0
    
    for root, dirs, files in os.walk(path):
        for file in files:
            filepath = os.path.join(root, file)
            
            try:
                mtime = datetime.fromtimestamp(os.path.getmtime(filepath))
                
                if mtime < cutoff_date:
                    size = os.path.getsize(filepath)
                    deleted_files.append((filepath, size, mtime))
                    total_size += size
                    
                    if not dry_run:
                        os.remove(filepath)
                        click.echo(f'  โœ“ ์‚ญ์ œ: {filepath}')
                    else:
                        click.echo(f'  โ€ข ์‚ญ์ œ ์˜ˆ์ •: {filepath}')
            except OSError:
                pass
    
    click.echo()
    if deleted_files:
        click.echo(f'๐Ÿ“Š ํ†ต๊ณ„:')
        click.echo(f'  โ€ข ํŒŒ์ผ ์ˆ˜: {len(deleted_files):,}๊ฐœ')
        click.echo(f'  โ€ข ํ™•๋ณด ๊ณต๊ฐ„: {format_size(total_size)}')
        
        if dry_run:
            click.echo(click.style('\n๐Ÿ’ก ์‹ค์ œ๋กœ ์‚ญ์ œํ•˜๋ ค๋ฉด --dry-run ์˜ต์…˜์„ ์ œ๊ฑฐํ•˜์„ธ์š”.', fg='yellow'))
    else:
        click.echo('โœ… ์ •๋ฆฌํ•  ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.')

@cli.command()
@click.argument('path', type=click.Path(exists=True))
def info(path):
    """ํŒŒ์ผ/๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ธ ์ •๋ณด"""
    click.echo(f'๐Ÿ“‹ {path} ์ •๋ณด\n')
    
    stat = os.stat(path)
    
    # ๊ธฐ๋ณธ ์ •๋ณด
    if os.path.isfile(path):
        click.echo(f'ํƒ€์ž…: ๐Ÿ“„ ํŒŒ์ผ')
        click.echo(f'ํฌ๊ธฐ: {format_size(stat.st_size)}')
    else:
        click.echo(f'ํƒ€์ž…: ๐Ÿ“ ๋””๋ ‰ํ† ๋ฆฌ')
    
    # ์‹œ๊ฐ„ ์ •๋ณด
    created = datetime.fromtimestamp(stat.st_ctime)
    modified = datetime.fromtimestamp(stat.st_mtime)
    accessed = datetime.fromtimestamp(stat.st_atime)
    
    click.echo(f'\nโฐ ์‹œ๊ฐ„ ์ •๋ณด:')
    click.echo(f'  โ€ข ์ƒ์„ฑ: {created.strftime("%Y-%m-%d %H:%M:%S")}')
    click.echo(f'  โ€ข ์ˆ˜์ •: {modified.strftime("%Y-%m-%d %H:%M:%S")}')
    click.echo(f'  โ€ข ์ ‘๊ทผ: {accessed.strftime("%Y-%m-%d %H:%M:%S")}')
    
    # ๊ถŒํ•œ ์ •๋ณด
    click.echo(f'\n๐Ÿ”’ ๊ถŒํ•œ:')
    click.echo(f'  โ€ข ์ฝ๊ธฐ: {"โœ“" if os.access(path, os.R_OK) else "โœ—"}')
    click.echo(f'  โ€ข ์“ฐ๊ธฐ: {"โœ“" if os.access(path, os.W_OK) else "โœ—"}')
    click.echo(f'  โ€ข ์‹คํ–‰: {"โœ“" if os.access(path, os.X_OK) else "โœ—"}')

if __name__ == '__main__':
    cli()
์ด ๋„๊ตฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”:
# ๋””๋ ‰ํ† ๋ฆฌ ํฌ๊ธฐ ํ™•์ธ
python filemanager.py size /path/to/dir -r -h

# ํŒŒ์ผ ๋ณต์‚ฌ
python filemanager.py copy source.txt dest.txt

# ํŒŒ์ผ ๊ฒ€์ƒ‰
python filemanager.py find /path/to/dir --ext .py --ext .txt --size-min 1000

# ์˜ค๋ž˜๋œ ํŒŒ์ผ ์ •๋ฆฌ (์‹œ๋ฎฌ๋ ˆ์ด์…˜)
python filemanager.py cleanup /path/to/dir --days 90 --dry-run

# ํŒŒ์ผ ์ •๋ณด ๋ณด๊ธฐ
python filemanager.py info myfile.txt

# ๋„์›€๋ง
python filemanager.py --help
python filemanager.py size --help
๐Ÿ’ก ์‹ค์ „ ํŒ โ€ข ๋ช…๋ น์–ด๋Š” ์ง๊ด€์ ์ด๊ณ  ๊ธฐ์–ตํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋งŒ๋“œ์„ธ์š”
โ€ข ์œ„ํ—˜ํ•œ ์ž‘์—…(์‚ญ์ œ, ๋ฎ์–ด์“ฐ๊ธฐ)์€ ํ™•์ธ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋„ฃ์œผ์„ธ์š”
โ€ข --dry-run ์˜ต์…˜์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์„ธ์š”
โ€ข ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ๊ผผ๊ผผํ•˜๊ฒŒ ํ•ด์„œ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์ƒํ™ฉ์— ๋Œ€๋น„ํ•˜์„ธ์š”
โ€ข ์ง„ํ–‰ ์ƒํ™ฉ์„ ๋ณด์—ฌ์ฃผ๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ์ข‹์•„์ ธ์š”
## ๐Ÿ“ฆ ํŒจํ‚ค์ง•๊ณผ ๋ฐฐํฌ ๋งŒ๋“  CLI ๋„๊ตฌ๋ฅผ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค๊ณผ ๊ณต์œ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ํŒจํ‚ค์ง•์ด ํ•„์š”ํ•ด์š”! ๐ŸŽ ### setup.py ์ž‘์„ฑํ•˜๊ธฐ
from setuptools import setup, find_packages

setup(
    name='filemanager-cli',
    version='1.0.0',
    packages=find_packages(),
    include_package_data=True,
    install_requires=[
        'Click>=8.0',
    ],
    entry_points={
        'console_scripts': [
            'fm=filemanager:cli',
        ],
    },
    author='Your Name',
    author_email='your.email@example.com',
    description='ํŒŒ์ผ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ CLI ๋„๊ตฌ',
    long_description=open('README.md').read(),
    long_description_content_type='text/markdown',
    url='https://github.com/yourusername/filemanager-cli',
    classifiers=[
        'Development Status :: 4 - Beta',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: MIT License',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.8',
        'Programming Language :: Python :: 3.9',
        'Programming Language :: Python :: 3.10',
        'Programming Language :: Python :: 3.11',
    ],
    python_requires='>=3.8',
)
### pyproject.toml ์‚ฌ์šฉํ•˜๊ธฐ (์ตœ์‹  ๋ฐฉ์‹)
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "filemanager-cli"
version = "1.0.0"
description = "ํŒŒ์ผ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ CLI ๋„๊ตฌ"
readme = "README.md"
authors = [
    {name = "Your Name", email = "your.email@example.com"}
]
license = {text = "MIT"}
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.8",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
]
requires-python = ">=3.8"
dependencies = [
    "click>=8.0",
]

[project.scripts]
fm = "filemanager:cli"

[project.urls]
Homepage = "https://github.com/yourusername/filemanager-cli"
### ์„ค์น˜ ๋ฐ ์‚ฌ์šฉ
# ๊ฐœ๋ฐœ ๋ชจ๋“œ๋กœ ์„ค์น˜ (์ˆ˜์ •์‚ฌํ•ญ์ด ๋ฐ”๋กœ ๋ฐ˜์˜๋จ)
pip install -e .

# ์ด์ œ ์–ด๋””์„œ๋“  ์‚ฌ์šฉ ๊ฐ€๋Šฅ!
fm size /path/to/dir
fm find . --ext .py
fm --help

# PyPI์— ์—…๋กœ๋“œ
python -m build
twine upload dist/*
๐Ÿ“ฆ ํŒจํ‚ค์ง• ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… README.md ์ž‘์„ฑ (์‚ฌ์šฉ๋ฒ•, ์˜ˆ์ œ ํฌํ•จ)
  • โœ… LICENSE ํŒŒ์ผ ์ถ”๊ฐ€
  • โœ… requirements.txt ๋˜๋Š” pyproject.toml
  • โœ… .gitignore ์„ค์ •
  • โœ… ๋ฒ„์ „ ๊ด€๋ฆฌ ์ „๋žต ์ˆ˜๋ฆฝ
  • โœ… ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ
  • โœ… CI/CD ์„ค์ • (GitHub Actions ๋“ฑ)
## ๐Ÿš€ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค Click์˜ ๋” ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์•Œ์•„๋ณผ๊นŒ์š”? ๐ŸŽฏ ### ์ปค์Šคํ…€ ํƒ€์ž… ๋งŒ๋“ค๊ธฐ
import click

class EmailType(click.ParamType):
    """์ด๋ฉ”์ผ ์ฃผ์†Œ ํƒ€์ž…"""
    name = 'email'
    
    def convert(self, value, param, ctx):
        if '@' not in value or '.' not in value.split('@')[1]:
            self.fail(f'{value}๋Š” ์œ ํšจํ•œ ์ด๋ฉ”์ผ ์ฃผ์†Œ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.', param, ctx)
        return value.lower()

EMAIL = EmailType()

@click.command()
@click.option('--email', type=EMAIL, required=True, help='์ด๋ฉ”์ผ ์ฃผ์†Œ')
def send_mail(email):
    """์ด๋ฉ”์ผ ๋ฐœ์†ก"""
    click.echo(f'๐Ÿ“ง {email}๋กœ ๋ฉ”์ผ์„ ๋ฐœ์†กํ•ฉ๋‹ˆ๋‹ค...')
    click.echo('โœ… ๋ฐœ์†ก ์™„๋ฃŒ!')

if __name__ == '__main__':
    send_mail()
### ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํ™œ์šฉ
import click

@click.command()
@click.option('--api-key',
              envvar='API_KEY',
              required=True,
              help='API ํ‚ค (ํ™˜๊ฒฝ๋ณ€์ˆ˜: API_KEY)')
@click.option('--endpoint',
              envvar='API_ENDPOINT',
              default='https://api.example.com',
              help='API ์—”๋“œํฌ์ธํŠธ')
def api_request(api_key, endpoint):
    """API ์š”์ฒญ"""
    click.echo(f'๐ŸŒ {endpoint}๋กœ ์š”์ฒญ ์ค‘...')
    click.echo(f'๐Ÿ”‘ API ํ‚ค: {api_key[:4]}****')
    click.echo('โœ… ์š”์ฒญ ์™„๋ฃŒ!')

if __name__ == '__main__':
    api_request()
์‚ฌ์šฉ ์˜ˆ์‹œ:
# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •
export API_KEY=your_secret_key
export API_ENDPOINT=https://custom.api.com

# ์‹คํ–‰ (ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์‚ฌ์šฉ)
python api.py

# ๋˜๋Š” ์ง์ ‘ ์ง€์ •
python api.py --api-key your_key --endpoint https://api.example.com
### ์ฝœ๋ฐฑ ํ•จ์ˆ˜ ์‚ฌ์šฉ ์˜ต์…˜ ๊ฐ’์„ ๊ฒ€์ฆํ•˜๊ฑฐ๋‚˜ ๋ณ€ํ™˜ํ•  ๋•Œ ์ฝœ๋ฐฑ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”:
import click

def validate_port(ctx, param, value):
    """ํฌํŠธ ๋ฒˆํ˜ธ ๊ฒ€์ฆ"""
    if value < 1 or value > 65535:
        raise click.BadParameter('ํฌํŠธ๋Š” 1-65535 ์‚ฌ์ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.')
    return value

def normalize_url(ctx, param, value):
    """URL ์ •๊ทœํ™”"""
    if not value.startswith(('http://', 'https://')):
        value = 'https://' + value
    return value.rstrip('/')

@click.command()
@click.option('--port',
              type=int,
              default=8080,
              callback=validate_port,
              help='์„œ๋ฒ„ ํฌํŠธ')
@click.option('--url',
              callback=normalize_url,
              help='์„œ๋ฒ„ URL')
def server(port, url):
    """์„œ๋ฒ„ ์‹œ์ž‘"""
    click.echo(f'๐Ÿš€ ์„œ๋ฒ„ ์‹œ์ž‘: {url}:{port}')

if __name__ == '__main__':
    server()
### ํ”Œ๋Ÿฌ๊ทธ์ธ ์‹œ์Šคํ…œ Click์œผ๋กœ ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ฐ€๋Šฅํ•œ CLI๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์š”:
import click

class PluginCLI(click.MultiCommand):
    """ํ”Œ๋Ÿฌ๊ทธ์ธ ์ง€์› CLI"""
    
    def list_commands(self, ctx):
        """์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ช…๋ น์–ด ๋ชฉ๋ก"""
        # ์‹ค์ œ๋กœ๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์Šค์บ”
        return ['plugin1', 'plugin2', 'plugin3']
    
    def get_command(self, ctx, name):
        """๋ช…๋ น์–ด ๋กœ๋“œ"""
        # ์‹ค์ œ๋กœ๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ ๋ชจ๋“ˆ์„ ๋™์ ์œผ๋กœ ๋กœ๋“œ
        @click.command()
        def dynamic_command():
            click.echo(f'โœจ {name} ํ”Œ๋Ÿฌ๊ทธ์ธ ์‹คํ–‰!')
        
        return dynamic_command

@click.command(cls=PluginCLI)
def cli():
    """ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ธฐ๋ฐ˜ CLI"""
    pass

if __name__ == '__main__':
    cli()
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ โ€ข ์ปค์Šคํ…€ ํƒ€์ž…์€ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“œ์„ธ์š”
โ€ข ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋Š” ๋ฏผ๊ฐํ•œ ์ •๋ณด(API ํ‚ค, ๋น„๋ฐ€๋ฒˆํ˜ธ)์— ์‚ฌ์šฉํ•˜์„ธ์š”
โ€ข ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ ์œ ์ง€ํ•˜์„ธ์š”
โ€ข ํ”Œ๋Ÿฌ๊ทธ์ธ ์‹œ์Šคํ…œ์€ ๋ณด์•ˆ์— ์ฃผ์˜ํ•˜์„ธ์š”
## ๐Ÿ’ก ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค Click์œผ๋กœ ํ›Œ๋ฅญํ•œ CLI ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ํŒ๋“ค์ด์—์š”! โญ ### 1. ๋ช…ํ™•ํ•œ ๋ช…๋ น์–ด ์ด๋ฆ„
โœ… ์ข‹์€ ์˜ˆ db backup, user create, log show

โŒ ๋‚˜์œ ์˜ˆ
do_backup, usr_crt, showlog
### 2. ์ผ๊ด€๋œ ์˜ต์…˜ ์‚ฌ์šฉ
# ๊ณตํ†ต ์˜ต์…˜์€ ์ผ๊ด€๋˜๊ฒŒ ์‚ฌ์šฉ
--verbose / -v    # ์ƒ์„ธ ์ถœ๋ ฅ
--quiet / -q      # ์ตœ์†Œ ์ถœ๋ ฅ
--force / -f      # ํ™•์ธ ์—†์ด ์‹คํ–‰
--dry-run         # ์‹œ๋ฎฌ๋ ˆ์ด์…˜
--help / -h       # ๋„์›€๋ง
--version         # ๋ฒ„์ „ ์ •๋ณด
### 3. ์œ ์šฉํ•œ ๋„์›€๋ง ์ž‘์„ฑ
@click.command()
@click.option('--format',
              type=click.Choice(['json', 'yaml', 'csv']),
              default='json',
              help='์ถœ๋ ฅ ํ˜•์‹. JSON์€ ๊ธฐ๊ณ„ ํŒ๋…์šฉ, YAML์€ ์‚ฌ๋žŒ ํŒ๋…์šฉ, CSV๋Š” ์Šคํ”„๋ ˆ๋“œ์‹œํŠธ์šฉ.')
@click.option('--output', '-o',
              type=click.File('w'),
              help='์ถœ๋ ฅ ํŒŒ์ผ ๊ฒฝ๋กœ. ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ํ‘œ์ค€ ์ถœ๋ ฅ์œผ๋กœ ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.')
def export(format, output):
    """
    ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์–‘ํ•œ ํ˜•์‹์œผ๋กœ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค.
    
    ์˜ˆ์ œ:
    
        $ myapp export --format json -o data.json
        
        $ myapp export --format yaml > data.yaml
        
        $ myapp export --format csv | less
    """
    pass
### 4. ์—๋Ÿฌ ์ฒ˜๋ฆฌ
import click
import sys

@click.command()
@click.argument('filename', type=click.Path(exists=True))
def process(filename):
    """ํŒŒ์ผ ์ฒ˜๋ฆฌ"""
    try:
        with open(filename, 'r') as f:
            content = f.read()
        
        # ์ฒ˜๋ฆฌ ๋กœ์ง
        result = content.upper()
        
        click.echo('โœ… ์ฒ˜๋ฆฌ ์™„๋ฃŒ!')
        
    except PermissionError:
        click.echo(click.style('โŒ ์—๋Ÿฌ: ํŒŒ์ผ ์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.', fg='red'), err=True)
        sys.exit(1)
    
    except UnicodeDecodeError:
        click.echo(click.style('โŒ ์—๋Ÿฌ: ํŒŒ์ผ ์ธ์ฝ”๋”ฉ์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.', fg='red'), err=True)
        sys.exit(1)
    
    except Exception as e:
        click.echo(click.style(f'โŒ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์—๋Ÿฌ: {str(e)}', fg='red'), err=True)
        sys.exit(1)

if __name__ == '__main__':
    process()
### 5. ์„ค์ • ํŒŒ์ผ ์ง€์›
import click
import json
from pathlib import Path

def load_config():
    """์„ค์ • ํŒŒ์ผ ๋กœ๋“œ"""
    config_path = Path.home() / '.myapp' / 'config.json'
    
    if config_path.exists():
        with open(config_path) as f:
            return json.load(f)
    
    return {}

@click.command()
@click.option('--api-key', help='API ํ‚ค')
@click.option('--endpoint', help='API ์—”๋“œํฌ์ธํŠธ')
def api_call(api_key, endpoint):
    """API ํ˜ธ์ถœ"""
    config = load_config()
    
    # ๋ช…๋ น์ค„ ์˜ต์…˜์ด ์šฐ์„ , ์—†์œผ๋ฉด ์„ค์ • ํŒŒ์ผ ์‚ฌ์šฉ
    api_key = api_key or config.get('api_key')
    endpoint = endpoint or config.get('endpoint', 'https://api.example.com')
    
    if not api_key:
        click.echo('โŒ API ํ‚ค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.')
        click.echo('๋ช…๋ น์ค„ ์˜ต์…˜์œผ๋กœ ์ง€์ •ํ•˜๊ฑฐ๋‚˜ ~/.myapp/config.json์— ์„ค์ •ํ•˜์„ธ์š”.')
        raise click.Abort()
    
    click.echo(f'๐ŸŒ {endpoint}๋กœ ์š”์ฒญ ์ค‘...')
    click.echo('โœ… ์™„๋ฃŒ!')

if __name__ == '__main__':
    api_call()
### 6. ๋กœ๊น… ๊ตฌํ˜„
import click
import logging
from pathlib import Path

def setup_logging(verbose):
    """๋กœ๊น… ์„ค์ •"""
    log_dir = Path.home() / '.myapp' / 'logs'
    log_dir.mkdir(parents=True, exist_ok=True)
    
    log_file = log_dir / 'myapp.log'
    
    level = logging.DEBUG if verbose else logging.INFO
    
    logging.basicConfig(
        level=level,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler(log_file),
            logging.StreamHandler()
        ]
    )
    
    return logging.getLogger('myapp')

@click.command()
@click.option('--verbose', '-v', is_flag=True, help='์ƒ์„ธ ๋กœ๊ทธ ์ถœ๋ ฅ')
def process(verbose):
    """๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ"""
    logger = setup_logging(verbose)
    
    logger.info('์ฒ˜๋ฆฌ ์‹œ์ž‘')
    logger.debug('๋””๋ฒ„๊ทธ ์ •๋ณด: ์„ค์ • ๋กœ๋“œ ์ค‘...')
    
    # ์ž‘์—… ์ˆ˜ํ–‰
    click.echo('โœ… ์ฒ˜๋ฆฌ ์™„๋ฃŒ!')
    
    logger.info('์ฒ˜๋ฆฌ ์™„๋ฃŒ')

if __name__ == '__main__':
    process()
๐Ÿ’ก ์ถ”๊ฐ€ ํŒ โ€ข ๋ช…๋ น์–ด๋Š” ๋™์‚ฌ๋กœ ์‹œ์ž‘ํ•˜์„ธ์š” (create, delete, list, show)
โ€ข ์˜ต์…˜์€ ๋ช…์‚ฌ๋‚˜ ํ˜•์šฉ์‚ฌ๋กœ (--name, --verbose, --force)
โ€ข ์งง์€ ์˜ต์…˜(-v)๊ณผ ๊ธด ์˜ต์…˜(--verbose) ๋‘˜ ๋‹ค ์ œ๊ณตํ•˜์„ธ์š”
โ€ข ์œ„ํ—˜ํ•œ ์ž‘์—…์€ ํ™•์ธ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋„ฃ์œผ์„ธ์š”
โ€ข ์ง„ํ–‰ ์ƒํ™ฉ์„ ๋ณด์—ฌ์ฃผ์„ธ์š” (ํŠนํžˆ ๊ธด ์ž‘์—…)
โ€ข ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋Š” ๊ตฌ์ฒด์ ์ด๊ณ  ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ œ์‹œํ•˜์„ธ์š”
โ€ข ์˜ˆ์ œ๋ฅผ ๋„์›€๋ง์— ํฌํ•จํ•˜์„ธ์š”
CLI ๋„๊ตฌ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค ์‚ฌ์šฉ์„ฑ ์ง๊ด€์ ์ธ ๋ช…๋ น์–ด ๋ช…ํ™•ํ•œ ๋„์›€๋ง ์ผ๊ด€๋œ ์ธํ„ฐํŽ˜์ด์Šค ์œ ์šฉํ•œ ์˜ˆ์ œ ์•ˆ์ •์„ฑ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ž…๋ ฅ ๊ฒ€์ฆ ์•ˆ์ „ํ•œ ๊ธฐ๋ณธ๊ฐ’ ๋ณต๊ตฌ ๊ฐ€๋Šฅํ•œ ์ž‘์—… ์„ฑ๋Šฅ ๋น ๋ฅธ ์‘๋‹ต ์ง„ํ–‰ ํ‘œ์‹œ ํšจ์œจ์ ์ธ ์ฒ˜๋ฆฌ ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ ํ™•์žฅ์„ฑ ๋ชจ๋“ˆํ™”๋œ ๊ตฌ์กฐ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ง€์› ์„ค์ • ํŒŒ์ผ API ์ œ๊ณต ์œ ์ง€๋ณด์ˆ˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋ฌธ์„œํ™” ๋ฒ„์ „ ๊ด€๋ฆฌ ๋กœ๊น… ๋ณด์•ˆ ์ž…๋ ฅ ๊ฒ€์ฆ ๊ถŒํ•œ ํ™•์ธ ๋ฏผ๊ฐ์ •๋ณด ๋ณดํ˜ธ ์•ˆ์ „ํ•œ ๊ธฐ๋ณธ๊ฐ’
## ๐ŸŒ ์‹ค์ œ ์‚ฌ์šฉ ์‚ฌ๋ก€ Click์œผ๋กœ ๋งŒ๋“ค์–ด์ง„ ์œ ๋ช…ํ•œ ํ”„๋กœ์ ํŠธ๋“ค์„ ์‚ดํŽด๋ณผ๊นŒ์š”? ์˜๊ฐ์„ ์–ป์„ ์ˆ˜ ์žˆ์„ ๊ฑฐ์˜ˆ์š”! ๐ŸŒŸ ### Flask ๋„ค, ๋งž์•„์š”! ์œ ๋ช…ํ•œ ์›น ํ”„๋ ˆ์ž„์›Œํฌ Flask์˜ CLI๊ฐ€ Click์œผ๋กœ ๋งŒ๋“ค์–ด์กŒ์–ด์š”:
flask run
flask shell
flask db migrate
flask routes
Flask๋ฅผ ์‚ฌ์šฉํ•ด๋ณธ ์ ์ด ์žˆ๋‹ค๋ฉด ์ด๋ฏธ Click์„ ๊ฒฝํ—˜ํ•œ ๊ฑฐ์˜ˆ์š”! ๐Ÿ˜Š ### Pipenv ํŒŒ์ด์ฌ ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ ๋„๊ตฌ Pipenv๋„ Click์„ ์‚ฌ์šฉํ•ด์š”:
pipenv install
pipenv shell
pipenv lock
pipenv graph
### AWS CLI v2 AWS์˜ ๊ณต์‹ CLI ๋„๊ตฌ๋„ Click ๊ธฐ๋ฐ˜์ด์—์š”:
aws s3 ls
aws ec2 describe-instances
aws lambda invoke
๐ŸŽฏ Click์ด ์ธ๊ธฐ ์žˆ๋Š” ์ด์œ  โ€ข ๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์—์„œ ๊ฒ€์ฆ๋จ
โ€ข ํ™œ๋ฐœํ•œ ์ปค๋ฎค๋‹ˆํ‹ฐ์™€ ์ง€์†์ ์ธ ์—…๋ฐ์ดํŠธ
โ€ข ํ’๋ถ€ํ•œ ๋ฌธ์„œ์™€ ์˜ˆ์ œ
โ€ข ํŒŒ์ด์ฌ๋‹ค์šด(Pythonic) ์ฝ”๋“œ ์Šคํƒ€์ผ
โ€ข ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฌ์šด ๊ตฌ์กฐ
## ๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค Click์„ ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ์ž๋ฃŒ๋“ค์„ ์ฐธ๊ณ ํ•˜์„ธ์š”! ๐Ÿ“š ### ๊ณต์‹ ๋ฌธ์„œ โ€ข Click ๊ณต์‹ ๋ฌธ์„œ: https://click.palletsprojects.com/
โ€ข GitHub ์ €์žฅ์†Œ: https://github.com/pallets/click
โ€ข ์˜ˆ์ œ ๋ชจ์Œ: https://github.com/pallets/click/tree/main/examples ### ์ถ”์ฒœ ํŠœํ† ๋ฆฌ์–ผ โ€ข Real Python์˜ Click ๊ฐ€์ด๋“œ
โ€ข Python Click Tutorial (YouTube)
โ€ข Building CLI Applications with Click ### ์ปค๋ฎค๋‹ˆํ‹ฐ โ€ข Stack Overflow์˜ Click ํƒœ๊ทธ
โ€ข Reddit์˜ r/Python ์ปค๋ฎค๋‹ˆํ‹ฐ
โ€ข Discord์˜ Python ์„œ๋ฒ„
๐Ÿ’ก ํ•™์Šต ํŒ 1. ์ž‘์€ ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜์„ธ์š”
2. ๊ณต์‹ ์˜ˆ์ œ๋ฅผ ์ง์ ‘ ์‹คํ–‰ํ•ด๋ณด์„ธ์š”
3. ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ์˜ ์ฝ”๋“œ๋ฅผ ์ฝ์–ด๋ณด์„ธ์š”
4. ์ž์‹ ๋งŒ์˜ CLI ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”
5. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ๊ฒฝํ—˜์„ ๊ณต์œ ํ•˜์„ธ์š”!
## ๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ ์ž, ์—ฌ๊ธฐ๊นŒ์ง€ Click ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ๋Œ€ํ•ด ์•Œ์•„๋ดค์–ด์š”! ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์ฃ ? ๐Ÿ˜Š

Click์€ ํŒŒ์ด์ฌ์œผ๋กœ CLI ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค ๋•Œ ์ตœ๊ณ ์˜ ์„ ํƒ์ด์—์š”. ๊ฐ„๋‹จํ•œ ์Šคํฌ๋ฆฝํŠธ๋ถ€ํ„ฐ ๋ณต์žกํ•œ ๋ช…๋ น์ค„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊นŒ์ง€, Click์€ ๋ชจ๋“  ๊ฒƒ์„ ์šฐ์•„ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค. ๐ŸŽฏ

๐ŸŽ ํ•ต์‹ฌ ์š”์•ฝ 1. ์„ค์น˜์™€ ์‹œ์ž‘: pip install click์œผ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ์‹œ์ž‘

2. ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๊ธฐ๋ฐ˜: @click.command()๋กœ ํ•จ์ˆ˜๋ฅผ CLI ๋ช…๋ น์–ด๋กœ ๋ณ€ํ™˜

3. ์˜ต์…˜๊ณผ ์ธ์ž: ๋‹ค์–‘ํ•œ ํƒ€์ž…๊ณผ ๊ฒ€์ฆ ๊ธฐ๋Šฅ ์ œ๊ณต

4. ๋ช…๋ น์–ด ๊ทธ๋ฃน: ๋ณต์žกํ•œ CLI ๊ตฌ์กฐ๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ๊ตฌ์„ฑ

5. ์Šคํƒ€์ผ๋ง: ์ƒ‰์ƒ, ์ง„ํ–‰ ํ‘œ์‹œ์ค„๋กœ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ–ฅ์ƒ

6. ํ…Œ์ŠคํŠธ: CliRunner๋กœ ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธ

7. ํŒจํ‚ค์ง•: setup.py๋‚˜ pyproject.toml๋กœ ๋ฐฐํฌ
์ด์ œ ์—ฌ๋Ÿฌ๋ถ„๋„ ๋ฉ‹์ง„ CLI ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์š”! ๐Ÿš€

์ฒ˜์Œ์—๋Š” ๊ฐ„๋‹จํ•œ ๋„๊ตฌ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด๋ณด์„ธ์š”. ์˜ˆ๋ฅผ ๋“ค์–ด:
โ€ข ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ๋ช…๋ น์–ด๋ฅผ ์ž๋™ํ™”ํ•˜๋Š” ๋„๊ตฌ
โ€ข ํŒŒ์ผ์„ ์ •๋ฆฌํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ
โ€ข ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™˜ํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ
โ€ข ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๋•๋Š” ๋„๊ตฌ

๊ทธ๋ฆฌ๊ณ  ๋งŒ๋“  ๋„๊ตฌ๋ฅผ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๊ณต์œ ํ•˜๋ฉด์„œ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ์†Œํ†ตํ•ด๋ณด์„ธ์š”. ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›๊ณ  ๊ฐœ์„ ํ•˜๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ์ „๋ฌธ๊ฐ€๊ฐ€ ๋˜์–ด ์žˆ์„ ๊ฑฐ์˜ˆ์š”! ๐Ÿ’ช

โš ๏ธ ๋งˆ์ง€๋ง‰ ์กฐ์–ธ โ€ข ์ฒ˜์Œ๋ถ€ํ„ฐ ์™„๋ฒฝํ•  ํ•„์š”๋Š” ์—†์–ด์š”. ์ž‘๋™ํ•˜๋Š” ๋ฒ„์ „์„ ๋จผ์ € ๋งŒ๋“ค๊ณ  ๊ฐœ์„ ํ•˜์„ธ์š”
โ€ข ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ์„ ์ ๊ทน์ ์œผ๋กœ ์ˆ˜์šฉํ•˜์„ธ์š”
โ€ข ๋ฌธ์„œํ™”๋ฅผ ๊ฒŒ์„๋ฆฌํ•˜์ง€ ๋งˆ์„ธ์š”
โ€ข ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”
โ€ข ๋ณด์•ˆ์„ ํ•ญ์ƒ ์—ผ๋‘์— ๋‘์„ธ์š”
Click์œผ๋กœ ๋งŒ๋“  CLI ๋„๊ตฌ๋Š” ์—ฌ๋Ÿฌ๋ถ„์˜ ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ฌ ๊ฑฐ์˜ˆ์š”. ๋ฐ˜๋ณต์ ์ธ ์ž‘์—…์„ ์ž๋™ํ™”ํ•˜๊ณ , ํŒ€์›๋“ค๊ณผ ๋„๊ตฌ๋ฅผ ๊ณต์œ ํ•˜๊ณ , ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ์— ๊ธฐ์—ฌํ•  ์ˆ˜๋„ ์žˆ์ฃ ! ๐ŸŒŸ

์ด ๊ธ€์ด ์—ฌ๋Ÿฌ๋ถ„์˜ Click ์—ฌ์ •์— ๋„์›€์ด ๋˜์—ˆ๊ธฐ๋ฅผ ๋ฐ”๋ผ์š”. ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๋‹ค๋ฉด ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜๊ฑฐ๋‚˜, ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•ด๋ณด์„ธ์š”. ๊ทธ๋ฆฌ๊ณ  ์žฌ๋Šฅ๋„ท์—์„œ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ๊ฒฝํ—˜์„ ๋‚˜๋ˆ„๋Š” ๊ฒƒ๋„ ์žŠ์ง€ ๋งˆ์„ธ์š”! ๐Ÿ˜Š

ํ–‰๋ณตํ•œ ์ฝ”๋”ฉ ๋˜์„ธ์š”! ๐ŸŽ‰โœจ

๐Ÿš€ ์ง€๊ธˆ ๋ฐ”๋กœ Click์œผ๋กœ ์ฒซ CLI ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”! ๐Ÿš€

๋‹น์‹ ์˜ ์•„์ด๋””์–ด๊ฐ€ ๋ฉ‹์ง„ ๋„๊ตฌ๊ฐ€ ๋˜๋Š” ์ˆœ๊ฐ„์„ ๊ฒฝํ—˜ํ•˜์„ธ์š”!

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.