#6 useCallback, useMemo / 로또 추첨기

153240 단어 ReactReact

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


코드

클래스 버전

LottoClass.jsx

import React, {Component} from 'react';
import Ball from './ballClass';

function getWinNumbers() {
	console.log("getWinNumber");
	const candidate = Array(45).fill().map((v, i) => i + 1);
	const shuffle = [];
	while (candidate.length > 0) {
		shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);
	}
	const bonusNumber = shuffle[shuffle.length - 1];
	const winNumber = shuffle.slice(0, 6).sort((p , c) => p - c);
	return [...winNumber, bonusNumber];
}

class Lotto extends Component {
	state = {
		winNumber : getWinNumbers(), // 숫자의 배열
		winBalls : [], // 앞의 6개
		bonus : null,
		redo : false,
	}
	timeouts = [];

	runTimeout = () => {
		console.log("runTimeout");
		const {winNumber} = this.state;
		for (let i = 0; i < winNumber.length - 1; i++) {
			this.timeouts[i] = setTimeout(() => {
				this.setState((prevState) => {
					return {
						winBalls : [...prevState.winBalls, winNumber[i]],
					}
				})
		}, (i + 1) * 1000);
	}
	this.timeouts[6] = setTimeout(() => {
		this.setState({
			bonus : winNumber[6],
			redo : true,
		})
	}, 7000);
	}
	componentDidMount() {
		console.log("componentDidMount");
		this.runTimeout();
}

	componentDidUpdate(prevProps, prevState) {
		console.log("componentDidUpdate");

		if (this.state.winBalls.length === 0)
		{
			this.runTimeout();
		}
	} 

	componentWillUnmount() {

		this.timeouts.forEach((v) => 
		clearTimeout(v));
	}

	onClickRedo = () => {
		console.log("onClickRedo");

		this.setState({
			winNumber : getWinNumbers(), // 숫자의 배열
			winBalls : [], // 앞의 6개
			bonus : null,
			redo : false,
		});
		this.timeouts = [];
	}

	render() {
		const {winBalls, bonus, redo} = this.state;
		return (
			<>
			 <div>당첨 숫자</div>
      <div id="결과창">
        {winBalls.map((v) => <Ball key={v} number={v} />)}
      </div>
      <div>보너스!</div>
      {bonus && <Ball key={bonus} number={bonus}/>}
      {redo && <button onClick={this.onClickRedo}>한 번 더!</button>}
			</>
		)
	}
}

export default Lotto;

ballClass.js

import React, { PureComponent } from 'react';

class Ball extends PureComponent {
	render () {
	const {number} = this.props;
	let background;
	if (number <= 10) {
		background = 'red';
	  } else if (number <= 20) {
		background = 'orange';
	  } else if (number <= 30) {
		background = 'yellow';
	  } else if (number <= 40) {
		background = 'blue';
	  } else {
		background = 'green';
	  }
		return (
			<div className="ball" style={{background}}>{number}</div>
			)
	}
}

export default Ball;

함수 버전

LottoFunction.jsx

import React, {useRef, useState, useEffect, useMemo, useCallback} from 'react';
import Ball from './ballClass';

function getWinNumbers() {
	console.log("getWinNumber");
	const candidate = Array(45).fill().map((v, i) => i + 1);
	const shuffle = [];
	while (candidate.length > 0) {
		shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);
	}
	const bonusNumber = shuffle[shuffle.length - 1];
	const winNumbers = shuffle.slice(0, 6).sort((p , c) => p - c);
	return [...winNumbers, bonusNumber];
}

const Lotto = () => {
	const [winBalls, setWinBalls] = useState([]);
	const lottoNumbers = useMemo(() => getWinNumbers(), [winBalls]);
	const [winNumbers, setWinNumber] = useState(lottoNumbers);
	const [bonus, setBonus] = useState(null);
	const [redo, setRedo] = useState(false);
	const timeouts = useRef([]);

	useEffect(() => {
		console.log("useEffect");
		runTimeout();
		return () => {
			timeouts.current.forEach((v) => {
				clearTimeout(v);
			})
		}
	}, [timeouts.current])

	const runTimeout = () => {
		for (let i = 0; i < winNumbers.length - 1; i++) {
			timeouts.current[i] = setTimeout(() => {
			setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
			}, (i + 1) * 1000);
	}
	timeouts.current[6] = setTimeout(() => {
		setBonus(winNumbers[6]);
		setRedo(true);	
		}, 7000);
	}

	const onClickRedo = useCallback(() => {
		console.log(winNumbers);
		console.log("onClickRedo");
			setWinNumber(getWinNumbers());
			setWinBalls([])
			setBonus(null);
			setRedo(false);
		timeouts.current = [];
	}, [winNumbers]);

		return (
			<>
			 <div>당첨 숫자</div>
      <div id="결과창">
        {winBalls.map((v) => <Ball key={v} number={v} />)}
      </div>
      <div>보너스!</div>
      {bonus && <Ball key={bonus} number={bonus}/>}
      {redo && <button onClick={onClickRedo}>한 번 더!</button>}
			</>
		)

}

export default Lotto;

ballFunction.js

import React, {memo} from 'react';

const Ball = memo(({number}) => {
		let background;
		if (number <= 10) {
			background = 'red';
		  } else if (number <= 20) {
			background = 'orange';
		  } else if (number <= 30) {
			background = 'yellow';
		  } else if (number <= 40) {
			background = 'blue';
		  } else {
			background = 'green';
		  }
			return (
				<div className="ball" style={{background}}>{number}</div>
				)
});

export default Ball;

#6-1 로또 추첨기 컴포넌트

//Rotto.jsx
import React, {Component} from 'react';

function getWinNumbers() {
	console.log("getWinNumber");
	const candidate = Array(45).fill().map((v, i) => i + 1);
	const shuffle = [];
	while (candidate.length > 0) {
		shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);
	}
	const bonusNumber = shuffle[shuffle.length - 1];
	const winNumber = shuffle.slice(0, 6).sort((p , c) => p - c);
	return [...winNumber, bonusNumber];
}

class Lotto extends Component {
	state = {
		winNumber : getWinNumbers(), // 숫자의 배열
		winballs : [], // 앞의 6개
		bonus : null,
		redo : false,
	}

	render() {
		const {winballs, bonus, redo} = this.state;
		return (
			<>
			 <div>당첨 숫자</div>
      <div id="결과창">
        {winBalls.map((v) => <Ball key={v} number={v} />)}
      </div>
      <div>보너스!</div>
      {bonus && <Ball number={bonus} onClick={onClickRedo} />}
      {redo && <button onClick={onClickRedo}>한 번 더!</button>}
			</>
		)
	}
}

export default Rotto;

1부터 45까지 뒤섞인 배열을 미리 만들어서 앞의 6개를 가지고 당첨 숫자를 알아낸다.

반복문(map)은 자식 컴포넌트를 만들고 props를 전달할 좋은 기점이 된다.

html 태그에 스타일을 집어넣을 때는 중괄호를 두겹 쓴다?

  import React from 'react';

const Ball = memo(({number}) => {
		let background;
		if (number <= 10) {
			background = 'red';
		  } else if (number <= 20) {
			background = 'orange';
		  } else if (number <= 30) {
			background = 'yellow';
		  } else if (number <= 40) {
			background = 'blue';
		  } else {
			background = 'green';
		  }
			return (
				<div className="ball" style={{background}}>{number}</div>
				)
});

export default Ball;

hooks가 아닌 함수 컴포넌트로 전환.

hooks는 함수 컴포넌트를 말하는 게 아니다. useState, useEffect가 hooks고 그것을 사용하는 것이 함수 컴포넌트일 뿐.

memo처럼 컴포넌트를 다른 컴포넌트로 감싸는 것을 고차 컴포넌트(하이오더 컴포넌트, HOC)라고 한다.

PureComponent를 다른 컴포넌트로 감쌀 필요는 없다.


#6-2 setTimeout 여러 번 사용하기

let을 쓰면 비동기여도 클로저 문제가 생기지 않는다.

import React, {Component} from 'react';
import Ball from './ballClass';

function getWinNumbers() {
	console.log("getWinNumber");
	const candidate = Array(45).fill().map((v, i) => i + 1);
	const shuffle = [];
	while (candidate.length > 0) {
		shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);
	}
	const bonusNumber = shuffle[shuffle.length - 1];
	const winNumber = shuffle.slice(0, 6).sort((p , c) => p - c);
	return [...winNumber, bonusNumber];
}

class Lotto extends Component {
	state = {
		winNumber : getWinNumbers(), // 숫자의 배열
		winBalls : [], // 앞의 6개
		bonus : null,
		redo : false,
	}
	timeouts = [];
	componentDidMount() {
		const {winNumber} = this.state;
		for (let i = 0; i < winNumber.length - 1; i++) {
			this.timeouts[i] = setTimeout(() => {
				this.setState((prevState) => {
					return {
						winBalls : [...prevState.winBalls, winNumber[i]],
					}
				})
		}, (i + 1) * 1000);
	}
	this.timeouts[6] = setTimeout(() => {
		this.setState({
			bonus : winNumber[6],
			redo : true,
		})
	}, 7000);
}
	componentWillUnmount() {
		this.timeouts.forEach((v) => 
		clearTimeout(v));
	}
	onClickRedo = () => {

	}

	render() {
		const {winBalls, bonus, redo} = this.state;
		return (
			<>
			 <div>당첨 숫자</div>
      <div id="결과창">
        {winBalls.map((v) => <Ball key={v} number={v} />)}
      </div>
      <div>보너스!</div>
      {bonus && <Ball number={bonus}/>}
      {redo && <button onClick={this.onClickRedo}>한 번 더!</button>}
			</>
		)
	}
}

export default Lotto;

for문을 돌리며 setTimeout을 반복 설정, 기다리는 시간을 for의 i가 증감하는 것과 비례하여 늘려준다. 1초 대기, 2초 대기, 3초 대기... 형식으로 1초마다 나타나는 것처럼.

setTimeout, setInterval은 메모리상에서 남아있기 때문에 항상 componentWillUnmount()에서 제거해주어야 한다.

  • setTimeout은 한 번 실행되고 말지 setInterval는 계속 실행되기 때문에 특히 더 주의깊게 봐야한다.

클래스 내부에 timeouts이라는 객체를 만들어 setTimeout, setInterval을 저장해서 componentWillUnmount에서 제거할 수 있게끔 한다.

componentWillMount, componentWillReceiveProps, componentWillUpdate도 라이프사이클 중 하나이나 곧 사라질 것들, 쓸 필요 없다.

Array(45).fill().map((v, i) => i + 1);

Array(45)로 빈 슬롯이 45개인 배열을 생성한다. (Array 앞에 new 를 붙여도 무방하다.)

빈 배열을 가지고 map함수를 호출한다. map 함수의 첫 번째 인자는 각 배열, 두 번째 인자는 해당 배열의 인덱스를 나타낸다.

0부터 시작하는 인덱스에 1을 더해 리턴하면 1부터 45까지의 오름차순 배열이 된다.

화살표 함수 표기로 생략된 부분을 풀어쓰면 Array(45).fill().map(function(v, i){return i + 1})이 된다.

shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);

차례대로 정렬되어 있는 candidate 배열에서 Math.random을 이용해 랜덤한 인덱스에서부터 1개를 배열로 뽑아서 push한다. 이를 45번 반복하면 뒤죽박죽의 배열이 완성된다.

const winNumber = shuffle.slice(0, 6).sort((p , c) => p - c);

당첨 숫자들을 오름차순으로 정렬한다.

&& 조건문

render() {
		const {winBalls, bonus, redo} = this.state;
		return (
			<>
			 <div>당첨 숫자</div>
      <div id="결과창">
        {winBalls.map((v) => <Ball key={v} number={v} />)}
      </div>
      <div>보너스!</div>
      {bonus && <Ball number={bonus} />}
      {redo && <button>한 번 더!</button>}
			</>
		)
	}
}

&& 연산자를 이용해 jsx로 state의 값이 참이면 출력, 거짓이면 출력되지 않게 할 수 있다.


#6-3 componentDidUpdate

쿼리 셀렉터나 jquery처럼 직접 DOM을 건들지 않고(ref 제외) State(데이터)만 바꿀 수 있는 것이 리액트의 특징.

import React, {Component} from 'react';
import Ball from './ballClass';

function getWinNumbers() {
	console.log("getWinNumber");
	const candidate = Array(45).fill().map((v, i) => i + 1);
	const shuffle = [];
	while (candidate.length > 0) {
		shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);
	}
	const bonusNumber = shuffle[shuffle.length - 1];
	const winNumber = shuffle.slice(0, 6).sort((p , c) => p - c);
	return [...winNumber, bonusNumber];
}

class Lotto extends Component {
	state = {
		winNumber : getWinNumbers(), // 숫자의 배열
		winBalls : [], // 앞의 6개
		bonus : null,
		redo : false,
	}
	timeouts = [];

	runTimeout = () => {
		console.log("runTimeout");
		const {winNumber} = this.state;
		for (let i = 0; i < winNumber.length - 1; i++) {
			this.timeouts[i] = setTimeout(() => {
				this.setState((prevState) => {
					return {
						winBalls : [...prevState.winBalls, winNumber[i]],
					}
				})
		}, (i + 1) * 1000);
	}
	this.timeouts[6] = setTimeout(() => {
		this.setState({
			bonus : winNumber[6],
			redo : true,
		})
	}, 7000);
	}
	componentDidMount() {
		console.log("componentDidMount");
		this.runTimeout();
}

	componentDidUpdate(prevProps, prevState) {
		console.log("componentDidUpdate");

		if (this.state.winBalls.length === 0)
		{
			this.runTimeout();
		}
	} 

	componentWillUnmount() {

		this.timeouts.forEach((v) => 
		clearTimeout(v));
	}

	onClickRedo = () => {
		console.log("onClickRedo");

		this.setState({
			winNumber : getWinNumbers(), // 숫자의 배열
			winBalls : [], // 앞의 6개
			bonus : null,
			redo : false,
		});
		this.timeouts = [];
	}

	render() {
		const {winBalls, bonus, redo} = this.state;
		return (
			<>
			 <div>당첨 숫자</div>
      <div id="결과창">
        {winBalls.map((v) => <Ball key={v} number={v} />)}
      </div>
      <div>보너스!</div>
      {bonus && <Ball key={bonus} number={bonus}/>}
      {redo && <button onClick={this.onClickRedo}>한 번 더!</button>}
			</>
		)
	}
}

export default Lotto;

버튼을 눌러서 state를 초기화한 후, 다시 setTimeout을 실행하게끔 해야 한다.

setState가 실행될때마다 componentDidUpdate도 실행된다. 적절한 조건문을 걸어서 state가 초기화 된 경우에만 실행되도록 해야 한다.

메서드가 실행될 때마다 console.log()를 찍어보는 것은 흐름을 이해하는데 도움이 된다.

componentDidUpdate(prevProps, prevState)

바뀌기 이전의 데이터는 인자인 prevState, prevProps에 들어있고, 바뀐 후의 데이터는 this.props, this.state에 들어있다.


#6-4 useEffect로 업데이트 감지하기

hooks로 라이프사이클을 바꾸는 것은 까다로운 작업이다.

import React, {useRef, useState, useEffect} from 'react';
import Ball from './ballClass';

function getWinNumbers() {
	console.log("getWinNumber");
	const candidate = Array(45).fill().map((v, i) => i + 1);
	const shuffle = [];
	while (candidate.length > 0) {
		shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);
	}
	const bonusNumber = shuffle[shuffle.length - 1];
	const winNumber = shuffle.slice(0, 6).sort((p , c) => p - c);
	return [...winNumber, bonusNumber];
}

const Lotto = () => {
	const [winNumber, setWinNumber] = useState(getWinNumbers());
	const [winBalls, setWinBalls] = useState([]);
	const [bonus, setBonus] = useState(null);
	const [redo, setRedo] = useState(false);
	const timeouts = useRef([]);

	useEffect(() => {
		console.log("useEffect");
		runTimeout();
		return () => {
			timeouts.current.forEach((v) => {
				clearTimeout(v);
			})
		}
	}, [timeouts.current])

	const runTimeout = () => {
		for (let i = 0; i < winNumber.length - 1; i++) {
			timeouts.current[i] = setTimeout(() => {
			setWinBalls((prevWinBalls) => [...prevWinBalls, winNumber[i]]);
			}, (i + 1) * 1000);
	}
	timeouts.current[6] = setTimeout(() => {
		setBonus(winNumber[6]);
		setRedo(true);	
		}, 7000);
	}

	const onClickRedo = () => {
		console.log("onClickRedo");

			setWinNumber(getWinNumbers());
			setWinBalls([])
			setBonus(null);
			setRedo(false);
		timeouts.current = [];
	}

		return (
			<>
			 <div>당첨 숫자</div>
      <div id="결과창">
        {winBalls.map((v) => <Ball key={v} number={v} />)}
      </div>
      <div>보너스!</div>
      {bonus && <Ball key={bonus} number={bonus}/>}
      {redo && <button onClick={onClickRedo}>한 번 더!</button>}
			</>
		)

}

export default Lotto;

앞선 강의와 같이 hooks의 라이프사이클은 useEffect로 제어한다.

useEffect의 두 번째 인자에 요소가 없다면 componentDidMount의 역할을, 있다면 componentDidMount와 componentDidUpdate의 역할을 한다.

return의 내용이 componentWillUnmount를 담당한다.

useEffect(() => {
		console.log("useEffect");
		runTimeout();
		return () => {
			timeouts.current.forEach((v) => {
				clearTimeout(v);
			})
		}
	}, [timeouts.current])

useEffect의 []안에는 state 뿐만 아니라 조건문, Ref도 올 수 있다.

**timeouts.current[i] = []timeouts.current = [] 는 다른 표현이다. 전자는 current가 바뀌었다고 감지하지 못하지만 후자는 바뀌었다고 처리된다.**


#6-5 useMemo와 useCallback

import React, {useRef, useState, useEffect, useMemo, useCallback} from 'react';
import Ball from './ballClass';

function getWinNumbers() {
	console.log("getWinNumber");
	const candidate = Array(45).fill().map((v, i) => i + 1);
	const shuffle = [];
	while (candidate.length > 0) {
		shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);
	}
	const bonusNumber = shuffle[shuffle.length - 1];
	const winNumbers = shuffle.slice(0, 6).sort((p , c) => p - c);
	return [...winNumbers, bonusNumber];
}

const Lotto = () => {
	const [winBalls, setWinBalls] = useState([]);
	const lottoNumbers = useMemo(() => getWinNumbers(), [winBalls]);
	const [winNumbers, setWinNumber] = useState(lottoNumbers);
	const [bonus, setBonus] = useState(null);
	const [redo, setRedo] = useState(false);
	const timeouts = useRef([]);

	useEffect(() => {
		console.log("useEffect");
		runTimeout();
		return () => {
			timeouts.current.forEach((v) => {
				clearTimeout(v);
			})
		}
	}, [timeouts.current])

	const runTimeout = () => {
		for (let i = 0; i < winNumbers.length - 1; i++) {
			timeouts.current[i] = setTimeout(() => {
			setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
			}, (i + 1) * 1000);
	}
	timeouts.current[6] = setTimeout(() => {
		setBonus(winNumbers[6]);
		setRedo(true);	
		}, 7000);
	}

	const onClickRedo = useCallback(() => {
		console.log(winNumbers);
		console.log("onClickRedo");
			setWinNumber(getWinNumbers());
			setWinBalls([])
			setBonus(null);
			setRedo(false);
		timeouts.current = [];
	}, [winNumbers]);

		return (
			<>
			 <div>당첨 숫자</div>
      <div id="결과창">
        {winBalls.map((v) => <Ball key={v} number={v} />)}
      </div>
      <div>보너스!</div>
      {bonus && <Ball key={bonus} number={bonus}/>}
      {redo && <button onClick={onClickRedo}>한 번 더!</button>}
			</>
		)

}

export default Lotto;

Hooks의 state가 수정될 때마다 함수 전체가 다시 실행된다. 만약 특정 함수를 실행하는데 걸리는 시간이 setTimeout의 간격보다 크다면 이는 문제가 될 수 있다.

Hooks는 순서가 매우 중요하다

state 간의 연관이 있을 때는 순서를 잘 맞추어주어야 한다.

useMemo

useMemo(() => {}, [])

Hooks에서 특정 값들이 다시 실행되면서 초기화시키고 싶지 않을 때, 해당 값이 변하기 전까지 저장해놓을 수 있다.

첫 번째 인자인 함수를 실행해서 리턴한 값을 두 번째 인자에 기억해둔다 → 두 번째 인자인 배열에 들어간 요소가 바뀌지 않는다면 다시 실행되지 않는다. → 첫 번째 인자를 실행하지 않는다.

useRef는 일반적인 값을, useMemo는 복잡한 함수의 리턴값을 기억한다.

두번째 인자에 특정 state를 넣을 경우, 해당 state가 변할 때마다 함수가 다시 실행된다. → 값이 갱신된다.

useCallback

useCallback(() => {}, [])

함수 그자체를 기억한다. 함수가 무거운 함수일 경우 재생성되지 않게, 재사용할 수 있게 한다.

기본적으로 useCallback에서 state값은 최초의 상태가 계속 고정되기 때문에, useEffect처럼 값을 갱신하는 조건을 지정해주어야 한다.

배열안에 값을 갱신하게 되는 분기 역할을 할 state를 넣는다. 해당 state가 변하면 값을 갱신한다.

자식 컴포넌트에 props으로 함수를 전달하게 될 때, useCallback이 없다면 매번 새로운 함수를 생성하게 된다. 새로 생성해서 전달하게 되면 새로운 데이터라고 판단하고 쓸데없는 리렌더링을 하게 된다.


#6-6 Hooks에 대한 자잘한 팁들

Hooks는 순서가 굉장히 중요한다.

UseState를 조건문 안에 절대 넣으면 안 된다.

  • 조건에 따라 특정 state를 설정하지 않으면 순서가 꼬여버린다.

같은 이유로 함수나 반복문 안에도 UseState를 웬만하면 넣어서는 안된다.

useMemo는 함수의 리턴값을 기억한다. - []가 바뀔 때까지

useCallback은 함수를 기억한다. - []가 바뀔 때까지

useEffect는 코드를 실행한다. - []가 바뀔 때마다

두 번째 인자가 바뀔때마다 첫 번째 인자의 내용이 다시 실행된다.

바뀔 수 있는 값 === state인 까닭에 주로 state가 []안에 들어간다.

클래스

componentDidMount() {
	this.setState({
		imgCoord : 1,
		score : 2,
		result : 3,
})
}

componentDidMount(), componentDidUpdate(), componentWillUnmount() 등 정해진 메서드에서만 처리할 수 있다.

각각의 라이프사이클에서 모든 state를 담당한다.

하나의 라이프사이클 당 모든 state

Hooks(함수)

useEffect(() => {
	setImgCoord();
	setScore();
	}		
}, [imgCoord, score]);
useEffect(() => {
	setImgCoord();
	setResult();
	}		
}, [imgCoord, result]);

UseEffect를 여러 번 쓰는 게 가능하다. 즉 state 마다 각기 다른 처리를 해줄 수 있다.

  • 단, 두번째 인자인 배열 내부에는 반드시 useEffect를 실행할 때마다 값이 바뀔 state를 넣어주어야 한다.

개별의 state에 모든 라이프사이클을 설정할 수 있다.

개별 혹은 다수의 state 당 모든 라이프사이클

componentDidMount는 건너뛰고 componentDidUpdate만 실행할 수 있는 방법

const mounted = useRef(false);
useEffect(() => {
if (!mounted.current) {
	mounted.current = true;
} else {
	//본문
}
}, [state]);

꼼수(?) 디자인 패턴.

처음 실행(componentDidMount) 시 ref인 mounted의 값이 false기에 ture로만 바꿔주고 useEffect는 끝이 남. 그후 [state]의 값이 바뀌었을 때(componentDidUpdate) 정상적으로 코드 실행.

좋은 웹페이지 즐겨찾기