본문 바로가기

FrontEnd

리액트가 함수 컴포넌트와 클래스 컴포넌트를 구분하는 방법

반응형
React는 함수 컴포넌트와 클래스 컴포넌트를 어떻게 구분하나요?

본문 내용은 리액트보다는 js에 관한 내용입니다.

개발하는데 몰라도 전혀 지장없는 내용이니 스킵하셔도 됩니다.

TLDR : 리액트는 React.Component를 상속한 클래스만 클래스 컴포넌트로 취급합니다.

해당 클래스의 객체 플래그를 이용해 리액트는 해당 컴포넌트가 클래스인지 구분합니다.

Component.prototype.isReactComponent = {};

함수 컴포넌트와 클래스 컴포넌트. 개발은 이렇게 합니다

함수 컴포넌트

function Greeting() {
  return <p>Hello</p>;
}

클래스 컴포넌트

class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

사용할 때는 아무 차이 없죠

// Class or function — whatever.
<Greeting />

하지만 리액트에게는 다릅니다.

함수 컴포넌트는 아래와 같이 호출합니다.

// Your code
function Greeting() {
  return <p>Hello</p>;
}

// Inside React
const result = Greeting(props); // <p>Hello</p>

클래스 컴포넌트는 아래와 같이 호출합니다.

즉, 인스턴스화 하고 렌더 메서드를 호출합니다.

// Your code
class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

// Inside React
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>

두 경우 모두 React의 목표는 렌더링된 노드를 얻는 것입니다(이 예에서는 <p>Hello</p>).
그러나 정확한 단계는 Greeting이 정의된 방식에 따라 다릅니다.

그렇다면 React는 무언가가 클래스인지 함수인지 어떻게 알 수 있을까요?

이 내용은 이전 게시물 (previous post)과 마찬가지로 실무 개발 시에 전혀 지장이 없습니다.
오히려 리액트보다는 js에 관한 내용이며, 저(dan) 또한 수 년간 해당 내용을 전혀 모르고 있었습니다.
제발 면접에서 이 질문을 하지 마시길 바랍니다(...).
이것은 긴 여정입니다. 안전 벨트 매세요.
이 게시물에는 React 자체에 대한 정보는 많지 않지만,
new, this, class, arrow functions, prototype, __proto__, instanceof 및 이러한 것들이
JavaScript에서 리액트와 어떻게 함께 작동하는지 살펴보겠습니다.
운 좋게도 React를 사용할 때 그런 것에 대해 많이 생각할 필요가 없습니다.
만약 당신이 React 라이브러리를 구현한다면 말이죠.
 
먼저 함수와 클래스를 다르게 취급하는 것이 왜 중요한지 이해해야 합니다.
클래스를 호출할 때 new 연산자를 사용하는 방법에 유의하세요.
// If Greeting is a function
const result = Greeting(props); // <p>Hello</p>

// If Greeting is a class
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>

자바스크립트에서 new 연산자가 무엇을 하는지 대략적으로 알아봅시다.


예전에는 자바스크립트에 클래스가 없었습니다.
그러나 일반 함수를 사용하여 클래스와 유사한 패턴을 표현할 수 있습니다.
구체적으로, 호출 전에 new를 추가하여 클래스 생성자와 유사한 역할의 모든 함수를 사용할 수 있습니다.
// Just a function
function Person(name) {
  this.name = name;
}

var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 Won’t work
오늘날에도 이와 같은 코드를 작성할 수 있습니다! DevTools에서 사용해 보세요.
new 없이 Person('Fred') 을 호출했다면 내부의 this는 전역적이고 쓸모없는 것(예: window 또는 undefined)을 가리킬 것입니다.
따라서 코드가 충돌하거나 window.name 설정과 같은 어리석은 작업을 수행합니다.
 
호출 앞에 new를 추가하여 우리는 다음과 같이 말합니다.
자바스크립트씨
{} 객체를 생성하고
Person 함수 내부 this가 이 객체를 가리키도록 하세요
저는 해당 객체에 this.name과 같은 항목을 할당할 거에요
그 다음 그 새로운 객체를 리턴해 주세요"

그것이 new 오퍼레이터가 하는 일입니다.

var fred = new Person('Fred'); // Same object as `this` inside `Person`
new 연산자는 또한 우리가 Person.prototype에 넣은 모든 것을 fred 객체에서 사용할 수 있도록 합니다.
function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  alert('Hi, I am ' + this.name);
}

var fred = new Person('Fred');
fred.sayHi();
이것이 JavaScript가 ES6에서 클래스 지원을 언어 차원에서 직접 추가하기 전에 사람들이 클래스를 에뮬레이트한 방법입니다.

new는 한동안 JavaScript에서 사용되었습니다.
그러나 class 더 최근입니다.
그들은 우리의 의도와 더 밀접하게 일치하도록 위의 코드를 다시 작성할 수 있습니다.
class Person {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    alert('Hi, I am ' + this.name);
  }
}

let fred = new Person('Fred');
fred.sayHi();
개발자의 의도를 파악하는 것은 언어 및 API 설계에서 중요합니다.

함수를 작성하면 JavaScript는 그것이 alert()처럼 호출되도록 의도된 것인지 또는
new Person()과 같은 생성자 역할을 하는지 추측할 수 없습니다.
Person과 같은 함수에 대해 new를 지정하는 것을 잊어버리면 혼란스러운 동작이 발생할 수 있습니다.

 

클래스 구문을 사용하면 다음과 같이 말할 수 있습니다.
"이것은 단순한 함수가 아닙니다. 클래스이고 생성자가 있습니다." 호출할 때 new를 잊어버리면 JavaScript에서 오류가 발생합니다.
주 : 클래스도 함수일 뿐!
let fred = new Person('Fred');
// ✅  If Person is a function: works fine
// ✅  If Person is a class: works fine too

let george = Person('George'); // We forgot `new`
// 😳 If Person is a constructor-like function: confusing behavior
// 🔴 If Person is a class: fails immediately

 

이것은 this.name과 george.name 대신 window.name으로 처리되는 버그가 발생하는 것을 기다리는 대신
실수를 조기에 포착하는 데 도움이 됩니다.

 

그러나 이는 React가 클래스를 호출하기 전에 new를 넣어야 함을 의미합니다.
JavaScript가 오류로 처리하므로 일반 함수로 호출할 수 없습니다!

class Counter extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

// 🔴 React can't just do this:
const instance = Counter(props);

이것은 함수 컴포넌트와 클래스 컴포넌트를 동일하게 취급하면 오류가 발생함을 의미합니다.


React가 이 문제를 해결하는 방법을 보기 전에 
React를 사용하는 대부분의 사람들이 Babel과 같은 컴파일러를 사용하여
구형 브라우저용 클래스와 같은 최신 기능을 컴파일한다는 것을 기억하는 것이 중요합니다.

따라서 설계 시 컴파일러를 고려해야 합니다.

초기 버전의 Babel에서는 new 없이 클래스 함수를 호출할 수 있었습니다.
그러나 이것은 이후 몇 가지 추가 코드를 생성하여 수정되었습니다.

function Person(name) {
  // A bit simplified from Babel output:
  if (!(this instanceof Person)) {
    throw new TypeError("Cannot call a class as a function");
  }
  // Our code:
  this.name = name;
}

new Person('Fred'); // ✅ Okay
Person('George');   // 🔴 Cannot call a class as a function
번들에서 이와 같은 코드를 본 적이 있을 것입니다. 이것이 모든 _classCallCheck 함수가 하는 일입니다.
(체크 없이 "느슨한 모드"를 선택하여 번들 크기를 줄일 수 있지만 이는 실제 네이티브 클래스로의 최종 전환을 복잡하게 만들 수 있습니다.)

이제 new로 호출하거나 new 없이 호출하는 것의 차이점을 대략적으로 이해해야 합니다.
  new Person() Person()
class this는 Person의 인스턴스 🔴 타입에러
function  this는 Person의 인스턴스 😳 this는 window 혹은 undefined
이것이 React가 컴포넌트를 올바르게 호출하는 것이 중요한 이유입니다.
컴포넌트가 클래스로 정의된 경우 React는 이를 호출할 때 new를 사용해야 합니다.
그래서 React는 클래스인지 아닌지 확인할 수 있습니까?
 
쉽지 않습니다!

JavaScript의 함수에서 클래스를 구분할 수 있다고 해도
Babel과 같은 도구로 처리되는 클래스에서는 여전히 작동하지 않습니다.
브라우저에게는 단순한 함수일 뿐입니다. React에게는 운이 좋지 않습니다.


자, 그럼 React는 모든 호출에서 new를 사용할 수 있을까요? 불행히도 항상 동작하는 것은 아닙니다.
일반 함수를 사용하여 new로 호출하면 이와 같은 객체 인스턴스가 제공됩니다.
위의 Person과 같이 생성자로 작성된 함수에는 바람직하지만 함수 컴포넌트에는 혼동을 줄 수 있습니다.
function Greeting() {
  // We wouldn’t expect `this` to be any kind of instance here
  return <p>Hello</p>;
}
그래도 쓸 수는 있습니다만, 이 아이디어를 폐기하는 두 가지 다른 이유가 있습니다.

항상 new를 사용하는 아이디어가 동작하지 않는 첫 번째 이유는
기본 화살표 함수(Babel에서 컴파일한 함수가 아님)의 경우 new로 호출하면 오류가 발생하기 때문입니다.
const Greeting = () => <p>Hello</p>;
new Greeting(); // 🔴 Greeting is not a constructor
이 동작은 의도적이며 화살표 함수의 설계를 따릅니다.
화살표 함수의 주요 장점 중 하나는 고유한 this 값이 없다는 것입니다.
대신 this은 가장 가까운 일반 함수에서 리졸브 됩니다. (주 : 결국 클래스의 인스턴스가 됨)
class Friends extends React.Component {
  render() {    
    const friends = this.props.friends;
    return friends.map(friend =>
      <Friend
        // `this` is resolved from the `render` method        
        size={this.props.size}        
        name={friend.name}
        key={friend.id}
      />
    );
  }
}
좋습니다, 화살표 함수에는 고유한 this가 없습니다.
그러나 그것은 생성자로서 완전히 쓸모가 없다는 것을 의미합니다!
const Person = (name) => {
  // 🔴 This wouldn’t make sense!
  this.name = name;
}

따라서 JavaScript는 new로 화살표 함수를 호출하는 것을 허용하지 않습니다.
이건 어쨋든 실수이므로 일찍 알리는 것이 좋습니다.
이것은 JavaScript가 new 없이 클래스를 호출하는 것을 허용하지 않는 것과 유사합니다.

 

이것은 좋지만 우리의 계획을 좌절시키기도 합니다.
React는 화살표 함수 기능을 깨뜨리기 때문에 모든 것에 대해 new를 호출할 수 없습니다!
대신 프로토타입이 없는 것을 이용해 화살표 함수를 감지하려고 시도할 수 있습니다.
(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}
그러나 이것은 Babel로 컴파일된 함수에서는 작동하지 않습니다. (wouldn’t work)
이것은 큰 문제가 아닐 수도 있지만 이 접근 방식을 막다른 골목으로 만드는 또 다른 이유가 있습니다.

항상 new를 사용할 수 없는 또 다른 이유는
React가 문자열이나 다른 기본 타입을 반환하는 컴포넌트를 지원하지 못하게 하기 때문입니다.
function Greeting() {
  return 'Hello';
}

Greeting(); // ✅ 'Hello'
new Greeting(); // 😳 Greeting {}
이것은 다시 new operator 디자인의 단점과 관련이 있습니다.
앞에서 보았듯이
new는 JavaScript 엔진에 객체를 생성하고
해당 객체를 함수 내에서 this로 만든 다음
나중에 new의 결과로 해당 객체를 리턴하도록 지시합니다.

그러나 JavaScript는 new로 호출된 함수가 다른 객체를 반환하여 new의 반환 값을 재정의하는 것도 허용합니다.
아마도 이것은 인스턴스를 재사용하려는 풀링과 같은 패턴에 유용한 것으로 간주되었습니다.

// Created lazily
var zeroVector = null;

function Vector(x, y) {
  if (x === 0 && y === 0) {
    if (zeroVector !== null) {
      // Reuse the same instance
      return zeroVector;
    }
    zeroVector = this;
  }
  this.x = x;
  this.y = y;
}

var a = new Vector(1, 1);
var b = new Vector(0, 0);
var c = new Vector(0, 0); // 😲 b === c

그러나 new는 리턴의 객체가 아닌 경우 함수의 반환 값을 완전히 무시합니다.
문자열이나 숫자를 반환하면 아무것도 반환하지 않은 것과 같습니다.

function Answer() {
  return 42;
}

Answer(); // ✅ 42
new Answer(); // 😳 Answer {}
따라서 new로 호출할 때 함수에서 기본 반환 값(숫자 또는 문자열과 같은)을 읽을 수 있는 방법은 없습니다.
따라서 React가 항상 new를 사용했다면 문자열을 반환하는 컴포넌트를 추가할 수 없었을 것입니다!
받아들일 수 없는 일이므로 타협해야 합니다.

우리는 지금까지 무엇을 배웠나요?

React는 new로 클래스(Babel 출력 포함)를 호출해야 하지만
new 없이 일반 함수 또는 화살표 함수(Babel 출력 포함)를 호출해야 합니다.

그리고 그것들을 구별할 수 있는 확실한 방법은 없습니다.

 

일반적인 문제를 해결할 수 없다면 더 구체적인 문제를 해결할 수 있습니까?

컴포넌트를 클래스로 정의할 때 this.setState()와 같은 내장 메소드를 사용하기 위해

React.Component를 확장하고 싶을 것입니다.
모든 클래스를 감지하려고 하는 대신 React.Component 자손만 감지하면 어떨까요?

 

스포일러: 이것이 바로 React가 하는 일입니다.

class A {}
class B extends A {}

console.log(B.prototype instanceof A); // true
"프로토타입 체인"에 대해 잘 알고 계실 것입니다.
JavaScript의 모든 객체는 "프로토타입"을 가질 수 있습니다.
fred.sayHi()를 작성했지만 fred 객체에 sayHi 속성이 없으면 fred의 프로토타입에서 sayHi 속성을 찾습니다.
거기에서 찾지 못하면 체인의 다음 프로토타입인 fred의 프로토타입의 프로토타입을 봅니다. 이는 계속해서 이어집니다...
 
혼란스럽게도 클래스나 함수의 프로토타입 속성은 해당 값의 프로토타입을 가리키지 않습니다.

따라서 "프로토타입 체인"은 prototype.prototype.prototype보다
__proto__.__proto__.__proto__와 비슷합니다.
이것을 이해하는 데 몇 년이 걸렸습니다.

 

그렇다면 함수나 클래스의 프로토타입 속성은 무엇입니까?
해당 클래스나 함수로 새로 생성된 모든 객체에 부여되는 __proto__입니다!

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  alert('Hi, I am ' + this.name);
}

var fred = new Person('Fred'); // Sets `fred.__proto__` to `Person.prototype`
그리고 그 __proto__ 체인은 JavaScript가 속성을 찾는 방법입니다:
fred.sayHi();
// 1. Does fred have a sayHi property? No.
// 2. Does fred.__proto__ have a sayHi property? Yes. Call it!

fred.toString();
// 1. Does fred have a toString property? No.
// 2. Does fred.__proto__ have a toString property? No.
// 3. Does fred.__proto__.__proto__ have a toString property? Yes. Call it!
실제로 프로토타입 체인과 관련된 것을 디버깅하지 않는 한 코드에서 __proto__를 직접 건드릴 필요가 거의 없습니다.
fred.__proto__에서 사용할 수 있도록 하려면 Person.prototype에 넣어야 합니다.
적어도 원래는 그렇게 설계되었습니다.
__proto__ 속성은 프로토타입 체인이 내부 개념으로 간주되었기 때문에 처음에는 브라우저에 노출되지 않아야 했습니다.
그러나 일부 브라우저는 __proto__를 추가했고 결국 마지못해 표준화되었습니다.
(하지만 Object.getPrototypeOf()를 선호 사용해야 하며, deprecated된 것으로 취급합시다).
그러나 나는 여전히 프로토타입이라는 속성이 값의 프로토타입을 제공하지 않는다는 것이 매우 혼란스럽다는 것을 알게 되었습니다.
(예를 들어, fred는 함수가 아니기 때문에 fred.prototype은 정의되지 않음).
개인적으로 경험 많은 개발자들이 JavaScript 프로토타입을 오해하는 경향이 있는 가장 큰 이유라고 생각합니다.

obj.foo라고 말할 때 JavaScript는 실제로 obj, obj.__proto__, obj.__proto__.__proto__ 등에서 foo를 찾습니다.

클래스를 사용하면 이 메커니즘에 직접 노출되지 않지만 extends 또한 오래된 좋은 개념인 프로토타입 체인 위에서 동작합니다.
이것이 우리의 React 클래스 인스턴스가 setState와 같은 메소드에 액세스하는 방법입니다.

class Greeting extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

let c = new Greeting();
console.log(c.__proto__); // Greeting.prototype
console.log(c.__proto__.__proto__); // React.Component.prototype
console.log(c.__proto__.__proto__.__proto__); // Object.prototype

c.render();      // Found on c.__proto__ (Greeting.prototype)
c.setState();    // Found on c.__proto__.__proto__ (React.Component.prototype)
c.toString();    // Found on c.__proto__.__proto__.__proto__ (Object.prototype)
다시 말해, 클래스를 사용할 때 인스턴스의 __proto__ 체인은 클래스 계층 구조를 "미러링"합니다.
// `extends` chain
Greeting
  → React.Component
    → Object (implicitly)

// `__proto__` chain
new Greeting()
  → Greeting.prototype
    → React.Component.prototype
      → Object.prototype

두 단계의 상위 체인이 있습니다. (to React, to Object)


__proto__ 체인은 클래스 계층 구조를 반영하므로
Greeting.prototype으로 시작하여 __proto__ 체인을 따라가면

Greeting이 React.Component를 확장하는지 확인할 수 있습니다.

// `__proto__` chain
new Greeting()
  → Greeting.prototype // 🕵️ We start here    
	→ React.Component.prototype // ✅ Found it!      
		→ Object.prototype

편리하게도 x instanceof Y는 정확히 이런 종류의 검색을 수행합니다.
거기에서 Y.prototype을 찾는 x.__proto__ 체인을 따릅니다.
일반적으로 무언가가 클래스의 인스턴스인지 여부를 결정하는 데 사용됩니다.

 

일반적으로 무언가가 클래스의 인스턴스인지 여부를 결정하는 데 사용됩니다.
let greeting = new Greeting();

console.log(greeting instanceof Greeting); // true
// greeting (🕵️‍ We start here)
//   .__proto__ → Greeting.prototype (✅ Found it!)
//     .__proto__ → React.Component.prototype 
//       .__proto__ → Object.prototype

console.log(greeting instanceof React.Component); // true
// greeting (🕵️‍ We start here)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype (✅ Found it!)
//       .__proto__ → Object.prototype

console.log(greeting instanceof Object); // true
// greeting (🕵️‍ We start here)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype
//       .__proto__ → Object.prototype (✅ Found it!)

console.log(greeting instanceof Banana); // false
// greeting (🕵️‍ We start here)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype 
//       .__proto__ → Object.prototype (🙅‍ Did not find it!)
그러나 클래스가 다른 클래스를 확장하는지 확인하는 것도 잘 작동합니다.
console.log(Greeting.prototype instanceof React.Component);
// greeting
//   .__proto__ → Greeting.prototype (🕵️‍ We start here)
//     .__proto__ → React.Component.prototype (✅ Found it!)
//       .__proto__ → Object.prototype
그리고 이 검사는 어떤 것이 React 컴포넌트 클래스인지 일반 함수인지 판별하는 방법입니다!

하지만 React는 이렇게 하지 않습니다. 😳
instanceof 솔루션에 대한 한 가지 주의 사항은
페이지에 React 컴포넌트의 복사본이 여러 개 있을 때 동작하지 않고
우리가 확인하는 컴포넌트가 다른 React 사본의 React.Component에서 상속된다는 것입니다.
단일 프로젝트에서 여러 개의 React 복사본을 섞어쓰는 것은 여러 가지 이유로 좋지 않지만
역사적으로 우리는 가능한 한 문제를 피하려고 노력했습니다.
(Hooks를 사용하면 중복 제거를 강제해야 할 수도 있습니다. might need to)
또 다른 가능한 경험적 방법은 프로토타입에 렌더 메서드가 있는지 확인하는 것입니다.
그러나 당시에는 컴포넌트 API가 어떻게 발전할지 명확하지(wasn’t clear) 않았습니다.
모든 check에는 비용이 있으므로 하나 이상 추가하고 싶지 않습니다.
이는 render가 class 속성 구문과 같이 인스턴스 메서드로 정의된 경우에도 잘 동작하지 않습니다.

 

대신 React는 base 컴포넌트에 특별한 플래그를 추가(added)했습니다.
React는 해당 플래그의 존재를 확인합니다.
이것이 React 컴포넌트 클래스인지 아닌지를 아는 방법입니다.
 
원래 플래그는 기본 React.Component 클래스 자체에 있었습니다.
// Inside React
class Component {}
Component.isReactClass = {};

// We can check it like this
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ✅ Yes

그러나 우리가 목표로 삼고자 하는 일부 클래스 구현은 정적 속성을 복사하지 않았으므로 (또는 비표준 __proto__를 설정함)

플래그가 손실되었습니다.

 

이것이 React가 이 플래그를 React.Component.prototype으로 이동한(moved) 이유입니다.
// Inside React
class Component {}
Component.prototype.isReactComponent = {};

// We can check it like this
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent); // ✅ Yes

이게 다입니다!

왜 플래그가 단지 부울이 아니라 객체인지 궁금할 것입니다.
실제로는 별로 중요하지 않지만 Jest의 초기 버전(Jest was Good™️ 이전)에는 기본적으로 자동 모킹이 켜져 있었습니다.
생성된 모의 객체는 기본 속성을 생략하여 체킹이 안됐습니다(breaking the check). 고마워, 제스트.
isReactComponent 체킹은 현재까지도 React에서 사용됩니다. (used in React)

React.Component를 확장하지 않으면 React는 프로토타입에서 isReactComponent를 찾지 못하고 
component를 클래스로 취급하지 않습니다.

이제 함수 오류로 클래스를 호출할 수 없음에 대해 가장 많이 찬성된 답변이 (the most upvoted answer)
extends React.Component를 추가하는 것인 이유를 알았습니다.
 
마지막으로 prototype.render가 존재하지만 prototype.isReactComponent가 존재하지 않을 때 경고하는 기능이 추가되었습니다.

이 이야기는 일종의 당근과 채찍( a bait-and-switch)이라 할 수 있습니다.
실제 솔루션은 정말 간단하지만 React가 이 솔루션을 선택한 이유와 대안이 무엇인지 설명하기 위해 엄청난 노력을 소모했습니다.
 
내 경험상 라이브러리 API의 경우가 위와 같은 논의가 종종 있습니다.
API를 사용하기 간단하게 하려면
언어 의미론(semantics)(향후 방향을 포함하여 여러 언어에 대해),
런타임 성능,
컴파일 타임을 통한 인체공학(vue)
생태계 및 패키징 솔루션의 상태,
조기 경고 및 기타 여러 가지.
최종 결과가 항상 가장 우아한 것은 아니지만 실용적이어야 합니다.

최종 API가 성공하면 사용자는 이 프로세스에 대해 생각할 필요가 없습니다.
대신 앱을 만드는 데 집중할 수 있습니다.
그러나 당신이 내부 또한 궁금하다면… 그것이 어떻게 작동하는지 아는 것이 좋습니다.

 

반응형