클린 코드에 관한 내 책 "코드 세척"에서 발췌한 내용을 읽고 계십니다. PDF, EPUB, 단행본, Kindle 에디션으로 제공됩니다. 지금 사본을 받으세요.
코드를 모듈이나 함수로 구성하는 방법과 코드를 복제하는 대신 추상화를 도입해야 할 적절한 시기를 아는 것은 중요한 기술입니다. 다른 사람들이 효과적으로 사용할 수 있는 일반적인 코드를 작성하는 것은 또 다른 기술입니다. 코드를 함께 유지해야 하는 이유만큼이나 코드를 분할해야 하는 이유도 많습니다. 이 장에서는 이러한 이유 중 일부에 대해 논의하겠습니다.
우리 개발자들은 같은 일을 두 번 하는 것을 싫어합니다. DRY는 많은 사람들에게 만트라입니다. 그러나 동일한 작업을 수행하는 코드 조각이 2~3개 있는 경우 아무리 매력적이라고 느껴지더라도 추상화를 도입하기에는 아직 너무 이르습니다.
정보: Don't Repeat yourself(DRY) 원칙은 "모든 지식이 시스템 내에서 단일하고 명확하며 권위 있는 표현을 가져야 함"을 요구하며, 이는 종종 다음과 같이 해석됩니다. 모든 코드 중복은 엄격히 금지됩니다.
한동안 코드복제의 고통을 안고 살아갑니다. 어쩌면 결국 그렇게 나쁘지 않을 수도 있고 코드가 실제로 정확히 동일하지 않을 수도 있습니다. 일정 수준의 코드 중복은 건전하며 문제가 발생할 염려 없이 코드를 더 빠르게 반복하고 발전시킬 수 있습니다.
몇 가지 사용 사례만 고려하면 좋은 API를 찾기도 어렵습니다.
많은 개발자와 팀이 참여하는 대규모 프로젝트에서 공유 코드를 관리하는 것은 어렵습니다. 한 팀의 새로운 요구 사항이 다른 팀에서는 작동하지 않아 코드가 깨질 수도 있고, 수십 가지 조건을 지닌 유지 관리가 불가능한 스파게티 괴물이 될 수도 있습니다.
A팀이 페이지에 이름, 메시지, 제출 버튼 등 댓글 양식을 추가한다고 상상해 보세요. 그런 다음 B팀에는 피드백 양식이 필요하므로 A팀의 구성 요소를 찾아서 재사용하려고 합니다. 그런 다음 A팀도 이메일 필드를 원하지만 B팀이 해당 구성 요소를 사용한다는 사실을 모르기 때문에 필수 이메일 필드를 추가하고 B팀 사용자를 위한 기능을 중단합니다. 그런 다음 B팀에는 전화번호 필드가 필요하지만 A팀이 이 필드 없이 구성요소를 사용하고 있다는 것을 알고 전화번호 필드를 표시하는 옵션을 추가합니다. 1년 후, 두 팀은 서로의 코드를 깨뜨렸다는 이유로 서로를 미워했고, 컴포넌트는 조건이 가득 차 유지가 불가능했다. 입력 필드나 버튼과 같이 하위 수준의 공유 구성 요소로 구성된 별도의 구성 요소를 유지한다면 두 팀 모두 많은 시간을 절약하고 서로 건강한 관계를 유지할 수 있을 것입니다.
팁: 코드가 설계되고 공유된 것으로 표시되지 않는 한 다른 팀이 코드를 사용하지 못하도록 금지하는 것이 좋습니다. 종속성 크루저(Dependency Cruiser)는 이러한 규칙을 설정하는 데 도움이 될 수 있는 도구입니다.
추상화를 롤백해야 하는 경우도 있습니다. 조건과 옵션을 추가하기 시작할 때 우리는 스스로에게 질문해야 합니다. 이것이 여전히 동일한 것의 변형인가 아니면 분리되어야 하는 새로운 것입니까? 모듈에 조건과 매개변수를 너무 많이 추가하면 API 사용이 어려워지고 코드 유지 관리 및 테스트가 어려워질 수 있습니다.
잘못된 추상화보다 중복이 더 저렴하고 건강합니다.
정보: 자세한 설명은 Sandi Metz의 기사 The Wrong Abstraction을 참조하세요.
코드 수준이 높을수록 추상화하기 전에 기다려야 하는 시간이 길어집니다. 낮은 수준의 유틸리티 추상화는 비즈니스 로직보다 훨씬 명확하고 안정적입니다.
코드 재사용은 코드 조각을 별도의 함수나 모듈로 추출하는 유일하거나 가장 중요한 이유는 아닙니다.
코드 길이는 모듈이나 함수를 분할해야 할 때 측정 기준으로 자주 사용되지만 크기만으로는 코드를 읽거나 유지 관리하기 어렵게 만들지 않습니다.
긴 알고리즘이라 할지라도 선형 알고리즘을 여러 함수로 분할한 다음 이를 차례로 호출하면 코드 가독성이 높아지는 경우가 거의 없습니다. 함수(그리고 훨씬 더 많은 파일) 사이를 이동하는 것은 스크롤보다 어렵습니다. 코드를 이해하기 위해 각 함수의 구현을 조사해야 한다면 추상화가 올바른 것이 아닙니다.
정보: Egon Elbre는 코드 가독성 심리학에 관한 훌륭한 기사를 썼습니다.
다음은 Google 테스트 블로그에서 가져온 예입니다.
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
Pizza 클래스의 API에 대해 궁금한 점이 너무 많은데, 저자가 어떤 개선 사항을 제안하는지 살펴보겠습니다.
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
이미 복잡하고 복잡했던 것이 이제는 더욱 복잡해지고 코드의 절반은 함수 호출뿐입니다. 이렇게 하면 코드를 더 쉽게 이해할 수는 없지만 작업하기가 거의 불가능해집니다. 이 기사에서는 리팩터링된 버전의 전체 코드를 보여주지 않습니다. 아마도 요점을 더욱 설득력 있게 만들기 위함일 것입니다.
Pierre “catwell” Chapuis는 자신의 블로그 게시물에서 새로운 기능 대신 댓글을 추가하라고 제안합니다.
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
이미 분할 버전보다 훨씬 좋습니다. 더 나은 솔루션은 API를 개선하고 코드를 더 명확하게 만드는 것입니다. Pierre는 오븐을 예열하는 것이 createPizza() 함수의 일부가 되어서는 안 된다고 제안합니다(그리고 직접 많은 피자를 굽는 것도 전적으로 동의합니다!) 왜냐하면 실제 생활에서는 오븐이 이미 거기에 있고 아마도 이전 피자보다 이미 뜨거울 것이기 때문입니다. Pierre는 또한 이 함수가 피자가 아닌 상자를 반환해야 한다고 제안합니다. 왜냐하면 원래 코드에서는 모든 슬라이싱 및 포장 마법 후에 상자가 사라지고 우리는 결국 얇게 썬 피자를 손에 쥐게 되기 때문입니다.
문제를 코딩하는 방법이 다양한 것처럼 피자를 요리하는 방법도 다양합니다. 결과는 동일해 보일 수 있지만 일부 솔루션은 다른 솔루션보다 이해, 수정, 재사용 및 삭제가 더 쉽습니다.
추출된 모든 함수가 동일한 알고리즘의 일부인 경우 이름 지정도 문제가 될 수 있습니다. 코드보다 명확하고 주석보다 짧은 이름을 만들어야 하는데 이는 쉬운 일이 아닙니다.
정보: 주석 방지 장에서는 코드 주석 달기에 대해 설명하고 이름 지정은 어렵다 장에서 이름 지정에 대해 설명합니다.
제 코드에서는 작은 함수를 많이 찾을 수 없을 것입니다. 제 경험상 코드를 분할하는 가장 유용한 이유는 변경 빈도와 변경 이유입니다.
주파수 변경부터 시작해 보겠습니다. 비즈니스 로직은 유틸리티 기능보다 훨씬 더 자주 변경됩니다. 자주 변경되는 코드와 매우 안정적인 코드를 분리하는 것이 합리적입니다.
이 장의 앞부분에서 논의한 댓글 형식은 전자의 예입니다. camelCase 문자열을 kebab-case로 변환하는 함수는 후자의 예입니다. 의견 양식은 시간이 지남에 따라 새로운 비즈니스 요구 사항이 발생할 때 변경되고 다양해질 수 있습니다. 대소문자 변환 기능은 전혀 변경될 가능성이 없으며 여러 곳에 재사용해도 안전합니다.
일부 데이터를 표시하기 위해 멋진 테이블을 만들고 있다고 상상해 보세요. 이 테이블 디자인이 다시는 필요하지 않을 것이라고 생각할 수도 있으므로 테이블의 모든 코드를 단일 모듈에 유지하기로 결정했습니다.
다음 스프린트에는 테이블에 다른 열을 추가하는 작업이 있으므로 기존 열의 코드를 복사하고 거기에서 몇 줄을 변경합니다. 다음 스프린트에서는 동일한 디자인의 다른 테이블을 추가해야 합니다. 다음 스프린트에는 테이블 디자인을 바꿔야 하는데…
우리 테이블 모듈에는 변경 이유 또는 책임
이 세 가지 이상 있습니다.이로 인해 모듈을 이해하기가 더 어려워지고 변경하기가 더 어려워집니다. 표현 코드는 많은 장황함을 추가하여 비즈니스 논리를 이해하기 어렵게 만듭니다. 책임을 변경하려면 더 많은 코드를 읽고 수정해야 합니다. 이로 인해 둘 중 하나를 반복하는 것이 더 어렵고 느려집니다.
일반 테이블을 별도의 모듈로 사용하면 이 문제가 해결됩니다. 이제 테이블에 다른 열을 추가하려면 두 모듈 중 하나만 이해하고 수정하면 됩니다. 공개 API를 제외하고 일반 테이블 모듈에 대해 아무것도 알 필요가 없습니다. 모든 테이블의 디자인을 변경하려면 일반 테이블 모듈의 코드만 변경하면 되며 개별 테이블을 전혀 건드릴 필요가 없을 것입니다.
그러나 문제의 복잡성에 따라 모놀리식 접근 방식으로 시작하고 나중에 추상화를 추출하는 것이 괜찮고 더 나은 경우가 많습니다.
코드 재사용도 코드를 분리하는 타당한 이유가 될 수 있습니다. 한 페이지에서 일부 구성 요소를 사용하면 곧 다른 페이지에서 필요할 가능성이 높습니다.
모든 기능을 자체 모듈로 추출하고 싶을 수도 있습니다. 그러나 단점도 있습니다:
저는 모듈 시작 부분에 하나의 모듈에서만 사용되는 작은 기능을 유지하는 것을 선호합니다. 이렇게 하면 동일한 모듈에서 사용하기 위해 가져올 필요가 없지만 다른 곳에서 재사용하는 것은 불편할 것입니다.
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
위 코드에는 이 모듈에서만 사용되는 구성 요소(FormattedAddress)와 함수(getMapLink())가 있으므로 파일 상단에 정의되어 있습니다.
이러한 기능을 테스트해야 한다면(그래야 합니다!) 모듈에서 내보내어 모듈의 기본 기능과 함께 테스트할 수 있습니다.
특정 기능이나 구성요소와 함께만 사용하도록 의도된 기능에도 동일하게 적용됩니다. 동일한 모듈에 유지하면 모든 기능이 함께 속한다는 것이 더 명확해지고 이러한 기능을 더 쉽게 검색할 수 있습니다.
또 다른 이점은 모듈을 삭제할 때 해당 종속 항목도 자동으로 삭제된다는 것입니다. 공유 모듈의 코드는 코드베이스에 영원히 남아 있는 경우가 많습니다. 왜냐하면 아직 사용되고 있는지 알기 어렵기 때문입니다(TypeScript를 사용하면 이 작업이 더 쉬워집니다).
정보: 이러한 모듈을 딥 모듈이라고도 합니다. 복잡한 문제를 캡슐화하지만 간단한 API를 포함하는 상대적으로 큰 모듈입니다. 딥 모듈의 반대는 얕은 모듈입니다. 즉, 서로 상호 작용해야 하는 많은 작은 모듈입니다.
여러 모듈이나 기능을 동시에 변경해야 하는 경우가 많으면 단일 모듈이나 기능으로 병합하는 것이 더 나을 수 있습니다. 이러한 접근 방식을 코로케이션이라고도 합니다.
다음은 코로케이션의 몇 가지 예입니다.
코로케이션에 따라 파일 트리가 어떻게 변경되는지는 다음과 같습니다.
Separated | Colocated |
---|---|
React components | |
src/components/Button.tsx | src/components/Button.tsx |
styles/Button.css | |
Tests | |
src/util/formatDate.ts | src/util/formatDate.ts |
tests/formatDate.ts | src/util/formatDate.test.ts |
Ducks | |
src/actions/feature.js | src/ducks/feature.js |
src/actionCreators/feature.js | |
src/reducers/feature.js |
정보: 코로케이션에 대해 자세히 알아보려면 Kent C. Dodds의 기사를 읽어보세요.
코로케이션에 대한 일반적인 불만은 구성 요소가 너무 크다는 것입니다. 이러한 경우 마크업, 스타일, 로직과 함께 일부 부분을 자체 구성 요소로 추출하는 것이 좋습니다.
코로케이션 개념은 관심사 분리와도 충돌합니다. 이는 웹 개발자가 HTML, CSS 및 JavaScript를 별도의 파일에(종종 파일 트리의 별도 부분에) 보관하게 만든 구식 아이디어입니다. 너무 길어서 웹페이지에 가장 기본적인 변경 사항을 적용하려면 세 개의 파일을 동시에 편집해야 합니다.
정보: 변경 이유는 "모든 모듈, 클래스 또는 함수는 기능의 단일 부분에 대해 책임을 져야 한다"는 단일 책임 원칙이라고도 합니다. 소프트웨어에서 제공하며 그 책임은 전적으로 수업에 포함되어야 합니다.”
가끔 사용하기 어렵거나 오류가 발생하기 쉬운 API를 사용하여 작업해야 하는 경우도 있습니다. 예를 들어, 특정 순서로 여러 단계가 필요하거나 항상 동일한 여러 매개변수를 사용하여 함수를 호출해야 합니다. 이것이 우리가 항상 올바르게 수행할 수 있도록 유틸리티 함수를 만드는 좋은 이유입니다. 보너스로 이제 이 코드에 대한 테스트를 작성할 수 있습니다.
URL, 파일 이름, 대소문자 변환, 형식 지정과 같은 문자열 조작은 추상화의 좋은 후보입니다. 아마도 우리가 하려는 작업에 대한 라이브러리가 이미 있을 것입니다.
다음 예를 고려해보세요.
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
이 코드가 파일 확장자를 제거하고 기본 이름을 반환한다는 것을 깨닫는 데는 시간이 걸립니다. 불필요하고 읽기 어려울 뿐만 아니라 확장자가 항상 3자로 가정하는데 그렇지 않을 수도 있습니다.
내장 Node.js의 경로 모듈인 라이브러리를 사용하여 다시 작성해 보겠습니다.
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
이제 무슨 일이 일어나고 있는지 명확하고, 마법의 숫자도 없으며, 어떤 길이의 파일 확장자에서도 작동합니다.
추상화의 다른 후보로는 날짜, 장치 기능, 양식, 데이터 유효성 검사, 국제화 등이 있습니다. 새로운 유틸리티 함수를 작성하기 전에 기존 라이브러리를 찾아보는 것이 좋습니다. 우리는 종종 단순해 보이는 기능의 복잡성을 과소평가합니다.
다음은 이러한 라이브러리의 몇 가지 예입니다.
때때로 우리는 코드를 단순화하지도, 더 짧게 만들지도 않는 추상화를 만들곤 합니다.
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
또 다른 예:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
이러한 경우에 우리가 할 수 있는 최선의 방법은 강력한 인라인 리팩토링을 적용하는 것입니다. 즉, 각 함수 호출을 해당 본문으로 대체합니다. 추상화도 문제 없습니다.
첫 번째 예는 다음과 같습니다.
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
두 번째 예는 다음과 같습니다.
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
결과가 더 짧고 가독성이 더 높아졌습니다. 이제 집에서 만든 추상화 없이 JavaScript 기본 기능을 사용하므로 독자는 이러한 기능이 무엇인지 추측할 필요가 없습니다.
많은 경우에는 약간의 반복이 좋습니다. 다음 예를 고려해보세요:
function FormattedAddress({ address, city, country, district, zip }) { return [address, zip, district, city, country] .filter(Boolean) .join(', '); } function getMapLink({ name, address, city, country, zip }) { return `https://www.google.com/maps/?q=${encodeURIComponent( [name, address, zip, city, country].filter(Boolean).join(', ') )}`; } function ShopsPage({ url, title, shops }) { return ( <PageWithTitle url={url} title={title}> <Stack as="ul" gap="l"> {shops.map(shop => ( <Stack key={shop.name} as="li" gap="m"> <Heading level={2}> <Link href={shop.url}>{shop.name}</Link> </Heading> {shop.address && ( <Text variant="small"> <Link href={getMapLink(shop)}> <FormattedAddress {...shop} /> </Link> </Text> )} </Stack> ))} </Stack> </PageWithTitle> ); }
완전히 괜찮아 보이고 코드 검토 중에 어떤 질문도 제기되지 않을 것입니다. 그러나 이러한 값을 사용하려고 하면 자동 완성에서는 실제 값 대신 숫자만 표시합니다(그림 참조). 이로 인해 올바른 값을 선택하기가 더 어려워집니다.
baseSpacing 상수를 인라인할 수 있습니다.
const file = 'pizza.jpg'; const prefix = file.slice(0, -4); // → 'pizza'
이제 코드가 적어 이해하기 쉽고 자동 완성 기능이 실제 값을 표시합니다(그림 참조). 그리고 이 코드는 자주 변경되지 않을 것 같습니다. 아마도 절대 변경되지 않을 것입니다.
양식 유효성 검사 기능에서 발췌한 다음 내용을 고려하세요.
const file = 'pizza.jpg'; const prefix = path.parse(file).name; // → 'pizza'
여기에서 무슨 일이 일어나고 있는지 파악하기가 매우 어렵습니다. 유효성 검사 논리가 오류 메시지와 혼합되어 있고 많은 확인이 반복됩니다...
이 기능을 여러 부분으로 나눌 수 있으며 각 부분은 한 가지만 담당합니다.
검증을 배열로 선언적으로 설명할 수 있습니다.
// my_feature_util.js const noop = () => {}; export const Utility = { noop // Many more functions… }; // MyComponent.js function MyComponent({ onClick }) { return <button onClick={onClick}>Hola!</button>; } MyComponent.defaultProps = { onClick: Utility.noop };
각 유효성 검사 함수와 유효성 검사를 실행하는 함수는 매우 일반적이므로 이를 추상화하거나 타사 라이브러리를 사용할 수 있습니다.
이제 어떤 필드에 어떤 유효성 검사가 필요한지, 특정 검사가 실패할 경우 어떤 오류를 표시할지 설명하여 모든 양식에 유효성 검사를 추가할 수 있습니다.
정보: 전체 코드와 이 예에 대한 자세한 설명은 조건 방지 장을 참조하세요.
저는 이 과정을 '무엇'과 '어떻게'의 분리라고 부릅니다.
이점은 다음과 같습니다.
많은 프로젝트에는 개발자가 더 나은 위치를 찾을 수 없을 때 유틸리티 기능을 추가하는 utils.js, helpers.js 또는 misc.js라는 파일이 있습니다. 종종 이러한 기능은 다른 곳에서는 재사용되지 않고 유틸리티 파일에 영원히 남아 있으므로 계속해서 증가합니다. 그렇게 몬스터 유틸리티 파일이 탄생합니다.
Monster 유틸리티 파일에는 몇 가지 문제가 있습니다.
제 경험 법칙은 다음과 같습니다.
JavaScript 모듈에는 두 가지 유형의 내보내기가 있습니다. 첫 번째는 명명된 수출입니다:
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
다음과 같이 가져올 수 있습니다.
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
두 번째는 기본 내보내기입니다.
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
다음과 같이 가져올 수 있습니다.
function FormattedAddress({ address, city, country, district, zip }) { return [address, zip, district, city, country] .filter(Boolean) .join(', '); } function getMapLink({ name, address, city, country, zip }) { return `https://www.google.com/maps/?q=${encodeURIComponent( [name, address, zip, city, country].filter(Boolean).join(', ') )}`; } function ShopsPage({ url, title, shops }) { return ( <PageWithTitle url={url} title={title}> <Stack as="ul" gap="l"> {shops.map(shop => ( <Stack key={shop.name} as="li" gap="m"> <Heading level={2}> <Link href={shop.url}>{shop.name}</Link> </Heading> {shop.address && ( <Text variant="small"> <Link href={getMapLink(shop)}> <FormattedAddress {...shop} /> </Link> </Text> )} </Stack> ))} </Stack> </PageWithTitle> ); }
기본 내보내기에는 아무런 이점이 없지만 몇 가지 문제가 있습니다.
정보: 기타 기술 장의 Greppable 코드 작성 섹션에서 Greppability에 대해 자세히 설명합니다.
안타깝게도 React.lazy()와 같은 일부 타사 API에는 기본 내보내기가 필요하지만 다른 모든 경우에는 명명된 내보내기를 고수합니다.
배럴 파일은 여러 다른 모듈을 다시 내보내는 모듈(일반적으로 index.js 또는 index.ts라고 함)입니다.
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
가장 큰 장점은 깨끗한 수입품입니다. 각 모듈을 개별적으로 가져오는 대신:
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
배럴 파일에서 모든 구성 요소를 가져올 수 있습니다.
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
그러나 배럴 파일에는 몇 가지 문제가 있습니다.
정보: TkDodo는 배럴 파일의 단점을 아주 자세하게 설명합니다.
배럴 파일의 장점은 사용을 정당화하기에는 너무 미미하므로 사용하지 않는 것이 좋습니다.
제가 특히 싫어하는 배럴 파일 유형 중 하나는 단일 구성 요소를 ./comComponents/button/button 대신 ./comComponents/button으로 가져올 수 있도록 내보내는 파일입니다.
DRYers(코드를 절대 반복하지 않는 개발자)를 트롤링하기 위해 누군가 WET, 모든 것을 두 번 작성 또는 타이핑을 즐깁니다라는 다른 용어를 만들었습니다. 추상화로 대체할 때까지 적어도 두 번은요. 농담이고 그 아이디어에 전적으로 동의하지는 않지만(때로는 일부 코드를 두 번 이상 복제해도 괜찮습니다) 모든 좋은 것에는 적당함이 가장 좋다는 점을 상기시켜 줍니다.
다음 예를 고려해보세요.
function createPizza(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); if (order.kind === 'Veg') { pizza.toppings = vegToppings; } else if (order.kind === 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
이것은 코드 건조의 극단적인 예이며, 특히 대부분의 상수가 한 번만 사용되는 경우 코드를 더 읽기 쉽거나 유지 관리하기 쉽게 만들지 않습니다. 여기서 실제 문자열 대신 변수 이름을 보는 것은 도움이 되지 않습니다.
이러한 모든 추가 변수를 인라인해 보겠습니다. (안타깝게도 Visual Studio Code의 인라인 리팩토링은 개체 속성 인라인을 지원하지 않으므로 수동으로 수행해야 합니다.)
function prepare(order) { const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); addToppings(pizza, order.kind); return pizza; } function addToppings(pizza, kind) { if (kind === 'Veg') { pizza.toppings = vegToppings; } else if (kind === 'Meat') { pizza.toppings = meatToppings; } } function bake(pizza) { const oven = new Oven(); heatOven(oven); bakePizza(pizza, oven); } function heatOven(oven) { if (oven.temp !== cookingTemp) { while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } } function bakePizza(pizza, oven) { if (!pizza.baked) { oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } } function pack(pizza) { const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(pizza.size); pizza.ready = box.close(); } function createPizza(order) { const pizza = prepare(order); bake(pizza); pack(pizza); return pizza; }
이제 코드가 훨씬 줄어들었고, 진행 상황을 더 쉽게 이해하고 테스트를 업데이트하거나 삭제하기가 더 쉬워졌습니다.
저는 테스트에서 절망적인 추상화를 너무 많이 만났습니다. 예를 들어 다음 패턴은 매우 일반적입니다.
function createPizza(order) { // Prepare pizza const pizza = new Pizza({ base: order.size, sauce: order.sauce, cheese: 'Mozzarella' }); // Add toppings if (order.kind == 'Veg') { pizza.toppings = vegToppings; } else if (order.kind == 'Meat') { pizza.toppings = meatToppings; } const oven = new Oven(); if (oven.temp !== cookingTemp) { // Heat oven while (oven.temp < cookingTemp) { time.sleep(checkOvenInterval); oven.temp = getOvenTemp(oven); } } if (!pizza.baked) { // Bake pizza oven.insert(pizza); time.sleep(cookTime); oven.remove(pizza); pizza.baked = true; } // Box and slice const box = new Box(); pizza.boxed = box.putIn(pizza); pizza.sliced = box.slicePizza(order.size); pizza.ready = box.close(); return pizza; }
이 패턴은 각 테스트 사례에서 mount(...) 호출이 반복되는 것을 방지하려고 시도하지만 테스트가 필요 이상으로 혼란스러워집니다. mount() 호출을 인라인해 보겠습니다.
function FormattedAddress({ address, city, country, district, zip }) { return [address, zip, district, city, country] .filter(Boolean) .join(', '); } function getMapLink({ name, address, city, country, zip }) { return `https://www.google.com/maps/?q=${encodeURIComponent( [name, address, zip, city, country].filter(Boolean).join(', ') )}`; } function ShopsPage({ url, title, shops }) { return ( <PageWithTitle url={url} title={title}> <Stack as="ul" gap="l"> {shops.map(shop => ( <Stack key={shop.name} as="li" gap="m"> <Heading level={2}> <Link href={shop.url}>{shop.name}</Link> </Heading> {shop.address && ( <Text variant="small"> <Link href={getMapLink(shop)}> <FormattedAddress {...shop} /> </Link> </Text> )} </Stack> ))} </Stack> </PageWithTitle> ); }
게다가 beforeEach 패턴은 각 테스트 케이스를 동일한 값으로 초기화하려는 경우에만 작동하며, 이런 경우는 거의 없습니다.
const file = 'pizza.jpg'; const prefix = file.slice(0, -4); // → 'pizza'
React 구성요소를 테스트할 때 일부 중복을 피하기 위해 저는 종종 defaultProps 객체를 추가하고 각 테스트 케이스 내부에 분산시킵니다.
const file = 'pizza.jpg'; const prefix = path.parse(file).name; // → 'pizza'
이렇게 하면 중복이 많지 않지만 동시에 각 테스트 사례가 격리되어 읽을 수 있습니다. 이제 각 테스트 케이스의 고유한 속성을 더 쉽게 확인할 수 있으므로 테스트 케이스 간의 차이점이 더 명확해졌습니다.
동일한 문제를 더욱 극단적으로 변형한 사례는 다음과 같습니다.
// my_feature_util.js const noop = () => {}; export const Utility = { noop // Many more functions… }; // MyComponent.js function MyComponent({ onClick }) { return <button onClick={onClick}>Hola!</button>; } MyComponent.defaultProps = { onClick: Utility.noop };
이전 예제에서와 동일한 방식으로 beforeEach() 함수를 인라인할 수 있습니다.
const findByReference = (wrapper, reference) => wrapper.find(reference); const favoriteTaco = findByReference( ['Al pastor', 'Cochinita pibil', 'Barbacoa'], x => x === 'Cochinita pibil' ); // → 'Cochinita pibil'
더 나아가서 test.each() 메서드를 사용하겠습니다. 왜냐하면 우리는 다양한 입력으로 동일한 테스트를 실행하기 때문입니다.
function MyComponent({ onClick }) { return <button onClick={onClick}>Hola!</button>; } MyComponent.defaultProps = { onClick: () => {} };
이제 예상 결과와 함께 모든 테스트 입력을 한곳에 모아 새로운 테스트 사례를 더 쉽게 추가할 수 있게 되었습니다.
정보: Jest 및 Vitest 치트 시트를 확인하세요.
추상화의 가장 큰 과제는 너무 경직된 것과 너무 유연한 것 사이의 균형을 찾는 것과 추상화를 시작할 때와 중지할 때를 아는 것입니다. 실제로 무언가를 추상화해야 하는지 확인하기 위해 기다릴 가치가 있는 경우가 많습니다. 많은 경우에는 그렇게 하지 않는 것이 좋습니다.
전역 버튼 구성 요소가 있으면 좋지만 너무 유연하고 다양한 변형 간에 전환하기 위한 12개의 부울 속성이 있으면 사용하기 어려울 것입니다. 하지만 너무 엄격하면 개발자는 공유 버튼 구성 요소를 사용하는 대신 자체 버튼 구성 요소를 만들게 됩니다.
다른 사람이 우리 코드를 재사용할 수 있도록 주의해야 합니다. 이로 인해 독립적이어야 하는 코드베이스 부분 간에 긴밀한 결합이 발생하여 개발 속도가 느려지고 버그가 발생하는 경우가 너무 많습니다.
생각해 보세요:
의견이 있으면 저에게 마스토돈을 보내거나, 저에게 트윗을 보내거나, GitHub에서 문제를 공개하거나, artem@sapegin.ru로 이메일을 보내주세요. 사본을 받으세요.
위 내용은 코드 세척: 분할하여 정복하거나 병합하여 완화의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!