Ver3.0 ๐ ํ์ด์ฌ์ผ๋ก ๋ง๋๋ CLI ๋๊ตฌ: Click ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ ์ ๋ณต

๐ ํ์ด์ฌ์ผ๋ก ๋ง๋๋ CLI ๋๊ตฌ: Click ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ ์ ๋ณต
๋ช ๋ น์ค ์ธํฐํ์ด์ค๋ฅผ ์ฐ์ํ๊ฒ ๋ง๋๋ ๋ง๋ฒ ๊ฐ์ ๋๊ตฌ โจ
๊ฐ๋ฐ์๋ผ๋ฉด ๋ฐ๋ณต์ ์ธ ์์ ์ ์๋ํํ๊ฑฐ๋, ํ์๋ค๊ณผ ๊ณต์ ํ ์ ์๋ ๋๊ตฌ๋ฅผ ๋ง๋ค๊ณ ์ถ์ ๋๊ฐ ์์ฃ . ์๋ฅผ ๋ค์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฐฑ์ , ๋ก๊ทธ ๋ถ์, ํ์ผ ๋ณํ ๊ฐ์ ์์ ๋ค์ ๋งค๋ฒ ์๋์ผ๋ก ํ๊ธฐ์ ๋๋ฌด ๊ท์ฐฎ์์์? ์ด๋ด ๋ CLI ๋๊ตฌ๋ฅผ ๋ง๋ค๋ฉด ์ ๋ง ํธ๋ฆฌํฉ๋๋ค! ๐
ํ์ด์ฌ์๋ CLI ๋๊ตฌ๋ฅผ ๋ง๋ค ์ ์๋ ์ฌ๋ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์์ด์. argparse๋ ๊ธฐ๋ณธ ๋ด์ฅ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ง๋ง ์ฝ๋๊ฐ ๊ธธ์ด์ง๊ณ ๋ณต์กํด์. docopt์ ๋ ํนํ ์ ๊ทผ ๋ฐฉ์์ด์ง๋ง ์ ์ฐ์ฑ์ด ๋จ์ด์ง์ฃ . ๊ทธ๋ฐ๋ฐ Click ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์ ๋ง ์ง๊ด์ ์ด๊ณ ์ฐ์ํ๊ฒ CLI ๋๊ตฌ๋ฅผ ๋ง๋ค ์ ์๋ต๋๋ค! ๐
โข ์๋์ผ๋ก ์์ฑ๋๋ ๋์๋ง ๋ฉ์์ง
โข ํ์ ๊ฒ์ฆ๊ณผ ๋ณํ ์๋ ์ฒ๋ฆฌ
โข ์ค์ฒฉ ๋ช ๋ น์ด ๊ตฌ์กฐ ์ง์
โข ํ ์คํธํ๊ธฐ ์ฌ์ด ๊ตฌ์กฐ
โข Flask ๊ฐ๋ฐ์๊ฐ ๋ง๋ ์ ๋ขฐ์ฑ
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.echo(): print() ๋์ ์ฌ์ฉํ๋๋ฐ, ์ ๋์ฝ๋ ์ฒ๋ฆฌ๊ฐ ๋ ์์ ์ ์ด์์.
๋ ์คํธ๋ง("""..."""): ์๋์ผ๋ก ๋์๋ง ๋ฉ์์ง๊ฐ ๋ฉ๋๋ค!
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
์ด๋ ๊ฒ ํ๋ฉด --name๊ณผ -n ๋ ๋ค ์ฌ์ฉํ ์ ์์ด์!
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
์) ํ์ผ๋ช , URL, ์ฌ์ฉ์ ID
์ต์ (Option): ์ ํ์ ์ค์ , ๋์ ๋ฐฉ์ ์กฐ์
์) ์ถ๋ ฅ ํ์, ์์ธ ์ ๋ณด ํ์, ๋๋ฒ๊ทธ ๋ชจ๋
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() | ์ ํ์ง ์ค ํ๋ | ์ถ๋ ฅ ํ์ |
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: ****
๐ค ์ฒ ์๋, ๋ก๊ทธ์ธ ์๋ ์ค...
โ
๋ก๊ทธ์ธ ์ฑ๊ณต!
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.
## ๐๏ธ ๋ช
๋ น์ด ๊ทธ๋ฃน ๋ง๋ค๊ธฐ
์ค์ 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
# ์ผ๋ฐ ๋ชจ๋๋ก ํ
์คํธ ์คํ
ctx.obj: ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๋์ ๋๋ฆฌ
ctx.parent: ๋ถ๋ชจ ๋ช ๋ น์ด์ ์ปจํ ์คํธ ์ ๊ทผ
์ค์ , ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ, ๋ก๊ฑฐ ๋ฑ์ ๊ณต์ ํ ๋ ์ ์ฉํด์!
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
'w': ์ฐ๊ธฐ ๋ชจ๋ (ํ ์คํธ)
'a': ์ถ๊ฐ ๋ชจ๋ (ํ ์คํธ)
'rb': ์ฝ๊ธฐ ๋ชจ๋ (๋ฐ์ด๋๋ฆฌ)
'wb': ์ฐ๊ธฐ ๋ชจ๋ (๋ฐ์ด๋๋ฆฌ)
'-'๋ฅผ ํ์ผ๋ช ์ผ๋ก ์ฌ์ฉํ๋ฉด ํ์ค ์ ์ถ๋ ฅ์ ์๋ฏธํด์!
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()
๋ฐฐ๊ฒฝ์(bg): ์ ๊ฒฝ์๊ณผ ๋์ผ
์คํ์ผ:
โข bold=True: ๊ตต๊ฒ
โข dim=True: ํ๋ฆฌ๊ฒ
โข underline=True: ๋ฐ์ค
โข blink=True: ๊น๋นก์
โข reverse=True: ๋ฐ์
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์ด ์ด์ ๊ฑธ๋ฆฌ๋ ์์ ์ ์ฌ์ฉํ์ธ์
โข ์ค์ํ ์์ ์ ์๋ ๋ฐ๋์ ํ์ธ ํ๋กฌํํธ๋ฅผ ๋ฃ์ผ์ธ์
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')
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 ์ต์ ์ผ๋ก ์์ ํ๊ฒ ํ ์คํธํ ์ ์๊ฒ ํ์ธ์
โข ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ๊ผผ๊ผผํ๊ฒ ํด์ ์์์น ๋ชปํ ์ํฉ์ ๋๋นํ์ธ์
โข ์งํ ์ํฉ์ ๋ณด์ฌ์ฃผ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ด ์ข์์ ธ์
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 ๋ฑ)
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 ํค, ๋น๋ฐ๋ฒํธ)์ ์ฌ์ฉํ์ธ์
โข ์ฝ๋ฐฑ ํจ์๋ ๊ฐ๋จํ๊ฒ ์ ์งํ์ธ์
โข ํ๋ฌ๊ทธ์ธ ์์คํ ์ ๋ณด์์ ์ฃผ์ํ์ธ์
โ ๋์ ์
do_backup, usr_crt, showlog
# ๊ณตํต ์ต์
์ ์ผ๊ด๋๊ฒ ์ฌ์ฉ
--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()
โข ์ต์ ์ ๋ช ์ฌ๋ ํ์ฉ์ฌ๋ก (--name, --verbose, --force)
โข ์งง์ ์ต์ (-v)๊ณผ ๊ธด ์ต์ (--verbose) ๋ ๋ค ์ ๊ณตํ์ธ์
โข ์ํํ ์์ ์ ํ์ธ ํ๋กฌํํธ๋ฅผ ๋ฃ์ผ์ธ์
โข ์งํ ์ํฉ์ ๋ณด์ฌ์ฃผ์ธ์ (ํนํ ๊ธด ์์ )
โข ์๋ฌ ๋ฉ์์ง๋ ๊ตฌ์ฒด์ ์ด๊ณ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ ์ํ์ธ์
โข ์์ ๋ฅผ ๋์๋ง์ ํฌํจํ์ธ์
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
โข ํ๋ฐํ ์ปค๋ฎค๋ํฐ์ ์ง์์ ์ธ ์ ๋ฐ์ดํธ
โข ํ๋ถํ ๋ฌธ์์ ์์
โข ํ์ด์ฌ๋ค์ด(Pythonic) ์ฝ๋ ์คํ์ผ
โข ํ ์คํธํ๊ธฐ ์ฌ์ด ๊ตฌ์กฐ
โข 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 ์๋ฒ
2. ๊ณต์ ์์ ๋ฅผ ์ง์ ์คํํด๋ณด์ธ์
3. ์คํ์์ค ํ๋ก์ ํธ์ ์ฝ๋๋ฅผ ์ฝ์ด๋ณด์ธ์
4. ์์ ๋ง์ CLI ๋๊ตฌ๋ฅผ ๋ง๋ค์ด๋ณด์ธ์
5. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ๊ฒฝํ์ ๊ณต์ ํ์ธ์!
Click์ ํ์ด์ฌ์ผ๋ก CLI ๋๊ตฌ๋ฅผ ๋ง๋ค ๋ ์ต๊ณ ์ ์ ํ์ด์์. ๊ฐ๋จํ ์คํฌ๋ฆฝํธ๋ถํฐ ๋ณต์กํ ๋ช ๋ น์ค ์ ํ๋ฆฌ์ผ์ด์ ๊น์ง, Click์ ๋ชจ๋ ๊ฒ์ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌํ ์ ์๋ต๋๋ค. ๐ฏ
2. ๋ฐ์ฝ๋ ์ดํฐ ๊ธฐ๋ฐ: @click.command()๋ก ํจ์๋ฅผ CLI ๋ช ๋ น์ด๋ก ๋ณํ
3. ์ต์ ๊ณผ ์ธ์: ๋ค์ํ ํ์ ๊ณผ ๊ฒ์ฆ ๊ธฐ๋ฅ ์ ๊ณต
4. ๋ช ๋ น์ด ๊ทธ๋ฃน: ๋ณต์กํ CLI ๊ตฌ์กฐ๋ฅผ ๊น๋ํ๊ฒ ๊ตฌ์ฑ
5. ์คํ์ผ๋ง: ์์, ์งํ ํ์์ค๋ก ์ฌ์ฉ์ ๊ฒฝํ ํฅ์
6. ํ ์คํธ: CliRunner๋ก ์ฝ๊ฒ ํ ์คํธ
7. ํจํค์ง: setup.py๋ pyproject.toml๋ก ๋ฐฐํฌ
์ฒ์์๋ ๊ฐ๋จํ ๋๊ตฌ๋ถํฐ ์์ํด๋ณด์ธ์. ์๋ฅผ ๋ค์ด:
โข ์์ฃผ ์ฌ์ฉํ๋ ๋ช ๋ น์ด๋ฅผ ์๋ํํ๋ ๋๊ตฌ
โข ํ์ผ์ ์ ๋ฆฌํ๋ ์ ํธ๋ฆฌํฐ
โข ๋ฐ์ดํฐ๋ฅผ ๋ณํํ๋ ์คํฌ๋ฆฝํธ
โข ๊ฐ๋ฐ ์ํฌํ๋ก์ฐ๋ฅผ ๋๋ ๋๊ตฌ
๊ทธ๋ฆฌ๊ณ ๋ง๋ ๋๊ตฌ๋ฅผ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๊ณต์ ํ๋ฉด์ ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ์ํตํด๋ณด์ธ์. ํผ๋๋ฐฑ์ ๋ฐ๊ณ ๊ฐ์ ํ๋ค ๋ณด๋ฉด ์ด๋์ ์ ๋ฌธ๊ฐ๊ฐ ๋์ด ์์ ๊ฑฐ์์! ๐ช
โข ์ฌ์ฉ์ ํผ๋๋ฐฑ์ ์ ๊ทน์ ์ผ๋ก ์์ฉํ์ธ์
โข ๋ฌธ์ํ๋ฅผ ๊ฒ์๋ฆฌํ์ง ๋ง์ธ์
โข ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ์ธ์
โข ๋ณด์์ ํญ์ ์ผ๋์ ๋์ธ์
์ด ๊ธ์ด ์ฌ๋ฌ๋ถ์ Click ์ฌ์ ์ ๋์์ด ๋์๊ธฐ๋ฅผ ๋ฐ๋ผ์. ๊ถ๊ธํ ์ ์ด ์๋ค๋ฉด ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ๊ฑฐ๋, ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํด๋ณด์ธ์. ๊ทธ๋ฆฌ๊ณ ์ฌ๋ฅ๋ท์์ ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ๊ฒฝํ์ ๋๋๋ ๊ฒ๋ ์์ง ๋ง์ธ์! ๐
ํ๋ณตํ ์ฝ๋ฉ ๋์ธ์! ๐โจ
๐ ์ง๊ธ ๋ฐ๋ก Click์ผ๋ก ์ฒซ CLI ๋๊ตฌ๋ฅผ ๋ง๋ค์ด๋ณด์ธ์! ๐
๋น์ ์ ์์ด๋์ด๊ฐ ๋ฉ์ง ๋๊ตฌ๊ฐ ๋๋ ์๊ฐ์ ๊ฒฝํํ์ธ์!
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.