#3 숫자야구 게임 / 함수, 클래스 컴포넌트 비교, 렌더링 막기

sham·2021년 8월 21일
0

인프런에 올라간 제로초님의 강의를 보고 정리한 내용입니다.
https://www.inflearn.com/course/web-game-react


코드

클래스 버전

Baseball.jsx

import React, {Component, createRef} from 'react';
import Try  from './Try.js';

function getNumbers() { //숫자 4개를 랜덤하게
  const numberList = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  const array = [];
  for (let i = 0; i < 4; i++) {
    const selected = numberList.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
    array.push(selected);
  }
  console.log(array);
  return array;
}

class Baseball extends React.Component {
  state = {
    result : "",
    value : "",
    answer : getNumbers(),
    tries : [],
  };
  onSubmitForm = (e) => {
    e.preventDefault();
    console.log(this.state.answer.join('') + ' ' + this.state.value);
    if (this.state.answer.join('') === this.state.value)
    {
      this.setState((prevState) => {
        return {
          result : "홈런!",
          tries : [...this.state.tries, {try: this.state.value, result:"홈런!"}],
          value : "",
        }
      });
      alert("홈런!");
      alert("게임 재시작");
        this.setState({
          result : "",
          value : "",
          answer : getNumbers(),
          tries : [],
        })
      }
    else
    {
      const answerArray = this.state.value.split('').map((e)=>parseInt(e));
      let strike = 0;
      let ball = 0;
      if (this.state.tries.length >= 9) {
        this.setState({result: `실패! 정답은 ${answerArray.join('')}입니다.`});
        alert("게임 재시작");
        this.setState({
          result : "",
          value : "",
          answer : getNumbers(),
          tries : [],
        })
      } else {
        for (let i = 0; i < 4; i++)
        {
          if (answerArray[i] === this.state.answer[i])
            strike++;
          else if (this.state.answer.includes(answerArray[i]))
            ball++;
        }
        this.setState((prevState) => {
          return {
            result : `${strike}스크라이크 ${ball}`,
            tries : [...this.state.tries, {try: this.state.value, result:`${strike}스크라이크 ${ball}`}],
            value : "",
          }
        });
      }
      this.inputRef.current.focus();
    }
     
  };
  onChangeInput = (e) => {
      this.setState({value : e.target.value});
  };


  inputRef = createRef();

 
  render() {
    const {tries, value} = this.state;
    return (
    <>
    <h1>숫자야구</h1>
    <form onSubmit={this.onSubmitForm}>
    <input ref={this.inputRef} maxLength={4} value={value} onChange={this.onChangeInput}/>
    </form>
    <div>시도 : {tries.length}</div>
    <ul>
    {tries.map((v, i) => {return(<Try key={i} tryInfo={v}/>)})}

    </ul>

   
    </>
    )}
}

export default Baseball;

Try.js

import React, {PureComponent} from 'react';

class Try extends PureComponent {

render() {
const { tryInfo } = this.props;
return (
	<li>
		<div>{tryInfo.try}</div>
		<div>{tryInfo.result}</div>
	</li>
		)
	}
}
 */
export default Try;

함수 버전

Baseball.jsx

import React, {useRef, useState, memo} from 'react';
import Try  from './Try.js';

function getNumbers() { //숫자 4개를 랜덤하게
  const numberList = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  const array = [];
  for (let i = 0; i < 4; i++) {
    const selected = numberList.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
    array.push(selected);
  }
  console.log(array);
  return array;
}
const Baseball = memo(() => {

  const [result, setResult] = useState('');
  const [value, setValue] = useState('');
  const [answer, setAnswer] = useState(getNumbers());
  const [tries, setTries] = useState([]);
  const inputEl = useRef(null);

  const onSubmitForm = (e) => {
    e.preventDefault();
    console.log(answer.join('') + ' ' + value);
    if (answer.join('') === value)
    {
      setResult("홈런!");
      setTries((prevTries) => { return [...prevTries, {try : value, result : "홈런!"}] });
      setValue();
      alert("홈런!");
      alert("게임 재시작");
      setResult("");
      setValue("");
      setAnswer(getNumbers());
      setTries([]);
    }
    else
    {
      const answerArray = value.split('').map((e)=>parseInt(e));
      let strike = 0;
      let ball = 0;
      if (tries.length >= 9) {
        alert(`실패! 정답은 ${answerArray.join('')}입니다.`);
        alert("게임 재시작");
      setResult("");
      setValue("");
      setAnswer(getNumbers());
      setTries([]);
      } 
      else {
        for (let i = 0; i < 4; i++)
        {
          if (answerArray[i] === answer[i])
            strike++;
          else if (answer.includes(answerArray[i]))
            ball++;
        }
        setResult(`${strike}스크라이크 ${ball}`);
        setTries((prevTries) => {
          return [...prevTries, {try : value, result : `${strike}스크라이크 ${ball}`}]});
        setValue("");
      };  
    };
    inputEl.current.focus();
  };

  const onChangeInput = (e) => {
      setValue(e.target.value);
  };


  return (
    <>
    <h1>숫자야구</h1>
    <form onSubmit={onSubmitForm}>
    <input ref={inputEl} maxLength={4} value={value} onChange={onChangeInput}/>
    </form>
    <div>시도 : {tries.length}</div>
    <div>결과 : {result}</div>
    <ul>
    {tries.map((v, i) => {return(<Try key={i} tryInfo={v}/>)})}
    </ul>
    </>
  )

})

Try.js

import React, {memo} from 'react';


const Try = memo(({tryInfo}) => {
	return (
		<li>
			<div>{tryInfo.try}</div>
			<div>{tryInfo.result}</div>
		</li>
			)
});

export default Try;

#3-1 import와 require 비교

require

  • 노드의 모듈 시스템. 남이 만든 모듈이나 다른 파일에서 모듈화시킨 파일을 불러올 수 있다. 노드에서 주로 쓰인다.
  • 내보낼 때 : module.exports = {리엑트 컴포넌트 이름}
  • 불러올 때 : const {객체} = require('{파일 주소나 모듈 이름}');

import

  • ES2015의 문법. 리액트에서 주로 쓰인다.
  • 내보낼 때 : export default "{리액트 컴포넌트 이름}"
  • 불러 올 때 : import {객체} from "{파일 주소나 모듈 이름}"

import 할 때 default로 export 된 것이 아닌 것들은 중괄호 안에 감싸서 import해야 한다.

export default는 한 파일에서 한 번씩만 쓸 수 있다. 주로 컴포넌트를 export 할 때 사용된다. 그냥 export하는 것은 얼마든지 가능하다.

export const hello = 'hello'
hello 객체를 'hello'라는 이름으로 내보낸다.

엄밀히 따지면 두 표기법은 다르지만 지금은 호환된다는 선에서 멈춰도 된다.


#3-2 리액트 반복문(map)

숫자 야구.

사용자가 만든 메서드를 쓸 때는 항상 화살표 함수를 써야 한다.

value와 onChange, value와 defaultValue는 한 세트.

map

https://velog.io/@daybreak/Javascript-map함수

<ul>
      {['사과', '배', '포도'].map( (e) => {
       
        return <li>{e}</li>
      })}
    </ul>

배열의 각 원소들을 앞에서부터 하나씩 읽는다. 보통 화살표 함수와 같이 쓰인다.

  • ['a', 'b', 'c', 'd'].map((e)=>{console.log({e})})
    • 해당 배열의 요소들을 받아 올 수 있다.

#3-3 리액트 반복문(key)

일차원 배열이 아닌 key, value형태를 요구할 시, 다른 방법을 사용해야 한다.

  1. 2차원 배열로 만드는 방법.
<ul>
{[['사과', '달다'], ['배', '시원하다'], ['포도', '시큼하다']].map( (e) => {
        return <li>{e[0]} {e[1]}</li>
      })}
    </ul>
  • e가 배열 자체가 된다.
    • 배열은 []
  1. 객체로 만드는 방법.
<ul>
      {[{name : '사과', taste : '달다'}, {name : '배', taste : '시원하다'}, {name : '포도', taste : '시다'}].map( (e) => {
        return <li>{e.name} {e.taste}</li>
      })}
    </ul>
  • e가 객체 자체가 된다.
    • 객체는 {}
  1. props로 만드는 방법.
<ul>
    {[{name : '사과', taste : '달다'}, 
    {name : '배', taste : '시원하다'}, 
    {name : '포도', taste : '시다'}].map((e) => <li key={e.name + e.taste}>{e.name} {e.taste}</li>)}
    </ul>
  • 가독성, 성능 측면에서 우수하다.
  • map을 사용할 때는 반드시 객체들의 고유한 값인 key를 설정해 주어야 한다.
    • 사용자에게 보여지지는 않지만 성능 최적화에 사용이 된다.
      • 리액트가 key를 보고 같은 컴포넌트인지 아닌지 판단한다.
  • map(e, i)를 하면 e에는 객체, i에는 해당 객체의 인덱스 값이 들어온다.
    • key를 설정할 때 인덱스 값으로 설정하게 되면 순서가 뒤바뀌었을 때 문제가 생긴다.

화살표 함수에서는 리턴, 소괄호를 생략해줄 수 있다.


#3-4 컴포넌트 분리, props

파일을 쪼개 여러 개의 컴포넌트를 최종적으로 하나의 파일에서 렌더링하게 할 수 있다.

  • 가시성, 성능 문제를 해결한다.

컴포넌트에 필요한 정보가 다른 파일에 있을 때 해당 컴포넌트를 사용할 때 같이 보내주어야 한다.

  • HTML의 속성(atrribute)와 같은 개념, 리액트에서는 props라는 명칭을 사용한다.

#3-5 주석, 메서드 바인딩

Redux vs MobX vs Context API

  • props가 깊어질 때 관리하기 쉽게 해준다.
  • props을 관리하는 context, 좀 더 복잡한 일을 할 수 있는 Redux

주석 처리

{/* */}

화살표 함수의 필요성

{메서드}(e){
console.log(this);
}
//undefined
{메서드} = (e) => {
console.log(this);
}
//클래스 객체

화살표 함수를 사용해야만 this가 해당 클래스 메서드를 가리키게 된다.

constructor(props) {
super(props);
this.state = {
result : '',
value : '',
answer : getNumbers(),
tries : [],
};
this.onSubmitForm = this.onSubmitForm.bind(this);
this.onChangeInput = this.onChangeInput.bind(this);}

함수처럼 쓰고 싶다면 constructor를 사용해야 한다.

화살표 함수가 bind(this) 역할을 자동으로 해주는 것.


#3-6 숫자야구, 불변성

리액트에서 배열에 값을 추가할 때 push()를 쓰면 안된다.

  • 리액트에서 무엇이 바뀌었는지 감지하지 못한다.
  • 예전 state와 현재 state의 참조가 달라야 렌더링한다.
const arr = [];
arr.push(1);
console.log(arr === arr); //ture
const new_arr = [...arr, 2];
console.log(arr === new_arr) //false

기존 배열을 복사해서 만든 새로운 배열을 할당해주는 방식으로 배열을 갱신해야 한다.

import React from 'react';
import Try  from './Try.js';

function getNumbers() { //숫자 4개를 랜덤하게
  const numberList = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  const array = [];
  for (let i = 0; i < 4; i++) {
    const selected = numberList.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
    array.push(selected);
  }
  console.log(array);
  return array;
}

class Baseball extends React.Component {
  state = {
    result : "",
    value : "",
    answer : getNumbers(),
    tries : [],
  };
  onSubmitForm = (e) => {
    e.preventDefault();
    console.log(this.state.answer.join('') + ' ' + this.state.value);
    if (this.state.answer.join('') === this.state.value)
    {
      this.setState({
        result : "홈런!",
        tries : [...this.state.tries, {try: this.state.value, result:"홈런!"}],
        value : "",
      })
      alert("홈런!");
      alert("게임 재시작");
        this.setState({
          result : "",
          value : "",
          answer : getNumbers(),
          tries : [],
        })
      }
    else
    {
      const answerArray = this.state.value.split('').map((e)=>parseInt(e));
      let strike = 0;
      let ball = 0;
      if (this.state.tries.length >= 9) {
        this.setState({result: `실패! 정답은 ${answerArray.join('')}입니다.`});
        alert("게임 재시작");
        this.setState({
          result : "",
          value : "",
          answer : getNumbers(),
          tries : [],
        })
      } else {
        for (let i = 0; i < 4; i++)
        {
          if (answerArray[i] === this.state.answer[i])
            strike++;
          else if (this.state.answer.includes(answerArray[i]))
            ball++;
        }
        this.setState({
          result : `${strike}스크라이크 ${ball}`,
          tries : [...this.state.tries, {try: this.state.value, result:`${strike}스크라이크 ${ball}`}],
          value : "",
        })
      }
    }
     
  };
  onChangeInput = (e) => {
      this.setState({value : e.target.value});
  };

  render() {
    return (
    <>
    <h1>숫자야구</h1>
    <form onSubmit={this.onSubmitForm}>
    <input maxLength={4} value={this.state.value} onChange={this.onChangeInput}/>
    </form>
    <div>시도 : {this.state.tries.length}</div>
    <ul>
    {this.state.tries.map((v, i) => {return(<Try key={i} tryInfo={v}/>)})}
    </ul>
    </>
    )}
}

export default Baseball;

{배열}.splice()

  • 배열의 요소를 빼내서 배열 형태로 리턴한다.
  • 첫 번째 인자로 빼낼 인덱스, 두 번째 인자로 해당 인덱스부터 얼마나 빼낼 것인지를 넣는다. 둘 다 정수형이다.

Math.floor()

  • 괄호 안의 실수를 내림해서 리턴한다.

Math.random()

  • 0부터 1까지 중 무작위 실수를 리턴한다.
  • 원하는 범위를 곱하는 형식으로 쓰인다.

{배열}.push()

  • 배열 맨 뒤에 인자를 추가한다.

{배열}.join('')

  • 배열을 하나의 문자열로 합친다. 괄호 안에 든 것을 배열 사이 구분자로 추가한다.

{배열}.includes()

  • 배열 안에 인자로 넣은 것과 동일한 요소가 있다면 참을 리턴한다.

리액트에서의 반복문은 대부분 map을 사용한다.

  render() {
const {}
    return (
    <>
    <h1>숫자야구</h1>
    <form onSubmit={this.onSubmitForm}>
    <input maxLength={4} value={this.state.value} onChange={this.onChangeInput}/>
    </form>
    <div>시도 : {this.state.tries.length}</div>
    <ul>
    {this.state.tries.map((v, i) => {return(<Try key={i} tryInfo={v}/>)})}
    </ul>
    </>
    )}
}

state의 속성을 사용할 때 구조 분해로 state의 속성을 변수에 대입하면 this.state를 생략할 수 있다.

메서드가 아닌 함수(클래스 밖에 배치)로 할 수 있는 경우this를 사용하지 않을 경우 클래스 바깥에 뺄 수 있다.


#3-8 hooks 전환

class exampleClass extends React.Component {
State = {
		a : "unchange",
		b : 1,
}
this.setState({
	a : "change",
	b : 3
})
}

const exampleFunction = () => {
[a, setA] = useState("unchange");
[b, setB] = useState(1);

setA("change");
setB(3); 
}

클래스 컴포넌트에서 함수 컴포넌트로 변경 시

State → useState import해서 사용.

this.state 삭제

ref 변경

const obj = {a : "123", b : "456", c  : "789"};
const func = ({a, b, c}) =>
{
	console.log(a, b, c);
}
func(obj);

함수에서 props를 받아올 때 {}안에 props의 속성을 집어넣어서 할당시킬 수 있다.

setState((prevTries) => {
return [...prevTries, {try : value, result : "홈런!"}] 
})

옛날 데이터를 새로운 데이터를 만들 때 그대로 쓰게 된다면 화살표 함수를 써주어야만 한다.

//함수 컴포넌트로 변경.
import React, {useState} from 'react';
import Try  from './Try.js';

function getNumbers() { //숫자 4개를 랜덤하게
  const numberList = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  const array = [];
  for (let i = 0; i < 4; i++) {
    const selected = numberList.splice(Math.floor(Math.random() * (9 - i)), 1)[0];
    array.push(selected);
  }
  console.log(array);
  return array;
}

const Baseball = () => {

  const [result, setResult] = useState('');
  const [value, setValue] = useState('');
  const [answer, setAnswer] = useState(getNumbers());
  const [tries, setTries] = useState([]);

  const onSubmitForm = (e) => {
    e.preventDefault();
    console.log(answer.join('') + ' ' + value);
    if (answer.join('') === value)
    {
      setResult("홈런!");
      setTries((prevTries) => {
        return [...prevTries, {try : value, result : "홈런!"}] 
      });
      setValue();
      alert("홈런!");
      alert("게임 재시작");
      setResult("");
      setValue("");
      setAnswer(getNumbers());
      setTries([]);
    }
    else
    {
      const answerArray = value.split('').map((e)=>parseInt(e));
      let strike = 0;
      let ball = 0;
      if (tries.length >= 9) {
        setResult(`실패! 정답은 ${answerArray.join('')}입니다.`);
        alert("게임 재시작");
      setResult("");
      setValue("");
      setAnswer(getNumbers());
      setTries([]);
      } else {
        for (let i = 0; i < 4; i++)
        {
          if (answerArray[i] === answer[i])
            strike++;
          else if (answer.includes(answerArray[i]))
            ball++;
        }
        setResult(`${strike}스크라이크 ${ball}`);
        setTries((prevTries) => {
          return [...prevTries, {try : value, result : `${strike}스크라이크 ${ball}`}]
        });
        setValue("");
      }     
    }
  };

  const onChangeInput = (e) => {
      (e.target.value);
  };

  return (
    <>
    <h1>숫자야구</h1>
    <form onSubmit={onSubmitForm}>
    <input maxLength={4} value={value} onChange={onChangeInput}/>
    </form>
    <div>시도 : {tries.length}</div>
    <ul>
    {tries.map((v, i) => {return(<Try key={i} tryInfo={v}/>)})}
    </ul>
    </>
  )
}
export default Baseball;

함수 컴포넌트로 변경.


#3-9 React Devtools

props를 활용하다 보면 문제가 많이 생긴다.

  • 렌더링이 자주 일어나 성능이 나빠지는 이슈

크롬 확장 프로그램 중 React DevTool을 사용하면 리액트 컴포넌트를 확인할 수가 있다.

배포 모드에서는 소스 코드, 압축 및 최적화가 되어 있다.

  • 웹팩 파일에서 개발 모드를 배포 모드로 수정해야 한다.

#3-10 shouldComponentUpdate

기존 코드는 하나의 state를 수정해도 모든 컴포넌트가 랜더링하도록 되어 있다.

import React, {Component} from 'react';

class Test extends Component {
	state = {
		counters: 0,
	}

	onClick = () => {
		this.setState({
			counters: 0,
		})
	}

	shouldComponentUpdate(nextProps, nextState, nextContext) {
		if (this.state.counter === nextState.counter) {
			return false;
		}
		else {
			return true;
		}
	}
	render() {
console.log("렌더링");
		return (
			<div>
				<button onClick={this.onClick}>클릭!</button>
				<div>{this.state.counters}</div>
			</div>
		)
	}
}

export default Test;

리액트에서는 setState를 호출하기만 하면 render()를 다시 실행해서 렌더링하게 된다.

이를 막기 위해 shouldComponentUpdate()를 사용해서 함수의 인자인 미래의 값들과 현재의 값들을 비교해 값이 동일하다면 다시 렌더링 되지 않게 해줄 수 있다.


#3-11 PureComponent와 React.memo

다시 렌더링 되는 것을 막는 또다른 방법은 PureComponent를 만드는 것.

import React, {PureComponent} from 'react';

class Test extends PureComponent {
	state = {
		counters: 0,
	}

	onClick = () => {
		this.setState({
			counters: 0,
		})
	}

	render() {
		console.log("렌더링");
		return (
			<div>
				<button onClick={this.onClick}>클릭!</button>
				<div>{this.state.counters}</div>
			</div>
		)
	}
}

export default Test;

shouldComponentUpdate()가 자동으로 구현되어 있다.

  • state가 바뀌었는지 바뀌지 않았는지를 자동으로 체크하고 판단한다.

객체나 배열 같은 복잡한(참조) 구조가 있다면 제대로 작동하지 않을 수 있다.

onClick = () => {
		const arr = this.state.array;
		arr.push(1);
		this.setState({
			counters: 0,
			array : arr,
		})
	} 

배열을 추가할 때 push()로 추가한다면 참조하는 주소가 동일하기 때문에 변했다고 판단하지 않는다.

	onClick = () => {
		this.setState ({
			counters: 0,
			array : [this.state.array, 1],
		});
	}

그렇기에 더더욱 기존 객체를 그대로 가져오지 말고 기존의 배열에 새로운 값을 추가한 배열을 할당하는 방식을 써주어야 한다.

가급적 클래스 컴포넌트의 state에 객체(object) 구조를 안 쓰는 것이 좋다.

//Try.js

import React, {PureComponent} from 'react';

//클래스 컴포넌트
class Try extends PureComponent {

render() {
const { tryInfo } = this.props;
return (
	<li>
		<div>{tryInfo.try}</div>
		<div>{tryInfo.result}</div>
	</li>
		)
	}
}
 
export default Try;

기존의 코드 수정.

컴포넌트가 복잡해지면 퓨어컴포넌트가 안 먹힐 수도 있고, 컴포넌트를 써야 하는 경우도 많다.

컴포넌트에 shouldComponentUpdate()를 쓰는 경우도 많다.

import React, {PureComponent, memo} from 'react';

//함수 컴포넌트

const Try = memo(({tryInfo}) => {
	return (
		<li>
			<div>{tryInfo.try}</div>
			<div>{tryInfo.result}</div>
		</li>
			)
});

export default Try;

함수 컴포넌트의 경우 컴포넌트를 memo()함수로 감싸주면 된다.

자식 컴포넌트가 모두 퓨어 컴포넌트나 memo가 적용되었다면 부모도 똑같이 적용시켜도 무방하다.


#3-12 React.createRef

DOM에 접근하기 위해 해당 태그 속성에 ref를 넣을 때 클래스와 함수 컴포넌트 각각 방법이 다름.

클래스

원하는 태그에 ref={(c) => this.input = c;} , 혹은 ref={inputRef}(메서드에서 (c) => this.input = c; 구문 실행).

클래스 내부의 input이라는 객체로 해당 태그에 접근 가능.

this.input.focus();

함수

useRef를 import해서 사용.

const input = useRef(null);

원하는 태그에 ref={input} 로 설정.

input로 해당 태그 접근 가능.

input.current.focus();

React.createRef

클래스 컴포넌트에서도 함수 컴포넌트처럼 ref를 사용할 수 있게 해준다.

createRef를 import해서 사용.

input = createRef();

원하는 태그에 ref={input} 으로 메서드 호출.

input으로 접근가능.

함수처럼 this.input.current.focus();

createRef가 만능이냐면 그것은 아니다. 편리하기는 하지만 클래스 컴포넌트에서 ref로 함수를 호출하게 되면 조금 더 디테일한 설정이 가능해진다.
방식의 차이에 대해 이해하면 내가 원하는 것을 정확하게 구현할 수 있다.


#3-13. props와 state 연결하기

개념 정리, 팁

render()안에서 this.setState()를 호출하면 setState → state값 변경 → render() 호출 무한반복이 일어난다.

부모 컴포넌트가 전달해준 props을 자식 컴포넌트가 바꾸려고 해서는 안된다. 자식에 의해 부모의 state가 바뀌게 된다.

  • 만일 불가피하게 해야 한다면 useState로 state를 만든 후 해당 state를 바꿔서 부모에게 영향이 없게 해야 한다.
  • 클래스의 경우에도 마찬가지로 state를 생성해서 자식의 state를 바꾸는 방법을 쓸 수 있다.

정밀한 작업이 필요할 때 constructor를 사용하는 방법이 있다.

클래스에서 constructor(props)를 사용시 항상 super(props)를 써줘야 한다.

컨텍스트란?

A → B → C → D 형태로 컴포넌트 간 자식 관계가 형성되어 있다고 가정하자.

만일 A 에서 D로 props를 전달하고 싶을 때, 위의 경로대로 전달하게 되면 쓸데없이 렌더링 되는 경우가 생길 수 있다.

A에서 D로 직통으로 전달할 수 있는 방법이 바로 컨텍스트다.

진화관계 ) props < 컨텍스트 < 리덕스

profile
씨앗 개발자

0개의 댓글

관련 채용 정보