스무디 한 잔 마시며 끝내는 리액트 + TDD (10)

y_cat·2022년 12월 18일
0

클릭 이벤트 연동

HTML에서 다음과 같이 클릭 이벤트를 연결하여 사용할 수 있다.

<div onclick="alert('Hello');">Hello world!</div>

React에서는 이와 비슷한 방식으로 컴포넌트에 이벤트를 연결한다. Button 컴포넌트에서 클릭 이벤트를 연결해보자.


./src/Component/Button/index.tsx 파일을 열어서 다음과 같이 수정한다.

export const Button = ({ 
  label,
  backgroundColor = '#304FFE',
  hoverColor = '#1E40FF'
 }: Props) => {
  return (
    <Container backgroundColor={backgroundColor} hoverColor={hoverColor} 
      onClick={() => alert('test')}>
      <Label>{label}</Label>
    </Container>
  );
}

React에서의 클릭 이벤트를 지정하고 싶을 때는 onclick -> onClick으로 사용하면 된다.


그리고 해당 index 페이지에서 확인해보면 버튼을 누르게 되면 test라는 메시지의 경고창이 나오게 된다.

Button 컴포넌트는 공통 컴포넌트로써 React 애플리케이션의 여러 곳에서 사용된다. 그러므로 클릭 이벤트의 역할이 사용되는 곳마다 다를 수도 있어서 이 역할을 부모 컴포넌트로부터 전달받을 필요가 있다.

클릭 이벤트의 역할을 하는 함수를 부모 컴포넌트로부터 전달받기 위해 Button 컴포넌트의 typescript 인터페이스를 다음과 같이 수정한다.

// in ./src/Component/Button/index.tsx
interface Props {
  readonly label: string;
  readonly backgroundColor?: string;
  readonly hoverColor?: string;
  readonly onClick?: () => void;
}

지금까지는 Props를 통해 단순히 문자열 데이터를 전달받았지만, 이번에는 위와 같이 함수를 전달받도록 하였다. 수정한 typescript 인터페이스를 살펴보면 전달받을 함수는 반환값이 없는 void 타입의 함수이며 필수로 넘겨줄 필요가 없는 Props임을 알 수 있다.


이렇게 선언한 데이터 타입을 사용하여 부모 컴포넌트로부터 전달받는 데이터를 다음과 같이 js의 구조 분할 할당을 통해 할당받는다.

export const Button = ({ 
  label,
  backgroundColor = '#304FFE',
  hoverColor = '#1E40FF',
  onClick
 }: Props) => {
  return (
    <Container backgroundColor={backgroundColor} hoverColor={hoverColor}
      onClick={onClick}>
      <Label>{label}</Label>
    </Container>
  );
}

위와 같이 수정하고 다시 앱에서 버튼을 클릭해보면 아무 반응이 없다...
아직 부모 컴포넌트에서 자식 컴포넌트로 onClick 함수를 전달하지 않았기 때문이다.

그렇다면 부모인 App 컴포넌트를 수정하여 자식 컴포넌트인 Button 컴포넌트에 onClick 함수를 전달해보도록 하자.

// in ./src/Component/App.tsx
function App() {
  return (
    <Container>
      <Contents>
        <Button label="삭제" backgroundColor='#FF1744' hoverColor='#F01440'
          onClick={() => alert('삭제!')}/>
      </Contents>
    </Container>
  );
}



지금까지 Button 컴포넌트를 만들어 보면서 부모인 App 컴포넌트로부터 데이터를 전달받기 위해 Props를 사용하는 방법을 연습했다. 우선 자식 컴포넌트에서 typescript의 인터페이스를 정의하고, 이 인터페이스에 전달받을 데이터 타입을 지정한 후, js 분해 할당을 사용하여 함수의 매개변수로 할당한 다음, 사용하고자 하는 곳에서 변수를 사용하면 된다는 것을 알게 되었다.

또한, typescript의 인터페이스에서 물음표 기호(?)를 통해 필수/필수가 아닌 매개변수를 어떻게 구분하는지, 함수를 전달받기 위해서는 타입을 어떤 방식으로 선언해야 한다는 것을 배웠다.

마지막으로, js의 구조 분할 할당에 초기값을 설정하는 방법과 styled-components에서 동적 매개변수를 사용하여 부모 컴포넌트부터 전달받은 값을 설정하는 방법에 대해서도 알아보았다.



Input 컴포넌트

이제 할 일 목록 앱에서 사용자로부터 할 일을 입력받는 Input 컴포넌트를 만들어 본다.
앞서 Button 컴포넌트를 만들 때와 같은 방식으로 제작한다.

우선 ./src/Component/App.tsx를 다음과 같이 수정한다.

const InputContainer = styled.div`
  display: flex;
`;

const Input = styled.input`
  font-size: 16px;
  padding: 10px 10px;
  border-radius: 8px;
  border: 1px solid #BDBDBD;
  outline: none;
`;

function App() {
  return (
    <Container>
      <Contents>
        <InputContainer>
          <Input placeholder="할 일을 입력해 주세요" />
          <Button label="추가" onClick={() => alert('추가!')}/>
        </InputContainer>
      </Contents>
    </Container>
  );
}


위와 같이 디자인한 Input 컴포넌트도 재사용 가능한 컴포넌트로 만들기 위해 ./src/Component/Input/index.tsx 파일을 만들고 다음과 같이 수정한다.

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

const InputBox = styled.input`
	flex: 1;
	font-size: 16px;
	padding: 10px 10px;
	border-radius: 8px;
	border: 1px solid #BDBDBD;
	outline: none;
`;

export const Input = () => {
	return (
		<InputBox placeholder='할 일을 입력해 주세요' />
	)
}

위와 같이 수정했으면 컴포넌트를 추가할 때 좀 더 편하게 추가하려고 만들었던 ./src/Component/index.tsx 파일을 열어 다음과 같이 수정한다.

export * from './Button';
export * from './Input';

이를 통해 Input 컴포넌트도 Button 컴포넌트와 마찬가지로 import를 사용할 때 Component 경로를 통해 추가할 수 있게 되었다.


마지막으로, 이렇게 만든 Input 컴포넌트를 화면에 표시하기 위해 ./src/App.tsx 파일을 다음과 같이 수정한다.

import React from 'react';
import styled from 'styled-components';
import { Button, Input } from 'Component';

...

const InputContainer = styled.div`
  display: flex;
`;

function App() {
  return (
    <Container>
      <Contents>
        <InputContainer>
          <Input />
          <Button label="추가" onClick={() => alert('추가!')}/>
        </InputContainer>
      </Contents>
    </Container>
  );
}

export default App;

이제 Input 컴포넌트의 재사용성을 향상시키기 위해 placeholder의 내용을 Props를 사용하여 부모 컴포넌트로부터 전달받은 데이터를 표시하도록 수정해보자.


placeholder의 내용을 부모 컴포넌트로부터 전달받기 위해 ./src/Component/Input/index.tsx 파일을 열어 다음과 같이 수정한다.


...

interface Props {
	readonly placeholder?: string;
}

export const Input = ({placeholder}: Props) => {
	return (
		<InputBox placeholder={placeholder} />
	)
}

부모 컴포넌트인 App 컴포넌트에서 자식 컴포넌트인 Input 컴포넌트에 placeholder라는 Props를 통해 데이터를 전달하기 위해 ./src/App.tsx 파일을 열어 다음과 같이 수정한다.

function App() {
  return (
    <Container>
      <Contents>
        <InputContainer>
          <Input placeholder='할 일을 입력해 주세요'/>
          <Button label="추가" onClick={() => alert('추가!')}/>
        </InputContainer>
      </Contents>
    </Container>
  );
}



onChange 이벤트 연동

이제 사용자가 입력하는 데이터를 js에서 사용하는 방법에 대해서 살펴본다.
React는 단방향 데이터 바인딩을 사용하기 떄문에 사용자가 입력하는 데이터를 js 변수에 할당하기 위해서는 이벤트를 활용해야 한다.
./src/Component/Input/index.tsx 파일을 열어 다음과 같이 이벤트를 추가하도록 한다.


...

export const Input = ({placeholder}: Props) => {
	return (
		<InputBox 
			placeholder={placeholder} 
			onChange={(event) => console.log(event.target.value)}/>
	)
}

HTML의 onChange 이벤트 함수를 통해 사용자의 입력 데이터를 전달받아 js의 console.log를 사용하여 전달받은 데이터를 표시하도록 했다.

Chrome 웹브라우저 개발자 도구 - 콘솔을 누르면 입력했던 데이터가 표시되는 것을 확인할 수 있다.


Input 컴포넌트는 재사용이 가능한 공통 컴포넌트이므로 사용자로부터 입력받은 데이터를 필요한 곳에서 사용할 수 있도록 만들어야 한다. Input 컴포넌트를 다음과 같이 수정하여 부모 컴포넌트에서도 사용자가 입력한 데이터를 활용할 수 있도록 만들자.

// in ./src/Component/Input/index.tsx

...

interface Props {
	readonly placeholder?: string;
	readonly onChange?: (text: string) => void;
}

export const Input = ({placeholder, onChange}: Props) => {
	return (
		<InputBox 
			placeholder={placeholder} 
			onChange={(event) => {
				if(typeof onChange === 'function') {
					onChange(event.target.value);
				}
			}}/>
	)
}

App 컴포넌트에서도 수정한다.

function App() {
  return (
    <Container>
      <Contents>
        <InputContainer>
          <Input placeholder='할 일을 입력해 주세요' onChange={(text) => console.log(text)}/>
          <Button label="추가" onClick={() => alert('추가!')}/>
        </InputContainer>
      </Contents>
    </Container>
  );
}

이로써 Input 컴포넌트를 재사용 가능한 컴포넌트로 만들었으며, 사용자로부터 입력받은 데이터를 활용할 수 있는 준비를 했다.



ToDoItem 컴포넌트

사용자가 등록한 하나의 할 일을 표시하기 위한 ToDoItem 컴포넌트를 만들어본다.

./src/App.tsx 파일을 열어 다음과 같이 수정한다.


...

const ToDoItem = styled.div`
  display: flex;
  border-bottom: 1px solid #BDBDBD;
  align-items: center;
  margin: 10px;
  padding: 10px;
`

const Label = styled.div`
  flex: 1;
  font-size: 16px;
  margin-right: 20px;
`

function App() {
  return (
    <Container>
      <Contents>
        <ToDoItem>
          <Label>추가된 할 일</Label>
          <Button
            label="삭제"
            backgroundColor='#FF1744'
            hoverColor='#F01440'
            onClick={() => alert('삭제')} />
        </ToDoItem>
        <InputContainer>
          <Input placeholder='할 일을 입력해 주세요' onChange={(text) => console.log(text)}/>
          <Button label="추가" onClick={() => alert('추가!')}/>
        </InputContainer>
      </Contents>
    </Container>
  );
}

이렇게 만든 컴포넌트를 재사용 가능한 컴포넌트로 만들어보자. ./src/Component/ToDoItem/index.tsx 파일을 만들어서 다음과 같이 작성한다.
import React from 'react';
import styled from 'styled-components';

import { Button } from 'Component/Button';

const Container = styled.div`
	display: flex;
	border-bottom: ipx solid #BDBDBD;
	align-items: center;
	margin: 10px;
	padding: 10px;
`

const Label = styled.div`
	flex: 1;
	font-size: 16px;
	margin-right: 20px;
`

export const ToDoItem = () => {
	return (
		<Container>
			<Label>추가된 할 일</Label>
			<Button
				label='삭제'
				backgroundColor='#FF1744'
				hoverColor='#F01440'
				onClick={() => alert('삭제')} />
		</Container>
	)
}

./src/Component/index.tsx 파일을 열어 다음과 같이 수정한다.
export * from './Button';
export * from './Input';
export * from './ToDoItem';

마지막으로 이렇게 공통 컴포넌트로 만든 ToDoItem 컴포넌트를 부모 컴포넌트인 App 컴포넌트에서 사용하도록 수정해보자. 부모 컴포넌트인 App 컴포넌트에서 공통 컴포넌트로 제작한 ToDoItem 컴포넌트를 사용하기 위해 ./src/App.tsx 파일을 열어 다음과 같이 수정한다.


...

import { Button, Input, ToDoItem } from 'Component';

...

const InputContainer = styled.div`
  display: flex;
`;

function App() {
  return (
    <Container>
      <Contents>
        <ToDoItem />
        <InputContainer>
          <Input placeholder='할 일을 입력해 주세요' onChange={(text) => console.log(text)}/>
          <Button label="추가" onClick={() => alert('추가!')}/>
        </InputContainer>
      </Contents>
    </Container>
  );
}

export default App;

이제 공통 컴포넌트로 만든 ToDoItem 컴포넌트를 부모 컴포넌트로부터 데이터를 받을 수 있도록 수정해보자.
./src/Component/ToDoItem/index.tsx 파일을 열어 다음과 같이 수정한다.


...

interface Props {
	readonly label: string;
	readonly onDelete?: () => void;
}

export const ToDoItem = ({label, onDelete}: Props) => {
	return (
		<Container>
			<Label>{label}</Label>
			<Button
				label='삭제'
				backgroundColor='#FF1744'
				hoverColor='#F01440'
				onClick={onDelete} />
		</Container>
	)
}

마지막으로 App 컴포넌트를 수정한다.

...

function App() {
  return (
    <Container>
      <Contents>
        <ToDoItem label='추가된 할 일' onDelete={() => alert('삭제')}/>
        <InputContainer>
          <Input placeholder='할 일을 입력해 주세요' onChange={(text) => console.log(text)}/>
          <Button label="추가" onClick={() => alert('추가!')}/>
        </InputContainer>
      </Contents>
    </Container>
  );
}

위의 이미지와 같이 웹브라우저에서 ToDoItem 컴포넌트가 잘 표시되는 것을 확인할 수 있다.



Github Repo

profile
토이 프로젝트와 기술들 정리하는 블로그

0개의 댓글