복잡한 스타일의 input 컴포넌트 재사용하기

김채은·2024년 5월 13일
0
post-thumbnail

Text Me!는 귀여운 디자인이 포함된 방명록 서비스이다. 이전에 form 도메인 컨텍스트를 관리하여 공통 컴포넌트를 만드는 법에 대해 공유한 적이 있는데, 이것은 로그인/회원가입 페이지처럼 정형화된 페이지에 적합한 방식이라, 편지 작성 컴포넌트에 적용하는 건 미뤄두었다.

단국대 총학생회와의 제휴를 통해 이벤트 페이지를 만들면서, 기존의 편지 쓰기 페이지에 이벤트용 input이 추가돼야 하는 요구사항이 있었다.

이 부분은 스타일/도메인이 강하게 묶여있는 페이지라서 기존에는 WriteLetter라는 컴포넌트로 한번에 관리하고 있었다.

<WriteLetter
	// 편지 보내기 API
	sendLetter={sendLetter}
	// 편지 데이터
	receiverId={receiverId}
    //
    to={receiverName}
/>

WriteLetter 안에서 Form을 다음과 같이 작성하고 React Hook Form 라이브러리를 통해 관리하고 있다. 목표는 LetterContainerButton 사이에 새로운 input을 추가하는 것이다.

<Form onSubmit={handleSubmit(sendData, validateData)}>
   // 편지 배경을 감싸는 컨테이너
   <LetterContainer imgurl={pictureUrl} id="box">
      // 받는 사람
      <ToDiv>To. {to}</ToDiv>
	  // 편지 내용
      <TextArea
         maxLength={500}
         {...register("contents", {...})}
         placeholder="편지를 입력해주세요."
      />
      // 보내는 사람
      <FromDiv>
         <p>From.</p>
         <FromInput
             placeholder="보내는 사람"
             maxLength={10}
             {...register("senderName", {...})}
         />
      </FromDiv>
   </LetterContainer>
   // 보내기 버튼
   <Button props={{ type: "submit" }} Style={GreenRightCorner}>
       보내기
   </Button>
</Form>

조건문을 사용해서 이벤트 페이지일 때 하나의 input을 더 띄우는 방법을 고려할 수 있겠지만, 다음에 요구사항이 추가된다면 이벤트 1의 경우, 2의 경우, 3의 경우 등 조건문 범벅이 된 컴포넌트가 생길 수 있다.
유연성을 높이기 위해 옵션 input을 외부에서 주입받을 것이다.

<WriteLetter
	sendLetter={sendLetter}
	receiverId={receiverId}
    to={receiverName}
	// 옵션으로 추가되는 input 주입
	inputOption={
    	<label>
      		<input/>
      	</label>
    }
/>

위와 같은 형태가 될 수 있는데, 이렇게 되면 form 데이터를 가져오는 데 필요한 React Hook Form의 register가 input에 입력될 수 없다. 렌더링되며 WriteLetter가 가지는 register 함수를 input에 props로 뿌려주기 위해 inputOption을 함수로 작성했다.

<WriteLetter
	// ...
	inputOption={(register) => {
    	return (
          	<label>
      			<input {...register("contact")}/>
      		</label>
    	);
    }}
/>

그리고 WriteLetter에 다음과 같이 작성한다.

<Form>
  <LetterContainer>...</LetterContainer>
  {inputOption(register)}
  <Button>...</Button>
</Form>

그럼 register를 뿌려주는 부분에서 Argument of type ~~ is not assignable to parameter of type ~~~~~. ts(2345) 오류가 나게 되는데, 기존에 지정해뒀던 useForm 타입에 새로운 input 필드가 정의돼있지 않기 때문이다.


type LetterForm = {
  contents: string;
  senderName: string;
}

const { register, handleSubmit } = useForm<LetterForm>();

처음에는 LetterForm 안에 옵션으로 들어갈 수 있는 필드를 contact?: string;처럼 옵션 필드로 넣어줄까했는데, 그렇게 되면 WriteLetter에서 옵션에 관련된 정보를 알아야 되기 때문에 유연성이 떨어진다고 생각했다. 해당 컴포넌트를 외부에서 가져오는 옵션 input과 최대한 분리하고 싶었다.

type LetterFormDefault = {
	contents: string;
    senderName: string;
};

type LetterForm = {
	[key: string]: string;
} | LetterFormDefault;

따라서 유니온 타입을 이용해 유연성이 높은 LetterForm 타입을 생성했다. [key: string]: stringLetterFormDefault를 포함하기 때문에 굳이 기본 타입을 따로 두지 않아도 되지만, 기본 타입만 사용되는 부분이 있을 것으로 생각되어 남겨두었다.

공통 컴포넌트를 만들 때 비즈니스 로직을 최대한 분리하는 게 테스트 측면이나 재사용에 좋지만, 스타일이나 레이아웃이 강하게 결합되는 경우는 너무 재사용에 집착하는 것은 오히려 코드의 복잡성을 높인다고 생각한다. 이처럼 비즈니스 로직에 강하게 결합된 컴포넌트도, 외부 로직을 모르도록 컴포넌트를 주입해준다면 깔끔하게 유연성을 높일 수 있다.

profile
배워서 남주는 개발자 김채은입니다 ( •̀ .̫ •́ )✧

0개의 댓글