원티드에서 진행하는 프리온보딩 챌린지에서 과제를 진행하며 코드를 리펙토링 하면서 이게 과연 올바른 방법일까?
리액트를 쓰다보면 onChange
이벤트를 굉장히 많이 접하게 된다. 예를 들어, 로그인 input, 회원가입 input, 과제로 진행하고 있는 todo의 input에서 해당 이벤트가 발생할 것이다.
코드는 아마도 이렇게 작성할 것이다.
const handleOnChangeEmail = debounce(
(e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
},
300
);
email input의 상태를 저장하기 위한 함수를 작성했다. jsx 태그에 onChnage={(e) => setEmail(e.target.value)}
라고 쓰는 것 보다는 함수를 따로 만들어서 써주는 것이 가독성 면에서 더 뛰어나 보이기에 이런식으로 많이 작성할 것이다.
debounce는 간단하게 setTimeout을 이용해서 값이 변할 때마다 렌더링이 발생하는 일을 최적화하기 위해 적용했다.
자 그럼 위에서 언급한 3가지의 상황에서 해당하는 똑같은 코드를 작성할 것이다.
클린코드 원칙..! 코드를 반복하는 것을 피해라..!
반복을 줄이기 위한 방법은 무엇인가. 함수를 만드는 것이다. 함수를 정의해서 사용하면 반복을 줄일 수 있다. 프로그래밍의 기본이다.
리액트에서 반복적으로 사용되는 로직을 함수로 분리해서 사용할 수 있는 방법은 custom hook을 만드는 것이다!
그래서 useInput
을 만들어 봤다.
const useInput = ({
type,
types,
required,
disabled,
placeholder,
}: InputTypes): [string, JSX.Element] => {
const [inputValue, setInputValue] = useState("");
const handleOnChange = debounce((e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
}, 200);
const input = (
<Input
type={type}
types={types}
onChange={handleOnChange}
required={required}
disabled={disabled}
placeholder={placeholder}
/>
);
return [inputValue, input];
};
글을 쓰면서 보니 type과 types가 무엇을 의미하는지 구분이 안 가는 것을 깨달았다..! 즉시 수정을 해야겠다.
아무튼 state를 정의하고 handleOnChange를 정의해서 input이라는 jsx를 반환하는 custom hook을 제작했다.
hook을 사용하는 방법은 매우 쉽다.
일단 import를 한다.
import useInput from "hooks/useInput";
custom hook에서 요구하는 값을 설정한다.
const [email, emailInput] = useInput({
type: "text",
types: "auth",
placeholder: "email",
required: true,
});
배열의 두 번째 요소는 jsx이기 때문에 컴포넌트 return문에서 쓰면 된다.
return (
<Container>
<SignUpForm onSubmit={handleOnSubmit}>
{emailInput} // 오잉
</SignUpForm>
</Container>
);
기존에 보던 jsx
인 <>
가 없어서 너무 어색하다. 심미적으로 뭔가 이상하다. 코드를 줄인 점은 좋지만 이렇게 쓰는 게 맞을까...???????
오히려 컴포넌트 구조가 {}
로 인해 복잡해질 우려는 없을까..?
클린코드 규칙에 따르면 반복을 피하는 코드를 작성해야 하는데 왜인지 모르게 저런 컴포넌트는 읽기가 꺼려진다. 그래서 이게 올바른 리펙토링인지 고민이 된다. 어찌 됐든 사람이 코드를 읽는 것인데 저렇게 작성해도 읽는데 무리가 없겠지..?
아직 클린코드에 대한 개념이 부족하다. 뭐가 좋은 코드고 나쁜 코드인지 구분이 잘 가지 않기 때문에 여러 시도를 해보는 게 좋을 것 같다. 내 코드가 무자비하게 더럽혀져야 깔끔한 코드를 작성하는 법을 배우지 않을까..
클린코드 규칙을 하나씩 적용해 보면서, 그리고 좋은 코드들을 보면서 내 코드와 비교를 해보자.
무척 어려운 내용이었지만.. 클린코드에 대해 (사실은 영어 공부였던 것 같기도..) 많은 자료들을 찾아봤고, 우리 나라에서 진행했던 프론트엔드 컨퍼런스를 보며 컴포넌트에 대한 생각을 조금 할 수 있었다. 이 영상은 두고두고 100번은 봐야될 그런 명강의다. 다른 영상들도 너무 재밌고 유익하다. 마블보다 재밌다.
아무튼 영상을 보면서 느낀 것은 무조건 분리하는 것이 좋은 코드가 아님
을 알게 되었다.
나는 handleOnChange 함수를 분리시키기 위해 무슨 짓을 했냐면..
첫 번째, input 상태를 외부로 보내버렸다..!
두 번째, input 컴포넌트도 외부로 보내버렸다..!
세 번째, logIn, signUp 컴포넌트가 휑 해졌다..!
그러니까 이게 어떤 것을 의미하는지 일상생활에서 누군가와 대화를 하고 있는데, 아무 설명 없이 고래고래 억지를 부리고 있어서 상대방이 절대 납득할 수 없는 최악의 상황이라고 할 수 있다.
쌩뚱 맞게 튀어나온 useInput
이 모든 것을 가지고 있어 처음 보는 사람에게는 이게 뭔 코드?를 시전할 수 있게 해버린 것이다.
그래서 '이게 뭔데'와 '그래서 뭐?'로 코드를 나눴다.
사막을 걷다가 저 멀리 표지판이 하나 보였다. 야자수 그림이 그려져 있다! 목이 마른 나머지 그 그림만 보고, 표지판 앞으로 헐레벌떡 앞으로 뛰어 갔는데 어느 방향으로 가야하는지 수식이 적혀 있는 게 아닌가..! 목이 말랐던 그 사람은 그 코드를 보다 결국 사망했다고 한다...
여기서 표지판은 이게 뭔데
, 정확히 가는 방법을 그래서 뭐
라고 나눠서 생각할 수 있다.
그렇다면 내 useInput
을b 올바르게 고칠 수 있는 방법은 handleOnChange의 로직에 대한 부분만 분리해서 컴포넌트를 구성하는 게 좋지 않나!
interface Props {
setValue: React.Dispatch<React.SetStateAction<string>>;
}
const useInput = ({ setValue }: Props) => {
const handleOnChange = debounce(
(
e:
| React.ChangeEvent<HTMLInputElement>
| React.ChangeEvent<HTMLTextAreaElement>
) => {
setValue(e.target.value);
},
200
);
return handleOnChange;
};
음 이제 로그인 컴포넌트가 멀리서 봐도 로그인에 필요한 input들과, 변경함수가 있구나를 느낄 수 있지 않나 조심스레 생각해본다. 여기서 handleOnSubmit도 분리를 시킬 수 있겠지?ㅎ
분리가 필요한지 없는지 또 고민해보자.
지금까지 컴포넌트에 타입 interface가 섞이거나, style이 섞여있는 것을 지양했었다. 왜냐면 섞임은 복잡성을 늘릴 것이라 생각했었다. 하지만 무조건 나누는 것이 옳은 방법은 아님을 깨달았다. 해당 컴포넌트에서만 사용되는 타입이나 스타일을 굳이 분리할 필요가 있을까? 그리고 한 파일 안에 있을 때 수정이 더 편해 지는 게 아닐까? props에 대한 타입은 같은 파일에 있는 게 유지보수 하기에 더 편할 것 같다는 생각이 들었다.
앞으로 더 복잡한 상황을 겪으면서 언제 어떻게 분리를 할지, what, how 코드를 어떻게 구성할지 많이 많이 겪어봅시다..!!