(1) 만들면서 알아보는 Hooks

프로젝트 구조

만들어볼 프로젝트의 구조는 아래와 같습니다.

프로젝트는 초기 세팅 상태로 진행되어집니다.

1) 레이아웃 구성

스타일링을 위해 styled-components 를 설치합니다.

$ npm i -S styled-components

먼저 왼쪽의 목록 컴포넌트부터 만들어보도록 하겠습니다. 목록 컴포넌트가 해주는 일은 간단합니다. 목록 데이터를 받아 그려줍니다. 목록의 메모를 클릭시 클릭된 메모를 오른쪽에 그려줍니다. 이번 예제에서는 Context API 를 이용하여 데이터의 상호작용이 이루어 질 수 있도록 합니다.

먼저 src 폴더 아래에 memos 폴더와 content 폴더를 만들고 내부에 index.js 들을 생성해주세요

// src/memos/index.js

import React from "react";
import styled from "styled-components";

const MemoFrame = styled.div``;

function Memos() { 
  return <MemoFrame>Memos</MemoFrame>;
}

export default Memos;

// src/content/index.js

import React from "react";
import styled from "styled-components";

const ContentFrame = styled.div``;

function Content() { 
  return <ContentFrame>Memo</ContentFrame>;
}

export default Content;

혹시 styled-component 의 사용법을 잘 모르신다면 이전 예제들을 확인해주세요

만들어진 컴포넌트들을 App.js 에서 불러와서 간단한 레이아웃 구조를 생성해보겠습니다.

// src/App.js

import React from "react";
import styled, { css } from "styled-components";

import Memos from "./memos";
import Content "./content";

// 왼쪽 오른쪽 구조를 나눠야 하기 때문에 flex 를 이용합니다.
const AppFrame = styled.div`
  display: flex;
  height: 100vh;
`;

/* 
  나눠진 왼쪽 오른쪽에 flex 사이즈를 유동적으로 조절하기 위해 flex 값을
  props 로 내려받습니다. 
*/
const Container = styled.div`
  ${({ flex }) =>
    flex &&
    css`
      display: flex;
      flex: ${flex};
    `}
`;

function App() {
  return (
    <AppFrame>
      <Container flex={1}>
        <Memos />
      </Container>
      <Container flex={2}>
        <Content />
      </Container>
    </AppFrame>
  );
}

export default App;

아래와 같이 나누어진 화면을 보실 수 있을거에요

2) useContext 를 이용하여 Context API 구성

왼쪽 목록의 컴포넌트에서는 선택된 메모의 값을 알아야하고 오른쪽 컴포넌트에서는 선택된 메모 값을 바탕으로 데이터를 그려줘야합니다. 이처럼 컴포넌트들 끼리의 상호작용이 일어날때 가장 간단한 방법은 두 컴포넌트를 감싸는 컴포넌트로 state 를 올려서 사용하는 방법이 있지만 이번에는 Context API 를 이용하여 데이터를 주고 받는 방법을 알아보려고합니다.

use-Context 라는 hook 을 이용하여 Context API 를 보다 손쉽게 사용하도록 할 수 있습니다.

다양한 Context 를 만들 수 있는 방법은 여기를 참고해주세요

src 아래에 application-context 라는 이름의 파일을 만들어주세요

// src/application-context.js

import React, { createContext, useContext, useState } from "react";

// Context 를 생성합니다.
const Context = createContext(null);

// Provider 로 감싸지는 컴포넌트들은 value 의 값을 props 로 받을 수 있습니다.
export function ApplicationContextProvider({ children }) {
  const [memos, setMemos] = useState(null);
  const [memo, setMemo] = useState(null);

  const value = {
    memos,
    setMemos,
    memo,
    setMemo
  };
  return <Context.Provider value={value}>{children}</Context.Provider>;
}

// 외부에서 context 를 손쉽게 가져다 쓸 수 있도록 도와줍니다.
export function useApplicationContext() {
  return useContext(Context);
}

여기서 잠깐, useState 란 ?

useState 는 이전에 보았던 class component 의 state = { // ... } 와 같습니다. 다른 점이 있다면 setState 로 변경했던 것과는 다르게 useState 는 해당 state 를 변경 할 수 있는 짝을 지원해 준다는 것입니다.

const [count, setCount] = useState(0)

// count 의 값을 바꿀 수 있는 방법은 setCount 를 이용하는 방법뿐이다.
// naming 규칙은 보통 set + state 명 입니다.

3) 만들어진 Context API 이용하기

Context API 는 말그대도 관련있는 Context 에서만 값을 이용할 수 있도록 만들어 줄 수 있습니다. 하지만 지금 우리는 Context 범위가 적기 때문에 두 컴포넌트를 감싸고 있는 App.js 에서 Provider 를 적용해주도록 하겠습니다.

// src/app.js

import React from "react";
import styled, { css } from "styled-components";

import { ApplicationContextProvider } from "./application-context";

import Memos from "./memos";
import Content from "./content";

const AppFrame = styled.div`
  display: flex;
  height: 100vh;
`;

const Container = styled.div`
  ${({ flex }) =>
    flex &&
    css`
      display: flex;
      flex: ${flex};
    `}
`;

function App() {
  return (
    <ApplicationContextProvider>
      <AppFrame>
        <Container flex={1}>
          <Memos />
        </Container>
        <Container flex={2}>
          <Content />
        </Container>
      </AppFrame>
    </ApplicationContextProvider>
  );
}

export default App;

이제 Provider 로 감싸져 있는 내부 요소들에서는 useApplicationContext 를 이용하여 Context 내부 값에 접근 할 수 있습니다.

4) Memos 컴포넌트에서 Context API 사용하기

Memos 컴포넌트를 작성하기전에 먼저 Context API의 memos에 dummy data 를 채워보고자합니다.

메모는 아래와 같은 데이터 구조를 갖습니다.

id: number = '메모의 고유한 id'
title: string = '메모의 제목'
content: string = '메모의 내용'

useState 는 인자로 default value 를 줄 수 있습니다. memos 의 default Value 로 dummy data 가 추가된 배열을 줍니다.

import React, { createContext, useContext, useState } from "react";

const Context = createContext(null);

export function ApplicationContextProvider({ children }) {
  // useState 의 default Value 를 이용하여 값을 채워줍니다. 
  const [memos, setMemos] = useState([
    {
      id: Date.now(),
      title: "임시 메모 데이터",
      content: "임시 메모 데이터의 내용"
    }
  ]);
  const [memo, setMemo] = useState(null);

  const value = {
    memos,
    setMemos,
    memo,
    setMemo
  };
  return <Context.Provider value={value}>{children}</Context.Provider>;
}

export function useApplicationContext() {
  return useContext(Context);
}

이제 memos 에서 context api 에 접근하여 memos 데이터가 잘 불러와지는지 확인해보겠습니다.

// src/memos/index.js

import React from "react";
import styled from "styled-components";

import { useApplicationContext } from "../application-context";

const MemosFrame = styled.div``;

function Memos() {
  const { memos } = useApplicationContext();
  console.log("memos", memos);

  return <MemosFrame>Memos</MemosFrame>;
}

export default Memos;

memos 데이터를 가지고 왼쪽 리스트를 구성해야하는데 레이아웃 그림에서 보신 것 처럼 같은 형식의 구조가 반복되어지고 있습니다.

이 컴포넌트를 리스트자체에서 그려주는 것 보다는 memo 라는 컴포넌트를 만들어 따로 그려주는것이 렌더링 이점과 사용 측면에서도 좋기 때문에 따로 분리를 하겠습니다.

memo 컴포넌트는 id, title, content 의 데이터를 props 로 받습니다.

// src/memo/index.js

import React from "react";
import styled from "styled-components";

const MemoFrame = styled.div``;

function Memo({ source: { title, content }}) {
  return <MemoFrame>Memo</MemoFrame>;
}

export default Memo;

만든 Memo 를 Memo 에서 사용하도록 추가합니다.

// src/memos/index.js

import React from "react";
import styled from "styled-components";

import { useApplicationContext } from "../application-context";

import Memo from "../memo";

// MemosFrame style 을 추가합니다.
const MemosFrame = styled.div`
  width: 100%;
  padding: 10px;
  box-sizing: border-box;
  overflow-y: auto;
`;

function Memos() {
  const { memos } = useApplicationContext();
  console.log("memos", memos);

  // 만들어진 Memo 를 가져와 사용합니다.
  return (
    <MemosFrame>
      {memos.map(memo => (
        <Memo key={memo.id} source={memo} />
      ))}
    </MemosFrame>
  );
}

export default Memos;

5) 공통 컴포넌트 만들기

다양한 텍스트를 표현하기 위해 공통적으로 사용할 Text 컴포넌트와 컴포넌트들을 감싸줄 Container 만들어보겠습니다. Text 의 경우 당장의 필요한 속성은 텍스트의 사이즈, 굵기, 말줄임, line-height 입니다. 앞으로 필요한 속성은 하나씩 추가하면서 살펴보겠습니다.

// src/text.js

import styled, { css } from "styled-components";

const Text = styled.div`
  font-size: ${({ size }) => size || 14}px;

  ${({ lineHeight }) =>
    lineHeight &&
    css`
      line-height: ${lineHeight};
    `}

  ${({ bold }) =>
    bold &&
    css`
      font-weight: bold;
    `}

    ${({ ellipsis }) => css`
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: normal;
      word-wrap: break-word;
      display: -webkit-box;
      -webkit-line-clamp: ${ellipsis};
      -webkit-box-orient: vertical;
    `}
`;

export default Text;

Container 는 float, margin, padding, display 가 필요합니다.

// src/container.js

import styled, { css } from "styled-components";

const Container = styled.div`
  width: 100%;

  ${({ justify }) =>
    justify &&
    css`
      justify-content: ${justify};
    `}

  ${({ margin }) =>
    margin &&
    css`
      margin-top: ${margin.top}px;
      margin-right: ${margin.right}px;
      margin-bottom: ${margin.bottom}px;
      margin-left: ${margin.left}px;
    `}

  ${({ padding }) =>
    padding &&
    css`
      padding-top: ${padding.top}px;
      padding-right: ${padding.right}px;
      padding-bottom: ${padding.bottom}px;
      padding-left: ${padding.left}px;
    `};

  ${({ flex }) =>
    flex &&
    css`
      display: flex;
      flex: ${flex};
    `}
`;

export default Container;

App 에 있는 Container 를 공통 Container 로 변경합니다.

// src/App.js

import React from "react";
import styled from "styled-components";

import { ApplicationContextProvider } from "./application-context";

import Container from './container'
import Memos from "./memos";
import Content from "./content";

const AppFrame = styled.div`
  display: flex;
  height: 100vh;
`;

function App() {
  return (
    <ApplicationContextProvider>
      <AppFrame>
        <Container flex={1}>
          <Memos />
        </Container>
        <Container flex={2}>
          <Content />
        </Container>
      </AppFrame>
    </ApplicationContextProvider>
  );
}

export default App;

6) Memo 구성해보기

위에서 만들어 놓은 text 를 이용하여 Memo 컴포넌트를 만들어보겠습니다.

content 부분은 너무 길어질 수 있기 때문에 말줄임을 적용합니다.

import React from "react";
import styled from "styled-components";

import Text from "../text";

const MemoFrame = styled.div`
  border: 1px solid #ebebeb;
  border-radius: 5px;
  padding: 10px;

  &:not(:last-child) {
    margin-bottom: 15px;
  }
`;

function Memo({ source: { title, content } }) {
  return (
    <MemoFrame>
      <Text size={16} lineHeight={1.53} bold>
        {title}
      </Text>
      <Text size={15} lineHeight={1.53} ellipsis={2}>
        {content}
      </Text>
    </MemoFrame>
  );
}

export default Memo;

7) Content 컴포넌트 구성하기

Content 컴포넌트는 선택된 Memo 에 대한 내용을 보여줍니다.

Content 에서는 Context API 의 Memo State 를 사용하여 내용을 구성합니다. Context API 에 dummy data 를 추가하여 view 부터 구성해보겠습니다.

우리가 추가해놓았던 임시메모데이터라는 메모가 선택되어졌다고 가정하겠습니다.

import React, { createContext, useContext, useState } from "react";

const Context = createContext(null);

export function ApplicationContextProvider({ children }) {
  const [memos, setMemos] = useState([
    {
      id: Date.now(),
      title: "임시 메모 데이터",
      content: "임시 메모 데이터의 내용",
      createdAt: new Date()
    }
  ]);
  
  // memos 와는 다르게 단일 객체입니다.
  const [memo, setMemo] = useState({
    id: Date.now(),
    title: "임시 메모 데이터",
    content: "임시 메모 데이터의 내용",
    createdAt: new Date()
  });

  const value = {
    memos,
    setMemos,
    memo,
    setMemo
  };
  return <Context.Provider value={value}>{children}</Context.Provider>;
}

export function useApplicationContext() {
  return useContext(Context);
}

Content 에서 Context API 의 memo 값을 가져옵니다. 이전에 만들어뒀던 Container 를 이용하여 간격을 조절하고 Text 를 이용하여 텍스트 스타일을 추가합니다

// src/content/index.js 

import React from "react";

import { useApplicationContext } from "../application-context";

import Text from "../text";
import Container from "../container";

function Content() {
  const { memo } = useApplicationContext();

  const { title, content } = memo;

  return (
    <Container padding={{ top: 10, right: 10, bottom: 10, left: 10 }}>
      <Container margin={{ bottom: 20 }}>
        <Text size={27} bold>
          {title}
        </Text>
      </Container>
      <Text size={16}>{content}</Text>
    </Container>
  );
}

export default Content;

8) Content 컴포넌트 상태에 따른 버튼

Content 컴포넌트에선 선택된 Memo 의 내용을 수정 할 수 있습니다. Content 본문 윗 쪽에 수정 모드로 전환 할 수 있는 버튼을 추가합니다.

3 가지의 상태에 따라 버튼은 동작과 텍스트가 달라집니다.

1) 선택된 Memo 가 있고 수정 상태가 아니면 (새 글 작성 + 수정) 버튼을 노출 2) 선택된 Memo 가 있고 수정 상태이면 (수정 + 취소) 버튼을 노출 3) 수정 상태가 아니라면 (새 글 작성 노출)

// src/content/index.js 

import React, { useState } from "react";
import styled from "styled-components";
import { useApplicationContext } from "../application-context";

import Text from "../text";
import Container from "../container";

const Button = styled.button`
  padding: 5px 15px;
  color: ${({ active }) => (active ? "#fff" : "#368fff")};
  background: ${({ active }) => (active ? "#368fff" : "#fff")};
  border: 1px solid #368fff;
  border-radius: 2px;
  font-size: 13px;
  font-weight: bold;

  &:not(:last-child) {
    margin-right: 5px;
  }
`;

function Content() {
  const { memo } = useApplicationContext();
  // 후에 메모를 수정 할 때 사용될 state 입니다.
  const [editMemo] = useState({
    title: "", // 수정 할 타이틀
    content: "", // 수정 할 컨텐츠
    isEditing: false // 수정 모드인지
  });

  const { title, content } = memo;
  // 수정 모드를 판단합니다.
  const { isEditing } = editMemo;

  return (
    <Container padding={{ top: 10, right: 10, bottom: 10, left: 10 }}>
      <Container flex justify="flex-end">
        {memo && !isEditing && <Button active>수정</Button>}
        {memo && isEditing && (
          <>
            <Button>취소</Button>
            <Button active>수정</Button>
          </>
        )}
        {!isEditing && <Button>새 글 작성</Button>}
      </Container>
      <Container margin={{ bottom: 20 }}>
        <Text size={27} bold>
          {title}
        </Text>
      </Container>
      <Text size={16}>{content}</Text>
    </Container>
  );
}

export default Content;

Last updated