6. Table of Contents
중첩된 배열 오브젝트(Nested Array Object) 형태의
폼 데이터(Form Data)를 다루며 마주한 문제와 해결에 대한 이야기
1. 미션
2. 첫 번째 문제: 복잡성
3. 두 번째 문제: control
4. 세 번째 문제: 유효성 검사&불변성
5. 내가 배운 것들
7. Mission
Hey Owen~ 우리가 새로운 프로젝트를 하려 함.
영유아 환자들이 병원에서 진료를 받기 전에 미리
작성할 수 있는 사전 문진 기능을 개발해야 해. PO
17. Mission
폼 데이터란?
사용자로부터 입력 받을 수 있는
데이터를 받아서 처리하는 방법
onClick, onSubmit, onChange 등
이벤트 리스너를 통해 입력 받은 값에
대한 유효성 검사가 가능
각각의 input 태그에 id 값이 있어서
key-value 형태의 이터러블 구조
18. Mission
구글 폼과 비슷한 데이터 구조
각각의 필드는 폼 데이터 형태로 값을 받아줄
수 있음
예를 들어 1번 섹션의 1번 질문의 1번 답을
찾고 싶다면
19. Mission
구글 폼과 비슷한 데이터 구조
각각의 필드는 폼 데이터 형태로 값을 받아줄
수 있음
예를 들어 1번 섹션의 1번 질문의 1번 답을
찾고 싶다면
medicalQuestionnaireState[0]
.children[0]
.children[0]
.value = ‘Hello Answer’;
49. Solution 3-1
zod resolver로 스키마 유효성 관리
• Input data의 타입이 무엇인지 검증
• 갯수 제한, 에러 메시지 추가
• 유니온 타입 형태의 유효성 검증도 가능
50. Problem 3-2
불변성(Immutability)
데이터가 한 번 생성이 되면 그 뒤에는 변하지 않는 성질
원시 타입(Primitive Type)은 불변성이 항상 유지되지만,
참조 타입(Reference Type)은 불변성이 항상 유지되지 않음
일반적으로 Object.assign()이나 스프레드 연산자 { … }를 통해 해결
66. What I Learned
문제 3.
유효성 검사와 불변성 관리가
제대로 되지 않음
해결 3.
유효성 검사 : zod resolver와 reValidateMode 옵션,
불변성 관리 : immer.js 도입을 통해 해결
67. What I Learned
PO와 협업하며 배운 점
일정은 가능한… 최대한 길게 잡아놓고,
차라리 개발이 빨리 끝나면 테스트를 꼼꼼히 하자.
백엔드 개발자와 협업하며 배운 점
백엔드 개발자와 API나 데이터 구조에 대해 이야기를 할 때 더 적극적으로
프론트엔드의 입장을 말한다.
프론트엔드 개발자와 협업하며 배운 점
동료들의 도움을 적극적으로 구하자. 내가 쓰는 도구를 나보다 잘 쓰는 동료가
있다면, 찾아가서 배우고 레버리지로 성장하자.
68. 참고자료
● MDN
● React-hook-form 공식 문서
● Zod 공식 문서
● immer.js 공식 문서
● Reddit
● Mutability vs Immutability in JavaScript (FreeCodeCamp)
● Zod vs Yup vs Joi vs io-ts for Creating Runtime TypeScript
Validation Schemas (Egghead.io)
71. 네트워킹 질문
- 나에게 할당된 개발 업무를 하다가 혼자서 도저히 해결이 안
되는 순간, 어떻게 주변의 도움을 받아 문제를 해결할 수 있을까요?
- 주니어 개발자가 레버리지를 통해 성장할 수 있는 방법에는 어떤
것들이 있을까요? (e.g. 테오콘 참석하기 등)
Editor's Notes
안녕하세요. <복잡한 오브젝트를 우아하게 처리하기>라는 주제로 발표를 맡게 된 오웬입니다. 만나서 반갑습니다.
세션을 시작하기 전에 간단한 제 소개를 먼저 해볼께요.
제 이름은 오웬이고 저를 한 마디로 어떻게 소개할까 고민하다가 '레버리지로 성장하는 개발자’ 라는 수식어를 붙여 보았습니다.
이제는 어디 가서 주니어라고 하면 안되는 연차가 되어 버렸지만.. 불과 최근까지만 해도 저는 성장에 진심인 주니어 웹 프론트엔드 개발자였습니다.
하지만 주니어가 성장할 수 있는 기회를 찾는 건 생각보다 쉽지 않았습니다. 저도 여러 시행착오를 거쳤고, 깨달은 결론은
‘혼자 힘으로 성장하려 하지 말고, 주변의 도움을 받자!’ 였습니다. 마치 우리가 투자를 받아 스타트업을 키우고, 대출을 받아 집을 사는 것 처럼 말이죠.
저는 굿닥이라는 헬스케어 스타트업에서 리액트 기반의 웹 프론트엔드와 리액트 네이티브 기반의 모바일 앱 개발을 하고 있으며, 자세한 사항은 제 블로그, 깃허브, 링크드인 등에 자세히 적어 놓았으니 궁금하신 분들은 확인해 주시면 됩니다.
그러면 본격적으로 저의 발표를 시작해 보도록 하겠습니다.
이번 발표에서 가장 중요한 키워드는 오브젝트입니다.
오브젝트는 자바스크립트의 데이터 타입 중 하나이며, key-value 형태로 복잡한 엔티티들을 다룰 때 사용합니다.
아마 여러분은 각자 하고 계시는 프로젝트에서 오브젝트를 다뤄본 경험이 한 번씩은 있으실 것이라 생각합니다.
한 분 한 분 다 여쭤볼 수는 없지만, 이번 발표를 들으면서 다음 질문을 스스로에게 해 보시면 좋을 것 같아요.
여러분은 지금 하고 계신 프로젝트에서 어떠한 형태의 오브젝트를 다루고 계신가요?
그리고 그 과정에서 어떠한 어려움을 가지고 계신가요?
이번 발표는 몇 개월 전 제가 회사에서 업무를 하면서 다소 복잡한 오브젝트를 다루며 경험했던 개발 이야기입니다.
오늘 저의 발표가 여러분들이 이러한 질문에 대하여 가지고 있는 고민들에 대해 조금이나마 도움이 되기를 바라며, 발표를 시작해 보도록 하겠습니다.
오늘 발표 주제와 목차는 다음과 같습니다.
제가 중첩된 배열 오브젝트 형태의 폼 데이터를 다루며 마주한 문제와 그 해결에 대한 이야기를 해 보려고 해요,
회사에서 제가 부여받은 미션이 무엇이었고,
그 미션을 해결하는 과정에서 제가 마주했던 문제를 세 가지 정도 공유해 보고자 합니다.
그리고 그 세 가지 문제를 고민하고 해결하며 제가 배운 것들에 대해서 마지막으로 공유를 하고 발표를 마치도록 하겠습니다.
때는 바야흐로 올해 상반기 어느 날.
저희 팀의 리더이신 PO로부터 새로운 프로젝트를 시작할 것이라는 이야기를 듣습니다.
이 프로젝트를 간단히 소개를 하면, 영유아 환자들이 병원을 가기 전에 사전 문진표를 작성해야 하는데, 그걸 지금은 다 종이로 수기로 받고 있고 이걸 모바일 앱에서 받을 수 있게 디지털로 전환하자는 내용이었습니다.
저는 당시 3년차 개발자로 회사 일에 큰 흥미를 느끼지 못하던 권태기의 직장인1이었기에
별 감흥 없이, 해당 작업을 하기 위한 구체적인 요구사항을 파악하기 시작했습니다.
PO는 열심히 요구사항에 대해서 설명을 해 주셨고, 딱 들어 보았을 때 이미 포맷이 다 있고 크게 어려운 작업은 아닐 것 같다고 생각했어요.
저는 아무 생각 없이 메모 하면서 받아 적었죠.. 그런데...
누가 그러던가요? 한국 말은 끝까지 들어야 한다고 말이죠.
마지막에 이 문진 기능을 병원에서 직접 CRUD가 가능하게도 만들고 싶다고 하시더라구요?
처음에는 사실 이 말이 그렇게 크게 느껴지진 않았습니다. 많이 해 본 작업이었고, 특별히 어려운 점은 없어 보였기 때문이죠.
금방 가능하냐고 물어보셔서, 저는 아무 생각 없이.
이렇게 대답을 해 버렸죠. ㅎㅎ
여기서 여러분께 제가 드리고 싶은 한 가지 말씀은…
일정은 아무리 여유 있어 보이더라도 일단 최대한 늘려서 받을 수 있으면 늘려서 받으라는 것입니다.
프로젝트가 시작하고, 백엔드 개발자 분과 API 스펙 공유 미팅을 하던 중
생각보다 데이터 스키마가 복잡해졌다는 이야기를 듣습니다.
저는 이런 결정에 대해서 그 당시 다소 수동적으로 작업을 진행했던 감이 지나고 나 보니 없지 않아 있더라구요.
이러한 의사결정이 프론트엔드 쪽에서 여러 가지 고민거리를 많이 만들 것이라고는 그 당시에 크게 생각하지 못하고 저는 작업을 시작했습니다.
그래서 저에게 주어진 미션은 한 문장으로 정리하면 3 depths의 오브젝트 배열 CRUD를 만들기 였습니다.
여러분들의 이해를 돕기 위해 실제로 나온 서비스를 먼저 보면 다음과 같습니다.
각 문진표 별로 제목이 있고, 섹션이 여러개 있으며 각 섹션 아래에 질문이 여러개가 있고 그 질문 아래에도 답변이 여러개가 있을 수 있는 구조였어요. 섹션과 질문, 답변은 자유롭게 추가, 수정, 삭제가 가능하고 각각의 섹션의 순서도 바꾸어 줄 수 있게 했습니다.
먼저 Form Data에 대해서 개념이 생소하신 분이 계실 수 있을 것 같아 간단하게 짚고 넘어가 보겠습니다.
실제 작성된 데이터 구조를 보면 다음과 같이 나타낼 수 있습니다. 불필요한 복잡한 부분은 걷어내고 추상화 해서 나타내 보았어요.
여러분들이 많이 쓰시는 서비스 중에서는 아마 구글 폼과 가장 유사하지 않을까 생각이 들었습니다. 각각의 뎁스별로 필드가 여러개 있는데 그 필드는 input form 형태로 데이터를 받아주게 됩니다.
인덱스를 기반으로 원하는 값을 찾아갈 수 있는데 3 depth 이므로 최대 3개까지 인덱스가 필요할 수 있는 상황이었습니다.
예시처럼 1번 섹션의 1번 질문의 1번 답을 찾고 싶다면..
이렇게 말이죠.
이렇게 찾아 주면 됩니다.
react-hook-form에 대해서 잘 모르시는 분들을 위해 간단히 설명해 드리면,
react에서 사용할 수 있는 form 라이브러리입니다.
uncontrolled form 방식으로 form 데이터를 관리하여 컴포넌트 리렌더링 측면에서 최적화가 되어 있다는 장점이 있습니다.
그리고 subscription 방식으로 데이터를 관리하고 있어서, form state를 잘 관리해 주고 컴포넌트에서는 잘 바라보게만 하면 업데이트가 잘 됩니다.
form 과 관련된 여러 이벤트들 focus, blur, click 등등에 대해서 충분한 인터페이스를 제공하고 있고 이를 통해 로직을 간편하게 짤 수 있습니다.
작업을 시작하고 처음에는 순조로웠습니다. 하지만 금방 저는 첫 번째 문제를 마주하게 됩니다. 그것은 바로
제가 해당 로직을 직저부 다 만들어 주려다 보니 로직이 너무 복잡해지는 문제였습니다,
예를 들어 이 배열의 첫 번째 뎁스의 특정 인덱스에 해당하는 오브젝트를 삭제하는 로직을 만든다고 가정해 보겠습니다.
그러면 저는 이 인덱스를 인자로 받아서 배열이기에 slice 메서드를 가지고 삭제해 줄 인덱스 앞뒤로 자른 후 합쳐서 다음과 같이 state를 업데이트 해 주는 방식을 선택했습니다.
아마 많은 분들이 처음 딱 짜신다면 이와 비슷한 방식으로 짜셨을 것이라 생각합니다.
첫 번째 뎁스는 어렵지 않았습니다. 문제는 두 번째 뎁스였습니다.
인덱스를 두 개를 받아와야 했고, 첫 번째 인덱스로 배열에서 오브젝트를 한 번 찾고, 그 다음 두 번째 인덱스로 한 번 더 찾은 뒤 아까 사용했던 slice 메서드를 사용해 주었습니다.
제 기준에 이 로직은 복잡했고, 동료가 이 코드를 본다면 바로 이해하기 힘들 것 같다고 판단했습니다.
그리고 지금 depth 2인데 만약 depth 3까지 가야 한다면 유지보수 하기가 너무 힘들 것이 벌써 보였습니다.
업데이트 로직도 상황은 비슷했습니다.
첫 번째 인덱스와 두 번째 인덱스로 각각 오브젝트를 찾아간 다음
바꾸려는 property와 value를 바꾸어 주고
다시 두 번 오브젝트를 배열에 갈아끼우는 식으로 해야 하죠.
저는 이 문제를 어떻게 더 단순하게 해볼 수 있을지 고민해 보았습니다.
제가 state 안에서 로직을 지지고 볶는 것에는 한계가 있었습니다. 그래서 저는 저희 팀에서 이전부터 사용하고 있던 react-hook-form 공식문서를 다시 한 번 읽어보기 시작했습니다.
제가 혼자서 앓는 소리를 하는 걸 보던 옆자리에 앉은 팀 동료의 한 마디
이 공식문서에서 저는 useFieldsArray라는 API를 발견하게 됩니다. 이 API는 공식 문서에 나온 설명을 그대로 읽어보면, 필드 배열의 동작을 위한 커스텀 훅이라고 되어 있습니다.
저는 바로 적용을 해 보았고, 코드가 정말 간결하고 명료해 지는 결과를 얻게 되었습니다. 문제가 해결이 되자 저는 작업에 속도가 다시 나기 시작했고 이렇게 금방 끝낼 수 있을 거라 굳은 확신을 가지게 되었습니다.
그러나...
저는 이 작업을 하면서 세 가지 정도의 문제를 마주하였습니다.
얼마 지나지 않아 두 번째 문제를 발견하게 됩니다. 그것은 바로 control이 제대로 동작하지 않는 이슈였습니다.
여기서 문제가 발생했는데,
중첩된 form data를 하나의 useFieldsArray로 관리하니 control이 하위 뎁스의 컴포넌트에서 정상적으로 동작하지 않는 이슈가 있었습니다.
여기서 문제가 발생했는데,
중첩된 form data를 하나의 useFieldsArray로 관리하니 control이 하위 뎁스의 컴포넌트에서 정상적으로 동작하지 않는 이슈가 있었습니다.
control이 무엇인지 간단하게 설명을 해 보도록 하겠습니다.
react-hook-form은 앞서 말씀드린 것 처럼 데이터를 중앙에서 관리하면서 그 데이터의 조작을 control이라는 useForm API의 프로퍼티를 가지고 하게 됩니다.
그래서 이 control을 useFieldsArray에서도 쓸 수가 있고, 오른쪽 예제 코드처럼 컴포넌트에서 Controller 컴포넌트에 control 필드로 넣으면 react-hook-form의 데이터가 바뀌는 로직을 맡길 수 있습니다.
제가 이 당시 구현했던 방식은 fields와 control을 context state에서 첫 번째 뎁스인 섹션 컴포넌트로 내려주고, 그걸 섹션 컴포넌트가 받아서 핸들링을 한 후 그 자녀 컴포넌트인 질문 컴포넌트로 내려주게 했습니다. 비슷한 방식으로 답변 컴포넌트까지 context의 중앙화된 fields와 control을 내려 받도록 구현했습니다. 이렇게 하니까 정상적으로 동작이 되지 않더라구요.
이 문제를 해결하기 위해 참 많은 시간과 노력을 들였던 기억이 납니다..
제가 이 당시 구현했던 방식은 fields와 control을 context state에서 첫 번째 뎁스인 섹션 컴포넌트로 내려주고, 그걸 섹션 컴포넌트가 받아서 핸들링을 한 후 그 자녀 컴포넌트인 질문 컴포넌트로 내려주게 했습니다. 비슷한 방식으로 답변 컴포넌트까지 context의 중앙화된 fields와 control을 내려 받도록 구현했습니다. 이렇게 하니까 정상적으로 동작이 되지 않더라구요.
이 문제를 해결하기 위해 참 많은 시간과 노력을 들였던 기억이 납니다..
이 문제를 해결한 방법은 고민의 시간과 무게에 비해서 비교적 간단했습니다.
기존에 context에서 useFieldsArray를 관리하고 여기서 fields를 섹션 컴포넌트로 내려주고, 그 fields의 섹션 값이 있는데 그 값의 children을 섹션 컴포넌트에서 또 다른 useFieldsArray API를 통해 depth2Fields 이런 식으로 새로 만들어 주었습니다. 그걸 질문 컴포넌트로 내려주었고 비슷한 방법으로 답변 컴포넌트로 갈 때도 depth3Fields를 새로 만들어 주었습니다.
이 문제를 해결하면서 useFieldsArray를 만든 의도가 해당 배열 안의 단순한 오브젝트에서 값을 CRUD하게 하기 위한 의도로 만들었다는 것을 알게 되었습니다.
저는 이 작업을 하면서 세 가지 정도의 문제를 마주하였습니다.
이제 진짜 되는가 싶더니 또 여러 가지 문제들이 저를 가로막았습니다.
바로 유효성 검사와 불변성에 관련된 이슈였습니다.
하나의 문진 데이터는 수십 개 이상의 input이 있고, 이 가운데 값이 계속해서 바뀌게 됩니다. 그리고 값이 바뀔 때 마다 유효성 검사를 진행해 주어야 합니다.
하나의 문진 데이터는 수십 개 이상의 input이 있고, 이 가운데 값이 계속해서 바뀌게 됩니다. 그리고 값이 바뀔 때 마다 유효성 검사를 진행해 주어야 합니다.
여기에서 하나의 데이터가 바뀔 때 다른 데이터까지 같이 유효성 검사를 해 주어야 하는 일이 발생했습니다. 예를 들면 같은 값을 가질 수 없는 여러 개의 인풋이 있는 경우 중복 체크가 필요한 상황이 있었습니다.
유효성 검사를 어떻게 깔끔하게 처리할지에 대한 고민을 하면서 또 react-hook-form 공식문서를 유심히 보게 되었고, 두 가지 정보를 바탕으로 해결할 수 있었습니다.
첫 번째는 useForm의 reValidateMode 였습니다. 이 값은 값이 제출될 때 마다 유효성 검사를 해주는 옵션이었는데요. 이 이벤트 리스너를 onChange로 설정하면 값이 바뀔 때 마다 모든 필드에 대해서 유효성 검사를 해줄 수 있었습니다.
React-hook-form 은 다양한 스키마 도구를 지원
그리고 react-hook-form과 궁합이 잘 맞는 zod라는 스키마 라이브러리를 도입했습니다.
이런이런 특징
zod 에서 react-hook-form에 제공하는 zod resolver를 통해 여러 중첩된 오브젝트의 필드별로 상이한 유효성 검사 로직을 하나의 스키마에서 깔끔하게 관리를 할 수 있게 되었습니다.
진짜 끝나나 싶었는데, 또 마지막에 한 가지 체크할 부분이 더 있었습니다.
바로 불변성과 관련된 부분이었습니다.
여러분들이 잘 아시는 것처럼 spread operator로 값을 복사하면 얕은 복사가 이루어지게 됩니다.
우리가 일반적으로 오브젝트를 다룰 때 뎁스가 그리 깊지 않은 경우가 많아서 그런 경우에는 문제가 되지 않습니다.
하지만 뎁스가 두 개, 세 개 깊어진다면 이야기가 달라집니다.
불변성이 유지가 되지 않고, 사이드 이펙트가 발생할 수 있으며, 이는 프로그램이 예측 가능성을 유지할 수 없기 때문에 해결을 하고 가야 합니다.
하지만 뎁스가 두 개, 세 개 깊어진다면 이야기가 달라집니다.
불변성이 유지가 되지 않고, 사이드 이펙트가 발생할 수 있으며, 이는 프로그램이 예측 가능성을 유지할 수 없기 때문에 해결을 하고 가야 합니다.
여러가지 방법이 있겠지만, 저는 이 부분에서는 immer라는 라이브러리의 도움을 받아 해결했습니다. immer는 상태에서 불변성을 유지할 수 있게 도와주는 라이브러리입니다.
각각의 장단점 소개
immer를 적용해 주니 코드가 불변성을 유지할 수 있게 되었고, 덤으로 더 간결해지고 깔끔해 졌습니다.