본문 바로가기

[React] State & Props로 간단한 트위터 기능 구현하기

[React] State & Props로 간단한 트위터 기능 구현하기

컴포넌트 위주로 간단히 구현한 결과물

 

깃헙 링크

추가로 구현한 디테일

(1) 전송 버튼을 누르면 userID는 그대로, 내용 입력칸은 초기화된다.

(2) Tweets에서 트위터 기존 글 + 새로 생성되는 글에 대한 총 데이터를 다뤘다. 그리고 화면에 렌더링하기 위해 그의 자식 컴포넌트인 tweet 으로 내려가는 데이터를 Tweets의 형제 컴포넌트도 받아와 filter을 거쳐 특정 user의 게시글만 보이게 했다.

 -> 여기서 많은 삽질이 일어난다.. 그건 제일 아래에 나온다... 

 

 

 

 

 


 

  • 핵심적으로 구현해야 할 기능은 유저 Doyu 🤍의 Twittler에서의 트윗 전송 기능 및 생성된 트윗 추가이다.
  • 이 기능 구현을 위해서 어떤 컴포넌트에 어떤 state가 필요하고, 이 state를 어떻게 변화시킬 것인지 고민해 볼 필요가 있다.

 

File Structure
파일 구조는 다음과 같다. 파일 및 폴더부터 체계적으로 정리하자!  
/
├── /React Twittler State Props
│   ├── README.md
│   ├── /public                     # create-react-app이 만들어낸 파일, yarn/npm start로 실행할 시에 쓰임
│   └── /src                        # React 컴포넌트가 들어가는 폴더
│        ├── static                 # dummyData가 들어가는 폴더
│        │    └── dummyData.js
│        ├── Pages                  # 페이지를 표시하는 컴포넌트가 들어가는 폴더
│        │    ├── About.css
│        │    ├── About.js
│        │    ├── Mypage.css
│        │    ├── Mypage.js
│        │    ├── Tweets.css
│        │    └── Tweets.js
│        ├── Components             # 단일 컴포넌트가 들어가는 폴더
│        │    ├── Tweet.css
│        │    └── Tweet.js
│        ├── App.css
│        ├── App.js
│        ├── Footer.js
│        ├── index.js
│        └── Sidebar.js
├  package.json
└ .gitignore

 

./static/dummyData.js

아직까지는 연습용 미리 만들어 둔 데이터를 사용해본다. src/Pages/MyPage.js, src/Pages/Tweets.js에서는 파일 최상단에 dummyTweets가 import 된다. 아래 트윗 객체로 구성된 배열을 static/dummyData.js에서 확인 가능하다.

  {
    id: 1,
    username: 'kimcoding',
    picture: `https://randomuser.me/api/portraits/women/1.jpg`,
    content: '모든 국민은 인간으로서의 ... ',
    createdAt: '2022-02-24T16:17:47.000Z',
    updatedAt: '2022-02-24T16:17:47.000Z'
  }

 


상세 컴포넌트 구현하기

 

컴포넌트 그리기

  • App 에서 왼쪽은 Sidebar 컴포넌트, 오른쪽은 페이지 전환을 위한 공간이다.
  • Sidebar에는 3개의 버튼이 있다. (home, mypage, about)
  • 전환될 페이지는 총 3개이며 각각 footer가 달려있다.
    (위 그림에서 '페이지가 전환되는 부분'에 구현된다. '페이지가 전환되는 부분'의 하위요소가 아니다. 편의상 그려놓음)
  • home과 mypage 페이지에는 새롭게 tweet 게시글이 생성되면 글 목록이 업데이트 되어야 한다.

 


* 필요한 부분만 정리하여 부분 포스팅

 

Tweets 컴포넌트 (Tweet.js) 구현 체크사항 - (Tweet 컴포넌트의 부모 컴포넌트)

 

기능 : 글 쓰는 양식에 글을 쓰고 '전송' 버튼을 누르면 기존 데이터에 신규 데이터가 추가된다.

 

1. 변경되는 값은 입력폼에 입력되는 '유저 아이디'와 '글 내용' + '전체 데이터'이다.

 state를 선언 및 할당해준다.
// 🟣 TODO : useState를 react로 부터 import  
import React, { useState } from "react";
import Footer from '../Footer';
import Tweet from '../Components/Tweet';
import './Tweets.css';
import dummyTweets from '../static/dummyData';

const Tweets = () => {
  const [username, setUsername] = useState("Doyu 🤍"); // 유저 아이디 (Doyu가 디폴트 값)
  const [msg, setMsg] = useState("");                  // 내용 (초기값은 없다)
  const [tweets, setTweets] = useState(dummyTweets);   // 전체 데이터 (초기 값에 기존 데이터들 넣어줌)

 

2.이벤트가 발생하면 state 값이 변경되도록 함수를 연결한다.

입력칸
   <div className="tweetForm__input">

                <input                                   // userID 입력하는 칸
                  type="text"
                  placeholder="your username here.."
                  className="tweetForm__input--username" 
                  onChange = {handleChangeUser}          // handleChangeUser 함수 연결
                  value={username}         // 🟣 value에 state 할당 (state 값이 바로 해당 칸에 표시)
                ></input>

                <textarea                               // 트윗 내용 입력하는 칸
                placeholder="무슨 생각을 하고 계신가요?"
                className="tweetForm__input--message"
                onChange = {handleChangeMsg}            // handleChangeMsg 함수 연결
                value={msg}                // 🟣 value에 state 할당 (state 값이 바로 해당 칸에 표시)
                ></textarea>

              </div>
  const handleChangeUser = (event) => {  
    setUsername(event.target.value);       // Tweet input 엘리먼트에 입력 시 작동하는 함수 
  };                                       // state 값을 입력폼에 입력한 내용으로 변경

  const handleChangeMsg = (event) => { 
    setMsg(event.target.value);           // Tweet textarea 엘리먼트에 입력 시 작동하는 함수 
  };                                      // state 값을 입력폼에 입력한 내용으로 변경

 

 전송 버튼
     <button 
     className= "tweetForm__submitButton"
     onClick={handleButtonClick}>전송</button>   // 버튼을 누르면 handleButtonClick 함수 호출

 

spread syntax를 이용해 새로운 객체 1개와 기존의 객체 배열을 풀어 쓴 형태를 전달 인자로 넣어준다.  
 const newTweet = [tweet, ...tweets]
      setTweets(newTweet)
      
 //또는
 
  setTweets([tweet, ...tweets])
  const handleButtonClick = (event) => {  // Tweet button 엘리먼트 클릭시 작동하는 함수
  
    const tweet = {
    id: tweets.length + 1,
    username: username,              // 유저ID state 값
    picture: 'https://~~~~~~',       // 이미지는 내가 원하는 유저 프로필 사진을 넣음
    content: msg,                    // 내용 state 값
    createdAt: new Date().toISOString();
    };
    
    setTweets([tweet, ...tweets]);   // 🟣 spread syntax를 활용해 배열 형태의 state 값 변경
    setMsg('')                       // 🟣 '전송' 버튼을 누르면 내용의 초기값이 ''가 된다.
    setUsername('Doyu 🤍')           // 🟣 '전송' 버튼을 누르면 ID의 초기값이 'Doyu 🤍'가 된다.
    dummyTweets.unshift(tweet)
  };

 

3. state 값 tweets(객체 형태의 개별 글의 정보가 모두 나열된 배열 데이터) 을 자식인 Tweet 컴포넌트에게 내려 보낸다.

  •  여기서 tweets은 전체 트윗글의 정보를 담은 배열 형태다. : [{최신 글1}, {최신 글2}, {.....}, .....]
  • 자식 컴포넌트에게 내려보내려면 map 함수를 이용해 하나씩 보내야 한다.
  • map의 전달 인자 tweet은 {최신 글1}의 형태로 대략 {id: 6, userID: 도유, 내용: '날씨 좋다!'}의 객체 형태다.
<ul className="tweets">
        {tweets.map((tweet) => 
        
        <Tweet key={tweet.id} tweet ={tweet} />
        
        )}
</ul>

 


 

Tweet 컴포넌트 (Tweet.js) 구현 체크사항 - (Tweets 컴포넌트의 자식 컴포넌트)

기능 : 
부모 Tweets 컴포넌트에 포함되어 있는 Tweet 컴포넌트는 부모로부터 전체 데이터를 받아와서 유저 아이디, 생성 날짜, 내용이 포함된 전체 글 목록을 화면에 렌더링 해준다.

 

dummyTweets가 아닌 다른 데이터가 props로 전달되어도 트윗 정보를 정확하게 표시해야 한다.

import React from 'react';
import './Tweet.css';


const Tweet = ({ tweet }) => {
  {/* 🟣 props, 여기서는 tweet를 받아온다. tweet은 객체의 형태로 전달된다. */}
  
  const parsedDate = new Date(tweet.createdAt).toLocaleDateString('ko-kr');

  return (
    <li className="tweet" id={tweet.id}>
      <div className="tweet__profile">
        <img src={tweet.picture} />
        .
        .
        .

 



깨달음의 메모

 

spread syntax와 map 함수.. 그리고 prop

  • map의 전달 인자 tweet은 {최신 글1}의 형태로 대략 {id: 6, userID: 도유, 내용: '날씨 좋다!'}의 객체 형태다.
// 참고한 예시코드

function Say() {
  const props = {name: "walli"};
  return <Hello {...props} />;
}

const Hello = (props) => <div>{props.name}</div>

 

  • 위의 코드를 참고하여 아래와 같이 구현했는데 에러가 났다. 
  • 해결 : 위와 같이 쓰려면 자식의 전달 인자가 tweet이어야 한다. 
{tweets.map((tweet) =>                 // 예) 배열 tweets의 0번째 요소 tweet은 객체 
  <Tweet key={tweet.id} {...tweet} />  // 🟣 객체 리터럴로 {} 안에 {ID: 6, username..} 등 바로 입력
)}

// 생략

const Tweet = ( tweet ) => {      // 🟣 {tweet}이 아니라 tweet
  return (
        <img src={tweet.picture} />
ECMAScript 2018에서 객체 리터럴이 추가되었다.
const obj = { a: "A", b: "B" };
console.log({ c: "C", ...obj }); // {c: "C", a: "A", b: "B"}

 

  • 다른 방법 : {} 안에 객체를 전달한다. 자식의 전달 인자가 {tweet} 이다.
{tweets.map((tweet) =>                      // 예) 배열 tweets의 0번째 요소 tweet은 객체
  <Tweet key={tweet.id} tweet ={tweet} />   //  🟣  {{최신 글의 정보}} 이런 식
)}
        
const Tweet = ({ tweet }) => {              // 🟣 그대로 {tweet} 형태로 자식한테 전달해야 한다.
  return (
   <img src={tweet.picture} />

 

어찌 됐든 객체 하나는 그냥 넘겨줄 수 있구나~ 를 깨달았다. 

{} 안의 내용이 객체이므로 자식에게도 {} 형태 그대로 전달해야 한다.

 


 

state hook 구현 과정

 

(1) dummyTweets.unshift(tweet); 를 이용해서 state hook을 쓰지 않고 시도해 보았다. (실패)

 

* 결과 :

  • 되긴 된다. 하지만 렌더링이 바로 되지 않기 때문에 state hook을 쓴 '내용 입력' 함수를 쓸 때에서야 글이 추가가 된다.
  • 다시 말해 unshift를 쓰면 dummyTweets에 배열 요소 하나가 추가가 되지만, 렌더링이 바로 되지 않기 때문에 변화가 없다.
  • 내용을 입력하고 '전송' 버튼을 눌렀는데도 동작하지 않고 다음 게시글을 생성하기 위해 input창에 입력을 할 때(state hook 연결되어 있음) 그때서야 렌더링이 되어 글이 추가가 된다.

 

(2) state hook을 걸고 unshift를 사용해 보았다. (실패)

tweets.unshift(tweet) 
    setTweets(tweets)

*  결과 :

  • state hook 의 기본 원칙은 state를 직접적으로 변경하지 않기이다. 하지만 state인 tweets를 직접 건드리고 있다! 
  • 또한 렌더링 조건은 주소값의 변경 여부로 결정된다. 하지만 이 경우 주소값이 바뀌지 않아 렌더링이 되지 않는다.

 

* 해결 방법 (권장 X):

  • slice()를 이용해 아예 새로운 배열로 복사를 하고 unshift를 해서 새 tweet을 넣고 그렇게 변화된 새 배열을 setTweets 함수 인자로 전달하면 된다. 근데 굳이...?  

 

(3) spread syntax를 이용해 새로운 객체 1개기존의 객체 배열을 풀어 쓴 형태를 전달 인자로 넣어준다. (성공!)

 const newTweet = [tweet, ...tweets]
      setTweets(newTweet)
      
 //또는
 
  setTweets([tweet, ...tweets])

 


 

삽질의 여정

 

형제 컴포넌트 사이 데이터 공유하기

문제 상황: 

  • Tweets 와 Mypage는 형제 컴포넌트다.
  • Tweets에서 사용된 state hook을 이용해 갱신된 트위터 글의 전체 목록 데이터가 Mypage 에도 필요했다.
  • 따라서 이와 관련된 고민을 한 포스팅이다.
// Tweets 컴포넌트에서 state 값인 tweets (트위터 글 목록 전체 데이터)를 자식 Tweet에게 내보내고 있다.
// Mypage 컴포넌트도 이 데이터가 필요하다!!!!

<ul className="tweets">
        {tweets.map((tweet) => 
        <Tweet key={tweet.id} tweet ={tweet} /> 
        )}
</ul>

 

해결 시도 방법 (1) : 데이터를 아예 공통된 부모 컴포넌트로 옮긴다. 

  • 원칙상 내가 원하는 데이터는 공통의 부모 컴포넌트인 이곳(App.js)에 위치해있어야만 한다.
  • 하지만, 보이는 것처럼 routering으로 링크가 연결되어 있으며 각 컴포넌트의 위치를 구축해놓은 상태이다. 
  • 만약, 원하는 데이터를 이곳에 옮기게 된다면 트위터의 입력폼과 전송버튼, state hook까지 데리고 와야 한다.
  • <Route path="/" element={이곳!!!} /> 에 실험 삼아 Tweets 컴포넌트의 내용을 다 옮겨와 보았지만 작동하지 않는다.
  • 인터넷을 찾아봐도 이런 식의 응용은 없었다.

 

해결 시도 방법 (2) :  Context API, 리덕스를 사용하여 전역적으로 값을 관리한다.

 

참고했던 자료들

-> 이 부분에 매달려 2시간 정도 할애했던 것 같다. 

  • 이것 저것 3~4가지 방법을 시도해보았으나 결론적으로 해당 방법은 이미 제작되어서 상당 부분 완성이 된 컴포넌트 간의 관계와 함수, 데이터 구조 상 쉽게 말하면 우겨넣어지는 느낌이었다.
  • bottom-up 방식에서 이미 컴포넌트와 데이터 흐름을 구상했을 때, 이미 routes, state hook 등으로 긴밀하게 구축된 관계를 엎고 굳이 특정 데이터를 전역으로 뺄 이유는 그닥 없어 보였다. (차라리 새로 만드는 게 나음) 
  • 나에게는 효율적 시간관리도 중요했기 때문에 (변명인가...?) 다음 과제에 전역 데이터 관리를 활용해보기로 했다.

 

해결 시도 방법 (3) :  다소 허무한 방법으로 해결했다.

  • 어찌됐든 Mypage에서 필요한 것은 새로 갱신된 데이터 배열이다.
  • 그럼 state hook에서 트위터 전체 게시물 데이터 갱신을 위해 선언한 tweets(아래 그림 4번째 줄)의 초기값으로 써먹었던 dummyTweets 배열 데이터를 활용하면 되는 것 아닌가...?
  • 아래 그림 마지막에서 2번째 줄처럼 그냥... 신규 데이트를 unshift를 이용해 제일 앞으로 넣어 주었다.
  • 끝.... 하하 
  • + 다른 장점은 다른 컴포넌트에 이동했다가 다시 본 페이지에 오면 리렌더링 되어 신규로 생성된 트윗 글이 없어지는 현상이 있었는데 이 방법을 쓰고 나서는 그대로 있었다. ㅎㅎ 이득이다.

마지막에서 2번째 줄

 

 

 


 

 

시간 가는 지도 모르고 코딩하다 보니 벌써 새벽 1시였다. 금요일이라서 가능했던 취침 시간 ㅎㅎ 

넘 재밌쟈나!!!! 

728x90
⬆︎