본문 바로가기
Udemy - React완벽가이드

Form 제출 처리하기

by Hanachoi 2023. 2. 20.

  • 이제 위의 form 에서 사용자가 값을 입력하고 제출할 수 있도록 만들어보자. 그리고 들어온 title / amount/ date를 모아서 하나의 객체로 결합해보자.
  • 이 방법을 위해서 <button>에 onClick 이벤트를 달아줄 수 있지만 여기서 데이터를 수신하기 위한 최선의 방법은 아니다. 
  • 브라우저에 내장된 기본 동작과 웹페이지에 내장된 폼이 있기 때문이다.  
  • <button>의 type = "submit"으로 설정되어 있는 상태에서 <form>대신 이 버튼이 클릭되게 되면 이 전체 폼 요소는 수신할 수 있는 이벤트를 생략한다. 
  • 그래서 <form>에 onSubmit 이벤트르 달아주고 함수를 넣어줘서 폼이 제출될 때 마다 일부 함수를 실행하도록 해준다. 

<form> 에 onSubmit을 넣어주고 함수를 만들어 포인터를 넣어준다.

  • 이렇게 만들어 놓고 버튼을 클릭하게 되면 계속해서 페이지가 새로고침되는걸 볼 수 있는데, 브라우저는 기본적으로 form이 제출되면 페이지를 디폴트로 새로고침하기 때문이다. 폼이 제출될 때 마다 서버에 요청을 보내기 때문.
  • 대신 자바스크립트로 수동으로 데이터를 수집하고 결합해서 무언가를 하는 방법으로 폼을 제출하게 할 수 있다. 
  • event를 매개변수로 받아와서 event.preventDefault()를 해주면 된다. 이렇게 되면 페이지가 재로딩 되지 않는다. 

  • 그 다음으로 form으로 제출된 데이터를 얻어서 객체에 넣어준다. 객체의 키 값은 마음대로 정할 수 있다.  state는 위에서 우리가 useState를 이용해서 받아온 현재상태를 넣어준다. 
  • 날짜는 해당 날짜 문자열을 분석해서 날짜객체로 변환하도록 만들어 준다.

  • 이렇게 만든 데이터들은 console.log(expenseData)를 찍어보면 콘솔에서 확인 가능하다

 

 

#양방향 바인딩 추가

  • 그렇다면 입력된 값을 어떻게 지울 수 있을까? 이게 바로 우리가 state를 이용하는 이유!
  • 만약 우리가 값들을 계속 사용하고 싶었다면 함수 밖에서 전역변수로 선언할 수도 있었다. 
  • state를 사용하면 양방향바인딩(Two-way binding)을 할 수 있다. 이 말은 변경되는 입력값만 수신하는게 아니라 입력에 새로운 값을 다시 전달할 수도 있다는 것! 프로그램에 따라 입력값을 재설정하거나 입력할 수 있다 
  • 그냥 기본 속성인 value를 입력요소에 추가하기만 하면 된다. 이게 input 박스에 들어오는 내부 값의 프로퍼티를 설정해준다. 그리고 우린 이걸 새로운 값으로 설정해줄 수 있다. 
  • 이렇게 해주면 state의 변경사항을 수신하는것뿐만 아니라 상태가 변경될때 입력도 변하게 해준다. 

  • 또 다른 장점은 form이 제출되었을 때 , 예를들어, setEnteredTitle을 호출해서 빈 문자열로 다시 설정해줄 수도 있다. 

  • 이제 form에 입력값이 들어오면 데이터는 객체에 저장되고 form의 입력란들은 모두 자동으로 비어지게 된다. 

 

#자식 대 부모 컴포넌트 통신(상향식)

  • 위에서 만든 ExpenseForm 컴포넌트를 통해서 값을 받아오고 비울 수 있었지만 사실 우리는 이 컴포넌트가 기술적으로 필요하지는 않다. 대신 NewExpense나 정확하게는 App.js 컴포넌트 파일에서 데이터가 필요하다. 왜냐면 App.js 안에 expenses 배열이 있고 궁극적인 우리의 목표는 사용자가 입력한 새로운 비용을 기존의 비용 목록에 추가하는 것이고 id 값까지 추가해서 좀 더 향상시키는 것이기 때문이다. 
  • 우리는 데이터를 App.js까지 전달해줘야 한다. 
  • 여태까지 데이터를 밑으로 내려주는 것만 배웠는데, 어떻게 해야 위로 올릴 수 있을까?
  • EpxpenseForm 컴포넌트에서 우리는 사용자가 입력한 title 값을 가져오기 위해서 onChange props의 titleChangeHandler 함수를 넣어서 사용자의 입력을 받아오고 있다. 사용자가 값을 입력할 때마다 이 함수가 실행되고 그러면서 디폴트 이벤트 객체를 얻게 되는데,  이건 브라우저가 제공하는 기본 객체이다. 
  • 사실 우리는 아래의 있는 input을 또 하나의 컴포넌트라고 볼 수 있다. 리액트가 제공하는 pre built 컴포넌트인것이다. DOM 의 입력요소로 해석된다.  그래서 컴포넌트에 'on' props를 설정해줄 수 있다. 

  • onChange props 안에 이벤트리스너를 넣어준다. 리액튼 onChange 프롭스 안에 설정된 값을 보고 렌더링 된 입력 요소에 있는 리스너를 추가한다. 이건 우리의 컴포넌트에도 사용할 수 있는 패턴이다. 
  • 자체 이벤트 속성을 사용해서 만약 이런식으로 호출하고 싶으면 값으로 함수를 가질 수 있고 부모컴포넌트로부터 자식컴포넌트로 함수를 전달할 수 있게 해줄 것이고 자식 컴포넌트에서 그 함수를 호출할 수 있다. 
  • 그리고 함수를 호출했을 때 그 함수에 매개변수로 데이터를 전달할 수 있다. 이런 식으로 자식에서 부모로 정보를 전달할 수 있다. 
  • 우리가 만들고 있는 예시를 통해서 살펴보자
  • 첫번째 단계로 ExpenseForm 컴포넌트에서 수집한 expenseData를 NewExpense컴포넌트에 전달하고 싶다고 해보자.  왜냐하면 궁극적으로 App 컴포넌트에 도달하고 싶기 때문이다. App 컴포넌트에 도달하려면 NewExpense컴포넌트에 먼저 도달해야된다.  NewExpense에서 ExpenseForm 컴포넌트를 사용하고 있기 때문이다.
  • props는 오로지 부모에서 자식으로만 전달될 수있다. 중간 컴포넌트 생략은 불가
  • 첫번째 단계로 expenseData가 NewExpense 컴포넌트로 확실히 전달될 수 있도록 해준다. ExpenseForm에 새로운 속성을 추가하는 것으로 가능하다.
  • NewExpense컴포넌트 파일에 넣어뒀던 <ExpenseForm> 컴포넌트 옆에 새로운 속성을 넣어준다.  이름은 자기가 정하기 나름이다. 

  • onSaveExpenseData함수는 NewExpense 함수 안에서 정의되어야 한다. 
  • 아래의 이미지처럼 enteredExpenseData를 받아오는 saeExpenseDataHandler 함수를 하나 만들어 준다. 그리고 ExpenseForm에서 만들었던 submitHandler함수안에 있는 expensedata 객체 안에 있는 값들을 스프레드 연산자로 가져오고, 아이디를 추가해준다. 

ExpenseForm안에 있던 expenseData 객체를 받아온다.

  • 이제 <ExpenseForm> 컴포넌트 안에 있는 onSaveExpenseData 프롭은 saveExpenseDataHandler안에 있는 함수를 값으로 받아오게 된다. 

  • saveExpenseDataHanlder를 포인터만 넣어주면 ExpenseForm으로 전달된다.  
  • 두번째 단계로, 이 함수를 우리의 사용자 지정 컴포넌트에서 사용해준다. 우리가 지금 <input>에 해야될 것은 없다. 인풋은 빌트인 컴포넌트이기 때문이다. 
  • 우리는 onChange에 titleChangeHandler 함수를 넣어줬다. 리액트가 얘를 이벤트 리스너로 넣어줄 것이다. 이벤트가 발생할때마다 이 이 함수가 실행될 것. 

ExpenseForm안에 각각의 Input에 이벤트가 실행될때마다 titleChangeHandler 함수가 실행된다.

  • 우리는 지금 ExpenseForm 이라는 사용자 지정 컴포넌트에서 작업하고 있기 때문에 전달된 함수를 수동으로 불러줘야 한다. 
  • ExpenseForm 컴포넌트를 사용할 때 우리가 설정한 onSaveExpenseData 프롭스가 있다. 그래서 ExpenseForm 컴포넌트 대신 이 속성을 전달해서 값을 추출할 수 있다. 예를들면 saveExpenseDataHandler같은 함수이다. 

  • ExpenseForm 컴포넌트에서 props를 매개변수로 받아준다. 지금 속성을 설정하기 때문이다. 

  • 그리고 submitHandler 함수로 가서 props.onSaveExpenseData()를 실행시켜준다. 

  • ExpenseForm 컴포넌트 안에서는 onSaveExpensedata를 통해서 saveExpenseDataHandler함수를 사용할 수 있게 된다. ExpenseForm 안에는 이 함수가 없음에도 말이다! onSaveExpenseData ={saveExpenseDataHandler}를 통해서 포인터를 전달하기 때문이다. 
  • 이렇게 자식에서 부모로 상향식 통신이 가능해진다. ExpenseForm이라는 자식 컴포넌트에서 NewExpense 부모컴포넌트로 소통이 가능해진다 . 
  • props.onSaveExpenseData의 매개변수로 (expenseData)를 넣어주면 NewExpense 컴포넌트의 saveExpenseDataHandler부분의 매개변수로 넣어줄 수 있게 된다.

  • expenseData를 콘솔에 찍어보면 새로운 값이 입력되었을때 아이디값도 함께 오브젝트가 잘 들어오는걸 볼 수있다. 
  • 이제 다시 더 상위단계에 있는 App컴포넌트와도 소통할 수 있도록 만들 수 있다. 
  • JSX 코드를 반환하기 위해서 App.js컴포넌트에 함수를 하나 더 만들어준다. addExpenseHandler 함수를 만들어준다.

  • <NewExpense> 에 포인터로 위의 함수를 전달해준다. 

  • NewExpense 컴포넌트에 props를 넣어준 뒤 props.onAddExpense를 불러주고 expenseData를 매개변수로 넣어준다. 

 

 

# state끌어올리기(Lifting state up )

  • 자식커모넌트에서 부모컴포넌트로 이동하는 방법. props를 사용해서 부모컴포넌트로부터 함수를 받고 자식컴포넌트에서 그 함수를 불러온다.
  • 지금 현재 우리가 만드는 컴포넌트 트리를 잠시 살펴보면, NewExpense 컴포넌트에서는 데이터를 생성하고 가져올 수 있게 했지만 사실 Expenses 컴포넌트에서 데이터가 필요한 상황이다. 
  • 하지만 NewExpense 컴포넌트에서 Expense컴포넌트로 데이터를 직접적으로 바로 넘길수는 없다. 무조건 부모에서 자식으로, 자식에서 부모로만 전달이 가능하다. 그래서 App컴포넌트를 꼭 통해서 넘어가야된다. 

  • App.js 컴포넌트 파일을 보면 Expense와 NewExpense 둘다 접근한건 얘 뿐이다. JSX 코드에서 두 컴포넌트를 모두 렌더링하기 때문이다. 

  • 우리는 state를 가장 가까운 관련 컴포넌트에 저장할 수 있는데, 지금 여기서는 두 컴포넌트와 모두 관련있는 부모 컴포넌트에 저장할 수 있다. 
  • Lifting state up data : 그래서 지금 우리는 NewExpense컴포넌트에있는 상태 데이터를 App.js로 끌어올려서 전달해줄 것이다. 
  • 아래를 보면 우리는 지금 props를 통해서 onAddExpense함수를 호출한다. 하지만 이것만으로는 state를 끌어올릴 수 없다. 이건 그냥 props로 함수를 불러주는것 뿐이다. 

  • 우리는 지금 expenseData를 NewExpense에 저장하고있지않다. 대신에 App.js로 끌어올려서 지금 console.log를 찍어둔 곳에 addExpenseHandler처럼 사용할 수 있다. 

App.js파일에 만든 addExpenseHandler

  •  이렇게 최상위 root 컴포넌트로 데이터를 lifting 해주는일은 흔한데,  이렇게 해줘야 props를 통해서 필요한 컴포넌트에 데이터를 내려줄 수 있기 때문이다.
  • 만약 직접적으로 두 자식 컴포넌트와 상호작용을 하는 App 컴포넌트를 가지고 있다면 이 방법은 작동하지 않는다.
  • 지금 여기의 케이스는 Expense라는컴포넌트에서 ExpenseForm을 통해 얻어온 데이터를 사용해주고 싶지만 형제 컴포넌트끼리는 바로 넘어갈 수 없기때문에 최상위 root컴포넌트인 App컴포넌트까지 state를 끌어올려서 거기서 다시 Expense로 내려줘야 하는 것이다. 
  • 이렇게 항상 최상위 컴포넌트인 App.js까지 파일을 끌어올려야되는건 아니다.필요한 만큼 끌어올려서 해당 컴포넌트에 데이터를 전달해줄 수 있을정도까지만 올려주면 된다. 

 

#제어된 컴포넌트와 제어되지않은 컴포넌트 및 stateless컴포넌트와 stateful 컴포넌트

  • 양방향 바인딩을 사용할때마다 컴포넌트가 제어된다. 
  • 새로만든 ExpenseFilter 컴포넌트 안에서 우리는 value 값을 통해 props.selecte를 넣어줬고 이걸로 컴포넌트를 제어하고 있다. 

  • 이말은 아래의 <select> 부분에 만든 드롭다운에서 선택된 값은 props를 통해 부모 컴포넌트에 전달되어 부모컴포넌트로부터 받은 컴포넌트라는 뜻이다. 

  • ExpenseFilter는 사실 단지 UI를 나타내는 컴포넌트로 dropdown feature를 가지고 있으며 리스너, props를 가지고 있다. 하지만 진짜 로직은 부모컴포넌트가 가지고 있다.  변경되는 값들은 이 컴포넌트에서 다루는게 아니라 부모컴포넌트에서 다루고 있는 것이다. 
  • Expense컴포넌트에서 ExpenseFilter컴포넌트를 제어하고 있다. 

Expense 컴포넌트에서 ExpenseFIlter 컴포넌트를 사용하고 있고 state도 가지고 있다

  • 프레젠테이셔널 vs 상태유지(stateful) 컴포넌트 / stateless VS stateful 컴포넌트 / dumb vs smart 컴포넌트 이렇게 용어들이 있는데, 이것도 알아두어야 한다.
  • 기본적으로 리액트를 사용하다보면 일부상태를 관리하는 몇 개의 컴포넌트를 가지게 되는데, 예를들어 여기서는 filter의 state를 관리하는 Expense 컴포넌트  / input sate를 관리하는 ExpenseForm 컴포넌트 같은 것들이 있다. 
  • 반면에 어떤 상태도 관리하지 않는 컴포넌트들이 있는데, 우리의 ExpenseItem 같은 것이다. 
  • 아래의 ExpenseItem에서 원래 데모버전으로 만들었던 <button>을 삭제하고 그 위에 useState나 함수들을 다 없애줬다. 이렇게 어떤 상태도 가지지 않는 컴포넌트를 프리젠테이셔널 혹은 dumb 컴포넌트라고 부른다. 

  • 위 컴포넌트는 어떤 상태도 관리하지 않고 단지 데이터를 출력하기 위해 존재한다. 
  • 기본적으로 리액트에서는 프레젠테이셔널 혹은 dumb 컴포넌트를 smart 혹은상태유지 컴포넌트보다 더 많이 가지게 된다. 
  • 이유는 리액트에서는 작은 단위로 컴포넌트 기능들을 나누는데 초점이 있는데 몇몇의 JSX 코드들은 단지 데이터 출력을 위해서만 존재하게 되기 때문이다. 오히려 소수의 컴포넌트들의 state를 관리하게 된다. 
  • 그리고 그 state는 props를 통해 전달된다. 
  • 지금 우리의 ExpenseFilter도 filteredYear를 전달해서 props를 통해 ExpenseFilter로 돌아온다. 

댓글