티스토리 뷰

개요

  • 스택오버플로우를 클론 코딩하는 Pre-project 팀 과제를 진행하던 중, 질문 상세 페이지와 수정 페이지, 답변 수정 페이지에서 중복되는 코드가 많이 발생하였다.

 

질문 등록 페이지

AskQuestion


  
function AskQuestion() {
const [titleValue, setTitleValue] = useState<string>("");
const [bodyValue, setBodyValue] = useState<string>("");
const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setBodyValue("");
};
return (
<S.Section>
<S.QuestionLayout>
<S.Header>
<S.QuestionTitle>Ask a public question</S.QuestionTitle>
<S.TitleImage />
</S.Header>
<QuestionNotice />
<S.FormLayout onSubmit={e => submitHandler(e)}>
<QuestionTitle setTitleValue={setTitleValue} />
<QuestionBody setBodyValue={setBodyValue} />
<S.ButtonLayout>
<button type="submit">Post your question</button>
<Link to="/">
<button>Discard draft</button>
</Link>
</S.ButtonLayout>
</S.FormLayout>
</S.QuestionLayout>
</S.Section>
);
}
export default AskQuestion;
  • 질문 등록 페이지에서는 제목과 내용에 들어갈 state와 submitHandler를 가지고 있다.
  • 그리고 각각 QuestionTitleQuestionBody 컴포넌트에 값의 상태를 변화시킬 setState 함수를 props로 내려준다.

 

컴포넌트

QuestionTitle


  
<S.InputTitle
type="text"
placeholder="e.g. Is ther R function for finding the index of an element in a vector?"
onChange={e => setTitleValue(e.target.value)}
autoFocus
/>
  • onChange로 값이 바뀔 때마다 setState 함수를 이용해 저장하게 된다.

QuestionBody


  
return (
<S.Container>
<S.InputBodyLayout>
<S.SubHeading>Body</S.SubHeading>
<S.SubContent>
The body of your question contains your problem details and results.
Minimum 220 characters.
</S.SubContent>
<TextEditor setBodyValue={setBodyValue} />
</S.InputBodyLayout>
<QuestionTip TipTitle={TipTitle} TipText={TipText} />
</S.Container>
);
}
  • 질문의 내용이 들어가는 QuesitonBody 컴포넌트에서는 Input 타입이 아니라 텍스트 에디터(React-Quill)를 사용하는데 텍스트 에디터는 재사용을 많이 할 컴포넌트이기 때문에 따로 분리하였다.
  • 그래서 setState 함수를 다시 TextEditor 컴포넌트에 내려주게 된다.

TextEditor


  
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
interface EditorProps {
setBodyValue: React.Dispatch<React.SetStateAction<string>>;
}
function TextEditor({ setBodyValue }: EditorProps) {
const onChangeHandler = (e: any) => {
setBodyValue(e);
};
return <ReactQuill onChange={onChangeHandler} style={{ height: "210px" }} />;
}
export default TextEditor;
  • TextEditor 컴포넌트에서는 QuestionTitle 컴포넌트에서처럼 값이 바뀔 때마다 setState를 통해 새로운 값을 저장하게 된다.

 

문제점

  • 질문 등록을 할 때 위와 같은 로직을 따르는데 질문 수정, 답변 등록, 답변 수정을 할 때 각 페이지마다 위와 마찬가지로 useState를 그대로 남발하게 된다.

QuestionEdit


  
function QuestionEdit() {
const [titleValue, setTitleValue] = useState<string>("");
const [bodyValue, setBodyValue] = useState<string>("");
const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setTitleValue("");
setBodyValue("");
};
return (
<S.FormLayout onSubmit={e => submitHandler(e)}>
<div>
<S.TitleBox>
<S.SubHeading>Title</S.SubHeading>
<S.InputTitle
type="text"
placeholder="e.g. Is ther R function for finding the index of an element in a vector?"
value={titleValue}
onChange={e => setTitleValue(e.target.value)}
/>
</S.TitleBox>
<div>
<S.SubHeading>Body</S.SubHeading>
<TextEditor setBodyValue={setBodyValue} />
<S.Viewer
dangerouslySetInnerHTML={{
__html: bodyValue,
}}
/>
</div>

 

Answer(등록)


  
function Answer({ answerData }: Props) {
const [bodyValue, setBodyValue] = useState<string>("");
const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setBodyValue("");
};
return (
.
.
.
<S.FormLayout>
<S.FormBox onSubmit={e => submitHandler(e)}>
<div>Your Answer</div>
<TextEditor setBodyValue={setBodyValue} />
<S.ButtonLayout>
<button type="submit">Post your answer</button>
</S.ButtonLayout>
</S.FormBox>
</S.FormLayout>

 

AnswerEdit


  
function AnswerEdit() {
const [bodyValue, setBodyValue] = useState("");
const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setBodyValue("");
};
return (
.
.
.
<div
dangerouslySetInnerHTML={{
__html: question.content,
}}
/>
<S.Grippie></S.Grippie>
</div>
<S.SubHeading>Answer</S.SubHeading>
<TextEditor setBodyValue={setBodyValue} />
<S.Viewer
dangerouslySetInnerHTML={{
__html: bodyValue,
}}
/>
  • 이러한 중복과 낭비를 없애기 위해 useInput이라는 CustomHook을 직접 만들어보기로 하였다!

 

UseInput 개발

  • 훅을 만들기 전에, 중복이 되는 부분과 가공이 되고난 뒤 필요한 부분이 무엇인지를 파악해야 했다.

 

중복 되는 부분 파악

  • 먼저 값의 상태를 변화시키는 useState 그리고 state 값을 변화시키는 changeHandler 가 useState가 위치하는 훅에 있으면 좋을 것 같다는 생각을 하였다.

 

필요한 부분 파악

  • useInput이라는 훅에서 가공되어 나온 새로운 값인 value가 필요할 것이고 각 input에서 onChange 속성을 사용할 것이므로 changeHandler 자체가 필요할 것 같다는 생각을 하였다.

 

useInput


  
import { useState, useCallback, ChangeEvent } from "react";
type UseInputProps<T> = [
value: T,
changeHandler: (e: ChangeEvent<HTMLInputElement>) => void,
reset: () => void
];
function useInput<T>(initialValue: T): UseInputProps<T> {
const [value, setValue] = useState<T>(initialValue);
const changeHandler = (e: ChangeEvent<HTMLInputElement> | any) => {
if (e.target) setValue(e.target.value);
else setValue(e);
};
const reset = useCallback(() => setValue(initialValue), [initialValue]);
return [value, changeHandler, reset];
}
export default useInput;
  • value값은 string만 들어올 것이 확실하지만 혹시나 scope가 확장되어서 다른 타입을 받아야할 수도 있기 때문에 제네릭 <T> 로 주었다.
  • 그리고 개발을 하며 알게된 점은, submitHandler에서 submit을 한 뒤에 textArea의 값을 비워주기 위해 setState("") 를 하는 부분이 있는데 더이상 컴포넌트에서 State를 사용하지 않기 때문에 이 또한 훅에서 처리해주어야 했다.
  • 그래서 값을 초기값으로 되돌리는 reset 이라는 함수 또한 훅에 추가하였다!
  • 또한 텍스트 에디터인 React-quill은 onChange 속성에서 다른 input값들과 달리 target.value가 아닌 value를 그대로 반환하기 때문에 조건문을 사용해서 분기하였다.

 

askQuestion


  
function AskQuestion() {
const [titleValue, changeTitleHandler, TitleReset] = useInput("");
const [bodyValue, changeBodyHandler, BodyReset] = useInput("");
const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
TitleReset();
BodyReset();
};
return (
.
.
<S.FormLayout onSubmit={e => submitHandler(e)}>
<QuestionTitle
changeHandler={changeTitleHandler}
titleValue={titleValue}
/>
<QuestionBody
changeHandler={changeBodyHandler}
bodyValue={bodyValue}
/>
)
  • 더이상 useState를 사용하지 않고 useInput에서 받는 값과 핸들러를 그대로 내려준다.
  • 그리고 submit 후 useInput 안에 있는 reset 함수를 호출해서 텍스트창을 비워주게 된다.

 

QuestionTitle


  
function QuestionTitle({
changeHandler,
titleValue,
}: TitleProps) {
return (
.
.
.
<S.InputTitle
type="text"
placeholder="e.g. Is ther R function for finding the index of an element in a vector?"
onChange={changeHandler}
value={titleValue}
autoFocus
/>
  • 값이 바뀌면 useInput 안에 있는 핸들러를 호출해서 값을 변화시키고 그 안에서 가공된 value를 그대로 받아서 업데이트하게 된다.

 

개선해야 되는 점

  • 질문이나 답변을 수정하기 위해 Edit 버튼을 눌렀을 때 사용자UX 측면에서 기존에 등록했던 내용이 초깃값으로 그대로 노출이 되어야 한다.
  • 그래서 useInput을 호출할 때 initialValue를 주었던 것인데
  • 문제는 처음에 렌더링이 될 때 데이터를 받아와서 그 데이터를 그대로 useInput의 초깃값으로 주어도 수정 작업에 의해 changeHandler가 호출되지 않는 이상 그 자체로는 useInput이 호출되지 않기 때문에 기존 내용이 노출되지 않는다.

 

 


  
function QuestionEdit() {
const [initialTitle, setInitialTitle] = useState("");
const [initialBody, setInitialBody] = useState("");
const [titleValue, changeTitleHandler, titleReset] = useInput(initialTitle);
const [bodyValue, changeBodyHandler, bodyReset] = useInput(initialBody);
const navigate = useNavigate();
const { id } = useParams();
useEffect(() => {
axios.get(`/api/questions/${id}`).then(res => {
setInitialTitle(res.data.data.title);
setInitialBody(res.data.data.content);
titleReset();
bodyReset();
});
}, [initialTitle, initialBody]);
  • 그래서 또 다시 useState를 사용해서 데이터를 받아온 뒤에 그 데이터를 초기값으로 주고 reset 함수를 실행함으로써 useInput을 호출하도록 하였다.

 

  • 일단 해결 자체는 되었는데 useState의 사용이 단순히 초기값을 주기위한 용도로 사용되었다는 점이 마음에 걸린다 ㅠㅠ
  • 더 좋은 방법이 있을 것 같아서 어떻게 개선해야 할지 고민이 된다고 할 수 있겠다.
반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함