FrontEnd/[리액트 2부] 고급 주제와 훅

[1.4장 장바구니 화면]

Tony Lim 2024. 2. 27. 09:34

CartPage / ProductItem

const ProductItem = ({ product, onClick }) => {
  const { name, price, thumbnail } = product;

  return (
    <div className="ProductItem">
      <div className="description">
        <h2>{name}</h2>
        <div>{price.toLocaleString()}원</div>
        {onClick && <Button styleType="brand" onClick={onClick}>
          주문하기
        </Button>}
      </div>
      <div className="thumbnail">
        <img src={thumbnail} alt={`${name} ${price.toLocaleString()}원`} />
      </div>
    </div>
  );
};

export default ProductItem;

기존의 ProductItem component 는 CartPage, ProductPage 두 곳에서 item list들을 렌더링 할 때 쓰였다.

CartPage에서는 이미 주문한 목록의 item들이기 때문에 주문하기 button이 필요하지 않았다.

이를 위해 ProductItem을 onclick을 주입 받았을 때만 button이 존재하게 변경하였다.

const OrderableProductItem = ({ product }) => {
  const handleClick = () => {
    console.log("// Todo 장바구니 화면으로 이동");
  };

  return <ProductItem product={product} onClick={handleClick}></ProductItem>;
};

export default OrderableProductItem;

ProductPage 에서는 버튼이 필요함으로 OrderableProductItem component를 정의해서 onClick을 주입시켜줬다.

class ProductPage extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      productList: [],
    };
  }

  componentDidMount() {
    this.fetch();
  }

  async fetch() {
    try {
      const productList = await ProductApi.fetchProductList();
      this.setState({ productList });
    } catch (e) {
      console.error(e);
    }
  }

  render() {
    return (
      <div className="ProductPage">
        <Page header={<Title>메뉴목록</Title>} footer={<Navbar />}>
          <ul>
            {this.state.productList.map((product) => (
              <li key={product.id}>
                <OrderableProductItem product={product}></OrderableProductItem>
              </li>
            ))}
          </ul>
        </Page>
      </div>
    );
  }
}

product 만 넘겨줘서 Item들을 버튼과 함께 렌더링을 할 수 있게 되었다.


Form Control

const FormControl = ({ label, htmlFor, required, children }) => {
  return (
    <div className="FormControl">
      <label htmlFor={htmlFor}>
        {label} {required && <span className="required">*</span>}
      </label>
      {children}
    </div>
  );
};

export default FormControl;
      <FormControl label="이름" htmlFor={'name'} required>
        <input type="text"></input>
      </FormControl>

Form 페이지의 form의 input들을 구성하게 해주는 FormControl component이다.

유연하게 적용할 사용할 수 있게 필수 인것과 label 그리고 input element를 주입받도록 되어있다.

 

이때 input 값에 값을 입력하면 그게 렌더링 되어서 반영이된다.

이는 브라우저가 상태를 관리했기 때문이다. 리액트 관점에서는 이것을 비제어 컴포넌트라고 부른다.

폼이 간단할 때는 비제어 컴포넌트가 편하지만 , 입력값 검증, 오류 UI처리 등 세세한 폼 처리를 위해서는 제어 컴포넌트가 유리하다.


OrderForm

const OrderForm = () => {
  return (
    <form className="OrderForm" id="order-form">
      <FormControl label="주소" htmlFor={"deliveryAddress"} required>
        <input
          type="text"
          name="deliveryAddress"
          id="deliveryAddress"
          placeholder="배달받을 주소를 입력하세요"
          required
          autoFocus
        ></input>
      </FormControl>
      <FormControl label="연락처" htmlFor={"deliveryContact"} required>
        <input
          type="text"
          name="deliveryContact"
          id="deliveryContact"
          placeholder="연락처를 입력하세요"
          required
          pattern="^\d{2,3}-\d{3,4}-\d{4}$"
        ></input>
      </FormControl>
      <FormControl label="결재수단" htmlFor={"paymentMethod"} required>
        <select name="paymentMethod" id="paymentMethod" value="">
          <option value="마이페이">마이페이</option>
          <option value="만나서 결제">만나서 결제</option>
        </select>
      </FormControl>
      <FormControl label="가게 사장님께" htmlFor={"messageToShop"}>
        <textarea
          name="messageToShop"
          id="messageToShop"
        ></textarea>
      </FormControl>
      <FormControl label="라이더님께" htmlFor={"messageToRider"}>
        <textarea
          name="messageToRider"
          id="messageToRider"
        ></textarea>
      </FormControl>
    </form>
  );
};
const CartPage = () => (
  <div className="CartPage">
    <Page
      header={<Title backUrl="/">장바구니</Title>}
      footer={
        <Button styleType="brand-solid" block form="order-form">
          결제하기
        </Button>
      }
    >
      <ProductItem product={fakeProduct} />
      <OrderForm/>
    </Page>
  </div>
);

OrderForm component 안에는 button이 따로 존재하지않고 CartPage에 버튼이 존재한다.

이떄는 내부 Component form tag에 id를 지정하고 외부 Componenet 의 button에서 form="id" 를 지정해주면 연결이되어 동작하게 된다.


Ref 와 Dom

컴포넌트 상태와 인자만 변경하면 엘리먼트는 자동으로 계산되는 구조를 따르는 것이 리액트 컴포넌트 개발의 기본 방향이다. 즉 선언적인 방식이다.

이와 반대로 직접 Dom API 를 호출해서 프로그래밍적으로 하는 법이 존재한다. 이럴때 react Ref를 사용하게 된다.

Ref는 선언적으로 문제를 해결할 수 없을 때 쓰는데 다음과 같은 상황이 존재한다.

  • 포커스, 텍스트 선택영역, 미디어 재생관리
  • 애니메이션 직접실행, 3rd party dom 라이브러리 사용(jquery 같은)
class MyComponent extends React.Component {
  divRef = React.createRef();
  render() {
    return <div ref={this.divRef}>hihi</div>;
  }

  componentDidMount() {
    console.log(this.divRef);
    const divElement = this.divRef.current;
    divElement.style.backgroundColor = "red";
  }
}

export default MyComponent;

렌더링 하려는 div element를 ref property에 this.divRef를 전달하여서 react dom에서 this.divRef.current 필드를 통해
직접 접근이 가능하다.

componentDidMount 시점에 Ref를 통해 원하는것을 할 수 있다. 즉 제어컴포넌트로 다룰수 있게 된 것이다.

constructor, render 메소드에서 Ref.current를 찍어보면 null이들어있다. 아직 브라우저에서 완전히 렌더링이 되지 않았기 때문이다.

<Foo ref={this.divRef}>div</div>

html dom element가 아니라 react element를 가르키는 경우 , Component 객체를 가르키게 된다.


class OrderForm extends React.Component {
  constructor(props) {
    super(props);

    this.formRef = React.createRef();
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  getInputValueByName(name) {
    if (!this.formRef.current) return;

    const inputElement = this.formRef.current.elements.namedItem(name);
    if (!inputElement) return "";

    return inputElement.value;
  }

  handleSubmit(e) {
    e.preventDefault();

    const deliveryAddress = this.getInputValueByName("deliveryAddress");
    const deliveryContact = this.getInputValueByName("deliveryContact");
    const paymentMethod = this.getInputValueByName("paymentMethod");
    const messageToShop = this.getInputValueByName("messageToShop");
    const messageToRider = this.getInputValueByName("messageToRider");

    console.log({
      deliveryAddress,
      deliveryContact,
      paymentMethod,
      messageToRider,
      messageToShop,
    });
  }

  render() {
    return (
      <form
        className="OrderForm"
        id="order-form"
        ref={this.formRef}
        onSubmit={this.handleSubmit}
      >
        <FormControl label="주소" htmlFor="deliveryAddress" required>
          <input
            type="text"
            name="deliveryAddress"
            id="deliveryAddress"
            placeholder="배달받을 주소를 입력하세요"
            required
            autoFocus
          />
        </FormControl>
        <FormControl label="연락처" htmlFor="deliveryContact" required>
          <input
            type="text"
            name="deliveryContact"
            id="deliveryContact"
            placeholder="연락처를 입력하세요"
            pattern="^\d{2,3}-\d{3,4}-\d{4}$"
            required
          />
        </FormControl>
        <FormControl label="결재수단" htmlFor="paymentMethod" required>
          <select name="paymentMethod" id="paymentMethod">
            <option value="마이페이">마이페이</option>
            <option value="만나서 결제">만나서 결제</option>
          </select>
        </FormControl>
        <FormControl label="가게 사장님께" htmlFor="messageToShop">
          <textarea name="messageToShop" id="messageToShop"></textarea>
        </FormControl>
        <FormControl label="라이더님께" htmlFor="messageToRider">
          <textarea name="messageToRider" id="messageToRider"></textarea>
        </FormControl>
      </form>
    );
  }
}

export default OrderForm;

getInputValueByName 메소드에서 dom element api 인 namedItem을 통해 원하는 element를 가져올 수 있다.

어차피 ref가 실제 dom element 를 가르키고 있으니까 가능한 것이다.

추가적으로 value field에 접근해서 form에 입력한 input 값들을 받아올 수 있게 된다.


역방향 데이터 흐름 추가하기

class CartPage extends React.Component {
  constructor(props) {
    super(props);
    this.state = { product: null };
  }

  async componentDidMount() {
    this.fetch();
  }

  async fetch() {
    try {
      const product = await ProductApi.fetchProduct("CACDA421");
      this.setState({ product });
    } catch (e) {
      console.error(e);
    }
  }

  render() {
    const { product } = this.state;
    return (
      <div className="CartPage">
        <Page
          header={<Title backUrl="/">장바구니</Title>}
          footer={<PaymentButton />}
        >
          {product && <ProductItem product={product} />}
          <OrderForm />
        </Page>
      </div>
    );
  }
}

부모 component인 CartPage에서 ProductItem에게 fetch 로 받아온 product를 넘기는 것은 정방향으로 데이터가 흘러 간것이다.

현재 OrderForm 에서 입력받은 데이터를 CartPage로 역방향으로 데이터 흐름을 만들고 싶다.

이때 쓸 수 있는것이 콜백함수를 부모가 넘겨주는 것이다.

  constructor(props) {
    super(props);
    this.state = { product: null };
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  
  handleSubmit(values) {
    console.log(values);
    console.log(this.state.product);
  }
  
<OrderForm onSubmit={this.handleSubmit} />

handleSubmit을 넘겨 줄때 bind를 현 객체에다 해줘야 this 가 현 객체로 bind된다.

만약 bind해주지 않으면 functional compoenent인 OrderForm이 호출할 시점에는 this가 browser의 windows 가 되어버린다.