✅ 코드가 복잡해지는 이유
강의에 따르면 개발자들의 고민 중 하나는 기존의 코드를 고치느라 시간을 많이 쓴다. 였다.
작은 기능을 위해 큰 비용을 지출해야 하는 것은 개발자 본인에게도 기업의 입장에서도 손해가 아닐 리 없다.
“프로그램이 동작하도록 만드는 데 엄청난 수준의 지식과 기술이 필요하지는 않다. 언제든 어린 고등학생이라도 할 수 있는 일이다. (...)
전 세계의 수많은 초급 프로그래머가 칸막이로 나뉜 작은 사무실에서 이슈 추적 시스템에 등록된 거대한 요구사항 문서들을 순전히 강인한 정신력만으로 힘겹게 해결해 내면서 시스템을 '동작'하도록 만든다.
이들이 작성한 코드는 그다지 깔끔하지 않을 순 있지만, 동작은 한다. 프로그램을 동작하게 만들기는 그리 어려운 일이 아니기 때문이다.” - 클린 아키텍처, 2p
“당신이 만든 시스템은 서로 강하게 연관되어 있고 복잡하게 결합되어서, 아주 사소한 변경에도 몇 주가 걸릴 뿐만 아니라 큰 위험을 감수해야 하지는 않았나?
잘못된 코드와 끔찍한 설계로 인해 방해를 받지 않았나? 시스템의 설계가 팀의 사기를 떨어뜨리거나 고객의 신뢰를 잃고 관리자의 인내심을 시험하는 부정적인 영향을 끼치지는 않았나?” - 클린 아키텍처, 3p
이번에 우테코 프리코스를 거치면서 학습한 객체지향 프로그래밍 개념도
미션에 적용해본 MVC 디자인 패턴도 모두 복잡도를 개선하기 위해 탄생했다.
App.js 파일에 작성한 스파게티코드를 MVC 디자인 패턴으로 리팩토링 했을 때 복잡도의 개선을 크게 느꼈다.
하지만 미션의 회차가 거듭될수록 요구사항은 많아지고
코드를 작성하기 전 모든 상황을 고려한 설계는 어려웠으며 코드는 더욱더 복잡해져 갔다.
복잡한 코드로 인해 리팩토링을 거치거나 기능을 추가할 때 수정해야 할 코드들이 많아졌다.
이 복잡함은 다른 분들의 코드리뷰를 할 때도 느낄 수 있었다.
현실에서는 일의 비용을 줄이기 위해서 일을 줄이는 일을 해야한다고 강사님은 설명했다.
이 일을 줄이는 일이란
1. 가독성이 좋은 코드작성
2. 다른 코드에 영향을 미치는 것을 줄이는 것이다.
말이 쉽다...!🥲
프로젝트나 과제를 제출하고나서 시간이 없었다는 핑계로 엉망이된 코드를 계속 리팩토링 했었다.
딱 떨어지는 정답이 없으니 계속 고쳐도 마음에 들지 않았다.
서비스를 기한내에 출시해야하는 상황에서 리팩토링할 시간이 부족할 수 밖에 없지만
사실 시간이 넉넉히 주어지더라도 내가 겪었던 것처럼 마음에 드는 결과를 얻진 못할 것 같다.
우테코 프리코스에서 객체지향 프로그래밍을 강조하면서 자연스럽게 추상화에 대한 관심이 생겼다.
추상화란 그 개념이 모호한데 강사님은 재치 있게 이렇게 정의했다.
각자의 머릿속 추상화 암튼 그거
'암튼 그거'를 팀프로젝트에 적용한다고 하면 각자 생각하는 '암튼 그거'가 다르기에 대화가 필요하다.
각자 생각하는 효율적인 방향이 다르기에 대화를 통해 협의점을 찾아가야 한다는 게 결국 포인트다.
조금 다른 얘기지만 우테코 프리코스에서도 우테코 측에서 제시한 에어비앤비 스타일 가이드를 기준으로 피드백이 많이 오갔다.
(자동 형변환을 하는 동등연산자보다는 에어비앤비 스타일가이드 ~번에 있는 일치연산자를 사용하는 것이 좋을 것 같아요~)
모든 사람들이 납득 가능하고 이해 가능한 규칙을 기준으로 삼았더니 코드 작성이나 코드리뷰를 효율적으로 할 수 있었다.
“자신이 작성하는 코드의 추상성을 높이고 싶다면 혼자서 고민하지 말고 다른 사람들과 협동하고, 대화하세요. 같이 그림도 그려보고 함께 소스코드를 편집하세요. 인간에게는 다른 인간과 소통하고 협력할 수 있는 놀라운 능력이 있습니다. 대화는 기적입니다.” - 함께 자라기, 128p
💡 데이터, 계산, 액션 구분하기
그래서 복잡도를 해결하기 위한 방법엔 무엇이 있을까?
강의에서는 데이터, 계산, 액션을 구분하는 것을 제안했다.
✔️ 데이터: 정보를 가지고 있는 값. (ex. 사용자가 입력한 값)
✔️ 계산: 입력으로 얻은 출력. 순수함수 (ex. 유효성검사)
✔️ 액션: 외부와 소통하는 행위. 부수효과 (ex. 데이터 통신)
여기서 데이터의 정의는 확실히 이해가 되었지만 계산과 액션의 개념이 모호하다.
메인프로젝트에서 진행했던 이메일 유효성 검사 기능 코드 일부를 수정하여 예시로 들고 와보았다.
const handleEmailChange = async(event) => {
const value = event.target.value;
//유효성검사 => 계산
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,24}$/;
//setState => 액션
setIsValidEmail(regex.test(value));
//네트워크요청 => 액션
if(isValidEamil){
await // 네트워크 요청 비동기로직
}else{
alert('이메일 또는 패스워드의 조건을 다시 확인해주세요.');
}
};
setIsValidEmail와 네트워크요청은 외부와 소통하는 부수효과를 일으키므로 액션이라 볼 수 있다.
정규표현식으로 진행한 유효성 검사는 외부상태를 변경하지 않고 동일한 입력에 대해 동일한 출력을 한다. 따라서 계산이라고 볼 수 있다.
액션과 계산로직이 합쳐져 있는 이 코드는 아래와 같은 단점이 있다.
1. setState를 포함해서 단위테스트를 하기 어렵다.
handleEmailChange함수를 테스트하려면
setState와 네트워크요청에 대한 mock 함수를 만들어 테스트코드를 작성해야 하는 번거로움이 있다.
2. 이메일 유효성 검사를 재사용할 수 없다.
만약 이메일 유효성 검사코드를 다른 곳에서도 사용하려면 중복된 코드를 작성해야 한다.
위의 논리에 따라 나눈 계산에 해당하는 로직들을 컴포넌트의 바깥으로 분리해 보았다.
const validateEmail = (email) => {
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,24}$/;
const isValid = regex.test(email);
return isValid;
}
function SignUpComponent(){
const handleEmailChange = (e) => {
const value = e.target.value;
setIsValidEmail(validateEmail(value));
};
const signUp = async (eamil, password) => {
if(isValidEmail && isValidPassword){
await // 네트워크 요청
}else{
alert('이메일 또는 패스워드의 조건을 다시 확인해주세요.');
}
}
}
이렇게 계산 로직을 분리하면 순수함수인 validateEmail만 테스트하면 되므로 테스트가 간단해진다.
그리고 이메일 유효성 검사를 할 일이 다른 곳에서 발생될 때 validateEmail 함수를 재사용할 수 있게 되었다.
🧐 setState가 왜 부수효과를 일으키는 액션인가?
이번 강의에서 가장 흥미롭게 학습한 내용이다.
우리가 알고 있는 useState 같은 Hooks는 리액트 16.8 버전에서 함수형 컴포넌트와 함께 등장했다.
그전에는 class의 인스턴스로 관리를 했다.
우테코 프리코스에서 학습한 "5. 객체는 객체스럽게 사용한다"에서 그 경험을 했는데
게임 진행 상황을 (점수, 시도 횟수) class 내부의 필드로 저장하고, 인스턴스를 넘겨 필요한 값을 사용했었다.
아래는 클래스 컴포넌트에서 생명주기에 따른 메서드 사용으로 상태를 관리하는 코드이다.
import React, { Component } from 'react';
class MyComponent extends Component {
constructor(props) {
super(props);
// 초기 상태 설정
this.state = {
count: 0,
};
}
// 생명 주기 메서드: 컴포넌트가 마운트된 직후 호출됨
componentDidMount() {
console.log('Component is mounted');
}
// 생명 주기 메서드: 컴포넌트가 업데이트된 직후 호출됨
componentDidUpdate(prevProps, prevState) {
console.log('Component is updated', prevState, '->', this.state);
}
// 생명 주기 메서드: 컴포넌트가 언마운트되기 직전에 호출됨
componentWillUnmount() {
console.log('Component will unmount');
}
// 상태 업데이트 메서드
incrementCount = () => {
this.setState({ count: this.state.count + 1 });
};
decrementCount = () => {
this.setState({ count: this.state.count - 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.incrementCount}>Increment</button>
<button onClick={this.decrementCount}>Decrement</button>
</div>
);
}
}
export default MyComponent;
하지만 함수형 컴포넌트는 실행이 완료되면 사용된 값들을
가비지 컬렉터가 메모리에서 정리하기 때문에 상태를 가질 수 없다.
그렇다면 어떤 원리로 상태를 관리하고 있는 걸까?
바로 면접 준비하면서 주구 장창 외웠던 클로저 때문이다.
지금 기억나는 대로 클로저의 정의를 적어보자면 함수와 그 함수가 호출될 때 주변 환경을 기억하는 현상이다.
//useState 소스코드
export function useState(initialState){
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
const TodoList = () => {
const [todos, setTodos] = useState([]);
}
useState로부터 불러오는 todos와 같은 상태는
컴포넌트 외부에서 불러온 리액트에 저장된 클로저에서 읽어오고 있는 값이다.
todos가 useState를 참조해서 참조카운트가 0이 되지 않기 때문에
가비지 컬렉터의 대상에서 벗어나 useState의 내부의 값을 상태로 저장할 수 있다.
따라서 컴포넌트 외부에 있는 값을 업데이트하는 setState함수는 컴포넌트 입장에서 부수 효과로 볼 수 있다.
👀 마무리
기존 코드에 문제는 많아 보이긴 하지만 어디서부터 건들여야 좋을지 어려웠는데
액션과 계산의 분리로 간단하게 1차적 정리를 할 수 있어서 유익한 시간이었다!
'원티드' 카테고리의 다른 글
[12월 프리온본딩] 비즈니즈로직 분리하기 (1) | 2023.12.21 |
---|