리액트 실전 활용법

 

High Order Component

Controlled Component 와 Uncontrolled Component

[Project] Login 요청하기

localStorage

Rest API 로 데이터 가져오기

 

[Homework] 개발 서적 평가 서비스 로그아웃 및 책 추가하기

Software Engineer | Studio XID, Inc.

Microsoft MVP

TypeScript Korea User Group Organizer

Electron Korea User Group Organizer

Marktube (Youtube)

Mark Lee

HOC

Higher Order Component

  • advanced technique in React for reusing component logic.

  • not part of the React API

  • a pattern that emerges from React’s compositional nature.

HOC = function(컴포넌트) { return 새로운 컴포넌트; }

HOC

<컴포넌트> 를 인자로 받아

<새로운 컴포넌트> 를 리턴하는

함수

컴포넌트

props

UI

HOC

컴포넌트

새로운

컴포넌트

you already know HOC !

import React from "react";
import { withRouter } from "react-router-dom";

const LoginButton = props => {
  console.log(props);
  function login() {
    setTimeout(() => {
      props.history.push("/");
    }, 1000);
  }
  return <button onClick={login}>로그인하기</button>;
};

export default withRouter(LoginButton);

withRouter()

보통 with 가 붙은 함수가 HOC 인 경우가 많다.

export default withRouter(LoginButton);

사용하는 법

  • Use HOCs For Cross-Cutting Concerns

  • Don’t Mutate the Original Component. Use Composition.

  • Pass Unrelated Props Through to the Wrapped Component

  • Maximizing Composability

  • Wrap the Display Name for Easy Debugging

withRouter(LoginButton)

주의할 점

  • Don’t Use HOCs Inside the render Method

  • Static Methods Must Be Copied Over

  • Refs Aren’t Passed Through (feat. React.forwardRef)

render() {
  // A new version of EnhancedComponent is created on every render
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // That causes the entire subtree to unmount/remount each time!
  return <EnhancedComponent />;
}
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // Must know exactly which method(s) to copy :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}
// Define a static method
WrappedComponent.staticMethod = function() {/*...*/}
// Now apply a HOC
const EnhancedComponent = enhance(WrappedComponent);

// The enhanced component has no static method
typeof EnhancedComponent.staticMethod === 'undefined' // true
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}
// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...export the method separately...
export { someFunction };

// ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';

Controlled Component

Uncontrolled Component

npx create-react-app controlled-uncontrolled

상태를 가지고 있는 엘리먼트

  • input

  • select

  • textarea

  • ...

엘리먼트의 '상태' 를 누가 관리하느냐

  • 엘리먼트를 가지고 있는 컴포넌트가 관리

    • Controlled

  • 엘리먼트의 상태를 관리하지 않고, 엘리먼트의 참조만 컴포넌트가 소유

    • Uncontrolled

import React from 'react';

export default class Controlled extends React.Component {
  state = { value: '' };

  render() {
    return (
      <div>
        <input />
      </div>
    );
  }
}

components/Controlled.jsx (1)

import React from 'react';

export default class Controlled extends React.Component {
  state = { value: '' };

  render() {
    const { value } = this.state;
    return (
      <div>
        <input value={value} onChange={this._change} />
      </div>
    );
  }

  _change = e => {
    // console.log(e.target.value);
    this.setState({ value: e.target.value });
  };
}

components/Controlled.jsx (2)

import React from 'react';

export default class Controlled extends React.Component {
  state = { value: '' };

  render() {
    const { value } = this.state;
    return (
      <div>
        <input value={value} onChange={this._change} />
        <button onClick={this._click}>전송</button>
      </div>
    );
  }

  _change = e => {
    // console.log(e.target.value);
    this.setState({ value: e.target.value });
  };

  _click = () => {
    console.log('최종 결과', this.state.value);
  };
}

components/Controlled.jsx (3)

import React from 'react';

export default class Uncontrolled extends React.Component {
  _input = React.createRef();

  render() {
    return (
      <div>
        <input ret={this._input} />
      </div>
    );
  }
}

components/Uncontrolled.jsx (1)

import React from 'react';

export default class Uncontrolled extends React.Component {
  _input = React.createRef();

  render() {
    return (
      <div>
        <input ref={this._input} />
        <button onClick={this._click}>전송</button>
      </div>
    );
  }

  _click = () => {
    console.log('최종 결과', this._input.current.value);
  };
}

components/Uncontrolled.jsx (2)

import React from 'react';

const Uncontrolled = () => {
  const inputRef = React.createRef();

  function click() {
    console.log('최종 결과', inputRef.current.value);
  }
  
  return (
    <div>
      <input ref={inputRef} />
      <button onClick={click}>전송</button>
    </div>
  );
};

export default Uncontrolled;

components/Uncontrolled.jsx (2)

[Project] Login 요청하기

git clone -b fc-school https://github.com/2woongjae/reactjs-books-review.git
// src/components/SignForm.jsx

import React from "react";
import { Input, Button, Divider, Col } from "antd";
import styled from "styled-components";
import { Link } from "react-router-dom";

const SigninForm = () => {
  const emailInput = React.createRef();
  const passwordInput = React.createRef();

  function click() {
    console.log(
      emailInput.current.state.value,
      passwordInput.current.state.value
    );
  }

  return (
    <Col
      span={12}
      style={{
        verticalAlign: "top"
      }}
    >
      <form>
        <Title>Log In. Start Searching.</Title>
        <InputTitle>
          Email
          <StyledSpan />
        </InputTitle>
        <InputArea>
          <StyledInput
            placeholder="Email"
            autoComplete="email"
            name="email"
            ref={emailInput}
          />
        </InputArea>
        <InputTitle top={10}>
          Password
          <StyledSpan />
        </InputTitle>
        <InputArea>
          <StyledInput
            type="password"
            autoComplete="current-password"
            ref={passwordInput}
          />
        </InputArea>
        <ButtonArea>
          <StyledButton size="large" loading={loading} onClick={click}>
            Sign In
          </StyledButton>
        </ButtonArea>
        <DividerArea>
          <Divider />
        </DividerArea>
        <LinkArea>
          <LinkTitle>Need to create an account?</LinkTitle>
          <LinkButtonArea>
            <Link to="/signup">
              <LinkButton>Sign up</LinkButton>
            </Link>
          </LinkButtonArea>
        </LinkArea>
        <LinkArea>
          <LinkTitle>Forgot your password?</LinkTitle>
          <LinkButtonArea>
            <Link to="/forgot">
              <LinkButton>Recovery</LinkButton>
            </Link>
          </LinkButtonArea>
        </LinkArea>
      </form>
    </Col>
  );
};

export default SigninForm;
const Title = styled.div`
  padding-top: 10px;
  padding-bottom: 10px;
  text-transform: uppercase;
  font-family: Roboto;
  font-size: 24px;
  font-weight: bold;
  margin-top: 60px;
  text-align: center;
`;

const InputTitle = styled.div`
  font-family: Roboto;
  font-size: 12px;
  font-weight: bold;
  margin-top: ${props => props.top || "40"}px;
  text-align: left;
  padding-left: 40px;
`;

const InputArea = styled.div`
  padding-top: 10px;
  padding-bottom: 10px;
  padding-left: 40px;
  padding-right: 40px;
`;

const StyledInput = styled(Input)`
  width: 100%;
  border-radius: 1px;
  border-width: 1px;
  font-family: Roboto;
`;

const ButtonArea = styled.div`
  text-align: left;
  padding-left: 40px;
  margin-top: 20px;
`;

const StyledButton = styled(Button)`
  border-color: #28546a;
  background-color: #28546a;
  text-transform: uppercase;
  border-radius: 1px;
  border-width: 2px;
  color: white;
  width: 120px;
  &:hover {
    background-color: #28546a;
    color: white;
  }
`;

const DividerArea = styled.div`
  font-family: Roboto;
  font-size: 12px;
  font-weight: bold;
  margin-top: 30px;
  text-align: left;
  padding-left: 40px;
  padding-right: 40px;
`;

const LinkArea = styled.div`
  padding-left: 40px;
  padding-right: 40px;
  margin-top: 15px;
  overflow: hidden;
`;

const LinkTitle = styled.div`
  float: left;
  padding-top: 5px;
`;

const StyledSpan = styled.span.attrs(() => ({
  children: "*"
}))`
  color: #971931;
`;

const LinkButtonArea = styled.div`
  float: right;
`;

const LinkButton = styled(Button)`
  background-color: #f3f7f8;
  border-color: #28546a;
  color: #28546a;
  text-transform: uppercase;
  border-radius: 1px;
  border-width: 2px;
  &:hover {
    background-color: #28546a;
    color: white;
  }
`;
npm i axios
// ...
import axios from 'axios';

const SigninForm = () => {
  // ...

  function click() {
    const email = emailInput.current.state.value;
    const password = passwordInput.current.state.value;

    axios
      .post('https://api.marktube.tv/v1/me', {
        email,
        password,
      })
      .then(function(response) {
        console.log(response);
      })
      .catch(function(error) {
        console.log(error);
      });
  }

  // ...
};

SignForm.jsx - Promise

// ...
import axios from 'axios';

const SigninForm = () => {
  // ...

  async function click() {
    const email = emailInput.current.state.value;
    const password = passwordInput.current.state.value;

    try {
      const response = await axios.post('https://api.marktube.tv/v1/me', {
        email,
        password,
      });
      console.log(response);
    } catch (error) {
      console.log(error);
    }
  }
  
  // ...
};

SignForm.jsx - async/await

import { withRouter } from 'react-router-dom';

const SigninForm = ({ history }) => {
  // ...

  async function click() {
    const email = emailInput.current.state.value;
    const password = passwordInput.current.state.value;

    try {
      const response = await axios.post('https://api.marktube.tv/v1/me', {
        email,
        password,
      });
      console.log(response);
      history.push('/');
    } catch (error) {
      console.log(error);
    }
  }
  
  // ...
};

export default withRouter(SigninForm);

SignForm.jsx - withRouter

const SigninForm = ({ history }) => {
  // ...

  const [loading, setLoading] = useState(false);

  async function click() {
    const email = emailInput.current.state.value;
    const password = passwordInput.current.state.value;

    try {
      setLoading(true);
      const response = await axios.post('https://api.marktube.tv/v1/me', {
        email,
        password,
      });
      console.log(response);
      setLoading(false);
      history.push('/');
    } catch (error) {
      console.log(error);
      setLoading(false);
    }
  }
  
  // ...
};

SignForm.jsx - loading (1)

const SigninForm = ({ history }) => {
  // ...

  return (
    <Col
      span={12}
      style={{
        verticalAlign: "top"
      }}
    >
      <form>
        ...
        <ButtonArea>
          <StyledButton size="large" loading={loading} onClick={click}>
            Sign In
          </StyledButton>
        </ButtonArea>
        ...
      </form>
    </Col>
  );  
};

SignForm.jsx - loading (2)

import { Input, Button, Divider, Col, message } from 'antd';

const SigninForm = ({ history }) => {
  // ...

  async function click() {
    const email = emailInput.current.state.value;
    const password = passwordInput.current.state.value;

    try {
      setLoading(true);
      const response = await axios.post("https://api.marktube.tv/v1/me", {
        email,
        password
      });
      console.log(response);
      setLoading(false);
      history.push("/");
    } catch (error) {
      console.log(error);
      setLoading(false);
      message.error('This is an error message');
    }
  }
  
  // ...
};

SignForm.jsx - error feedback

const SigninForm = ({ history }) => {
  // ...

  async function click() {
    const email = emailInput.current.state.value;
    const password = passwordInput.current.state.value;

    try {
      setLoading(true);
      const response = await axios.post("https://api.marktube.tv/v1/me", {
        email,
        password
      });
      console.log(response);
      setLoading(false);
      history.push("/");
    } catch (error) {
      console.log(error);
      setLoading(false);
      message.error(error.response.data.error);
    }
  }
  
  // ...
};

SignForm.jsx - error feedback

const SigninForm = ({ history }) => {
  // ...

  async function click() {
    const email = emailInput.current.state.value;
    const password = passwordInput.current.state.value;

    try {
      setLoading(true);
      const response = await axios.post("https://api.marktube.tv/v1/me", {
        email,
        password
      });
      console.log(response);
      setLoading(false);
      localStorage.setItem('token', response.data.token);
      history.push("/");
    } catch (error) {
      console.log(error);
      setLoading(false);
      message.error(error.response.data.error);
    }
  }
  
  // ...
};

SignForm.jsx - localStorage.setItem

import React from 'react';
import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom';
import Signin from './pages/Signin';
import Home from './pages/Home';
import Error from './pages/Error';

export default function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route path="/signin" exact component={Signin} />
        <Route
          path="/"
          exact
          render={props => {
            const token = localStorage.getItem('token');
            console.log(token);
            if (token === null) {
              return <Redirect to="/signin" />;
            }
            return <Home {...props} token={token} />;
          }}
        />
        <Route component={Error} />
      </Switch>
    </BrowserRouter>
  );
}

App.js - localStorage.getItem

// src/hocs/withAuth.js

export default function withAuth(Component) {
  return props => {
    const token = localStorage.getItem('token');
    console.log(token);
    if (token === null) {
      return <Redirect to="/signin" />;
    }
    return <Component {...props} token={token} />;
  };
}

withAuth

Rest API 로 데이터 가져오기

import React from 'react';
import axios from 'axios';

function Book(props) {
  return <div>title : {props.title}</div>;
}

export default class Home extends React.Component {
  state = { books: [] };

  render() {
    const { books } = this.state;

    return (
      <div>
        <h1>Home</h1>
        {books.map(book => (
          <Book title={book.title} key={book.bookId} />
        ))}
      </div>
    );
  }
}

page/Home.jsx

export default class Home extends React.Component {
  // ...

  async componentDidMount() {
    try {
      const response = await axios.get('https://api.marktube.tv/v1/book', {
        headers: {
          Authorization: `Bearer ${this.props.token}`,
        },
      });
      const books = response.data;
      console.log(books);
      this.setState({ books });
    } catch (error) {
      console.log(error.response.data.error);
    }
  }

  // ...
}

page/Home.jsx

// 책 목록보기

axios.get(
  'https://api.marktube.tv/v1/book',
  { headers: `Bearer ${token}` },
);
// 책 추가하기

axios.post(
  'https://api.marktube.tv/v1/book',
  {
    title,
    message,
    author,
    url,
  },
  { headers: `Bearer ${token}` },
);
// 책 상세보기

axios.get(
  `https://api.marktube.tv/v1/book/${book.id}`,
  { headers: `Bearer ${token}` },
);
// 책 수정하기

axios.patch(
  `https://api.marktube.tv/v1/book/${book.id}`,
  {
    title,
    message,
    author,
    url,
  },
  { headers: `Bearer ${token}` },
);
// 책 삭제하기

axios.delete(
  `https://api.marktube.tv/v1/book/${book.id}`,
  { headers: `Bearer ${token}` },
);

[Homework]
개발 서적 평가 서비스
로그아웃 및 책 추가하기