React Twittler code review

shleecloud·2021년 8월 17일
3
post-thumbnail

들어가며

React 배우면서 Twittler라는 과제를 Single Page Application로 구현했다. 코스로 알게된 지식을 십분 활용하고 조금 모자랐던 부분은 구글링을 적극 활용했다. 생소한 영역이라 돌다리 두드리듯, 다른 과목보다 훨씬 천천히 진행했던 것 같다. 내가 작성하는 단어 하나하나가 어떤 의미고 어떤 기능인지 곱씹으면서 신중하게. 리액트는 정말 좋은 툴 같다. 학습한 범위 안에서 제한된 기능만 사용해도 이정도인데 다른 패키지까지 설치해서 기능을 확장시켜나가면 훨씬 깔끔하게 만들었을 것이다.

그리고 그 과정에서 엄청난 삽질이 있었다. 코드도 많이 지저분한데 최대한 억제하면서 작성해도 이정도다. 이번 포스팅은 내가 겪었던 삽질이나 앞으로도 참고할만한 코드들에 대해서 작성하고자 한다. 다 만든 지금 작성하는게 가장 생생하게 기억에 남을 것 같다. 내 RAM이 Twittler를 덮어쓰기 전에 레츠고!

혹시나 Bare minimum 진행하시는 분들이 이 글을 본다면 뒤로가기를 누르시는 것을 권장한다. Advanced를 다 완성하니 테스트 상에서는 엄청난 에러를 뿜으면서 실패한다. 아직 Advanced 관련 테스트 기준이 없으므로 '좋아보이는데?' 하면서 이 글을 너무 참고하면 정작 중요한 테스트를 놓칠 수 있다.

📌 state & props

주로 사용했던 기술은 state와 props다. state는 잠깐 임시 상태를 유지하는데 사용하는 기술이다. 하지만 과제를 수행하는 시점에서 Backend가 없다보니 모든 데이터의 상태를 state로 저장하게 됐다.

  • props 건내주기
ReactDOM.render(
  <App dummyTweets={dummyTweets} />, // HTML 속성값 부여와 동일하다.
  document.getElementById('root')
);
  • props 받기
// 받은 props는 객체 형태로 전달받는다.
const App = (props) => { 
  const a = props.el;
...
// 구조분해할당으로 바로 전달받거나 객체에서 그대로 사용할 수 있다.
const App2 = ({el1, el2}) => { 
  const a = el1;
  const b = el2;
  • state 사용하기
const [name, setName] = useState('James');

내가 구현한 방식은 state로 생성된 변수와 함수들을 props 로 전달해주는게 주인공이었다. 이어지는 글은 그 활용법에 대한 회고다.

📌 App.js

index.js 에서 바로 호출하는 컴포넌트다. 여기서 dummytweet의 정보를 state로 저장하게 만들어서 tweet 페이지에서 mypage 페이지로 넘어가도 데이터 변경 상태가 그대로 유지되게 만들었다.
처음엔 ID값을 index값으로 했는데 추가 삭제를 반복하자 ID가 겹치게 된다. App.js, 중앙에서 ID값을 고유하게 저장하게 만들어서 추가 삭제를 반복해도 ID는 절대 겹치지 않게 구성했다. 이 부분은 useRef를 사용해도 됐었는데 아직 배우지 않아서 스킵했다.

const App = (props) => {
  const [loadedTweets, setLoadedTweets] = useState(props.dummyTweets);
  const [tweetId, setTweetId] = useState(props.dummyTweets.length + 1);

react-router-dom 에서 페이지를 분배해주는 부분. props로 state를 잔뜩 내려줬다. MyPage에서도 Tweets에서 변경된 내용이 보여지고 싶었다.

            <Switch>
              <Route exact path="/" >
                <Tweets loadedTweets={loadedTweets} setLoadedTweets={setLoadedTweets} tweetId={tweetId} setTweetId={setTweetId} />
              </Route>
              <Route path="/mypage">
                <MyPage loadedTweets={loadedTweets} setLoadedTweets={setLoadedTweets} />
              </Route>
              <Route path="/about">
                <About />
              </Route>
            </Switch>

📌 Pages/MyPage.js

MyPage에서도 변경된 값이 보여지기 위해서 props로 받아온다.

const MyPage = (props) => {
  const filteredTweets = props.loadedTweets.filter((myName) => myName.username === 'parkhacker');

React에서 조건문 &&

MyPage 사용자인 parkhacker의 tweet 갯수가 0개일 때 없는 인덱스를 참조한다고 에러가 발생한다. React에서 if문처럼 사용할 수 있는 && 문법을 사용해서 길이가 0개 이상일 때 뒤에 있는 filteredTweets.map 메소드가 실행될 수 있게 했다.

      <ul className="tweets__mypage">
        {/* <Tweet tweet={filteredTweets[0]}/> */}
        {filteredTweets.length > 0 && filteredTweets.map((filteredTweet, index) => <Tweet key={index} tweet={filteredTweet} setTweet={props.setLoadedTweets} allTweet={props.loadedTweets}/>)}
        {/* TODO : 주어진 트윗 목록(dummyTweets)중 현재 유져인 parkhacker의 트윗만 보여줘야 합니다. */}
      </ul>

📌 Pages/Tweets.js

React.Fragment

React.Fragment 컴포넌트는 여러 엘리먼트를 동시에 리턴할 때 가상으로 묶어주는 역할을 한다.
일반적으로 div 태그 하나로 묶지만 묶는 것 자체가 불필요할 때 사용한다.

React에서 컴포넌트가 여러 엘리먼트를 반환하는 것은 흔한 패턴입니다. Fragments는 DOM에 별도의 노드를 추가하지 않고 여러 자식을 그룹화할 수 있습니다.
https://ko.reactjs.org/docs/fragments.html

return (
    <React.Fragment>
      <div className="tweetForm__container">

새로운 Tweet 작성

입력을 위한 state 설정

const [myName, setMyName] = useState('parkhacker');
const [myMsg, setMyMsg] = useState('');

Username 입력

React의 이벤트 핸들러. event.target.value 구문은 자주 써먹을 것 같다. event.target 정도만 알아도 console.dir(event.target) 명령어로 콘솔에서 참조할 수 있는 내용을 확인하기 좋다.

  const handleChangeUser = (event) => {
    // TODO : Tweet input 엘리먼트에 입력 시 작동하는 함수를 완성하세요.
    setMyName(event.target.value);
...

<input
type="text"
defaultValue="parkhackerker"
placeholder="your username here.."
className="tweetForm__input--username"
onChange={handleChangeUser}
></input>

Message 입력

Username 부분과 비슷한 맥락이다. Textarea 태그를 사용했다. 이 부분에서 type="textarea"로 입력하니까 테스트에서 통과가 안됐었다.

  const handleChangeMsg = (event) => {
    // TODO : Tweet textarea 엘리먼트에 입력 시 작동하는 함수를 완성하세요.
    setMyMsg(event.target.value);
  };
...

<textarea
placeholder="your message here.."
className="tweetForm__input--message"
onChange={handleChangeMsg}
></textarea>

Tweet 전송

공이 많이 들어간 부분. 내용으론 특별한게 없는데 Id 처리를 인덱스와 별도로 하고 고유하게 유지하고 싶어서 state로 관리되고 있는 tweetId 변수를 참조했다.

props.setLoadedTweets([tweet].concat(props.loadedTweets));
tweet 전체 내용은 state로 관리되고 있기 때문에 setLoadedTweets 메소드를 사용했다. push 메소드를 쓰면 원본값이 변하기 때문에 state로 관리되고 있는 환경이 깨진다.
동시에 최상단에 추가하기 위해서 concat구문을 활용했다.

  const handleButtonClick = (event) => {
    props.setTweetId(props.tweetId + 1)
    const tweet = {
      id: props.tweetId,
      username: myName,
      picture: `https://randomuser.me/api/portraits/women/${getRandomNumber(
        1,
        98
      )}.jpg`,
      content: myMsg,
      createdAt: String(new Date()),
      updatedAt: String(new Date())
    };
    props.setLoadedTweets([tweet].concat(props.loadedTweets)); 
    // TODO : Tweet button 엘리먼트 클릭시 작동하는 함수를 완성하세요.
    // 트윗 전송이 가능하게 작성해야 합니다.
  };
...

<button
className="tweetForm__submitButton"
onClick={handleButtonClick}
>Tweet</button>

Tweet 보여주기 / Filter

내용이 많아서 복잡해보인다. 지금와서 보면 별도의 컴포넌트로 뺐어야 했다. 그래도 가독성을 위해서 변수 이름을 최대한 고민했고 덕분에 변수 이름만 보면 답이 나와있다.
최상단에 usernameFilter state의 초기 문자열 '*'일 경우 전부 표기하게 될 조건문에서 쓰인다.

  const [usernameFilter, setUsernameFilter] = useState('*');

filteredUserTweets 와 allUserTweets 의 차이는 필터 메소드를 썼냐 안썼냐의 차이다. props.loadedTweets.filter((userList) => userList.username === usernameFilter) Username이 선택된 유저만 걸러서 보여준다.

  const filteredUserTweets = props.loadedTweets.filter((userList) => userList.username === usernameFilter).map((loadedOneTweet) => {
    return <Tweet tweet={loadedOneTweet} setTweet={props.setLoadedTweets} allTweet={props.loadedTweets} key={loadedOneTweet.id}/>})
  const allUserTweets = props.loadedTweets.map((loadedOneTweet) => <Tweet tweet={loadedOneTweet} setTweet={props.setLoadedTweets} allTweet={props.loadedTweets} key={loadedOneTweet.id}/>)

set 자료형으로 중복된 항목을 제거

같은 이름의 유저가 여러개의 트윗을 생성하면 filter 메소드는 각각 다른 요소로 가져온다. 그래서 select 태그에서 같은 이름으로 여러개의 항목이 보인다.
이를 방지하려면 select 태그에 보내기 전에 먼저 set으로 형변환하여 같은 이름을 제거한 후 select 태그로 보내준다.

  // 중복된 이름을 제거하기 위해 map 메소드 결과를 set 으로 형변환
  const allUserList = new Set(props.loadedTweets.map(loadedOneTweet => loadedOneTweet.username))

select로 가져온 set 자료형은 map 메소드를 가지고 있지 않기 때문에 다시 배열 자료형으로 형변환한한다. [...allUserList].map((username, index)

<select onChange={handleUsernameFilter}>
          <option value='*'>----Username filter----</option>
          {/* {set으로 중복된 이름을 제거한 뒤 배열로 형변환 후 map 메소드 사용} */}
          {[...allUserList].map((username, index) => <option key={index} value={username}>{username}</option>)}
        </select>

Tweet 보여주기

트윗을 보여줄 때 삼항연산자를 사용한다. usernameFilter === '*' 여부를 확인해서 모든 유저의 트윗을 보여줄 것인지? 아니면 필터된 유저의 트윗을 보여줄 것인지 확인한다.

사실 문자열 '*'로 확인하는건 매우 아쉬운 부분이지만 HTML 태그를 넣을 때 value={} 값으로 undefined도 안되고 null 도 안됐다. 결국 포기해서 차선책으로 별표를 택했다.

<ul className="tweets">
{/* TODO : 하나의 트윗이 아니라, 주어진 트윗 목록(dummyTweets) 갯수에 맞게 보여줘야 합니다. */}
{usernameFilter === '*' ? allUserTweets : filteredUserTweets}
</ul>

📌 Tweet

Tweet 삭제

컴포넌트의 목적은 Tweet 1개를 표시한다. 그리고 1개당 삭제 버튼도 포함하고 있다. 삭제 후 상태를 갱신하기 위해서 propssetTweet를 받아왔기 때문에 전체 Tweet 내용을 바꿀 수 있다.
삭제 방식은 현재 Id만 보이지 않게 필터한 결과를 setTweet 메소드로 보낸다.

const handleDeleteTweet = (event) => {
props.setTweet(props.allTweet.filter((oneTweet) => oneTweet.id !== props.tweet.id))
console.dir(props.allTweet)
}
...

<div className="tweet__userInfo--buttonWrapper"><button onClick={handleDeleteTweet} className="fas fa-trash-alt tweet__deleteButton"></button></div>

날짜 형식변환

날짜를 ####. ##. ##. 형식으로 바꿔준다. 앞으로도 유용하게 사용할 것 같다.

const parsedDate = new Date(props.tweet.createdAt).toLocaleDateString('ko-kr');

📌 마치며

코드를 React도 조금 햇갈리고 state&props 활용도 어설펐으나 과제를 진행하면서 만난 수많은 삽질과 스터디에서 다른사람이 보여준 수많은 이슈를 해결하다보니 어느새 눈에 익고 손에 익어서 글을 쓸 정도까지 됐다. 광복절 대체휴일까지 써서 노가다한 보람이 느껴져서 매우 뿌듯하다.

나의 삽질

기억에 남는, 그리고 귀여운 삽질이라 적어둔다. 리액트에 익숙하지 않을 때 Tweets 컴포넌트에 보낸게 아닌 Route에 보내놓고 undefined 나온다고 고생했던 기억이 났다. 스터디에서 해결했는데 조금 부끄러웠다. 리액트는 태그, 엘리먼트가 많아서 익숙하지 않으면 무조건 햇갈린다. 나중에도 이런 실수가 없도록 확실하게 적어둔다.

// 에러 코드
<Route exact path="/" loadedTweets={loadedTweets} >
	<Tweets />
</Route>

도입하면 좋았을 내용

state 끌어올리기
https://ko.reactjs.org/docs/lifting-state-up.html

useRef id값 저장하기
https://ko.reactjs.org/docs/hooks-reference.html#useref

React에 Style까지 섞기 styled-components
https://react.vlpt.us/styling/03-styled-components.html

참조 URL

React Fragment
https://ko.reactjs.org/docs/fragments.html

profile
블로그 옮겼습니다. https://shlee.cloud

0개의 댓글