Skip to content

Files

Latest commit

7fc6729 ยท Oct 28, 2021

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
Oct 28, 2021

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ (HOC, Higher Order Component)

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋ž€, ์›๋ž˜์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•˜๋ฉด์„œ ์ถ”๊ฐ€์ ์ธ ๊ธฐ๋Šฅ์„ ํฌํ•จ์‹œํ‚ค๋„๋ก ํ•˜๋Š” ํ•จ์ˆ˜์ด๋‹ค. ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ, ์ถ”์ƒํ™”์™€ ๊ด€๋ จ์ด ์žˆ๋‹ค.

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ์— ์•ž์„œ... ๊ณ ์ฐจ ํ•จ์ˆ˜๋ž€?

๋ฆฌ์•กํŠธ์—์„œ ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋Š” ๊ณ ์ฐจ ํ•จ์ˆ˜์˜ ๊ฐœ๋…์ด ๋ฐ˜์˜๋œ ๊ฒƒ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ ๊ณ ์ฐจ ํ•จ์ˆ˜๊ฐ€ ๋ฌด์—‡์ธ์ง€์— ๋Œ€ํ•ด ๋จผ์ € ์งš๊ณ  ๋„˜์–ด๊ฐ€๋ณด์ž.

์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ ํ•จ์ˆ˜๋Š” ์ผ๊ธ‰ ๊ฐ์ฒด์ด๋‹ค. ๊ทธ๋ž˜์„œ ๊ฐ€๋Šฅํ•œ ๊ฐœ๋…์ด ๊ณ ์ฐจ ํ•จ์ˆ˜์ด๋‹ค. ๊ณ ์ฐจ ํ•จ์ˆ˜๋ž€ ์•„๋ž˜์˜ ์กฐ๊ฑด๋“ค ์ค‘ ํ•œ ๊ฐ€์ง€ ์ด์ƒ์„ ๋งŒ์กฑํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋งํ•œ๋‹ค.

  • ํ•จ์ˆ˜์˜ ์ธ์ˆ˜๋กœ ํ•จ์ˆ˜๋ฅผ ๋„ฃ์„ ์ˆ˜ ์žˆ๋‹ค.
  • ํ•จ์ˆ˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

์•„, ์ด๊ฒŒ ๊ณ ์ฐจ ํ•จ์ˆ˜๊ตฌ๋‚˜. ๊ทธ๋Ÿผ ๊ณ ์ฐจ ํ•จ์ˆ˜๋Š” ์–ด๋–ป๊ฒŒ ์“ฐ์ผ๊นŒ?

์ฐธ๊ณ : ์ฑ… [๋ชจ๋˜์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ž…๋ฌธ]

/* ์ˆ˜์—ด์„ ํ‘œ์‹œํ•˜๋Š” ํ”„๋กœ๊ทธ๋žจ */
let digits = '';
for (var i = 0; i < 10; i++) {
  digits += i;
}
console.log(digits); // 0123456789

/* ๋ฌด์ž‘์œ„ ์•ŒํŒŒ๋ฒณ์„ ํ‘œ์‹œํ•˜๋Š” ํ”„๋กœ๊ทธ๋žจ */
let randomChars = '';
for (var i = 0; i < 8; i++) {
  randomChars += String.fromCharCode(Math.floor(Math.random() * 26) + 'a'.charCodeAt(0));
}
console.log(randomChars);

์œ„ ๋‘ ํ”„๋กœ๊ทธ๋žจ์„ ์‚ดํŽด๋ณด๋ฉด ํ•˜๋Š” ์ผ์€ ๋‹ค๋ฅด์ง€๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๋กœ์ง์ด ๊ฐ™๋‹ค. ํ•ต์‹ฌ ๊ด€์‹ฌ์‚ฌ๋Š” ์ˆ˜์—ด, ๋ฌด์ž‘์œ„ ์•ŒํŒŒ๋ฒณ์— ๋Œ€ํ•œ ๊ฒƒ์œผ๋กœ ๊ฐ๊ฐ ๋‹ค๋ฅด์ง€๋งŒ, ๊ด€์‹ฌ์‚ฌ์— ํ•ด๋‹นํ•˜๋Š” ๋ฌธ์ž๋ฅผ ๋ชจ์•„ ๋ฌธ์ž์—ด๋กœ ๋„์ถœํ•˜๋Š” ๋ถ€๋ถ„, ์ฆ‰ ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ๊ฐ€ ๊ฐ™๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿผ '๋ฌธ์ž๋ฅผ ๋ชจ์•„ ๋ฌธ์ž์—ด๋กœ ๋„์ถœํ•˜๋Š” ๋ถ€๋ถ„'์„ ์ถ”์ƒํ™”ํ•˜์—ฌ ํ‘œํ˜„ํ•œ๋‹ค๋ฉด ํ”„๋กœ๊ทธ๋žจ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ง์ด๋‹ค!

// ๊ณ ์ฐจ ํ•จ์ˆ˜ joinStrings๋ฅผ ํ™œ์šฉํ•ด ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ ์ถ”์ƒํ™”
function joinStrings(n, f) {
  var s = '';
  for (var i = 0; i < n; i++) {
    s += f(i);
  }
  return s;
}

let digits = joinStrings(10, function (i) {
  return i;
});
let randomChars = joinStrings(8, function (i) {
  return String.fromCharCode(Math.floor(Math.random() * 26) + 'a'.charCodeAt(0));
});
console.log(digits); // 0123456789
console.log(randomChars); // mzobequt

ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ(Cross-Cutting Concerns)์— ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉํ•˜๊ธฐ

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋Š” ์•ž์„œ ์„ค๋ช…ํ•œ ๊ณ ์ฐจ ํ•จ์ˆ˜์˜ ๊ฐœ๋…์„ ์ปดํฌ๋„ŒํŠธ์— ๋ฐ˜์˜ํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿผ React ์ปดํฌ๋„ŒํŠธ ์˜ˆ์‹œ๋ฅผ ์‚ดํŽด๋ณด๋ฉฐ ์ดํ•ดํ•ด๋ณด์ž.

  • ์™ธ๋ถ€๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ๋Œ“๊ธ€ ๋ชฉ๋ก์„ ๋ Œ๋”๋งํ•˜๋Š” CommentList ์ปดํฌ๋„ŒํŠธ
  • ๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ๋ฅผ ๊ตฌ๋…ํ•˜๊ธฐ ์œ„ํ•œ BlogPost ์ปดํฌ๋„ŒํŠธ
  • DataSource๋Š” ๊ธ€๋กœ๋ฒŒ ๋ฐ์ดํ„ฐ ์†Œ์Šค
class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      comments: DataSource.getComments(),
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      comments: DataSource.getComments(),
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}
class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id),
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id),
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}

์œ„ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด, ๊ฐ ์ปดํฌ๋„ŒํŠธ๋Š” ํ•ต์‹ฌ ๊ด€์‹ฌ์‚ฌ๊ฐ€ ๋งค์šฐ ๋‹ค๋ฅด์ง€๋งŒ ๋™์ผํ•œ ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

  • componentDidMount : DataSource์— change ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
  • handleChange : setState ํ˜ธ์ถœ
  • componentWillUnmount : DataSource์— change ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ

์œ„ ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ๋Š” ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•ด ์ถ”์ƒํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์•„ ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜์ด๋‹ค. ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ withSubscription ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด์ž.

function withSubscription(WrappedComponent, selectData) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props),
      };
    }

    componentDidMount() {
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props),
      });
    }

    render() {
      // ๋ž˜ํ•‘๋œ ์ปดํฌ๋„ŒํŠธ๋Š” ์ƒˆ๋กœ์šด props, data์™€ ํ•จ๊ป˜ ์ปจํ…Œ์ด๋„ˆ์˜ ๋ชจ๋“  props๋ฅผ ์ „๋‹ฌ๋ฐ›๋Š”๋‹ค.
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}
const CommentListWithSubscription = withSubscription(CommentList, (DataSource) =>
  DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(BlogPost, (DataSource, props) =>
  DataSource.getBlogPost(props.id)
);

์ด๋ ‡๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋กœ์„œ ์ ์ ˆํžˆ ์ถ”์ƒํ™”ํ•˜์˜€๊ณ , ์ด๋Š” ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์— ํฐ ๊ธฐ์—ฌ๋ฅผ ํ•  ๊ฒƒ์ด๋‹ค.

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋ž€

  • ์ž…๋ ฅ๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ˆ˜์ •ํ•˜์ง€ ์•Š์œผ๋ฉฐ ์ƒ์†์„ ์‚ฌ์šฉํ•˜์—ฌ ๋™์ž‘์„ ๋ณต์‚ฌํ•˜์ง€๋„ ์•Š๋Š”๋‹ค.
  • ๊ทธ์ € ์›๋ณธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ปจํ…Œ์ด๋„ˆ ์ปดํฌ๋„ŒํŠธ๋กœ ํฌ์žฅ(wrapping)ํ•˜์—ฌ ์กฐํ•ฉ(compose)ํ•œ๋‹ค.
  • ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋Š” ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๊ฐ€ ์ „ํ˜€ ์—†๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜์ด๋‹ค.

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์€ ์ปดํฌ๋„ŒํŠธ์˜ ํ”„๋กœํ† ํƒ€์ž…์„ ์ˆ˜์ • ๋˜๋Š” ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค. ๋ณ€๊ฒฝ(mutation)๋œ ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋Š” ๋ˆ„์ถœ๋œ ์ถ”์ƒํ™”(leaky abstraction)์ด๋ผ๊ณ  ํ•œ๋‹ค. HOC๋Š” ๋ณ€๊ฒฝ(mutation) ๋Œ€์‹ ์— ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ปจํ…Œ์ด๋„ˆ ๊ตฌ์„ฑ ์š”์†Œ๋กœ ๊ฐ์‹ธ์„œ ์กฐํ•ฉ(composition)์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('Current props: ', this.props);
      console.log('Previous props: ', prevProps);
    }
    render() {
      // ์ž…๋ ฅ component๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๋Š” container ๐Ÿค—
      return <WrappedComponent {...this.props} />;
    }
  };
}

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ ์ปจ๋ฒค์…˜

๋ž˜ํ•‘๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ†ตํ•ด ๊ด€๋ จ ์—†๋Š” Props ์ „๋‹ฌํ•˜๊ธฐ

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋Š” ์ปดํฌ๋„ŒํŠธ์— ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ๋ฟ, ๋ณ€๊ฒฝํ•ด์„œ๋Š” ์•ˆ ๋œ๋‹ค. ๊ด€๋ จ ์—†๋Š” props๋Š” ๊ทธ๋Œ€๋กœ ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ•œ๋‹ค.

render() {
  const { extraProp, ...passThroughProps } = this.props;

  const injectedProp = someStateOrInstanceMethod;

  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

์กฐํ•ฉ ๊ฐ€๋Šฅ์„ฑ(Composability) ๋Œ์–ด์˜ฌ๋ฆฌ๊ธฐ

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋Š” Component๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ Component๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์ถœ๋ ฅ ํƒ€์ž…์ด ์ž…๋ ฅ ํƒ€์ž…๊ณผ ๋™์ผํ•œ ํ•จ์ˆ˜๋Š” ์ •๋ง ์‰ฝ๊ฒŒ ์กฐํ•ฉํ•  ์ˆ˜ ์žˆ๋‹ค.

React Redux์˜ connect

const ConnectedComment = connect(commentSelector, commentActions)(CommentList);

์œ„ ์ฝ”๋“œ๋ฅผ ๋ถ„ํ•ดํ•ด๋ณด๋ฉด connect๋Š” ๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ณ ์ฐจ ํ•จ์ˆ˜๋ผ๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

// connect๋Š” ๋‹ค๋ฅธ ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜
const enhance = connect(commentListSelector, commentListActions);
// ๋ฐ˜ํ™˜๋œ ํ•จ์ˆ˜๋Š” Redux store์— ์—ฐ๊ฒฐ๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ณ ์ฐจ ํ•จ์ˆ˜ ์ปดํฌ๋„ŒํŠธ
const ConnectedComment = enhance(CommentList);

displayName ์ž‘์„ฑ

๊ฐœ๋ฐœ ๋„๊ตฌ ๋””๋ฒ„๊น…์„ ์œ„ํ•˜์—ฌ HOC์˜ ๊ฒฐ๊ณผ์ž„์„ ์•Œ๋ฆฌ๋Š” displayName์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค. ๋„ค์ด๋ฐ์€ HOC์˜ ์ด๋ฆ„์œผ๋กœ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋ช…์„ ๊ฐ์‹ธ๋Š” ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•œ๋‹ค.

function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {
    /* ... */
  }
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`; // WithSubscription(CommentList)
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

211028 ์Šคํ„ฐ๋”” ์ •๋ฆฌ

๊ณ ์ฐจ ์ปดํฌ๋„ŒํŠธ ์šฉ๋„

  • Enhancer : ์ƒˆ๋กœ์šด props๋ฅผ ํ†ตํ•ด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์‹ถ์„ ๋•Œ (์™ธ๋ถ€์—์„œ property ๋ฐ›์Œ)
  • Injector : ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์„ ๋จผ์ € ๊ตฌํ˜„ํ•ด๋†จ๋‹ค๊ฐ€ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์„ ๋•Œ (์žฌ์ •์˜์ฒ˜๋Ÿผ)