위코드 기업협업에 참여하며 정리한 내용입니다.
아래 도식처럼 하위 컴포넌트에서 입력한 데이터가 위로 올라가 최종 view 에 반영해야 한다면 여러번 콜백함수를 거치게 된다. 아래 구조는 Editpanel 에 이벤트가 발생하는 버튼이 있어서, 그 하위에 EditpanelMain 에 버튼이 있어야 코드 가독성은 더 좋을 것이라는 피드백을 받았는데, 당장은 지금 구조로도 이해하기 급급하니 일단 지금 상태로 정리해둔다.
이 흐름을 어떻게 정리하면 좋을 까 생각해봤지만 정답은 없으니 일단 아래부터 차근차근 타고 올라가 본다.
inputTitle 은 텍스트를 입력받는 input 태그를 담은 컴포넌트다. 이 컴포넌트는 이곳 저곳에 중복으로 사용될 수 있다. 입려된 값을 콜백함수에 인자로 담아 전달해야 하므로, 프롭스로 onChange 콜백함수를 받는다. 전달해주는 곳에서는 onChange에 해당 인풋값을 관리하는 함수를 담는다. onChnage={해당 값 세팅하는 함수}
const InputTitle = ({
defaultTitle,
onChange, // 프롭스로 전달받은 콜백함수, 인풋태그의 onChange 와는 다른 것
}: {
defaultTitle: string;
onChange: (title: string) => void;
}) => {
const [inputTitleValue, setInputTitleValue] = useState( // 인풋값 관리
defaultTitle ? defaultTitle : ""
);
const onChangeHandler = (e: any) => { //
setInputTitleValue(e.target.value); // 인풋값을 state 로 관리하고
onChange(e.target.value); // 그렇게 관리한 값을 콜백함수에 담아서 전달.
};
return (
<input
type="text"
placeholder="제목을 입력하세요"
className="flex-1 text-xs text-dark_gray placeholder:text-dark_gray p-2.5 border border-middle_purple rounded-md w-full mb-2.5 "
onChange={onChangeHandler}
value={inputTitleValue} // value 값을 state 로 관리
/>
);
};
위 InputTitle 은 EditPanelMain 의 자식 컴포넌트다. 여기서 만든 콜백함수를 onChange 프롭스로 전달했던 것이고 그렇게 정보를 가지고 올라온 콜백함수가 여기서 실행된다. 이때 기존 데이터인 editInfo 의 값을 바꾸어 주어야 하므로 해당 객채 값을 참조하는 별도 값을 lodash 의 _.cloneDeep 으로 복사하고, 그 복사된 객체의 값을 바꾼 후 바꾼 객체를 데이터를 업세이트하는 함수로 전달한다. 다시 콜백이 이루어진다. updateEditInfo가 다시 정보를 가지고 올라간다.
interface functionProps {
editInfo: any;
updateEditInfo: (item: any) => void;
}
const EditPanelMain = ({ editInfo, updateEditInfo }: functionProps) => {
const onChangeMainTitle = (title: string) => { // 콜백함수로 전달받은 인풋값이 여기 title 로 입력
const newEditInfo = _.cloneDeep(editInfo); // 객체 복사
newEditInfo.main_section.main_title = title; // 입력된 값 반영해서 수정하고
updateEditInfo(newEditInfo); // 바뀐 객체 정보를 담아서 콜백으로 올림
};
return (
<div>
...
<div className="flex">
<div className="flex-1 mb-3">
<InputTitle defaultTitle="" onChange={onChangeMainTitle} /> // 여기서 프롭스 전달한 것
<InputColor />
</div>
</div>
</div>
);
};
EditPanel 은 EditPanelMain 의 부모 컴포넌트다. 아래에서는 focusId 에 따라 panel 컴포넌트를 선택해서 뿌려주는데 EditPanelMain 에 editInfo 와 updateEditInfo 가 프롭스로 전달된다. editInfo의 초기값에 기존 데이터 형식이 담겨있으니 이 값의 이름을 잘 확인해서 교체를 해주어야 한다. EditPanel 에는 이벤트가 발생하는 버튼이 있는데, 이 버튼을 누르면 그렇게 바뀐 입력값이 View 라는 부모 컴포넌트에 반영되어야 한다. 그래서 다시 한번 콜백으로.. 바뀐 editInfo 정보를 담아서 올려보낸다. 이 때 사용한 함수는 onApplyClickEvent 로 부모 컴포넌트에게 프롭스로 전달받은 것이다.
const EditPanel = ({ focusId, onApplyClickEvent }: ClickEventProps) => {
const [editInfo, setEditInfo] = useState<any>({
..
main_section: {
main_title: "위코드 벳플릭스",
main_title_color: "#eb0909",
sub_title: "글자를 바꾼다면",
sub_title_color: "#0900b5",
bg_image_url:
"https://images.unsplash.com/photo-1561037404-61cd46aa615b?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80",
},
});
const updateEditInfo = (updateInfo: any) => {
setEditInfo(updateInfo);
};
const EDIT_PANEL = [
{ id: "nav", panels: EditPanelNav },
{ id: "main", panels: EditPanelMain },
{ id: "imageSlide", panels: EditPanelImageSlider },
{ id: "leftTextRightImage", panels: EditPanelLeftRightImage },
{ id: "textCard", panels: EditPanelTextCard },
{ id: "introText", panels: EditPanelIntroText },
{
id: "leftTextRightMultiImages",
panels: EditPanelLeftTextRightMultiImages,
},
{ id: "textSlide", panels: EditPanelTextSlide },
{ id: "footer", panels: EditPanelFooter },
];
const focusEditPanel: any = EDIT_PANEL.find((item) => item.id === focusId);
return (
<div className="ml-5 ">
<div className="sticky top-8">
<div className="w-[500px] p-10 text-sm bg-white shadow-md ">
{focusId.length !== 0 ? (
<focusEditPanel.panels
editInfo={editInfo}
updateEditInfo={updateEditInfo}
/>
) : (
<EditPanelDefault />
)}
</div>
<button
className="w-[500px] mt-5 p-3 bg-main_purple text-white text-sm shadow-md rounded"
onClick={() => {
if (editInfo) {
onApplyClickEvent(editInfo); // 정제한 데이터 콜백으로 다시 던져주기
}
}}
>
적용하기
</button>
</div>
</div>
);
};
이제 View.txs 까지 올라와서 onApplyClickEvent 가 매개변수로 받은 데이터로 실행되면 되는데, 이때 전달받은 데이터의 id 값으로 구분이 되면 chagedValue.id 를 비교해 데이터를 수정해주는 것이 더 정확한 방식일 것 같다. 하지만,, 일단은 목으로 했던 것이가 해당 Id 값이 없었고, 선택된 것은 focusId 기준으로 데이터를 수정했다. 이 부분은 그냥 봐도 외부 값을 기준으로 조건을 걸어서 이상한 느낌이라 수정이 필요하다. 아무튼 그렇게 정제한 데이터를 기존 data 반영했고, useEffect 로 data 가 변경될 때마다 mainData 가 수정되도록 세팅했다.
const View = () => {
const [focusId, setFocusId] = useState("");
const [data, setData] = useState<any>(null);
const [mainData, setMainData] = useState<MainData>();
..
const onApplyClickEvent = (changedValue: any) => {
const newValue = _.cloneDeep(data);
const mainChangeValue = changedValue.main_section;
if (focusId == "main") {
newValue.main_section.main_title = mainChangeValue.main_title;
newValue.main_section.main_title_color = mainChangeValue.main_title_color;
newValue.main_section.sub_title = mainChangeValue.sub_title;
newValue.main_section.sub_title_color = mainChangeValue.sub_title_color;
newValue.main_section.bg_image_url = mainChangeValue.bg_image_url;
setData(newValue);
}
// if (changedValue.id === "main") {
// const value = changedValue as MainData;
// if (data) {
// newValue.main.main_title = value.main_title;
// newValue.main.main_title_color = value.main_title_color;
// newValue.main.sub_title = value.sub_title;
// newValue.main.sub_title_color = value.sub_title_color;
// setData(newValue);
// }
// }
};
useEffect(() => {
//json 데이터 변경시 처리해줘야할것들
//파싱해서 각각 state 분리해주거나
if (data) {
..
setMainData(data.main_section); // 메인 데이터 저장
..
}
}, [data]);
useEffect(() => { // 최초 목데이터 로딩
fetch("/DEFAULT_DATA.json")
.then((response) => {
return response.json();
})
.then((data) => {
setData(data); // 데이터 저장
});
}, []);
return (
<div className="flex justify-center pb-20 pt-10">
<div className="mr-10">
..
<div className="shadow-md cursor-pointer w-[800px]">
..
<ViewMain
data={mainData}
focusId={focusId}
sectionClick={sectionClick}
/>
..
</div>
</div>
<EditPanel focusId={focusId} onApplyClickEvent={onApplyClickEvent} /> // 요기서 프롭으로 전달
</div>
);
};
이렇게 적어보니 한번더 정리가 되긴 하는데, 아무래도 처음부터 다시 구조를 잡고 작성을 해야 할 것 같다.