https://jeonghwan-kim.github.io/dev/2022/03/29/react-form-and-formik.html
의 포스팅 내용에서 많은 인사이트를 얻을 수 있을것 같아 따라해본 과정입니다!!
머릿속에 강하게 남은 내용을 정리하고자 한다.
- 기존의 로직을 커스텀 훅으로 추출(연관된 로직이 전부 커스텀 훅에 위치하게 되는 과정, 세부구현은 밖에서 넣어준다)
- 추상화를 사용함으로써 속성은 숨기고 가독성은 증가(중요 개념만 남김)
- 리액트 컨텍스트를 사용한 상태의 공유 및 제어
인사이트들에 대해 정리해보기 이전에
Before/After 부터 살펴보면 좋다
아래의 iframe에서 직접 확인할 수 있다.
기존의 로직을 커스텀 훅으로 추출(연관된 로직이 전부 커스텀 훅에 위치하게 되는 과정, 세부구현은 밖에서 넣어준다)
기존 로직
import { useEffect, useState, useCallback } from "react";
const LoginForm3 = () => {
const [values, setValues] = useState({
email: "",
password: "",
});
const [errors, setErrors] = useState({
email: "",
password: "",
});
const [touched, setTouched] = useState({
email: false,
password: false,
})
const handleBlur = e => {
setTouched({
...touched,
[e.target.name]: true,
})
}
const handleChange = (e) => {
setValues({
...values,
[e.target.name]: e.target.value,
});
};
const validate = useCallback(() => {
const errors = {
email: "",
password: "",
};
if (!values.email) {
errors.email = "이메일을 입력하세요";
}
if (!values.password) {
errors.password = "비밀번호를 입력하세요";
}
setErrors({...errors});
}, [values]);
useEffect(() => {
validate()
}, [validate])
const handleSubmit = (e) => {
e.preventDefault();
setTouched({
email: true,
password: true,
})
validate();
if (Object.values(errors).some((v) => v)) {
return;
}
alert(JSON.stringify(values, null, 2));
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.email && errors.email && <span>{errors.email}</span>}
<input
type="password"
name="password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.password && errors.password && <span>{errors.password}</span>}
<button type="submit">로그인</button>
</form>
);
};
export default LoginForm3;
Component가 선언된 부분부터 jsx를 return하는 부분까지가 form이 동작하기 위한 코드이다.
3가지 종류의 상태를 가지고 있다.
각각의 상태를 handle하는 함수들을 일일히 지정해 주고 있다.
위의 방식은 재사용이 불가능 하다.
만약에 다른 컴포넌트에서 form을 사용한다면 위의 코드 전부가 복붙이 될것이다.
유지보수가 어떻게 될 지는 불을 보듯 뻔하다.
한가지 반성할 점은 이전까지 코드를 위와 같이 작성했었던 것 같다.
커스텀 훅을 작성하는 것에 잘못된 기준을 가지고 있었던 것 같다.
* 다른 곳에서 무조건 재사용 되어야 할것
(+ 파일의 수가 많아지는 것에 대해 반감을 가지고 있었다)
내가 추구했던 기준이었다.
지금은 생각이 다르다.
아래의 조건중 하나라도 해당한다면 커스텀 훅을 고려한다.
* 여러 상태를 보유해야 하는 경우 (커스텀 훅 내에서 useReducer의 사용을 고려한다)
* 컴포넌트와 다른 추상화 수준 ( jsx내의 추상화 수준과 위의 선언 로직 또한 같은 단계의 추상화 수준을 유지하는 것이 best이지 않을까? -> 이건 계속 생각 중이다. view단의 추상화와 위의 선언 로직의 추상화 또한 같은 레벨을 추구하는 것이 맞을까?)
* 덩치가 너무 커진 코드
기능을 구현해야 할때 딱 필요한 만큼의 코드를 짜는 것을 추구한다.
(닭을 잡아야 할때는 닭을 잡을때 쓰는 칼을 써야하는 느낌이다. 소를 잡을때 쓰는 칼을 쓰지 않는것처럼)
필요한 만큼의 코드를 짜야할 때는 클린코드의 개념이 선행되어야 한다고 생각한다.
프로그래머의 고뇌로 탄생한 글 이기에모든 코드는 유지되어야 할 가치가 있다.
하지만 짧은 코드는 클린코드의 개념에 반만 걸쳐져 있는것 같다.
짧은 코드는 읽기는 좋지만 의미를 파악하기에 충분치 않을 수도 있다.
의미를 효율적으로 전달하기에 긴 글이 필요하다면 당연히 긴 글을 선택해야 한다.
파일이 많아지고 덩치가 커지는 것이 의미를 더 효율적으로 전달할 수 있다면 클린코드의 방향에 그릇된 방향은 아니지 않을까
커스텀 훅 결과물
import { useEffect, useState, useCallback } from "react";
const useForm = ({ initialValues, validate, onSubmit }) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleBlur = (e) => {
setTouched({
...touched,
[e.target.name]: true,
});
};
const handleChange = (e) => {
setValues({
...values,
[e.target.name]: e.target.value,
});
};
const handleSubmit = (e) => {
e.preventDefault();
setTouched(
Object.keys(values).reduce((touched, field) => {
touched[field] = true
return touched
}, {})
);
const errors = validate(values);
setErrors(errors)
if (Object.values(errors).some(v => v)) {
return;
}
onSubmit(values);
};
const runValidator = useCallback(() => validate(values), [values])
useEffect(() => {
const errors = runValidator();
setErrors(errors)
}, [runValidator]);
const getFieldProps = name => {
const value = values[name]
const onBlur = handleBlur
const onChange = handleChange
return {
name,
value,
onBlur,
onChange,
}
}
return {
getFieldProps,
handleSubmit,
handleChange,
handleBlur,
values,
touched,
errors
}
};
export default useForm;
이 커스텀 훅은 작성할때 나중에 보게된 것이 있다.
세부구현은 밖에서 넣어준다
실 사용예시를 살펴보자
현재 form component 에서 핵심 기능은 어떤것일까?
0. 초기에 노출시켜줄 값(만약 edit이라면 이전의 값을 넣어주어야 할 것이다.)
1. 입력된 값의 검증
2.입력된 값의 제출
크게 3개로 정리해 보았다.(이외에 여러가지가 있을 수도 있고 저 위에 3가지가 해당하지 않을 수도 있다.)
3개에 대한 구현은 Form 컴포넌트를 이용하는 곳에서 사용한다.
Form 을 사용할때 저 구현들을 입력값으로 받지 않고 Form의 할당 부분에서 정의할 수도 있었을 것이다.(동작은 하겠지.. 그게 문제가 아니자나!!)
// 생략
return (
<Form>
// 생략
</Form>
)
// 생략
또한 위와 같이 작성되었다면 form의 동작을 확인하기 위해서는 Form이 정의된 다른 모듈을 참고해야한다.
(시간은 자원이다. 의미를 파악하기 위해 모듈간을 탐험해야 한다면 자원은 많이 소모될 것이다)
커스텀 훅을 만드는 이유는 재사용성에 있다고 생각한다.
여러 컴포넌트에서 사용해야 할 경우가 있다는 것이다.
이러면 경우의 수가 엄청나게 많이 생길수 있지 않을까(a에서 불러와 사용하는 경우, b에서 불러와 사용하는 경우 등등)
이러면 case by case로 커스텀 훅 내에서 대응할 게 아니라 오히려 사용법만 주고 이런걸 입력해야 해 라고 알려주는 것이 효율적으로 대응하는 수단이 아닐까 싶다.
마치 와플메이커를 가지고 사람들이 와플도 만들고 빵도 만들고 누룽지도 만드는 것처럼??
여기서 선언형 프로그래밍도 엿볼수 있지 않을까? (여기 내용은 아닐수도 있습니다!! 공부하고 나중에 수정될 수도 있습니다! 아직 주니어니까!)
세부구현은 뒤로 숨겨놓는다. 이때 세부구현은 명령형 프로그래밍으로 작성한다(a를 하고, b를 하고, c를 수행한후에 d를 return해)
그리고 프레임워크 or 라이브러리에게 말하는 것이다.
내가 a,b,c를 넣었으니 d를 가져와줘
<Component a={a} b={b} c={c}></Component> => d 결과물
<div>
{a && <p>a</p>}
{b && <p>b</p>}
{c && <p>c</p>}
</div> => d결과물
여튼 말하고 싶었던 내용은 커스텀 훅에 대한 두려움이 있었던 지난날을 반성하고
꽁꽁 숨기는 것보다는 오히려 드러내는 것(세부구현)이 재사용성, 가독성 면에서는 오히려 도움이 되지 않을까 하는 의견을 피력하고 싶었다.
추상화를 사용함으로써 속성은 숨기고 가독성은 증가(중요 개념만 남김)
위에서 글의 boundary를 알맞게 정하지 못해서 위에서 언급한 내용과 중복될 것 같다.
<!-- 1번 -->
<Form>
<Field type="email" name="email" />
<ErrorMessage name="email" />
<Field type="password" name="password" />
<ErrorMessage name="password" />
<button type="submit">로그인</button>
</Form>
<!-- ------------------------------------------ -->
<!-- 2번 -->
<Form
initialValues={{ email: "", password: "" }}
validate={validate}
onSubmit={handleSubmit}
>
<Field type="email" name="email" />
<ErrorMessage name="email" />
<Field type="password" name="password" />
<ErrorMessage name="password" />
<button type="submit">로그인</button>
</Form>
1번의 형태는
가독성이 좋다.
추상화 수준이 일관되어 있다. 라는 특징을 가지고 있다고 생각한다.
하지만 form내에 어떤값이 들어가는지 검증은 어떻게 하는지, submit 버튼을 누르면 어떻게 되는지 알수는 없다
이는 모듈간의 탐험을 시작해야 한다는 의미가 된다.
중요개념은 남겨주어야 한다.
중요개념만 props로 건네주면 의미를 파악하기위해 모듈간의 탐험은 시작하지 않아도 될것만 같다. (어렵다...)
(컴포넌트의 중요 의미를 정의하고 이 의미를 직접 주입하여 주자)
리액트 컨텍스트를 사용한 상태의 공유 및 제어
리액트의 컴포넌트는 상태를 가진다.
상태는 컴포넌트 내부에서 관리되며 다른 컴포넌트에게 props로 전달할 수 있다.
상태의 변화는 곧 데이터가 변화가 되었다는 것을 의미한다.
이는 곧 화면을 새로 보여주어야 한다는 의미가 된다.
상태의 변화는 리렌더링을 해야한다는 의미이다.
이런 상태는 크게 3가지 쓰임이 있을 수 있을것 같다.
1. 컴포넌트 내부에서의 쓰임
2. 여러 컴포넌트 간의 쓰임
3. 전역에서의 쓰임
1번은 하나의 컴포넌트만 리렌더링이 되므로 괜찮다.
(React에서는 메모리에 가상돔을 가지고 있고 재조정 알고리즘을 가지고 새로 렌더링 해줘야 하는 컴포넌트만 계산하여 렌더링을 시켜준다.
재조정 알고리즘에는 크게 2가지를 이용하는데
부모 컴포넌트가 변경되었다면 변경된 부모 컴포넌트의 모든 자식을 리렌더링,
형제 컴포넌트 간에는 key를 이용하여 key값이 변경된 컴포넌트만 리렌더링해준다.
이때 비교 알고리즘에는 Object.is 알고리즘을 사용한다)
2번은 부모 컴포넌트의 상태가 변경되면 부모를 포함한 모든 자식 컴포넌트가 리렌더링 된다.
3번은 전체 컴포넌트가 리렌더링 된다.
잦은 리렌더링은 클라이언트에게 부하를 일으킨다(cpu bound)
따라서 전역 상태의 추가는 지양되어야 한다. (전역 상태가 될 수 있는 대표적인 2가지(ight/dark 모드, 외국어 지원) 이외에는 프로젝트의 성격에 따라 다를것 같다)
3번은 최대한 지양한다해도 2번은 어쩔수가 없다.
이때 state를 props로 컴포넌트간에 넘겨준다하면 props driiling이라고 하는 상황을 마주하게 된다.(가독성을 크게 해치게 될 것이다.)
이 props driiling은 리액트의 컨텍스트를 이용하면 피할 수 있다. (물론 전역 상태로 정의해서 피할수도 있다)
컨텍스트에 대한 자세한 쓰임은 언급하지 않을것 같다.
Form 내부에서 사용되는 Field, ErrorMessage들은 useContext hook을 통해 form의 상태에 접근한다.
그리고 이를 제어한다.(리렌더링 발생)
상태는 전역으로 관리되지 않아야 하고 제어되는 곳에서 최대한 가까운 곳에서 정의되어 있어야 한다고 생각한다.
아래의 그림은 내가 생각하는 가장 적절한 상태 관리 흐름이다.
위의 그림은 dom tree를 형상화 한것이다.
상태의 정의는 체크 표시가 들어간 컴포넌트에서 하게 된다.
같은 색깔의 노드들은 같은 상태들을 공유한다.
3depth 이상에서는 Context를 사용할 것 같고 아니라면 props 로서 전달을 고려할 것 같다.
React에서는 createContext로 context를 생성후에 provider로 감싸주면 value로 전달된 값을
children에서 가져와 사용할 수 있다.
이때 값을 가져올때는 useContext훅을 사용하면 된다.
'Frontend > React' 카테고리의 다른 글
React 상태관리🌱외 minor concept (0) | 2022.04.07 |
---|---|
React css-in-js (0) | 2022.04.06 |
React keys (0) | 2022.04.02 |
React input onFocus, onBlur(focus out) 😶🌫️ (0) | 2022.04.02 |
React moment locale 적용하기 🌹 (1) | 2022.04.02 |