클린코드는 개발자라면 누구나 다 들어봤고, 선망하는 방식일것입니다. 이번 강의는 전체적으로 클린코드에 관하여 다루어졌습니다. 이번 강의를 통해, 어찌보면 의심않고 사용하여, 고정되어있던 제 코드 스타일에서 고쳐야 할점과, 좋은 방식의 코드란 무엇인가를 생각해보는 계기가 되었습니다.
(해당 게시글은 실제 강의 내용을 그대로 옮겨적은것은 아니며, 주관적인 이해 및 판단으로 요약되어진 글입니다.)
이번 강좌에서는 이전 과제의 피드백 없이, 바로 강의 주제로 넘어가게되었습니다.
클린코드는 사실 하나의 고정된 개발 단어는 아닙니다. 단지 Clean Code라는 책이 클린코드 작성법에 대해 설명하고, 또한 효율성이 입증되었기에 사람들이 많이 사용하는 단어일뿐입니다.
클린코드를 작성하려 한다는것은 좋은 코드를 지향한다는것이고, 이것은 곧 나쁜코드를 지양한다는것입니다.
나쁜 코드란 시간상의 이유등으로 이런식의 코드를 짜는 경우가 많습니다. 완성에 급급하여 하나의 로직이 이런저런 역을 맡다보면 결국에는 로직은 분리하고 수정해야 할 경우 작업이 너무 힘들어지게되고, 불가능하게 될 경우 소프트웨어 자체를 포기하게 된다는것입니다.
클린코드란, 이런 나쁜 코드를 기피하고, 좋은 코드를 쓰려 하는것입니다.
그렇다면 나쁜 코드를 기피하기 위한 좋은 코드를 위해선 무엇을 해야 하는것일지 알아보겠습니다.
좋은 코드라고해서 정형화된 원칙은 없습니다. 하지만 많은 개발자들은 스스로의 노하우를 공개하였고, 이것들을 연구하여 좋은 코드를 작성하기 위한 원칙과 방법들을 쓰기로 한것입니다.
분리 주제에 앞서, 관심사라는 단어는 간단히 말하면, 하나의 모듈이 수행하고자 하는 목적을 의미힙니다. (모듈은 함수,클래스 등의 수행하는데 필요한 단위라고 생각할수있습니다.)
관심사의 분리란 이러한 관심사를 잘개 쪼개어, 하나의 모듈이 폭 넓거나, 여러 관심사에 관여하게 작성되는것을 방지하는 것을 의미한다 할 수 있습니다.
이러한 관심사를 분리하는 이유는, 소프트웨어의 특정 부분의 수정이 필요로 하는 경우, 여러 모듈들이 여러 관심사를 동시에 다루고 있는 상황이라면 모든 모듈들을 일일히 수정해주어야 할것입니다. 하지만 관심사의 분리가 이루어진 모듈에서는 해당 모듈만 수정하면 됩니다.
하드웨어와 달리 소프트웨어는 수정,변화가 요구되는 경우가 잦습니다. 무형의 제품이기 때문입니다. 소프트웨어의 변화는 하드웨어와 달리 필연적이라 생각하여야 할 정도며, 좋은 소프트웨어 일수록 기능을 수정-확장하는것을 잘 할 수 있어야 합니다. 관심사의 분리와 같은것을 제대로 고려하지 못한 소프트웨어인 경우에는 수정이 매우 복잡해지고, 이것은 곧 변화가 되어야 하는 소프트웨어로서의 의미가 없어지게되고, 최종적으로 포기하게 되버리는 상황까지도 갈수있습니다.
이러한 관점에서 관심사의 분리는 유지-보수 측면에서 매우 유리한것입니다.
예시로서, 모든 페이지에서 필요로 하는 인증&인가에 관한 기능을 수정해야 할때, 여러 모듈들이 이 모듈에 대하여 관여하고 있다면 수정이 매우 복잡해질것입니다.
하지만 인증&인가에 대한 모듈을 하나만 작성하고, 이것을 다른 모듈이 사용만 하는경우에는 인증&인가와 관련된 모듈 하나만을 수정하면 됩니다.
이와 같은 관심사의 분리는 소프트웨어를 만드는 프로그래밍에서 가장 기본이 되는 원칙이며, 이러한 개념을 표현하기 위한 단어와 격언들이 있습니다.
React는 UI를 구축하기 위한 라이브러리입니다.
즉, React의 핵심 관심사는 UI인것이며, 추가적으로 이 UI를 변경시키는 로직. 이 2가지로 나뉘게 됩니다.
이러한 관심사를 나누기 위해서, 개발자들은 몇가지 방식으로서 관심사를 분리하였습니다.
Presentational - Container 방식이란 컴포넌트를 2계층으로 분리하는 방법입니다.
React에서는 실제 보여지는 컴포넌트(Presentational)에서는 UI 변경에 필요한 Data 또는 이 Data의 가공 처리등이 필요합니다. 하지만 Presentational - Container 방식에서는 이러한 Data를 받거나, 가공 처리가 필요한 부분을 Container에서 모두 처리합니다.
Container에서는 UI를 일체 담당하지 않으며, 컴포넌트에서는 마찬가지로 로직을 담당하지 않습니다. 이러한 방식을 사용하기 위해선 Container 컴포넌트에서 UI 담당 컴포넌트(Presentational) 를 감싸는 형태로 사용됩니다.
즉, Container에서는 데이터를 처리하고, 가공된 데이터를 Componet(Presentational)에 전달하여, 각각 UI와 로직의 관심사를 분리한것입니다.
하지만 이러한 방식은 Custom Hook의 등장으로 현재는 많이 사용되지 않는 패턴입니다.
커스텀 훅이란 리액트가 기본적으로 제공하는 Hook들을 사용하여 만든 함수를 말합니다.
커스텀 훅의 기본 원칙은 2가지 입니다.
커스텀 훅을 사용하는 이유는 아래와 같습니다.
횡단 관심사는 여러 서비스에 걸쳐서 동작해야 하는 관심사(코드,모듈)를 의미합니다.
서비스 A,B,C에서 모두 필요로 하는 로직이 있다하면,이것이 마치 서비스들을 횡단하는것과 같기때문에 횡단 관심사라 불립니다.
인증&인가 / 로깅 / 에러처리 / 트랜잭션 처리 등이 대표적인 횡단 관심사입니다.
횡단 관심사는 관심사의 분리 측면에서 중요한데, 이 관심사가 혼재되어 버리면 수정하기가 매우 힘들어지기 때문입니다.
가장 흔하게 생각할 수 있는 관심사는 인증&인가입니다.
Http 요청을 보낼때, 프론트엔드단에서는 매번 헤더에 특정 값을 보내야 하는 경우가 많습니다. 특히 Token과 같은 값이 그렇습니다.
이런 경우 아래와 같은 형태로 통신을 보내게 될것입니다.
fetch("url...", {
headers: {
Authorization: "ACCESS_TOKEN"
}
하지만 이러한 형식으로 매번 요청을 보내는것은 비 효율적입니다. 이러한 상황에서 url과 token값을 넣는 기능은 횡단 관심사로서 작용한다면, 아래와 같이 그 기능만을 담당하는 모듈을 만들 수 있습니다.
class HttpClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
fetch(url, options = {}) {
return fetch(`${this.baseURL}${url}`, {
...options,
headers: {
Authorization: sessionStorage.getItem("access_token"),
...options.headers
}
});
}
}
//다른 곳에서 사용
const httpClient = new HttpClient(url...);
httpClient.fetch(endPoint..);
위와 같은 방법으로 작성하다면, 기존 횡단 관심사인 fetch의 url을 유연하게 수정할수있고, 컴포넌트 내에서 호출할때 훨씬 간결하게 호출하는 방식의 클린코드를 만들수있습니다.
추가적으로 클래스를 사용하는 방식의 경우 baseURL 생성자와 같은 부분을 #baseURL를 사용하여, private 값으로 취급할수있습니다. 이렇게 사용할경우 해당 클래스를 호출하였을때 생성자 정보를 은닉할수있습니다.
시작에 앞서, 추상과 구체에 대해 설명하고자 합니다.
추상이란, 기대하는 결과만을 적어놓은것과 같습니다.
예를 들어, getTodo라는 형태만을 만들어놓고, 실제 구현은 해놓지 않으면 이것은 추상입니다. 단지, 이름에서 알수 있듯이 함수를 실행시키면 Todo를 줄것이라 예상할수있습니다.
getTodo를 실제로 구현한 함수는 구체입니다. 실제 로직을 작성하여 흐름을 제어해야만 합니다.
구체는 여러 이유 등으로 수정되어야 하는 경우가 많습니다.
하지만 추상은 수정되지 않습니다. 추상을 구현하는 구체가 수정이 필요하다면 수정할 뿐입니다.
다시, 의존성이란 주제로 돌아와서,
의존성이란 특정한 모듈이 동작하기 위해서 다른 모듈을 필요로 한다는것을 말합니다.
즉, 구체를 실행시키고자 할때, 다른 구체가 필요로 하는 경우를 말합니다.
의존성 역전 원칙이란 코드의 의존성이 추상에 의존하며, 구체에는 위존하지 않는것을 의미합니다.
이는 곧, 유연성이 극대화된 시스템을 만들기 위한 원칙입니다.
요구 사항이 변한다면 추상에 의존한 애플리케이션을 작성한 경우에는 수월하게 수정 할 수있지만, 구체에 의존한 애플리케이션은 구체가 의존성을 가질경우 수정하기가 힘들어질수 있습니다.
코드를 통해 예를 들어보겠습니다.
token을 이용하여 api 통신을 하려는 함수가 있을경우, 일반적으로 아래와 같이 작성할것입니다.
fetch("todos", {
headers:{
Authorization:localStorage.getItem("ACCESS_TOKEN");
}
}
하지만 위 코드는 localStorage라는 구체에 의존하고있습니다.
단순히 저러한 fetch를 사용하는 함수 뿐만 아니라, 비슷하게 localStorage의 토큰을 사용하는 함수는 모두 localStorage.getItem("ACCESS_TOKEN"); 라는 의존성을 가지고 있는것입니다.
이러한 토큰의 의존성을 제거하는 방법으로, token만을 담당하는 모듈을 만드는것이 좋을 수 있습니다.
아래와 같은 모듈을 만든다면 token값의 의존성을 제어할 수 있습니다.
/*
TokenRepositoryInterface
save(token:string):void
get():string
remove():void
*/
class LocalTokenRepository {
#TOKEN_KEY = "ACCESS_TOKEN";
save(token) {
localStorage.setItem(this.#TOKEN_KEY, token);
}
get() {
return localStorage.getItem(this.#TOKEN_KEY);
}
remove() {
localStorage.removeItem(this.#TOKEN_KEY);
}
}
const tokenRepository = new LocalTokenRepository();
우선적으로, 주석으로 처리된 부분은 추상(인터페이스)입니다.
해당 부분을 통해 행동과 결과만을 기술합니다.
그리고 LocalTokenRepository라는 클래스는 인터페이스를 따라야하만 하는 구체 모듈입니다.
구체는 추상에서 정의해둔 형태로구현해야 합니다.
위와 같은 코드를 구현한다면, 우선적으로 localStorage가 변경되어야 하는 경우 해당 모듈만 수정하면 됩니다.
또한,기존 코드의 흐름이 API 호출 -> localStorage 흐름에서
API 호출 코드 → tokenRepository Interface → tokenRepositry Class → localStorage 흐름으로 바뀌게 됩니다.
하지만 의존성적인 측면에서는 tokenRepository Interface ← tokenRepositry Class → localStorage 라는 흐름으로 바뀌게 됩니다.
클래스의 구체에 의존해 실현은 되지만, 클래스 자체는 인터페이스에 의존하게 됩니다.
이와 같이, 코드의 실행 흐름과 의존성이 방향이 반대로 뒤집힌 상황을 의존성 역전 원칙이라 부르며,IoC(Inversion of Control)이라고 표현합니다.
의존성 주입이란 특정한 모듈에서 필요한 의존성을 내부에서 가지고 있는게 아니라, 해당 모듈을 사용하는 입장에서 입력(주입)하여 사용하는 형태로 설계하는것을 의미합니다.
마찬가지로, 클래스를 예시로 들자면
constructor를 사용하지 않고, 자체적으로 내부에서 정의된 값을 사용하는 클래스와
constructor를 사용하여, 외부로부터 선언시 해당 값을 사용하는 클래스가 있다면
전자의 클래스는 유연하지 않습니다. 때문에 의존성이 모듈내부에 제한되어있습니다
하지만 후자와 같은 클래스는 값,의존성을 주입받아 유연하게 대처할수 있습니다.
의존성 주입이 없는 클래스 (url과 )
class AuthService {
signup(email, password) {
httpClient
.fetch("auth/signup", {
method: "POST",
body: JSON.stringify({
email,
password,
}),
})
.then((res) => res.json())
.then(({ access_token }) => tokenRepository.save(access_token));
}
의존성 주입을 이용하는 클래스 ()
class AuthService {
constructor(httpClient, tokenRepository) {
this.httpClient = httpClient;
this.tokenRepository = tokenRepository;
}
signup(email, password) {
this.httpClient
.fetch("auth/signup", {
method: "POST",
body: JSON.stringify({
email,
password,
}),
})
.then((res) => res.json())
.then(({ access_token }) => this.tokenRepository.save(access_token));
}
두 코드 모두 httpClient와 tokenRepository라는 클래스를 사용하고있습니다.
하지만 앞의 코드는 httpClient와 tokenRepository에 의존하고 있기 때문에, 만약 관련 동작을 수정하려 한다면 AuthService 자체를 수정해야 합니다.
하지만 아래의 코드와 같이, 클래스를 생성할때 외부에서 주입하는 형식으로 변경하게 되면 이후에 AuthService의 코드수정없이 httpClient와 tokenRepository에서 연관된 동작을 쉽게 변경해서 다양하게 사용할수있습니다.
이번 강의에서는 클린코드에 대해서 알아보았습니다.
웹 개발을 배우면서 클린 코드를 잘은 모르지만 해보고 싶다는 생각은 항상 가지고 있었습니다.
하지만 어떻게 해야하는지, 무엇을 중요시해야 하는지는 막연했습니다.
이번 강의를 통해서 대략적으로 클린코드를 위해서는 어떠한것을 중요시 해야하는지 알수 있게 되어 이후 개발에서는 이런 관점에서 코드를 짜야겠다는 생각을 할수있게 되었습니다.
이번강의를 통해서 해보고자 한것은 아래와 같습니다.