본문 바로가기

Study/실무

Clean Code 작성하기

“우리는 저자이다. 저자에게는 독자가 있다. 그리고 저자에게는 독자와 잘 소통해야할 책임이 있다.”

⌈Clean Code⌋의 저자, Robert C. Martin                       

 

설계 관점으로 클린코드를 이야기할 때 가장 많이 회자되는 것이 위의 SOLID 원칙 입니다.

 

 

 

 클린코드란?

클린 코드란 코드를 읽는 사람들이 이해하기 쉽도록 작성된 코드라고 정의할 수 있습니다.  

=>  유지 보수(코드 파악, 디버깅, 리뷰)의 시간을 단축할 수 있는 코드

 

 

 가독성이 중요한 이유?

일반적으로 기존 코드를 변경하고자 할 때, 해석하는 시간과 수정하는 비율이 10:1이라고 합니다. 

코드를 변경하는데 걸리는 전체 시간이 10시간이라고 하면,  코드를 분석하는 시간이 9시간 이상 걸린다는 말 입니다.

해석이 어려운 코드는 그만큼 분석하는 시간이 더 오래 걸리고 또한, 대부분의 결함은 기존 코드를 수정하는 동안에 발생한다고 합니다.

그러니 이해하기 쉬운 코드야말로 오류의 위험성을 최소화하는 셈입니다. 

 

 

 클린하지 못한 코드란?

1) 흐름 파악이 어렵다.

2) 맥락 표현이 안되어있다.

3) 동료(짠 사람)에게 물어봐야 알 수 있다.

 

 

 안일한 코드 추가의 함정

기능을 추가하게 될때, 기존의 코드를 파악하고, 간단하게 구현을 설계하고, 개발합니다.

PR로만 봤을때는 제대로 된 코드일 수 있습니다. 하지만 전체적으로 보게되면 이게 나쁜 코드일 수 있는데요.

 

- 하나의 목적인 코드가 흩뿌려져 있다. (관련된 로직들이 여기저기 흩어져 있다.)

- 하나의 함수가 여러 가지 일을 하고 있다. (받아오기, 수정하기, 전송하기 등 여러가지의 일을 한꺼번에 하고 있다.)

- 함수의 세부 구현 단계가 제각각이다. (비슷한 기능을 담당하고 있음에도 불구하고 내부 로직이 많이 다르다.)

 

이와 같이 추가,수정 또는 리팩토링을 할때는 다음과 같이 큰 그림을 보면서 해야합니다.

 

1.  함수 세부 구현 단계 통일 

2.  하나의 목적인 코드는 뭉쳐두기

3.  함수는 한 가지 일만 하도록 쪼개기

 

 

 실무에서 적용하기

1. 응집도 - 무엇을 뭉쳐야 하나?

1) 같은 목적의 코드는 뭉쳐두어야합니다.

여기저기 흩어져있는 코드는 파악이 한번에 안되고 버그 발생 위험도도 높기 때문 입니다.

 

2) 당장 몰라도 되는 디테일을 뭉쳐줍니다.

코드 파악에 필수적인 핵심정보를 제외하고, 당장 몰라도 되는 디테일(세부구현)을 뭉칩니다.

커스텀 훅에 세부구현을 숨겨놓고 핵심 데이터 들인 제목, 내용, 액션 등은 바깥에서 넘깁니다.

바깥에서 핵심 데이터들을 넘겨주면, 넘겨주는 데이터들만 봐도 이 커스텀 훅이 뭘 하는지 바로 파악할 수 있습니다.

이를 선언적 프로그래밍이라고 합니다. 

 

 

참고

선언적 프로그래밍의 특징

  1. 무엇을 하는 함수인지 빠르게 이해 가능
  2. 세부 구현은 내부에 뭉쳐 둠.
  3. 핵심 데이터만 바꿔서 재사용 가능.

[선언적 프로그래밍 예시]

<Popup
  onSubmit={회원가입}
  onSuccess={프로필로이동}
/>

 

 

명령형 프로그래밍의 특징

  • 로직을 하나하나 명령하듯 작성합니다.
  • 세부 구현이 모두 노출되어 있어서 커스텀하기 쉽습니다
  • 하지만 코드를 읽는데 오래 걸리고 재사용이 어렵다는 단점이 있습니다.

[명령형 프로그래밍 예시]

<Popup>
  <button onClick={async () => {
    const res = await 회원가입();
    if(res.success) {
      프로필로이동();
    }
 }}>전송</button>
</Popup>

 

2. 단일책임

하나의 일만 하는 뚜렷한 이름의 함수 또는 기능성 컴포넌트를 제작해야 합니다.

만약 하나의 함수에서 여러 개의 기능을 수행하고 있다면 가독성이 떨어지고 추후 유지 보수를 진행할 때 어려움을 겪습니다.

 

 

 

3. 추상화 - 핵심 개념을 뽑아라

추상화 단계

'얼마나 추상화할 것인가?'에 대해 고민이 필요합니다.

추상화 단계가 일관성이 있어야 세부 구현 단계가 제각각이지 않습니다.

만약 각 컴포넌트들 마다 세부 구현들이 다르다면 코드를 읽을 때 혼란스러울 수 있습니다.

특정 메시지를 띄우는 코드를 예시로 단계별 추상화를 해보겠습니다.

 

Level 0코드를 하나하나 적어줍니다.

<Button onClick={showConfirm}>
  전송
</Button>
{isShowConfirm && (
  <Confirm onClick={() => {showMessage('성공')}} />
)}

 

Level 1 -  함수와 메시지를 보내 메시지를 띄울 수 있습니다.

<ConfirmButton onConfirm={() => {showMessage('성공')}}>
  전송
</ConfirmButton>

 

Level 2메시지만 보내 메시지를 띄울수 있습니다.

<ConfirmButton message='성공'>
  전송
</ConfirmButton>

 

Level 3 - Button 아래에 모두 추상화합니다.

<ConfirmButton />

 

추상화를 하는 게 무조건 좋은 방법일까요?

그렇지 않습니다. 추상화 수준이 섞여있으면 코드를 읽을 때 혼란스러울 수 있습니다. 

성급한 추상화는 오히려 코드의 유연성이 떨어져 유지 보수할 때 리소스가 더 많이 필요할 수 있습니다.

코드의 유연성을 생각했을 때 추상화를 신중하게 해야 합니다.

코드의 중복이 어느 수준인지, 추후 어떻게 변화될 가능성이 있는지 잘 따져 봐야 합니다.

신중하게 고려한 뒤 높은 추상화 혹은 낮은 추상화 단계로 상황에 따라 일관성 있게 코드를 정리해야 합니다.

 

[추상화 예시]

React로 예를 들자면 모든 마크업을 작성하는게 아닌 컴포넌트로 제작하여 원하는 데이터만 넘겨줍니다.

상황에 따라 필요한 만큼만 추상화하시면 됩니다.

 

또한 추상화 수준은 비슷하게 하는 것이 좋습니다.

어지럽게 섞여있으면 가독성이 떨어지고 코드 파악이 어려워집니다.

꼭 높은 추상화로 모든 것을 정리할 필요 없이 상황에 따라 낮은 코드로도 정리가 가능합니다.

 

 

네이밍 컨벤션 

 

클래스(컴포넌트)  UpperCamelCase
함수와 변수 lowerCamelCase
상수 UPPER_DELIMITER_CASE
: 모두 대문자로 쓰고 언더바로 연결.
패키지와 모듈 lower-delimiter-version
: 모두 소문자를 쓰고 대쉬로 연결.

 

1) 네이밍컨벤션은 영어표기법을 상속받았다.

  1. 고유명사는 문장 어느 위치에 오든 첫글자를 대문자로쓴다. -  I went to [T]okyo.
  2. 이름 앞에 오는 직함은 첫글자를 대문자. - [D]octor [M]r. Micheal
  3. 책,신문,잡지,음악,영화등 제목에 나오는 첫단어와 마지막단어의 첫글자와 관사는 대문자를 쓴다. -  [M]arvel’s [T]he [A]vengers
  4. 출판물의 일부와 관련한 명사 다음에 숫자가 올때 명사의 첫글자를 대문자로 쓴다. - [S]ection 2
  5. 요일명, 휴일명, 달, 역사적 사건, 역사적 기간은 첫글자를 대문자로 쓴다. - [W]orld [W]ar [ll]
  6. 천체의 이름은 첫 글자를 대문자로 쓴다.  - It is the [M]ars

2) 복수형에 s 붙이기

배열(array)을 복수s로 나타내는 방법이있는데,

변수명userNames  와 같이 -s를 붙이면 쉽게 알아볼 수 있으나,

 

함수명 중간에 들어갈때는 s가 쉽게 눈에 띄지않는다.

ex) checkUserNamesExistsInDB()

 

함수명 중간에는 array listOf사용하는게 더 나을 수도 있다.

ex) checkUserNameArrayExistsInDB()

ex) checkListOfUserNameExistsInDB()

 

3) 중요한 단어를 앞에 쓴다.

변수이름을 여러 단어로 조합할 때는 순서를 잘 정해야한다.

검색등을 사용하여 찾아볼때, 중요한 것이 먼저 오는게 알아보기 쉽다.

 

나쁜예 좋은예
totalVisitor visitorTotal
totalRegister registerTotal
totalBuyer buyerTotal
totalSalesOfThisMonth salesOfThisMonthTotal

 

4) 발음하기 쉬운 이름을 사용하자

 

[안좋은 예]

const yyyymmdstr = moment().format('YYYY/MM/DD');

 

[좋은 예]

const currentDate = moment().format('YYYY/MM/DD');

 

5) 검색하기 쉬운 이름을 사용하자.

독자가 코드를 읽으면서 변수 이름을 자신이 아는 이름으로 변환해야 한다면 그 변수 이름은 바람직하지 못하다.

문자하나만 사용하는 변수이름은 문제가 있다.(반복루프제외)

 

안좋은 예 :

setTimeout(blastOff, 86400000); // 대체 86400000 무엇을 의미하는 걸까요?

좋은 예: 

// 대문자로 `const` 전역 변수를 선언하세요
const MILLISECONDS_IN_A_DAY = 86400000;
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);

 

6) 클래스이름과 객체이름은 명사나 명사구가 적합하다.

Customer, WikiPage, Account, AddressParser 등은 좋은예다.

Manager, Processor, Data, Info 같은 단어는 피하고 동사는 사용하지않는다.

 

7) 메서드이름은 동사나 동사구가 적합하다.

postPayment , deletePage, save 등이 좋은예다.

접근자, 변경자, 조건자는 값에따라 get set is를 붙일 수 있다.

 

8) 한개념에 한단어를 사용하자. 

똑같은 메서드를 클래스마다 fetch, retrieve, get 등으로 제각각 부르면 혼란스럽다.

 

9) 한단어를 두가지 목적으로 사용하지마라.

예를들어 지금까지 구현한 add는 모두가 기존값 두개를 더해서 새로운 값을 만드는데

새로 작성하는 메서드는 집합에 값하나를 더하는 것일때 add는 적절하지 않다.

append 나 insert 등을 사용하자.

 

10) 이름에 정보를 담아내자!

10-1. 특정한 단어 고르기

size 와 같은 단어보다는 보다 명확한 단어를 사용한다
ex) Height, NumNodes, MemoryBytes

 

10-2. 보편적인 이름 피하기(혹은 언제 이런 이름을 사용해야하는지 깨닫기)

const tmp = [];
tmp.push(user.name);
tmp.push(user.email);

와 같이 변수의 삶의 주기가 짧더라도 이럴때는 user_info와 같은 이름이 더 적절하다.

 

if(right<left){
   tmp=right;
  right=left;
  left=tmp;
}

 

하지만 두 변수를 서로 교환하는 다음과 같은 알고리즘에서는 tmp가 굉장히 적절하게 사용 될 수도 있다.

10-3. 추상적인 이름 대신 구체적인 이름 사용하기

 

10-4. 접두사 혹은 접미사로 이름에 추가적인 정보 붙이기

예를들어 const id = “af84ef845cd8” 과 같이 16진수 문자열을 담고있는 변수가 있다면 id 보다는 hex_id라는 이름이 더 좋다.
변수가 시간의 양이나 바이트의 수와 같은 측정치를 담고있다면 변수명에 단위를 포함시켜주는게 좋다.
ex) angle => degrees_cw,   limit => max_height,   size => size_mb

 

10-5. 추가적인 정보를 담을 수 있게 이름 구성하기

경계를 포함하는 한계값을 다룰 때는 min과 max를 사용하라.
한계를 설정하는 이름을 가장 명확하게 만드는 방법은 제한받는 대상의 이름 앞에 max나 min을 붙이는 것이다.

ex) MAX_LENGTH_STRING = 20;

 

11. 정확한 반대말 사용하기

show hide
visible invisible
header footer
under (미만) over (초과)
or under (이하) and over (이상)
before after
open close
input output
import export

 

12. 비슷한 단어 유의하여 사용하기

Stop

stop - 잠시 중단, 언제든 시작가능          <->    start or restart 사용 (재개)

end - 완전히 중단, 재시작 가능 x            <->    begin 사용 (새롭게시작)

finish - 끝장을 본 상태, 재시작 고려 x    <->   다시 시작하면 에러가 발생

pause - 아주 일시적 중단, 금방이라도 재시작 가능

suspend - 다음 단계의 시작을 중단한 것

hold - 어떤 의도를 가지고 중단한 것

 

get

retrieve - 검색해서 가져온다.

acquire - 다른함수가 가져가지 못하게 독점한다.

fetch - 현재 값을 가리키는 포인터가 다음 값으로 이동해 갖고온다.

 

create

create - 정해진 틀이 없으므로 먼저 틀을 만든다.

register - 이미 정해진 틀에 값을 집어넣는다.

 

change

change - 단순히 내용을 바꿈 

modify - 잘못된것을 바로잡음

revise - 기존에 없던 새로운 정보나 아이디어를 덧붙여 기존 내용과 달라졌음을 분명히 함

 

must

must - 필수 요구사항. 반드시 구현되야함   <->   must not - 절대 구현되면 안됨.

should - 권고 권장, 가능하면 구현   <->   should not - 구현하지 않으면 더 좋음.

 

 

 

주석 컨벤션

 

주석에서 널리 사용되는 표시
TODO 아직 하지 않은 일
FIXME 오동작을 일으킨다고 알려진 코드
HACK 아름답지 않은 해결책
XXX 주의! 여기 문제가 있다.

 

주석사용시 주의점
코드를 읽는 사람이 코드를 이해하게 돕는다.
코드를 읽는 사람의 입장에서 필요한 정보를 유추하기 
코딩을 수행하면서 머릿속에 있는 정보를 기록하기
주석은 간결하게하라. 쓸데없는 정보는 담지않는다. 
모호한 대명사는 피한다. (this, it, data 등)
함수의 동작을 명확하게 설명하라.
코드의 의도를 명시하자.

 

 

클린코드 규칙

1.  매직넘버 사용을 자제하자

매직넘버란? 소스코드안에 작성된 구체적인 수치값

 

임의의 숫자값을 사용하는 것은 코드파악에 어려움을 줍니다.

이를 변수로 선언 함으로써 코드파악의 어려움을 줄여줘야 합니다.

 

[안좋은 예]

if (password.length < 8) {
	// Display error message
}

for (i=0; i<7; i++){
	// Do something
}

setTimeout(someFunction, 86400);

 

[좋은 예]

const MIN_PASSWORD_LENGTH = 8;
if(password.length < MIN_PASSWORD_LENGTH){
	// Display error message
}

const DAYS_IN_A_WEEK = 7;
for (i=0; i < DAYS_IN_A_WEEK; i++){
	// Do something
}

const SECONDS_IN_A_DAY = 86400;
setTimeout(someFunction, SECONDS_IN_A_DAY)

 

 

2.  추가 컨텍스트를 피하자

어떠한 객체를 만들거나 메서드를 지정할 때, 그 객체의 속성이름에 대한 컨텍스트를 복사하지 않는게 가독성에 도움이 됩니다.

 

[안좋은 예]

const employee = {
	employeeName: 'John Doe',
    	employeeAge: 25,
    	employeeSalary: 80000
}

 

[좋은 예]

const employee = {
	name: 'John Doe',
    	age: 25,
    	salary: 80000
}

 

 

3.  디폴트 인수를 사용하는 팁

[ES6 이전의 Default 값을 사용하는 예시]

인수로 0이 전달 됐을 경우 0이 적용이 되지 않는 등 예기치 않은 오류를 생성.

function calculateDiscount(discount){
	const minDiscount = discount || 10
    	console.log(minDiscount);
}

calculateDiscount(0);  // 10

 

[좋은예]

function calculateDiscount(discount = 10){
	const minDiscount = discount
    	console.log(minDiscount);
}

calculateDiscount(0); // 0

 

 

4.  함수인자 수의 제한

함수에 전달할 수 있는 매개변수의 수를 제한하자.

 

함수가 두개이상의 매개변수를 받는 경우 옵션 객체로 전달하자. *ES6의 비구조화(destructuring) 구문

 

매개변수의 개수를 제한 하는 것은 함수 테스팅을 쉽게 만들어 주기 때문에 중요합니다.

만약 매개변수가 3개 이상일 경우엔 테스트 해야하는 경우의 수가 많아지고 각기 다른 인수들로 여러 사례들을 테스트 해야합니다.

만약 2개 이상의 인자를 가진 함수를 사용한다면 그 함수에게 너무 많은 역할을 하게 만든 것입니다.

 

[안좋은 예]

function createMenu(title, body, buttonText, cancellable) {
  // ...
}

 

[좋은 예]

function createMenu({ title, body, buttonText, cancellable }) {
  // ...
}

 

 

5.  한개의 함수는 한가지 일만 하도록 하자.

함수가 1개 이상의 행동을 한다면 작성하는 것도, 테스트하는 것도, 이해하는 것도 어려워집니다.

하나의 함수에 하나의 행동을 정의하는 것이 가능해진다면 함수는 좀 더 고치기 쉬워지고 코드들은 읽기 쉬워질 것입니다. 

 

[안좋은 예]

function emailClients(clients) {
  clients.forEach(client => {
    const clientRecord = database.lookup(client);
    if (clientRecord.isActive()) {
      email(client);
    }
  });
}

 

[좋은 예]

function emailClients(clients) {
  clients
    .filter(isClientActive)
    .forEach(email);
}

function isClientActive(client) {
  const clientRecord = database.lookup(client);
  return clientRecord.isActive();
}

 

 

6.  함수에 Boolean 변수를 전달하는것을 피하자.

함수 안에서 분기처리를 하지 말자!

 

함수에 boolean값을 넣어 함수안에서 분기처리를 한다면, 위 4번과 5번 규칙에 위배됩니다.

함수에 전달해야하는 인수의 양이 많아지고, 함수가 여러가지 일을 하게되므로 분기처리를 해야하는 함수를 나눠주는게 좋습니다.

 

[안좋은 예]

function getItemCost(itemCost, isMemeber){
	const MEMBER_DISCOUNT = 0.30;
	const NORMAL_DISCOUNT = 0.10;
    
    if(isMember){
    	const = itemCost * (1 - MEMBER_DISCOUNT)
    } else{
    	const = itemCost * (1 - NORMAL_DISCOUNT)
    }
    return cost;
}

 

[좋은 예]

function getItemCost(itemCost){
	const NORMAL_DISCOUNT = 0.10;
    cost = itemCost * (1 - NORMAL_DISCOUNT)
    return cost;
}

function getItemCostForMember(itemCost){
	const MEMBER_DISCOUNT = 0.30;
    cost = itemCost * (1 - MEMBER_DISCOUNT)
    return cost;
}

 

 

7.  조건문의 캡슐화

조건문에 또다른 조건을 넣지말자.

 

함수안에서 캡슐화를 진행하고 그것으로 조건문을 실행하는게 코드의 가독성 향상에 도움을 줍니다.

 

[안좋은 예]

if (status === "loading" && isEmpty(productList)) {
	// ...Rest of the code
}

 

[좋은 예]

function shouldDisplayLoader(status, productList) {
	return status === "loading" && isEmpty(productList)
}

if (shouldDisplayLoader) {
	// ...Rest of the code
}

 

 

8.  축약형 사용의 자제

무분별한 축약형 네이밍은 가독성과 검색에 어려움을 줍니다.

 

document => doc,   string=>str,   button=>btn 같은 흔히 쓰는 약어가 아닌

본인이 마음대로 축약해서 적는다면 다른사람들이 그 뜻을 이해하기 굉장히 힘들어진다.

 

 

[안좋은 예]

const curColor = 'blue'
function sndNotif() {}
function onBtnClk() {}

 

[좋은 예]

const currentColor = 'blue'
function sendNotification() {}
function onButtonClick() {}

 

 

9. 조건문은 가급적 if/else를 사용한다. 삼항연산자는 매우 간단할 때만사용하자.

[좋은 예]

if(exponent >= 0) {
	return mantissa * (1<<exponent);
} else {
	return mantissa / (1<<exponent);
}

 

[안좋은 예]

return exponent >= 0 ? mantissa * (1<<exponent):mantissa / (1<<-exponent);

 

삼항연산자는 아래와 같이 간결한 조건문일 때만 사용한다.

줄수를 최소화 하는 것보다 다른사람이 코드를 읽고 이해하는데 걸리는 시간을 최소화해야한다.

if (hour >= 12) {
	time_str += “pm”;
} else {
	time_str += “am”;
}
time_str += (hour>=12) ? “pm”:”am”

 

 

10) 거대한 표현은 작은 조각으로나눈다.

 

11) 드모르간의 법칙을 사용한다

if(!(a&&!b) 를 if(!a||b)로 사용한다 (not을 분배하고 or과 and를 치환한다).

 

12) 변수

1) 변수의 수가 많을수록  =>  결과를 즉시 처리하는 방식으로 중간 결과값을 저장하는 변수를 제거하면 좋다.

2) 변수의 범위가 넓어질수록  =>  전역변수를 최대한 줄여라 => 가능하다면 지역변수로 강등

3) 변수의 값이 자주 바뀔수록

이해하고, 기억하기 어렵다.

 

13) 분량이 적다고 항상 좋지는 않다.

assert((!(bucket = FindBucket(key))) || !bucket=>IsOccupied())); 

 

bucket = FindBucket(key);
if (bucket != Null) assert (!bucket => IsOccupied());

 

다음과 같이 한줄이 두줄짜리 코드보다 이해하는데 더 오랜 시간이 걸릴 수도 있다.

마찬가지로 주석 또한 코드를 더 빨리 이해하게 도와주기도 한다.

 

 

14) 개념은 빈 행으로 분리하자

패키지선언부, import 문, 각 함수사이에 빈행을 넣는다.

빈 행은 새로운 개념을 시작한다는 시각적 단서다.

 

세로밀집도

세로밀집도는 연관성을 의미한다. 즉, 서로 밀접한 코드행은 세로로 가까이 놓아야 한다는 뜻이다.

 

변수선언

변수는 사용하는 위치에 최대한 가까이 선언한다.  지역변수는 각 함수 맨 처음에 선언한다.

 

인스턴스변수

인스턴스변수는 클래스 맨 처음에 선언한다.

 

변수간에는 세로로 거리를 두지 않는다.

 

15) 함수

함수명은 함수가 무엇을 하는지 알 수 있어야 합니다

 

[안좋은 예]

function AddToDate(date, month) {
  // ...
}

const date = new Date();

// 뭘 추가하는 건지 이름만 보고 알아내기 힘듭니다.
AddToDate(date, 1);

 

[좋은 예]

function AddMonthToDate(date, month) {
  // ...
}

const date = new Date();
AddMonthToDate(date, 1);

 

 

클린 코드, 도전하기!

  1. 기존의 코드를 두려워하지 말고 담대하게 수정하기.
  2. 큰 그림 보는 연습을 자주 해봅니다. 기능 추가로만 보면 클린 코드로 보일지 몰라도, 전체적으로보면 엉망인 코드가 될 수 있습니다.
  3. 팀원들과 공감대를 형성해야 합니다. 코드에 정답은 없기 때문에 팀원과 함께 소통하며 발전합니다.
  4. 문서화를 해야 합니다. '현재 코드의 문제점이 무엇인지', '어떻게 개선할 수 있는지'에 대해 작성해 봅시다.

 

 

 

 

https://www.youtube.com/watch?v=vPXzVNmCPg4

https://www.youtube.com/watch?v=edWbHp_k_9Y

https://toss.im/slash-21/sessions/3-3

https://github.com/qkraudghgh/clean-code-javascript-ko

cleancode

읽기 좋은 코드가 좋은 코드다

개발자의 글쓰기

'Study > 실무' 카테고리의 다른 글

Docker  (0) 2023.01.05
styled-components svg 스타일링하기  (0) 2023.01.05
Jira 와 Github 연동하기  (0) 2022.11.10
CSS - boarder gradation  (0) 2022.11.02
CSS aspect-ratio - 가로 세로비율 조정하기  (0) 2022.10.27