개인 프로젝트를 하다가 서버의 데이터와 input의 value를 동기화해야 하는 상황을 만났다.(결론적으로는 두개의 상태를 만들었다) 처음 겪는 상황이라 고민을 많이 했다. 그리고 그 상황을 해결한 과정을 기록해보았다.
문제의 코드를 정리해보면 아래와 같았다.
value
라는 state로 관리하며 onChange 이벤트 핸들러에 () => setValue(e.target.value)
를 넣어서 현재 입력 값과 동기화되게 한다.<div>
태그 안에서 보여준다.여기서 문제는 이 value를 서버의 데이터와 동기화해야 한다는 것이었다.
위의 상황에서 떠오른 생각은 세가지가 있었다.
1. value
가 서버에서 가져온 값과 같아야 하는데, 하나의 value로 상태를 관리할 수 있을까?
2. value
를 서버의 값으로 업데이트시켜줘야 하나?
3. 상태를 두개 만들어서 input의 value와 div의 value를 다르게 관리해야 하나?
value
가 서버에서 가져온 값과 같아야 하는데, 하나의 상태만으로 답변들의 상태를 관리할 수 있을까?처음에는 하나의 상태로 관리하려고 했다. 즉 input의 속성으로 들어가는 value만 쓰려고 했다. 하지만 이렇게 관리하면 상태에 하나의 답변만 저장할 수 있었다. 실제로 이렇게 해봤을 때 해결되지 않았다.
value
를 서버의 값으로 업데이트시켜줘야 하나?또, 이제는 서버의 상태와 value
를 동기화시켜야 하겠다는 생각을 했다. 하지만 하나의 상태로 관리를 하면 value는 onChange 이벤트에서도 업데이트되고 submit 이벤트에서도 업데이트된다. input의 value가 onChange가 아니라 다른 이벤트에 대해서 업데이트되는 것을 본 적이 없어서 이렇게 해도 되는지 의심이 들었다.
그래서 마지막으로 생각한 것은 상태를 두개 만들어서 관리하는 것이었고 결국 이 방법으로 해결했다.
상태 1: input의 value
상태 2: 서버에서 가져오는 답변들의 상태
최상단 컴포넌트 App에answers
라는 상태를 만들었다. 그리고 syncTodoItemWithFirestore
함수 안에서 answers
를 업데이트했다.
syncTodoItemWithFirestore
함수는 CRUD를 할 때 서버와 클라이언트의 데이터를 동기화시키는 함수이다.
이 함수에 setAnswers()
를 넣어서 매번 동기화되게 했다.
// 📄 App.jsx
const [answers, setAnswers] = useState([]);
const syncTodoItemWithFirestore = () => {
setAnswers(
'서버의데이터'.map((todo) => {
return {
id: todo.id,
answer: todo.answer,
};
}),
);
});
하지만!! 꼭 이 위치에 넣어야만 하는지는 잘 모르겠다.
그리고 TodoItem 컴포넌트에서는 아래와 같이 input과 텍스트 부분에 들어갈 상태를 따로 분리해서 넣었다.
input에 들어가는 상태는 input에 입력되는 값 그 자체이고, 텍스트 부분에 들어가는 상태는 서버에서 가져오는 데이터이다. 이 데이터는 input에서 입력한 값이 전송되었을 때 만들어진다.
// 📄 TodoItem.jsx
const [value, setValue] = useState('이곳에 답변을 작성해 주세요.');
let answer = answers.find((answer) => answer.id === todo.id).answer;
{isEditClicked ? (
<input
// 1. input에는 value를 넣고
value={value}
onChange={(e) => setValue(e.target.value)}
/>
) : (
// 2. text 부분에는 answer를 넣는다
<div>{answer}</div>
)}
중간에 엄청난 실수를 해서 많은 시간이 소요됐다. 바로 상위 컴포넌트에서 prop을 안넘겨줬는데, 하위 컴포넌트에서 이 prop을 쓴 것이다.
이 prop은 undefined로 가져와졌으며, 에디터에 오류가 뜨지 않았다.
자세한 상황은 아래와 같다.
const Todo = () => {
// 바로 아래 줄처럼 prop으로 answers를 넘겨야 하는데 안넘김
// return <TodoItems answers={answers} />;
return <TodoItems />;
};
const TodoItems = ({ answers }) => {
// answers를 사용한다
}
왜 콘솔을 찍어보면 App, Todo에는 answers에 값이 들어가있는데 TodoItems와 TodoItem에서 콘솔을 찍어보면 모두 undefined가 나오는지 궁금했는데, prop을 안넘겨주어서 그런 것이었다🤣
오류가 뜨지 않고, 또 prop을 안넘겼는데 썼을 때 undefined가 된다는 사실을 새롭게 알게 되었다.
오늘 겪은 문제와 큰 관련은 없을 수도 있지만, 상태 관리 라이브러리에 대해 큰 관심이 생겼다.
왜냐하면 매번 useEffect를 써서 데이터를 가져와야 하는 것, 그리고 서버의 데이터와 클라이언트쪽의 상태를 동기화하는 부분에서 무언가 번거로웠다.
예를 들어 서버의 목표 데이터와 화면의 목표 데이터를 동기화하기 위해 syncTodoItemWithFirestore
함수를 만들어서 CRUD를 할 때마다 매번 이 함수를 썼다. 정확히는 모르지만 이 부분을 개선할 수 있지 않을까? 하는 생각이 든다.
그리고 이런 서버 데이터의 처리뿐만 아니라, 상태 관리가 너무 복잡하고 어려웠던 것 같다. 그래서 전역으로 관리하면 정말 좋겠다는 생각을 했다!!
정확히 어떤 점이 개선될지는 앞으로 배우면서 알아가야겠다.
사실 오늘 포스팅에서 다룬 내용이 맞는지 잘 모르겠다. 하지만 하루종일 고민했고 해결하려고 노력했기 때문에 기록으로 남기고 싶었다. 무엇보다, Redux, React Query같은 라이브러리를 사용해보고 싶은 큰 동기가 되어주어서 그것도 남기고 싶었다!
앞으로 상태관리 라이브러리를 배우고, 이 프로젝트에 적용해볼 것이다. 팀 프로젝트에서 Redux를 쓸 거라서, 두군데에 적용해보면 공부에 큰 도움이 될 것 같다.
포스팅을 다 쓰고 개선할 점을 찾았다.
syncTodoItemWithFirestore
라는 함수 안에서 setAnswers를 없애고 setTodos만 남긴다const syncTodoItemWithFirestore = () => {
const q = query(
collection(db, "todoItem"),
where("userId", "==", currentUser),
orderBy("createdTime", "desc")
);
getDocs(q).then((querySnapshot) => {
const firestoreTodoItemList = [];
querySnapshot.forEach((doc) => {
firestoreTodoItemList.push({
id: doc.id,
text: doc.data().text,
answer: doc.data().answer,
isFinished: doc.data().isFinished,
createdTime: doc.data().createdTime,
goalId: doc.data().goalId,
userId: doc.data().userId,
});
});
// 남길 것
setTodos(firestoreTodoItemList);
// 없앨 것
setAnswers(
firestoreTodoItemList.map((todo) => {
return {
id: todo.id,
answer: todo.answer,
};
})
);
});
};
const TodoItem = ({ id, answer }) => {
const [value, setValue] = useState(answer);
useEffect(() => {
setValue(answer);
}, [answer]);
return (
<>
{isEditClicked ?
<input value={value} onChange={(event) => setValue(event.target.value)} />
: <div>{value}</div>
</>
);
};