728x90

아래 포스팅에 자세히 설명을 해두었다. 

 

dolphinsarah.tistory.com/21?category=467228

 

[TIL_개발일기_210322] React에서 map 사용 시 key를 설정해줘야 하는 이유는

*하루동안 새롭게 알게된 부분, 에러를 해결한 방법 등을 작성하는 개발일기입니다. 다른 사람에게도 설명해줄 수 있도록 제 머릿속에 넣기 위해 정리를 시작하게 되었습니다. React에서 map 사

dolphinsarah.tistory.com

 

그래도 간단히 설명해보자면, map을 도는 배열의 key를 고유한 값으로 설정하라고 하는 것은,

 

그렇게 하지 않았을 때 => 배열 전체가 리렌더링 될 필요가 없음에도 리렌더링 되는 것을 방지하기 위해서다.  

 

이를테면, push를 통해 아이템이 배열 끝에 추가될 경우, 마지막 요소만 변경되었으니 전체를 리렌더링할 필요가 없는 경우

 

728x90
728x90

아래와 같이, 복사하기 버튼을 눌렀을 때 클립보드에 텍스트를 복사하려면 어떻게 해야할까? 

 

 

Document.execCommand()를 사용하거나 Clipboard API를 활용하는 방법도 있다.  

 

Document.exeCommand()

 

아래는 exeCommand()에 대한 MDN의 설명이다. 

 

When an HTML document has been switched to designMode, its document object exposes an execCommand method to run commands that manipulate the current editable region, such as form inputs or contentEditable elements.

 

즉, HTML document가 디자인 모드로 바뀌면서 exeCommand를 사용하면, form의 input과 같이 편집이 가능한 element를 조작할 수 있다는 것이다. 그래서 element의 내용을 클립보드로 복사할 수 있게 되었다고 이해하면 좋을 것 같다. 

 

따라서, 어떤 element의 innertext를 복사하고자 한다면, 그 element가 editable한 input이나 textarea여야 한다. 

 

만약, 해당 element가 input이나 textarea가 아니라면, 아래의 코드처럼 textarea를 만들었다가 지우는 방식을 사용하면 된다. 

 

    const pinyin = pinyinRef.current.innerText;
    const textField = document.createElement('textarea');
    textField.innerText = pinyin;
    document.body.appendChild(textField);
    textField.select();
    document.execCommand('copy');
    textField.remove();

 

pinyinRef가 내가 복사하려고 하는 text를 가지고 있는 element를 참조하는 ref 객체이다. 

 

그래서 그 pinyinRef.current의 innerText(복사하고자하는 텍스트)를 가져와서 textField라는 임시 textarea의 innerText로 넣어준다. 

 

document에 임시 textarea를 붙여주고, 그 textarea를 select하여 복사하고자 하는 텍스트를 선택하고, 

 

document.execCommand('copy')를 사용하여 해당 텍스트를 클립보드에 복사한다. 

 

그리고 다시, 임시 textarea를 지워주면 된다. 

 

하지만, Document.execCommand()는 MDN 문서에서 알 수 있듯 더 이상 추천하지 않는 방식이다. 

 

대신, clipboardAPI를 사용한다. 

 

 

Clipboard API

 

Clipboard API는 Promise 기반으로 클립 보드 내용을 비동기적으로 접근할 수 있는 API이다. 

 

아주 아주 간단하다. 

 

		const pinyin = pinyinRef.current.innerText;
		navigator.clipboard.writeText(pinyin);

 

navigator.clipboard.writeText 만을 사용해서 가능하다. 즉, 붙여넣고자 하는 text를 인자로 넣어주면 된다. 

 

728x90

'Front-end > React' 카테고리의 다른 글

next에 antd 적용 안 될 때  (0) 2021.04.10
Redux가 필요한 이유와 기본 개념  (0) 2020.11.14
React: props와 state  (0) 2020.10.25
728x90

*하루동안 새롭게 알게된 부분, 에러를 해결한 방법 등을 작성하는 개발일기입니다. 다른 사람에게도 설명해줄 수 있도록 제 머릿속에 넣기 위해 정리를 시작하게 되었습니다. 

React 버튼 클릭 효과 유지하기

페이지 버튼이든 카테고리 버튼이든, 아래처럼 버튼을 클릭했을 때 style을 유지하고 싶은 경우가 있다.

 

 

 

클릭 효과를 유지하기 위한 방법은 여러가지가 있겠지만, 나는 클릭 이벤트 리스너를 붙여 classList를 추가해주고 지우는 방식을 택했다.  

 

// 각 페이지 버튼마다 'pages'라는 클래스를 가지고 있다. 
const pages = document.getElementsByClassName('pages');  
    
const clickHandler = (e) => {
    // 처음 로딩되었을 때는 1번 페이지에 선택되어 있어야 하기 때문에, 
    // 1번 페이지 버튼은 'first-page' 클래스를 가지고 있다. 
    // 만약, 클릭된 클래스 리스트에 first-page가 없다면 첫번째 페이지는 
    // 계속 선택되어 있을 필요가 없기 때문에 remove 해준다. 
    if (!e.target.classList.contains("first-page")) {
        pages[1].classList.remove("first-page");
    }
    
	// 모든 페이지 버튼에서 활성 클래스인 'active-page'를 지워 초기화 해준다. 
    for (var i = 0; i < pages.length; i++) {
        pages[i].classList.remove("active-page");
    }
    // 현재 클릭된 버튼에만 활성 클래스인 'active-page'를 붙여준다. 
    e.target.classList.add("active-page");
}

// 모든 pages 클래스를 가진 요소에 클릭 이벤트 리스너를 붙여주었다. 
// 그래서 클릭 이벤트가 발생하면, clickHandler 함수가 실행된다. 
const init= () => {
    for (let i = 0; i < pages.length; i++) {
      pages[i].addEventListener("click", clickHandler);
    }
}

init();

 

 

  • addEventListener - 특정 이벤트가 발생했을 때 특정 함수를 실행할 수 있게 해주는 기능. 여기서는 click 이벤트가 발생했을 때 clickHandler 함수를 실행해준다. 
  • document.getElementsByClassName(className) - HTML에서 있는 요소 중, 주어진 클래스명을 가진 모든 요소들을 배열에 담아 리턴해준다. 
  • e.target - 이벤트 객체의 타깃, 예를 들어, 2번 페이지를 눌렀다면 <li class="pages active-page" style="user-select: auto;">2</li>가 리턴된다. 
728x90
728x90

*하루동안 새롭게 알게된 부분, 에러를 해결한 방법 등을 작성하는 개발일기입니다. 다른 사람에게도 설명해줄 수 있도록 제 머릿속에 넣기 위해 정리를 시작하게 되었습니다. 

React 게시판 최신순으로 정렬

아래와 같이 게시글들이 최신순으로 정렬되기 바란다면 어떻게 해야할까? 

 

 

 

서버에서 그대로 데이터를 받아온 뒤, 컴포넌트에서 reverse를 하려고 하면 리렌더링될때마다 reverse 될 수있다

 

그래서 Load User action에 성공한 뒤, data를 Users에 담을 때 아예 reverse()를 하여 담으면 된다. 

 

728x90
728x90

*하루동안 새롭게 알게된 부분, 에러를 해결한 방법 등을 작성하는 개발일기입니다. 다른 사람에게도 설명해줄 수 있도록 제 머릿속에 넣기 위해 정리를 시작하게 되었습니다. 

React에서 map 사용 시 key를 설정해줘야 하는 이유는

아래와 같이 컴포넌트를 반복하여 렌더링해줘야 하는 경우, map을 사용한다. 

 

 

 

그런데 이 때, 배열 내부의 엘리먼트에 key 값을 지정해주지 않으면, 'Each child in a list should have a unique key prop.' 경고문이 뜨게 된다. 

 

 

리액트 공식문서에서는, key어떤 아이템이 변화되거나 추가 또는 삭제되었는지를 알아차리기 위해 필요하다고 한다. 

 

리액트는 state에서 변경된 부분만 찾아 리렌더링해주는데, 이때 가상 dom과 비교하여 바뀐 부분만 리렌더링해준다. 만약, 배열에 요소가 추가되었다면 배열 전체가 변경된 것이라고 생각하고 전체를 리렌더링 하게 된다. 마지막 요소만 변경되었으니, 전체를 리렌더링할 필요는 없는데도 말이다. 

 

그래서 배열 내부 엘리먼트에 key를 지정해줘야 한다. key 값을 지정해주면 리액트는 배열에 추가된 요소에 대해서만 리렌더링한다. 즉, key는 배열의 어떤 원소에 변동이 있었는지 알아내고자 할 때 사용되는 것이다. 따라서, 데이터가 가진 고유의 값을 key로 설정해야 한다. 

 

const PostList = ({ posts }) => {
    return(
        posts.map((post) => {
            return(
                <tr key={post.id}>
                    <td>{post.id}</td>
                    <td>{post.nickname}</td>
                    <td>{post.role}</td>
                    <td>{post.phone}</td>
                </tr>
            );
        })
    ); 
}

export default PostList; 

 

 

key 값으로 index를 설정하는 경우도 있는데, 어떤 경우에는 key 설정의 장점을 하나도 살리지 못하고 배열 전체가 리렌더링 될 수 있다.  배열에 첫 번째 위치에 새로운 element를 넣게 되면, 기존 배열의 index가 하나씩 밀리게 된다. 그럼 결국, 배열 전체가 변경되었다고 생각하기 때문에 배열 전체가 리렌더링 되는 것이다. 

 

// 기존 배열 
key 0, {id: 0, title: 'hello', content: 'olleh'}, 
key 1, {id: 1, title: 'my', content: 'ym'}, 
key 2, {id: 2, title: 'name', content: 'eman'}, 

// 배열 첫 번째 위치에 새로 element를 넣음 
key 0, {id: 3, title: 'is', content: 'si'}, 
key 1, {id: 0, title: 'hello', content: 'olleh'}, 
key 2, {id: 1, title: 'my', content: 'ym'}, 
key 3, {id: 2, title: 'name', content: 'eman'}, 

 

그래서, element를 고유하게 식별할 수 있는 unique한 값을 key로 설정해주는 게 좋다. 가장 사용하기 쉬운 것은 Database의 id이다. AUTO_INCREMENT된 id 값을 사용하거나, id가 아니라도 unique한 값이라면 key로 사용하면 된다. 

 

Robin Pokorny에 의하면, 아래 3가지를 만족하는 경우 index를 key로 써도 된다고 한다. (key 값으로 index를 꼭 설정해야겠다면, 아래의 조건을 확인하자)

  1. 배열과 각 요소가 static이며 computed 되지 않고, 변하지 않아야 한다. 
  2. 데이터 내부에 id로 쓸만한 unique 값이 없다. 
  3. 데이터가 결코 reordered 또는 filtered 되지 않는다. 
728x90
728x90

*하루동안 새롭게 알게된 부분, 에러를 해결한 방법 등을 작성하는 개발일기입니다. 다른 사람에게도 설명해줄 수 있도록 제 머릿속에 넣기 위해 정리를 시작하게 되었습니다.

 

useHistory

저번에는 history를 사용하기 위해 컴포넌트를 withRouter로 감싸주고 history 객체에 접근했지만, useHistory를 사용해서도 history 객체에 접근할 수 있다. 

 

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

const Main = () => {
    const history = useHistory(); 

    return (
        <Center>
            <Button onClick={() => history.push('/chat')} style={{ marginBottom: '2rem' }}>잡담해요</Button>
            <Button onClick={() => history.push('/need')} style={{ marginBottom: '2rem' }}>필요해요</Button>
            <Button onClick={() => history.push('/chatwithdev')}>개발자와 소통해요</Button>
        </Center>
    );
};

export default Main;

 

728x90
728x90

*하루동안 새롭게 알게된 부분, 에러를 해결한 방법 등을 작성하는 개발일기입니다. 다른 사람에게도 설명해줄 수 있도록 제 머릿속에 넣기 위해 정리를 시작하게 되었습니다. 

 history.push()와 history.replace()의 차이점

라우팅 변경을 위해 가장 빈번히 사용되는 메소드 중 history.push()history.replac()의 차이점은 무엇일까? 

 

Home > A > B > C 순으로 페이지 이동을 할 때,

 

B에서 history.push()를 사용하면 

 

import React from 'react';

function B({ history }) {
  history.push('/C');
  return <div>B</div>;
}

export default B;

 

Home > A > B > C 순으로 history에 쌓여, 마지막 페이지에서 뒤로가기 버튼을 누르면 B 페이지로 되돌아간다. 

 

똑같이 Home > A > B > C 순으로 페이지 이동을 하는데, 

 

B에서 history.replace()를 사용하면 

 

import React from 'react';

function B({ history }) {
  history.replace('/C');
  return <div>B</div>;
}

export default B;

 

Home > A > C 순으로 history에 쌓여, 마지막 페이지에서 뒤로 가기 버튼을 누르면, A 페이지로 되돌아간다. 

 

즉, history를 스택이라고 생각했을 때, push는 쌓여 있는 history 위에 쌓는 것이고, replace쌓여 있는 history 제일 위의 원소와 현재 넣는 원소를 말 그대로, replace(대체)하는 것이다. 

728x90
728x90

*하루동안 새롭게 알게된 부분, 에러를 해결한 방법 등을 작성하는 개발일기입니다. 다른 사람에게도 설명해줄 수 있도록 제 머릿속에 넣기 위해 정리를 시작하게 되었습니다. 

withRouter는 언제 사용할까?

 

커뮤니티 게시판에서 회원가입 폼에 정보를 입력하고, 회원가입하기 버튼을 눌렀다면 메인 페이지로 넘어가게 된다. 이렇게 리액트에서 페이지를 이동할 수 있는 것은 'react-router-dom'을 이용해 페이지의 기록을 알 수 있기 때문이다. 

 

Router컴포넌트의 path와 라우팅할 컴포넌트를 정해줄 수 있는데, 해당하는 Router는 props를 통해 history 객체를 전달 받게 된다.

 

<Router>
            <Switch>
                <Route exact path="/" component={Auth(LogOutMain, false)} />
                <Route exact path="/login" component={Auth(LogInPage, false)} />
                <Route exact path="/signup" component={Auth(SignUpPage, false)} />
                <Route exact path="/main" component={Auth(Main, true)} />
                <Route path="/*">404 Not Found</Route>
            </Switch>
</Router>

 

history 객체goBack(), goFoward() 등 앞/뒤로 이동할 수 있는 메소드 등 다양한 메소드와 관련 객체가 있다. 

 

그 중 라우팅 변경을 위해 가장 빈번히 사용되는 메소드가 바로 push이다. 아래의 코드처럼 이동하고자 하는 path를 history.push 안에 넣어주면, 원하는 경로(컴포넌트)로 이동 가능하다. 

 

history.push('/main');

 

그래서 나도 '회원가입하기' 버튼을 눌렀을 때 메인페이지로 이동하게 하고자 history.push를 사용했으나 꿈쩍도 하지 않는 것이다. 

 

const onSubmitForm = useCallback(() => {
        dispatch(registerUser({ role, phone, nickname }));
        props.history.push('/main');
}, [role, phone, nickname]);

 

 

이유를 찾아보니, '라우터가 아닌' 컴포넌트에서 history(나 location, match)와 같은 라우터에서 사용하는 객체를 쓰려면 withRouter라는 HOC을 사용해야 한다. 'react-router-dom'에서 'withRouter'를 import하고, export 하는 컴포넌트를 withRouter로 감싸주면 된다. HOC은 어제도 배웠지만, 컴포넌트를 인자로 받아서 새로운 컴포넌트를 리턴하는 함수이다. 

 

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

const SignUpPage = (props) => {
	// ,,,,

    const onSubmitForm = useCallback(() => {
        dispatch(registerUser({ role, phone, nickname }));
        props.history.push('/main');
    }, [role, phone, nickname]);

    return (
        <Center>
            <form onSubmit={onSubmitForm} aria-label="회원가입 폼입니다.">
                // ,,,,,
                <Button type="submit">회원가입하기</Button>
            </form>
        </Center>
    );
};

export default withRouter(SignUpPage);
728x90
728x90

*하루동안 새롭게 알게된 부분, 에러를 해결한 방법 등을 작성하는 개발일기입니다. 다른 사람에게도 설명해줄 수 있도록 제 머릿속에 넣기 위해 정리를 시작하게 되었습니다. 

react redux axios post 

회원가입 폼 제출 시, 해당 user가 넘긴 정보(nickname, phone, role)가 dispatch되고 axios post를 하는 코드를 작성했다.

 

여기서 잠깐 redux 개념을 정리하고 가자면,,, 

redux데이터의 중앙 저장소이다. 각 컴포넌트에서 필요로 할때 꺼내 쓸 수 있는 장소라고 생각하면 된다. 

 

redux에서 data를 바꾸기 위해서action이 필요하고, action을 dispatch하면 중앙 저장소의 data가 변경된다. 그런데 action을 dispatch한다고 data가 자동으로 바뀌는 것은 아니고, reducer에서 어떻게 바꿀지를 구현해주어야 한다.

 

회원가입 폼을 제출하면, registerUser 액션을 dispatch 중앙저장소에 user data를 추가해준다.

 

// components > SignUpPage.js

const onSubmitForm = useCallback(
        async () => {
            const nickname = await makeNickname();
            await dispatch(registerUser({ role, phone, nickname }));
        },
        [role, phone],
);

 

registerUser 액션은 registerAction을 dispatch하고, axios post를 통해 해당 서버로 data를 넘겨준다.

 

// reducers > user.js

export const registerUser = async (data) => {
    return (dispatch) => {
        dispatch(registerAction(data));
        return axios.post('http://localhost:8080/users', data);
    };
};

export const registerAction = (data) => {
    return {
        type: REGISTER_USER,
        data,
    };
};

 

JavaScript insertion sort(삽입 정렬)

삽입 정렬은 array의 두번째 인덱스에서 시작해서 마지막 인덱스가 될때까지, 앞의 요소들과 비교해 작으면 삽입하는 정렬 방식이다. 

 

function solution(arr){
  const answer = arr;

  for(let i = 1; i < arr.length;  i++){
  	
    // tmp는 비교대상이 될 임시적 변수 
    const tmp = arr[i]; 
    
    // j는 tmp의 앞 인덱스
    let j = i - 1
    
    for(; j >= 0; j--){
      if(tmp < arr[j]){
        arr[j+1] = arr[j]; 
      } 
      
      else{
        // tmp가 더 크면 더 이상 비교할 필요 없다. 앞의 요소들은 이미 정렬된 상태이므로
        break; 
      }
    }
    
    // j가 -1이 된 경우, 즉 모든 요소가 tmp보다 컸다면 arr[0]에 tmp에 들어가야 하고
    /*
    	중간에 break된 경우, 
        예를 들면 j가 2일때 break 되었다면, 
        arr[2]보다는 tmp가 크고, arr[4]보다는 tmp가 작다는 뜻이므로
        j가 3인 자리에 tmp가 들어가야 한다. 
    */
    // 즉, 둘 중 어떤 경우라도 항상 인덱스가 (j + 1)인 자리에 tmp가 들어가야 한다.
    arr[j+1] = tmp; 
  }

  return answer; 
}

let arr = [11, 7, 5, 6, 10, 9]; 
console.log(solution(arr));

 

JavaScript Array.slice()

slice()는 배열에서 특정 인덱스부터 특정 인덱스까지의 복사가 필요할 때 주로 쓰인다. 즉, 원본 배열은 건드리지 않는 메소드이다. 

 

arr.slice(begin, end) 형태로 쓰이며, 'begin'번째 인덱스부터 'end - 1'번째 인덱스까지 복사된다. 즉, end 인덱스는 미포함이다.    

 

const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];

console.log(animals.slice(2, 4));
// 결과: ["camel", "duck"]
// 2번째 인덱스부터 (4-1)번째 인덱스까지 복사, 즉, 4번째 인덱스는 미포함

// 배열 전체를 복사하고 싶을 때
console.log(animals.slice());

console.log(animals.slice(2));
// 결과: ["camel", "duck", "elephant"]
// 2번째 인덱스부터 끝 인덱스까지 복사됨

 

 

728x90
728x90

*하루동안 새롭게 알게된 부분, 에러를 해결한 방법 등을 작성하는 개발일기입니다. 다른 사람에게도 설명해줄 수 있도록 제 머릿속에 넣기 위해 정리를 시작하게 되었습니다. 

 

React에서 Form을 사용해서 구현할 때 다양한 input을 받아야 하는 경우, 관리할 state가 많아지면서 코드가 쉽게 복잡해질 수 있다. 

 

또, React에서는 Form 양식을 제출할 때, 성가신 것들이 몇 가지 있다. 

 

  • Form 상태에서 값 가져오기
  • 유효성 검사 및 오류 메세지
  • Form submit 핸들링 

Formik라는 라이브러리를 사용하면 위의 세 가지 요소를 한 곳에 배치할 수 있어 편리하다고 한다. Formik는 Provider를 통해 상태와 메소드를 ui에 공유한다. 이에 따라 form 내에 있는 모든 컴포넌트들이 동일한 상태를 공유할 수 있게 된다.  

 

위와 같은 장점들이 있어, 이번 프로젝트에서 Formik를 사용해볼까 했었는데, 내가 하는 프로젝트는 '시각 장애인들을 위한 커뮤니티'이다. 회원가입 시 번거로움을 덜어 드리고 싶어서, 전화번호만 입력을 받을 예정이라 그리 많은 input이 필요하지는 않아 그냥 React의 Form을 사용하기로 했다. 

 

오늘은 만약 사용자가 중복된 전화번호를 입력했을 경우, 폼이 submit되지 않고 경고창이 뜨고, 전화번호 input이 reset되는 것을 구현했다.

 

 

이 때, 만약 중복된 전화번호를 입력했다면 '회원가입하기'를 눌렀을 때 아래와 같이 경고창이 뜨고

 

 

전화번호 input창이 reset되게 말이다. 그대로 둘 수도 있겠지만, 그러면 시각장애인들이 다시 input을 지워야 하니 번거로울 것 같아서 그냥 아예 reset을 했다. 

 

 

중복확인을 하더라도 submit되어 data가 넘어가면 어쩌지 했는데, 만약 registered된 사용자라면, 'alert'문을 return하면 경고창만 띄워지고, data가 submit되지는 않았다. 

 

const onSubmitForm = useCallback(
        async (e) => {

            // 전화번호 중복 확인
            // if(){ //중복이라면
            //     setRegistered(true);
            // }

            // 중복이라면 경고 메세지 띄우고,
            if (registered) {
                setPhone('');
                return alert('이미 등록된 사용자입니다. 전화번호를 다시 입력해주십시오.');
            }
            const nickname = await makeNickname();
            await dispatch(registerAction({ role, phone, nickname }));
        },
        [role, phone],
);
728x90

+ Recent posts