강의 듣다 보니... => 이런 당황스러운 문법이 나와서;; 잠시 멈추고 알아봤다.
Arrow function은 JS E6S에서 쓸 수 있는 함수 표현식이다.
function my_func(a){
return a + 1;
}
var my_func = function(a){
return a + 1;
}
var my_var = (a) => {return a + 1}
var my_var = a => return a + 1;
네 개 모두 함수를 표현한다.
Arrow Function 표현식에서 파라미터를 감싸는 괄호 ()나 return을 감싸는 중괄호 {}는 안의 내용이 단일 값일 때 생략이 가능하다.
위의 예시를 보면 var my_var = function()
, function 자체가 변수에 할당된 것을 볼 수 있다.
생소해 보이지만 Python에서 lambda function이랑 비슷하게 생각하면 된다. my_var = lambda a: print(a)
그럼 var my_var = function
(할당 assigning)가 function my_func
(선언 declaring)와 비교해 다른 점은 무엇일까?
function my_func
선언보다 먼저 my_func
을 호출해도 상관없다.my_var
은 함수 이름이 아니다. my_var
은 변수 이름이고, 변수에 할당된 함수는 이름이 있을 수도 있고 없을 수도 있다.var my_var = function ()
이면 함수 이름이 없는 것이고,var my_var = function my_func()
이면 함수 이름이 my_func
인 것이다.Declaring 대신 Assigning을 쓰는 게 좋은 케이스는 아직 못 찾았다. 예시를 봐도 declaring에 비해 큰 이점은 없는 것 같아서... 굳이 찾자면 호이스팅 제어 정도?
공부하다보면 더 유리한 케이스를 찾겠지 🙄
이전 시간에 JS 안에서 바로 인터랙티브 요소를 만들고 HTML로 렌더링하는 것을 배웠다.
// 요소 생성
const btn = React.createElement("button", {
id: "btn",
onClick: ()=> console.log("I'm Clicked"),
}, "Click me");
// 렌더링
const root = document.getElementById("root");
ReactDOM.render(btn, root);
이걸 HTML과 유사한 문법으로 쓰도록 도와주는 것이 JSX 문법이다. 동작은 동일하고, 그냥 표기 방법만 달라진다.
JSX 문법으로 쓰면 문법 변환기를 통해 자동으로 위의 코드로 해석돼서 실행이 된다.
// 요소 생성 with JSX
const Btn = (<button
id="btn"
onClick={()=> console.log("I'm Clicked")}>
Click me</button>);
((내가 HTML을 자주 안 써서 그런지 오히려 윗 방식이 더 편리해보이는 것 같기도...))
React에서 한 요소 안에 여러 요소를 넣을 때에는 createElement의 마지막 인자인 'content'에 내부 요소를 리스트로 전달해준다.
const btn = //...
const title = //...
const container = React.createElement("div", null, [title, btn])
이걸 JSX로 표현하면 똑같이 HTML 문법처럼 div안에 내부 요소를 집어 넣으면 된다.
<!--HTML-->
<div>
<title>I'm a Title</title>
<button>Clcik me</button>
</div>
// JSX
const Container = () => (
<div>
<Title />
<Btn />
</div>
);
ReactDOM.render(<Container />, root);
그런데 이렇게 할 때 주의할 점이 있다.
첫째, 생성한 컴포넌트는 대문자로 시작해야 한다. (Title
, Btn
) 소문자로 시작하면 HTML 태그로 인식을 해버린다.
둘째, 내부 요소는 함수로 만들어야 한다. const Btn = ( <button></button>)
가 아니라
const Btn = () => ( ... );
function Btn () { return ... };
로 해주어야 한다.
즉 div
태그 안에서 호출하는 <Title />
은 요소가 아니라 함수가 되어야 한다.
<Title />
은 무슨 뜻?
HTML 태그 열면서 닫는 그 태그다.
왜 함수로 만들어야 해?
그냥const Title = (<h3>Title</h3>);
로 만들고 호출하면 안되나? 싶지만...
Python에 비유하자면 변수를 만드는 것과 class를 만드는 것의 차이이다.
class로Title
을 만들면 호출될 때마다 새로운 인스턴스가 만들어지고, 인스턴스는 필요에 따라 각각 바뀔 수 있다.
하지만 변수로Title
을 만들면 고정된 값이기 때문에 무조건 하나의 값만 사용이 가능하다.
그래서Title
을 함수로 생성하는 것이다.
난 인스턴스 필요없는데...
그러면 고정된 요소로 만들고 다음과 같이 쓰면 된다.const Container = ( <div> {Title} {Title} </div> );
state는 데이터에 따라 변동하는 UI를 만들 때 사용하는 도구이다.
정확한 설명은 나중에 배우고... 우선 React JS에서 state 없이 데이터를 바꾸는 단순한 방법부터 배워보자.
// 텍스트 넣을 span 태그 가져오기
const my_span = document.querySelector("span");
// counter를 업데이트하고 span text를 다시 쓰기
function handleClick () {
counter = counter + 1;
my_span.innerText = `Total Clicks: ${counter}`;
};
button.addEventListener("click", handleClick);
let counter = 0;
// counter를 업데이트하고 다시 렌더링 하기
function handleClick() {
counter = counter + 1;
render();
}
// 만든 요소를 렌더링하기
function render() {
ReactDOM.render(<Container />, root);
};
// 함수가 포함된 요소 만들기
const Container = () => (
<h3>Total Clicks: {counter}</h3>
<button onClick={handleClick}>Click</button>
);
// 최초 렌더링하기
render();
Vanilla JS는 span 안의 text를 직접 바꾸고,
React JS는 요소 자체는 그대로 두되 다시 렌더링하는 방식으로 진행한다.
Vanilla JS는 텍스트 전체가 다시 업데이트되는 데 반해, React JS는 counter
만 업데이트가 된다. 변경되는 부분만 업데이트 하다보니 성능 상 도움이 된다.
다만 위의 예시는 요소가 바뀌는 함수마다 render()
를 호출해주어야 하는 귀찮음이 있어... 다음 강의에서는 이 점을 해소한다.
React JS는 데이터의 변화를 감지하면 자동으로 요소를 리렌더링시키는 hook을 제공한다. 이게 state
다!
어떻게 만드나?
React.useState()
로 생성한다.
이건 [state, modifier]
배열을 반환하는데 state는 변화를 감지할 데이터, modifier은 state를 변화를 만드는 함수이다.
위 예시에서 state는 counter
, modifier은 counter = counter +1
과 같다고 보면 된다.
어떻게 사용하나?
state에 변동하는 값을 넣고, modifier에 바뀔 state 값을 전달하면 된다. 예를 들어 function(1)
을 하면 state의 값은 1로 바뀐다. (보통 modifier 이름은 set_CounterName
으로 한다.)
주의할 점은 state와 modifier을 생성할 때에는 해당 state가 들어가는 요소 내부에서 생성해야 한다는 것이다.
hook을 만들 때 그 hook이 어디에 있는지 알려주어야 해서 그렇다.
// 올바른 방법. (요소 내부에서 생성)
// 이렇게 해야 리액트가 '내가 감시할 게 'Container' 컴포넌트 안에 있구나! 라고 알 수 있다.
function Container() {
// useState(0) -> state 초기값을 0 으로 선언한다.
const [counter, setCounter] = React.useState(0);
// onClick 발생하면 counter를 +1 로 바꾼다.
const onClick = () => {
setCounter(counter + 1);
};
return (
<div>
<h3>Total Clicks : {counter}</h3>
<button onClick={onClick}>Click me</button>
</div>
);
};
// 잘못된 방법. (요소 외부에서 생성)
// 이렇게 하면 React JS가 어느 컴포넌트를 감시해야 할 지 모른다.
const [counter, setCounter] = React.useState(0);
const onClick = () => {
setCounter(counter + 1);
};
function Container() {
return (
<div>
<h3>Total Clicks : {counter}</h3>
<button onClick={onClick}>Click me</button>
</div>
);
};
setCounter
)가 동작하면 (즉 state (counter
)가 바뀌면) 리액트는 Container
컴포넌트를 리렌더링한다.const [a, b] = [1, 2]
이렇게 하면const a = 1
,const b = 2
를 한 것과 같다. JS의 이 문법을 Destructing assignment (구조 분해 할당...?)라고 한다.
위 예시에서const [counter, setCounter] = React.useState(0);
도 마찬가지로 state를counter
로, function을setCounter
로 할당한 것과 같다.
setCounter(0)
해도 리렌더링 되나?
0과 같이 고정값을 전달해서 Counter 값이 전과 동일하다 해도 리렌더링은 실행된다. 즉 값 자체의 변화 여부보다 modifier가 동작했는지가 중요하다.
setCounter(counter + 1)
로 counter에 1을 더했지만, 이건 사실 권장하는 방법은 아니다.
강의에서는 setCounter(counter => counter + 1)
을 사용해야 최신의 counter 값을 쓸 수 있다고 설명한다.
이게 무슨 소리야? 뭐가 최신이라는거야? 하고 이해가 안 됐는데...
이 글을 읽으니 이해하는 데 도움이 되었다. 정리해보자.
앞의 방식 (counter + 1
)은 counter에 직접 1을 더해서 direct, 뒤의 방식은 (counter => counter + 1
)은 함수를 사용해서 functional이다.
// initial value of counter = 0
// direct
function onClick1() {
setCounter(counter + 1);
setCounter(counter + 1);
setCounter(counter + 1);
}
// functional
function onClick2() {
setCounter(current => current + 1);
setCounter(current => current + 1);
setCounter(current => current + 1);
}
놀랍게도(!) 둘의 결과는 다르다. dircet는 1이 나오고, functional은 3이 나온다.
이 차이는 리액트의 비동기 처리 때문에 발생한다. (정확히는 작업을 완전히 완료하지 않고, 예약만 해 둔 다음 나중에 모아서 처리하는 것 때문에)
direct에서 우리가 counter +1
을 세 번 하지만, 리액트는 계산한 결과를 곧이곧대로 counter
에 저장하지 않는다. 성능을 위해서 임의의 간격 (batch)을 두고 변화를 모아놨다가 한꺼번에 업데이트를 한다.
첫 번째로 호출했을 때에는 counter는 즉시 업데이트가 되지 않고, 미래의 상태만 예약이 된다. 즉 counter는 그대로 0이고 미래의 counter의 값이 1로 '예약'만 된다. 그래서 두 번째로 호출을 했을 때에도 여전히 counter는 0이기 때문에, 또다시 counter는 1로 '예약'이 된다. 이걸 반복하기 때문에 첫 번째 함수의 결과는 1이 되는 것이다.
즉, direct 방식은 직전 단계에서 변화를 주더라도 비동기 처리 때문에 그 변화는 변수에 반영이 되지 않는다. 그래서 앞의 변화는 무시가 되고, 마지막 것만 수행이 된 것 같은 결과가 나온다.
(만약 맨 마지막에 counter + 10
을 했으면 결과는 10이 나올 것이다.)
그럼 functional은 비동기가 아닌걸까? functional도 비동기이다. 다만 direct와 다르게 '예약' 된 값을 최신 값으로 본다. 즉 첫 번째 수행할 때 1을 예약하고, 두 번째 수행할 때 '예약된 1'을 가져와서 2를 예약한다. 그래서 결과가 3이 나오는 것이다.
그래서 강의에서 functional이 "최신의 counter 값을 쓰는" 방식이라고 표현한 것이다.
이런 이유로 강의에서는 direct 보다 functional을 권장하고 있다.
VSCODE에서 JSX 쓸 때 html 태그가 자동으로 안 닫힐 때가 있다.
해결 방법:
(혹시 이것만 했을 때 안 되면 Auto Close Tag Extension 설치해보기...)
강의 하나를 지금 몇 번째 보는건지... 하지만 깊게 배웠죠? (제발)
사용자가 input으로 입력하는 값을 실시간으로 state로 받는 상황을 가정하자.
이 때, 입력하는 값은 실시간으로 state로 저장이 되고 & input 필드 내의 값은 입력한 state 값이 계속 보여야 한다.
앞은 알겠는데 뒤는 뭔소리야? 당연히 input에는 사용자가 입력한 값이 보이지? 라고 생각했는데... 코드를 보자.
const [minutes, setMinutes] = React.useState(0);
const onChange = (event) => {
setMinutes(event.target.value);
};
return (
<div>
<label htmlFor="minutes">Minutes</label>
<input
id="minutes"
value={minutes} // 이걸 사용하면 Controlled, 사용하지 않으면 Uncontrolled
onChange={onChange}
/>
<h3>You are typing {minutes}</h3>
);
주석 처리한 value
속성을 보자. 초기값 0으로 렌더링했다가 -> 값을 입력하면 -> minutes
가 바뀌고 -> 그 값이 다시 input 필드에 보인다.
그런데 사실 value
속성을 주석 처리해도 (당연하게도) input 필드에는 똑같이 내가 입력한 값이 보인다.
안 해줘도 되는데 왜 굳이 저 속성을 주는걸까?
그 답은 React가 가지는 state와, DOM이 관리하는 input을 동일하게 유지하기 위해서다. (Functional과 유사한 이 느낌...)
만약 value={minutes}
속성을 주지 않았다고 가정해보자.
즉 보이는 값과 state가 일치하지 않을 수도 있다. 이럴 때 보이는 값 (input=5)을 Uncontrolled component라고 한다. input은 DOM (대충 말하면 HTML 문서 그 자체)가 관리하는 것이고, state는 리액트가 관리하는 것이기 때문에 value={minutes}
로 연결해주지 않으면 둘의 불일치가 날 수 있는 것이다.
저렇게 value
를 연결함으로써 input을 항상 리액트가 관리할 수 있는 Controlled Component로 만드는 것이다.
헤매다 왠지 열받아서 적어두는 리스트
class
는 (JS 문법에서 쓰는 것이라서) className
, for
는 htmlFor
... 이런 식.False
는 못 쓴다. false
만 가능. (왜 안돼...)<div>Hello</div><div>world</div>
이렇게 두 개의 <div>
태그를 내보내면 에러가 난다.<div></div>
혹은 React Fragment <></>
로 감싸서 내보내야 한다.state를 이용해서 컴포넌트의 속성, 예를 들어 value, id, ... 심지어 콘텐츠까지 바꿀 수 있다.
아래는 분 (min)을 입력하면 시간 (hour)으로, 시간을 입력하면 분으로 변환하는 코드이다.
여기서 value={flipped ? amount : amount * 60}
문법을 보면, flipped
라는 state의 true/false 여부에 따라 input 박스에 표시할 값이 달라지는 것을 볼 수 있다.
같은 방식으로 disabled={!flipped}
을 통해 input의 입력 가능 여부를 통제하고,
<button>{flipped ? "Mins to Hours" : "Hours to Mins"}</button>
으로 버튼의 콘텐츠 (텍스트)도 바꿀 수도 있다.
function App() {
const [amount, setAmount] = React.useState(0);
const [flipped, setFlipped] = React.useState(true);
const reset = () => setAmount(0);
const onChange = (event) => {
setAmount(event.target.value);
};
const onFlip = () => {
reset();
setFlipped((flipped) => !flipped);
};
return (
<>
<div>
<h1>Minutes and Hours Converter</h1>
<label htmlFor="minutes">Minutes</label>
<input
id="minutes"
type="number"
value={flipped ? amount : amount * 60} // IF 문법과 동일
onChange={onChange}
placeholder="minutes"
disabled={!flipped}
/>
</div>
<div>
<label htmlFor="hours">Hours</label>
<input
id="hours"
type="number"
value={flipped ? Math.round(amount / 60) : amount}
onChange={onChange}
placeholder="hours"
disabled={flipped}
/>
</div>
<div>
<button onClick={reset}>Reset</button>
<button onClick={onFlip}>
{flipped ? "Mins to Hours" : "Hours to Mins"}
</button>
</div>
</>
);
}
const root = document.getElementById("root");
ReactDOM.render(<App />, root);