blog

리액트 quick start 노트

리액트의 Quick start 페이지를 따라하면서 노트한 내용이다. js의 컨텍스트에서 이해할 수 있는 부분은 적지 않았다. 코드 스니핏도 간단히 알아볼 수 있게만 적어놔서 전체 내용이나 설명이 궁금하다면 본문을 확인하는게 좋겠다.

연습 환경 설치

node를 쓴지 오래되어서 업데이트부터 했다. nvm이 있어야 한다.

$ nvm ls-remote
$ nvm install v9.3.0 && nvm alias default v9.3.0
$ nvm use default
$ npm install -g yarn

npx를 이용해서 playground라는 이름으로 프로젝트를 생성한다. npx는 npm 5.2.0+부터 사용할 수 있다.

$ npx create-react-app playground
$ cd playground
$ yarn start

JSX

리액트는 마크업과 로직을 인위적으로 분리하지 않고 대신에 컴포넌트라는 단위를 만들어 약하게 결합하도록 만들었다. JSX를 꼭 사용할 필요는 없지만 시각적으로 더 편리하다.

리액트 엘리먼트는 ReactDOM.render()로 렌더링한다. root DOM 노드를 지정하면 그 내부의 모든 노드를 리액트가 관리한다.

const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
function formatName(user) { /* ... * /}

const element = (
  <h1>
    Hello, {formatName(user)}!
  </h1>
);

// 함수형 컴포넌트
function HelloBlock() {
  return <div>Hello</div>;
}

// 클래스 컴포넌트
class GoodbyeBlock extends React.Component {
  render() {
    return <div>Good bye</div>;
  }
}

노드 트리를 직접 조회하지 않고 ReactDOM을 통해 비교한 후, 변경된 사항만 반영하기 때문에 DOM을 직접 읽고 조작하는 방식보다 간결하다.

props

컴포넌트는 자바스크립트 함수와 같아서 인자 입력(props)을 받고 리액트 엘리먼트를 반환한다.

const Greeting = (props) => <h1>Hello, {props.name}!</h1>;

function Greeting(props) {
  return (
    <h1>Hello, {props.name}!</h1>
  );
}

class Greeting extends React.Component {
  render () {
    return <h1>Hello, {this.props.name}!</h1>
  }
}

props는 엘리먼트의 어트리뷰트로 전달한다. expression은 {}로, 문자열은 ""로 보낸다.

const element = <Comment user={user} text="string value blarblar" />;

모든 리액트 컴포넌트는 props를 변경하지 않는 순수 함수처럼 동작해야 한다. 이 규칙을 깨지 않고 출력값을 변경하기 위해 state라는 개념이 있다.

State

State는 props과 비슷하지만 private이고 컴포넌트가 전적으로 제어한다. state는 클래스로 작성한 컴포넌트에서 사용할 수 있다.

이 State를 제어하기 위해 componentDidMount()componentWillUnmount()와 같은 생애주기 훅(hook)을 사용한다. State에 대한 지정은 setState() 메소드를 사용한다.

this.state = {} 형태는 오직 constructor() 내에서만 사용 가능하며 그 외에는 setState()를 사용해야 한다. 그러지 않으면 렌더링에 반영되지 않는다.

한번 갱신하는데 setState() 호출을 여러 차례 한다면 경쟁 상태가 될 수 있다. 대신 함수 형태로 전달하는 것이 가능하다.

this.setState((prevState, props) => ({
  conter: prevState.counter + props.increment
}));

setState로 전달한 개체는 this.state에 병합되는 방식으로 동작한다. 전달하지 않은 프로퍼티는 영향을 받지 않는다.

state는 컴포넌트에 속했기 때문에 외부에서는 어떻게 정의되어 있는지 알 수 없고 알 필요도 없다. 지역적, 캡슐화되어 있다고 이야기하는 이유.

컴포넌트가 stateful, stateless인지는 때마다 다르게 정의해서 사용할 수 있음.

이벤트 제어

return <a href="#" onClick={handleClick}>Click Me</a>;

(이제는 시멘틱웹 얘기 부질 없는 것입니까. 나 너무 오래된 사람인듯.)

handleClicke.preventDefault()를 명시적으로 사용해야 함. (return false 넣는거 싫어하는 사람이라서 이런 방식 좋음.) 이 e는 W3C 스펙에서의 그 SyntheticEvent인데 리액트에 맞게 랩핑되어 있다.

이벤트에 넘겨줄 때는 js 특성 상 컨텍스트를 명시적으로 지정해야 한다. 즉, this 바인딩을 잊지 말아야 한다.

constructor(props) {
  super(props);
  // ...
  this.handleClick = this.handleClick.bind(this);
}

인자를 전달할 때는,

<button onClick={this.deleteRow.bind(this, id)}>Delete This</button>
// 이러면 클릭했을 때 `deleteRow(id, e)`로 호출해준다.

이 귀찮음을 피하기 위한 대안 두 가지로 public class fields 문법을 사용하는 방식과 익명 함수를 사용해 스코프를 전달하는 방식을 제안하는데 전자는 아직 확정된 문법이 아니다. 후자는 그나마 깔끔하지만 렌더링 될 때마다 새 콜백이 생성된다. 대부분 괜찮지만 하위 컴포넌트로 전달되었을 때 불필요한 추가 랜더링이 계속 나타나서 성능에 영향을 줄 수 있다.

// 1. public class fields syntax (experimental)
class Button extends React.Component {
  handleClick = () => {
    console.log('this is', this);
  }
  // ...
}

// 2. arrow funtion as callback
class Button extends React.Component {
  render () {
    return <button onClick={(e) => this.handleClick(e)}>Click me</button>;
  }
}

조건부 렌더링

if 사용하면 된다! 인라인으로 사용하고 싶다면 { expression && <p>Elements</p> } 식으로 사용한다. if-else는 삼항연산자를 사용한다.

null을 반환하면 화면에 렌더링하지 않는다. 예시로 에러 메시지 표시 나왔다. 렌더링 안해도 생애주기 훅은 여전히 호출된다.

리스트와 키

const faces = ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣'];
const listItems = faces.map((face, index) => (
  <li key={index}>
    {face}
  </li>
));

// 안정적인 id를 key로 사용하고 없으면 최후의 수단으로
// 배열 자체의 인덱스를 사용한다. 여기는 예시니까 그냥
// 인덱스를 사용했다.

const FaceList = () => <ul>{listItems}</ul>;

// or inline format
const FaceList = () => (
  <ul>
  {faces.map((face, index) => (
    <li key={index}>
      {face}
    </li>
  ))}
  </ul>
)

변화를 감지하고 반영하기 위해서는 id가 필요한데 key로 지정된 값을 활용한다. 또한 같은 계층에는 키가 유일해야 한다.

키는 리스트에서만 사용하고 각 항목 컴포넌트 정의에서 지정하지 않는다. 위에서 보면 li 역할하는 컴포넌트를 정의할 때 key를 지정하는게 아니라 listItems처럼 배열을 책임지는 컴포넌트에서 key를 지정해야 한다.

key는 prop처럼 작성하지만 실제로 해당 컴포넌트에 전달되진 않는다. 전달하려면 다른 이름의 prop을 명시적으로 지정해야 한다.

폼 Form

그냥 html 쓰듯 작성해도 문제 없다! 하지만 js가 있어야 미려한 기능을 만들 수 있는건 당연하고. 이럴 때는 controlled 컴포넌트를 만들어서 해결한다.

<input> 같은 엘리먼트는 상태를 스스로 관리한다. 리액트는 가변값을 state에 저장하고 setState()를 사용한다. 이 두가지를 하나로 합쳐 State를 “single source of truth”로 사용하고 사용자의 입력을 여기에 반영하는 식으로 만든다. 폼 엘리먼트를 리액트에서 관리하니까 controlled 컴포넌트라고 한다.

// bind 생략
handleChange(event) {
  this.setState({
    address: event.target.value
  });

  // 여러 input을 처리할 때는 computed property name 문법으로
  this.setState({
    [event.target.name]: event.target.value
  });
}
// ...
render() {
  return <input name="address" type="text" value={this.state.value} onChange={this.handleChange} />
}

input[type="file"]은 읽기 전용인데 이런 경우는 uncontrolled 컴포넌트라고 한다.

value에 직접 값을 전달하면 사용자가 값을 변경할 수 없다. 값을 전달한 이후에 다시 값을 변경할 수 있게 하려면 undefinednull을 다시 전달해야 한다.

state 위로 보내기 lifting state up

prop에 state를 조작할 수 있는 함수를 만들어서 전달하면 자식 노드에서도 부모 노드의 state를 간접적으로 조작할 수 있다. 이 방법을 “lifting state up” 라고 말한다.

아래는 NameInput에서 변경된 내용을 WelcomeBoard에서 전달받은 onNameChange를 사용해 갱신하는 방식이다.

class NameInput extends React.Component {
  handleChange(e) {
    this.props.onNameChange(e.target.value);
  }
  render() {
    return (
      <input type="text"
        value={this.props.name}
        onChange={this.handleChange.bind(this)} />
    )
  }
}

class WelcomeBoard extends React.Component {
  constructor(props) {
    super(props);
    this.state = {name: 'stranger'};
  }

  handleNameChange(name) {
    this.setState({
      name: name === '' ? 'stranger' : name,
    });
  }

  render() {
    return (
      <div>
        <p>Hello, {this.state.name}!</p>
        <NameInput
          name={this.state.name}
          onNameChange={this.handleNameChange.bind(this)} />
      </div>
    )
  }
}

구성 vs 상속

자식 노드로 어떤 것이 오게 될지 미리 알 수 없다. 그래서 특별한 prop으로 children이 존재한다.

function Box() {
  return <div>{props.children}</div>;
}

이제 다음과 같이 사용하면 알아서 내부 노드로 처리한다.

<Box>
  <NameInput />
  <NameInput />
  <NameInput />
</Box>

Containment의 예시로 SplitPane를 작성했다.

Specialization의 예시로 Dialog를 작성했다.

상속을 사용하는 경우는 적절한 유즈케이스가 없다고 한다. UI와 관련되지 않은 로직을 공유해야 할 경우라도 상속보다는 별도의 JS 모듈로 작성해서 import 하는 방식을 권장한다.

리액트 식으로 생각하기

리액트를 사용할 때 어떤 방식으로 접근해야 하는지 설명한다.

  1. Mock에서 시작. 프로토타이핑과 mock 데이터를 갖고 시작한다.
  2. UI를 나눠 컴포넌트 계층을 만든다.
    • 단일 책임 원칙
    • 데이터 구조에 맞게
  3. React에서 정적인 페이지로 작성한다.
    • state를 사용하지 않음
    • 데이터는 단방향으로
  4. UI state의 Representation을 모든 경우를 소화할 수 있으면서도 최소한으로 파악한다.
  5. state가 어디에 위치해야 하는지 파악한다.
    • owner 컴포넌트, 또는 상위 컴포넌트
    • 적당한 컴포넌트가 없다면 빈 컴포넌트라도 만들어서
  6. 데이터 흐름이 반대가 되야 하는 경우를 파악한다.
    • lifting state up

하우스키핑

신년맞이 블로그 정리

한참 미루던 블로그를 정돈했다. 새로운 도메인을 구입했는데 그쪽으로 옮길까 하다가 신경써야 할 부분이 너무 많아서 정리만 했다.

  • 새로운 테마를 만들까 싶었지만 엄두가 안나서 기본 테마를 손질하는 쪽으로 방향을 바꿨다. Twenty Fifteen를 사용했다. 깔끔. 서체 크기를 좀 변경하고 헤드라인을 추가했다.
  • 본명조Freight Text Pro를 적용했다. 지금은 Typekit을 사용하고 있는데 동적으로 로딩하는 식이라 뒤죽박죽 보일 때가 종종 있다. 호주 인터넷이 느려서 그렇겠지만. 본명조도 스포카 한 산스처럼 얼른 작고 빠른 웹폰트 패키지로 나왔으면 좋겠다.
  • dns 서비스를 cloudflare로 변경했다. 덕분에 https도 손쉽게 적용했다. dns propagation이 느려서 답답했다.
  • 위젯을 바꿨다. 기존에 쓰던건 그냥 많이 조회되는 순서로 나오는 위젯이었는데 이제 직접 선정한 글만 나온다. 한땀한땀 html로 되어 있는 목록이다.
  • 헤더 이미지로 svg를 넣을 수 있게 수정했다. (플러그인 있어서 설치) OpenGraph는 svg를 지원하지 않아서 소셜 카드에서는 좀 밍밍하게 보일 것 같다.
  • 오래된 글은 메시지를 넣었다.
  • 내용이 오래된 페이지는 메뉴에서 뺐다.
  • 코드 하일라이트 색상을 바꿨다. hightlight.js에서 조금 눅눅하고 밝은 색인 Atelier Estuary Light으로 골랐다.
  • 덧글 기능을 다시 활성화했다.

보기엔 크게 달라지진 않았지만 기분이 좋아졌다. 만드는 일도 즐겁지만 다듬는 일도 재미있다. 언제가 될지 모르겠지만 카테고리 분류가 엉망이라 색인을 다시 만들어서 정리할 생각이다.

깨끗

정리하면서 이전에 쓴 글도 읽게 되었다. 지금 내 생각과는 다른 부분도 많고 누군가 읽고 상처받을 만한 글도 보였다. 일상의 스트레스와 부정적인 감정을 블로그에 너무 많이 쏟아서 내 스스로도 이 블로그 주인은 일상에 문제가 좀 있나보군, 생각이 들 정도다. 잘 모를 때 썼거나 그냥 어리고 부족한 글도 많다. 이런 부분을 발견할 때마다 삭제 버튼을 누르고 싶어진다. 눈앞에서만 치운다고 내가 달라진다면 참 좋겠지만 과거의 경험으로 봐서는 그렇지 않은 것 같다. 부끄러운 글을 다시 볼 때마다 돌아보고, 반성하고, 개선하는 순환을 만들고 싶다.

앞으로는 좀 더 긍정적으로 생각하고 행동하고, 그런 삶이 글로도 나타났으면 좋겠다.

기억하고 싶은 글들

리디북스 결제하러 들어갔다가 밑줄쳤던 내용을 볼 수 있게 정리된 페이지가 있어서 다시 읽어봤다.

같은 일을 반복하면서 다른 결과를 기대하는 것은 미친 짓이다.

오래 전에 작성했던 코드를 지금에 와서도 고칠 부분이 없어 보인다면, 그것은 그동안 배운 것이 없다는 뜻이다.

— 소프트웨어 장인, 산드로 만쿠소

달인의 길은, 길 위에 머물러 있는 것이다.

— 달인, 조지 레너드 (밑줄 긋는 여자, 성수선에서 인용)

더 많이 아는 것은 곧 더 많이 이해하고 용서하는 것이다.

— 불안, 알랭 드 보통 (밑줄 긋는 여자, 성수선에서 인용)

사람들은 시간을 아끼면 아낄수록 가진 것이 점점 줄었다.

— 모모, 미하엘 엔데 (밑줄 긋는 여자, 성수선에서 인용)

바둑뿐 아니라 모든 분야가 그러할 것이다. 혼자서는 절대로 성장할 수 없다. 서로 나누면서 함께 성장해야 한다.

— 조훈현, 고수의 생각법, 조훈현

창조적 독점이란, 새로운 제품을 만들어서 모든 사람에게 혜택을 주는 동시에 그 제품을 만든 사람은 지속 가능한 이윤을 얻는 것이다. 경쟁이란, 아무도 이윤을 얻지 못하고 의미 있게 차별화 되는 부분도 없이 생존을 위해 싸우는 것이다.

— 제로 투 원, 블레이크 매스터스, 피터 틸

그녀는 상자 안에 물건을 수납함으로써 너무 많이 가진 사실을 깨닫지 못하는 현상을 ‘수납의 블랙박스화’라고 부른다.

전 자신이 좋아하는 일에 온전히 몰두할 수 있는 생활이야말로 정말 가치 있다고 생각해요. 물건이 많으면 아무래도 그것들을 관리하는 데 시간을 빼앗기게 되어서 오롯이 나만의 시간을 보낼 수 없거든요. 그런 생활은 자신이 원해서 하는 것처럼 보이지만, 실상은 물건에게 지배당하는 생활일 뿐이라고 생각해요.

— 아무것도 없는 방에 살고 싶다, 미니멀 라이프 연구회

우리는 기분이 나빠서 기분이 나빠진다. 죄책감을 느껴서 죄책감을 느낀다. 화가 나서 화를 낸다. 불안해서 불안해진다.

난 나이가 들고 경험을 쌓는 과정에서 틀린 점을 조금씩 덜어내 매일매일 덜 틀린 사람으로 거듭날 것이다.

‘무엇을 위해 투쟁할 것인가’ 라는 문제가 당신이라는 존재를 규정한다.

— 신경 끄기의 기술, 마크 맨슨