Ecoer Logo
VOTING POWER100.00%
DOWNVOTE POWER100.00%
RESOURCE CREDITS100.00%
REPUTATION PROGRESS37.96%
Net Worth
1.011USD
STEEM
0.029STEEM
SBD
1.927SBD
Effective Power
5.001SP
├── Own SP
1.059SP
└── Incoming Deleg
+3.941SP

Detailed Balance

STEEM
balance
0.029STEEM
market_balance
0.000STEEM
savings_balance
0.000STEEM
reward_steem_balance
0.000STEEM
STEEM POWER
Own SP
1.059SP
Delegated Out
0.000SP
Delegation In
3.941SP
Effective Power
5.001SP
Reward SP (pending)
0.000SP
SBD
sbd_balance
1.927SBD
sbd_conversions
0.000SBD
sbd_market_balance
0.000SBD
savings_sbd_balance
0.000SBD
reward_sbd_balance
0.000SBD
{
  "balance": "0.029 STEEM",
  "savings_balance": "0.000 STEEM",
  "reward_steem_balance": "0.000 STEEM",
  "vesting_shares": "1725.310526 VESTS",
  "delegated_vesting_shares": "0.000000 VESTS",
  "received_vesting_shares": "6418.349280 VESTS",
  "sbd_balance": "1.927 SBD",
  "savings_sbd_balance": "0.000 SBD",
  "reward_sbd_balance": "0.000 SBD",
  "conversions": []
}

Account Info

nameecho304
id840337
rank291,601
reputation18382173607
created2018-03-12T23:25:00
recovery_accountsteem
proxyNone
post_count3
comment_count0
lifetime_vote_count0
witnesses_voted_for0
last_post2018-05-25T14:36:54
last_root_post2018-05-25T14:36:54
last_vote_time2018-05-25T14:40:12
proxied_vsf_votes0, 0, 0, 0
can_vote1
voting_power0
delayed_votes0
balance0.029 STEEM
savings_balance0.000 STEEM
sbd_balance1.927 SBD
savings_sbd_balance0.000 SBD
vesting_shares1725.310526 VESTS
delegated_vesting_shares0.000000 VESTS
received_vesting_shares6418.349280 VESTS
reward_vesting_balance0.000000 VESTS
vesting_balance0.000 STEEM
vesting_withdraw_rate0.000000 VESTS
next_vesting_withdrawal1969-12-31T23:59:59
withdrawn0
to_withdraw0
withdraw_routes0
savings_withdraw_requests0
last_account_recovery1970-01-01T00:00:00
reset_accountnull
last_owner_update1970-01-01T00:00:00
last_account_update1970-01-01T00:00:00
minedNo
sbd_seconds0
sbd_last_interest_payment2018-07-24T08:28:27
savings_sbd_last_interest_payment1970-01-01T00:00:00
{
  "active": {
    "account_auths": [],
    "key_auths": [
      [
        "STM7WE2RE7yynfMLAAjPBXhXk8kRjyYs8kKiB12G2QT38CEBfsret",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "balance": "0.029 STEEM",
  "can_vote": true,
  "comment_count": 0,
  "created": "2018-03-12T23:25:00",
  "curation_rewards": 0,
  "delegated_vesting_shares": "0.000000 VESTS",
  "downvote_manabar": {
    "current_mana": 2035914951,
    "last_update_time": 1779061695
  },
  "guest_bloggers": [],
  "id": 840337,
  "json_metadata": "{}",
  "last_account_recovery": "1970-01-01T00:00:00",
  "last_account_update": "1970-01-01T00:00:00",
  "last_owner_update": "1970-01-01T00:00:00",
  "last_post": "2018-05-25T14:36:54",
  "last_root_post": "2018-05-25T14:36:54",
  "last_vote_time": "2018-05-25T14:40:12",
  "lifetime_vote_count": 0,
  "market_history": [],
  "memo_key": "STM6g1WjD4Bh1xXuRTuFy6zBEuxBHZB1HSkbWXYxSFcKnxuG6He5r",
  "mined": false,
  "name": "echo304",
  "next_vesting_withdrawal": "1969-12-31T23:59:59",
  "other_history": [],
  "owner": {
    "account_auths": [],
    "key_auths": [
      [
        "STM7heJSTjyiq1ERUuSP1XoWTP6UrHMdpcuVsMwTuHvJ1FzvwPzwd",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "pending_claimed_accounts": 0,
  "post_bandwidth": 0,
  "post_count": 3,
  "post_history": [],
  "posting": {
    "account_auths": [],
    "key_auths": [
      [
        "STM5nVW7yycrpZjzy63rgo5Cp1bjjewuWbBwb7fTvKUfnQG7R9V1A",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "posting_json_metadata": "",
  "posting_rewards": 1495,
  "proxied_vsf_votes": [
    0,
    0,
    0,
    0
  ],
  "proxy": "",
  "received_vesting_shares": "6418.349280 VESTS",
  "recovery_account": "steem",
  "reputation": "18382173607",
  "reset_account": "null",
  "reward_sbd_balance": "0.000 SBD",
  "reward_steem_balance": "0.000 STEEM",
  "reward_vesting_balance": "0.000000 VESTS",
  "reward_vesting_steem": "0.000 STEEM",
  "savings_balance": "0.000 STEEM",
  "savings_sbd_balance": "0.000 SBD",
  "savings_sbd_last_interest_payment": "1970-01-01T00:00:00",
  "savings_sbd_seconds": "0",
  "savings_sbd_seconds_last_update": "1970-01-01T00:00:00",
  "savings_withdraw_requests": 0,
  "sbd_balance": "1.927 SBD",
  "sbd_last_interest_payment": "2018-07-24T08:28:27",
  "sbd_seconds": "0",
  "sbd_seconds_last_update": "2018-07-24T08:28:27",
  "tags_usage": [],
  "to_withdraw": 0,
  "transfer_history": [],
  "vesting_balance": "0.000 STEEM",
  "vesting_shares": "1725.310526 VESTS",
  "vesting_withdraw_rate": "0.000000 VESTS",
  "vote_history": [],
  "voting_manabar": {
    "current_mana": "8143659806",
    "last_update_time": 1779061695
  },
  "voting_power": 0,
  "withdraw_routes": 0,
  "withdrawn": 0,
  "witness_votes": [],
  "witnesses_voted_for": 0,
  "rank": 291601
}

Withdraw Routes

IncomingOutgoing
Empty
Empty
{
  "incoming": [],
  "outgoing": []
}
From Date
To Date
steemdelegated 3.941 SP to @echo304
2026/05/17 23:48:15
delegateeecho304
delegatorsteem
vesting shares6418.349280 VESTS
Transaction InfoBlock #106142912/Trx 95ee8ae42942523a6563ed5eb9e18de4aaa4032c
View Raw JSON Data
{
  "block": 106142912,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "6418.349280 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-05-17T23:48:15",
  "trx_id": "95ee8ae42942523a6563ed5eb9e18de4aaa4032c",
  "trx_in_block": 0,
  "virtual_op": 0
}
steemdelegated 2.276 SP to @echo304
2026/05/12 02:00:42
delegateeecho304
delegatorsteem
vesting shares3706.138875 VESTS
Transaction InfoBlock #105973517/Trx 022db3acbdb2e8a8ae9f0c0cb1e9a695bb5921c3
View Raw JSON Data
{
  "block": 105973517,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "3706.138875 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-05-12T02:00:42",
  "trx_id": "022db3acbdb2e8a8ae9f0c0cb1e9a695bb5921c3",
  "trx_in_block": 0,
  "virtual_op": 0
}
steemdelegated 3.949 SP to @echo304
2026/04/25 23:09:54
delegateeecho304
delegatorsteem
vesting shares6430.865036 VESTS
Transaction InfoBlock #105510576/Trx df6f2c3dd98d3809b464f13b5283745cd08b70d6
View Raw JSON Data
{
  "block": 105510576,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "6430.865036 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-04-25T23:09:54",
  "trx_id": "df6f2c3dd98d3809b464f13b5283745cd08b70d6",
  "trx_in_block": 1,
  "virtual_op": 0
}
steemdelegated 2.301 SP to @echo304
2026/01/23 06:35:03
delegateeecho304
delegatorsteem
vesting shares3747.685694 VESTS
Transaction InfoBlock #102850350/Trx ec82ad59715177027ec8849f9fe1d9d5c874e079
View Raw JSON Data
{
  "block": 102850350,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "3747.685694 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-01-23T06:35:03",
  "trx_id": "ec82ad59715177027ec8849f9fe1d9d5c874e079",
  "trx_in_block": 3,
  "virtual_op": 0
}
steemdelegated 2.402 SP to @echo304
2024/12/17 01:54:30
delegateeecho304
delegatorsteem
vesting shares3911.904891 VESTS
Transaction InfoBlock #91296768/Trx 982bf7fa641545bc71f81414fc25c228ca812be9
View Raw JSON Data
{
  "block": 91296768,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "3911.904891 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2024-12-17T01:54:30",
  "trx_id": "982bf7fa641545bc71f81414fc25c228ca812be9",
  "trx_in_block": 0,
  "virtual_op": 0
}
steemdelegated 2.506 SP to @echo304
2023/11/13 17:37:21
delegateeecho304
delegatorsteem
vesting shares4081.038423 VESTS
Transaction InfoBlock #79850972/Trx e5cb1dcb38322aa77dd66607c3c0eee64f06e1b1
View Raw JSON Data
{
  "block": 79850972,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "4081.038423 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2023-11-13T17:37:21",
  "trx_id": "e5cb1dcb38322aa77dd66607c3c0eee64f06e1b1",
  "trx_in_block": 1,
  "virtual_op": 0
}
steemdelegated 4.310 SP to @echo304
2023/09/21 21:17:42
delegateeecho304
delegatorsteem
vesting shares7018.317209 VESTS
Transaction InfoBlock #78347187/Trx a08174d3ccd13d8947fa299815d0a1cafd36289d
View Raw JSON Data
{
  "block": 78347187,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "7018.317209 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2023-09-21T21:17:42",
  "trx_id": "a08174d3ccd13d8947fa299815d0a1cafd36289d",
  "trx_in_block": 3,
  "virtual_op": 0
}
steemdelegated 4.446 SP to @echo304
2022/11/03 11:09:42
delegateeecho304
delegatorsteem
vesting shares7239.998647 VESTS
Transaction InfoBlock #69112615/Trx 86a7d8e9492c2cce9cda0fe3e0145a3114841dc6
View Raw JSON Data
{
  "block": 69112615,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "7239.998647 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2022-11-03T11:09:42",
  "trx_id": "86a7d8e9492c2cce9cda0fe3e0145a3114841dc6",
  "trx_in_block": 10,
  "virtual_op": 0
}
steemdelegated 4.581 SP to @echo304
2022/01/17 10:28:15
delegateeecho304
delegatorsteem
vesting shares7460.531878 VESTS
Transaction InfoBlock #60808833/Trx e591af60137f76efa7461af2b210badc4ed104c8
View Raw JSON Data
{
  "block": 60808833,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "7460.531878 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2022-01-17T10:28:15",
  "trx_id": "e591af60137f76efa7461af2b210badc4ed104c8",
  "trx_in_block": 149,
  "virtual_op": 0
}
steemdelegated 4.694 SP to @echo304
2021/06/14 00:24:42
delegateeecho304
delegatorsteem
vesting shares7644.300536 VESTS
Transaction InfoBlock #54607245/Trx bf56e20dfab1ff37367726370428a7f17c544bc2
View Raw JSON Data
{
  "block": 54607245,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "7644.300536 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2021-06-14T00:24:42",
  "trx_id": "bf56e20dfab1ff37367726370428a7f17c544bc2",
  "trx_in_block": 3,
  "virtual_op": 0
}
steemdelegated 4.809 SP to @echo304
2020/12/11 10:44:30
delegateeecho304
delegatorsteem
vesting shares7831.722510 VESTS
Transaction InfoBlock #49354728/Trx d92fb959045d817f9b7990e81ec397d921896faf
View Raw JSON Data
{
  "block": 49354728,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "7831.722510 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-12-11T10:44:30",
  "trx_id": "d92fb959045d817f9b7990e81ec397d921896faf",
  "trx_in_block": 0,
  "virtual_op": 0
}
steemdelegated 1.174 SP to @echo304
2020/12/06 04:21:48
delegateeecho304
delegatorsteem
vesting shares1912.543513 VESTS
Transaction InfoBlock #49206295/Trx a42deeeab5130701e20afc622b9b36ea60eec5ab
View Raw JSON Data
{
  "block": 49206295,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "1912.543513 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-12-06T04:21:48",
  "trx_id": "a42deeeab5130701e20afc622b9b36ea60eec5ab",
  "trx_in_block": 0,
  "virtual_op": 0
}
steemdelegated 4.813 SP to @echo304
2020/12/05 14:22:42
delegateeecho304
delegatorsteem
vesting shares7837.930364 VESTS
Transaction InfoBlock #49189827/Trx 2591b7ba3b41bbbc9ed06cc85d83c7f448d0cbe0
View Raw JSON Data
{
  "block": 49189827,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "7837.930364 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-12-05T14:22:42",
  "trx_id": "2591b7ba3b41bbbc9ed06cc85d83c7f448d0cbe0",
  "trx_in_block": 2,
  "virtual_op": 0
}
steemdelegated 1.179 SP to @echo304
2020/11/02 14:44:48
delegateeecho304
delegatorsteem
vesting shares1920.017158 VESTS
Transaction InfoBlock #48256747/Trx bf8846a409963fe17077005e7d20a94acc613566
View Raw JSON Data
{
  "block": 48256747,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "1920.017158 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-11-02T14:44:48",
  "trx_id": "bf8846a409963fe17077005e7d20a94acc613566",
  "trx_in_block": 1,
  "virtual_op": 0
}
steemdelegated 4.938 SP to @echo304
2020/05/09 05:18:27
delegateeecho304
delegatorsteem
vesting shares8040.735723 VESTS
Transaction InfoBlock #43216528/Trx 6be4a31bf99d1c9f8f4b5b14b3d9b9fccfb1d6c4
View Raw JSON Data
{
  "block": 43216528,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "8040.735723 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-05-09T05:18:27",
  "trx_id": "6be4a31bf99d1c9f8f4b5b14b3d9b9fccfb1d6c4",
  "trx_in_block": 20,
  "virtual_op": 0
}
steemdelegated 1.200 SP to @echo304
2020/05/08 08:50:36
delegateeecho304
delegatorsteem
vesting shares1953.311140 VESTS
Transaction InfoBlock #43192549/Trx 468133f74a73966960563f593de21e00b748f9db
View Raw JSON Data
{
  "block": 43192549,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "1953.311140 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-05-08T08:50:36",
  "trx_id": "468133f74a73966960563f593de21e00b748f9db",
  "trx_in_block": 9,
  "virtual_op": 0
}
2020/03/13 00:26:18
authorsteemitboard
bodyCongratulations @echo304! You received a personal award! <table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@echo304/birthday2.png</td><td>Happy Steem Birthday! - You are on the Steem blockchain for 2 years!</td></tr></table> <sub>_You can view [your badges on your Steem Board](https://steemitboard.com/@echo304) and compare to others on the [Steem Ranking](https://steemitboard.com/ranking/index.php?name=echo304)_</sub> **Do not miss the last post from @steemitboard:** <table><tr><td><a href="https://steemit.com/steemitboard/@steemitboard/downvote-challenge-add-up-to-3-funny-badges-to-your-board"><img src="https://steemitimages.com/64x128/https://steemitimages.com/0x0/![](https://cdn.steemitimages.com/DQmUuJkZdnSpHVWssxF82ntymqXg4Pvk6K6bYvckUYVRsnj/image.png)"></a></td><td><a href="https://steemit.com/steemitboard/@steemitboard/downvote-challenge-add-up-to-3-funny-badges-to-your-board">Downvote challenge - Add up to 3 funny badges to your board</a></td></tr></table> ###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) to get one more award and increased upvotes!
json metadata{"image":["https://steemitboard.com/img/notify.png"]}
parent authorecho304
parent permlinkredux-saga
permlinksteemitboard-notify-echo304-20200313t002617000z
title
Transaction InfoBlock #41601235/Trx d266309c81f6aaea12048392ced148879fb340f1
View Raw JSON Data
{
  "block": 41601235,
  "op": [
    "comment",
    {
      "author": "steemitboard",
      "body": "Congratulations @echo304! You received a personal award!\n\n<table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@echo304/birthday2.png</td><td>Happy Steem Birthday! - You are on the Steem blockchain for 2 years!</td></tr></table>\n\n<sub>_You can view [your badges on your Steem Board](https://steemitboard.com/@echo304) and compare to others on the [Steem Ranking](https://steemitboard.com/ranking/index.php?name=echo304)_</sub>\n\n\n**Do not miss the last post from @steemitboard:**\n<table><tr><td><a href=\"https://steemit.com/steemitboard/@steemitboard/downvote-challenge-add-up-to-3-funny-badges-to-your-board\"><img src=\"https://steemitimages.com/64x128/https://steemitimages.com/0x0/![](https://cdn.steemitimages.com/DQmUuJkZdnSpHVWssxF82ntymqXg4Pvk6K6bYvckUYVRsnj/image.png)\"></a></td><td><a href=\"https://steemit.com/steemitboard/@steemitboard/downvote-challenge-add-up-to-3-funny-badges-to-your-board\">Downvote challenge - Add up to 3 funny badges to your board</a></td></tr></table>\n\n###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) to get one more award and increased upvotes!",
      "json_metadata": "{\"image\":[\"https://steemitboard.com/img/notify.png\"]}",
      "parent_author": "echo304",
      "parent_permlink": "redux-saga",
      "permlink": "steemitboard-notify-echo304-20200313t002617000z",
      "title": ""
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2020-03-13T00:26:18",
  "trx_id": "d266309c81f6aaea12048392ced148879fb340f1",
  "trx_in_block": 5,
  "virtual_op": 0
}
steemdelegated 5.014 SP to @echo304
2019/10/01 06:43:09
delegateeecho304
delegatorsteem
vesting shares8165.506592 VESTS
Transaction InfoBlock #36895844/Trx 4bbcf839378e66707b853bc1e8d6cff9d5429bb9
View Raw JSON Data
{
  "block": 36895844,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "8165.506592 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2019-10-01T06:43:09",
  "trx_id": "4bbcf839378e66707b853bc1e8d6cff9d5429bb9",
  "trx_in_block": 16,
  "virtual_op": 0
}
2019/03/13 11:59:36
authorsteemitboard
bodyCongratulations @echo304! You received a personal award! <table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@echo304/birthday1.png</td><td>Happy Birthday! - You are on the Steem blockchain for 1 year!</td></tr></table> <sub>_You can view [your badges on your Steem Board](https://steemitboard.com/@echo304) and compare to others on the [Steem Ranking](http://steemitboard.com/ranking/index.php?name=echo304)_</sub> **Do not miss the last post from @steemitboard:** <table><tr><td><a href="https://steemit.com/drugwars/@steemitboard/drugwars-early-adopter"><img src="https://steemitimages.com/64x128/https://cdn.steemitimages.com/DQmYGN7R653u4hDFyq1hM7iuhr2bdAP1v2ApACDNtecJAZ5/image.png"></a></td><td><a href="https://steemit.com/drugwars/@steemitboard/drugwars-early-adopter">Are you a DrugWars early adopter? Benvenuto in famiglia!</a></td></tr></table> ###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) to get one more award and increased upvotes!
json metadata{"image":["https://steemitboard.com/img/notify.png"]}
parent authorecho304
parent permlinkredux-saga
permlinksteemitboard-notify-echo304-20190313t115935000z
title
Transaction InfoBlock #31116618/Trx eb8dbf441672643881caaa5b5f8d09e4256c0e70
View Raw JSON Data
{
  "block": 31116618,
  "op": [
    "comment",
    {
      "author": "steemitboard",
      "body": "Congratulations @echo304! You received a personal award!\n\n<table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@echo304/birthday1.png</td><td>Happy Birthday! - You are on the Steem blockchain for 1 year!</td></tr></table>\n\n<sub>_You can view [your badges on your Steem Board](https://steemitboard.com/@echo304) and compare to others on the [Steem Ranking](http://steemitboard.com/ranking/index.php?name=echo304)_</sub>\n\n\n**Do not miss the last post from @steemitboard:**\n<table><tr><td><a href=\"https://steemit.com/drugwars/@steemitboard/drugwars-early-adopter\"><img src=\"https://steemitimages.com/64x128/https://cdn.steemitimages.com/DQmYGN7R653u4hDFyq1hM7iuhr2bdAP1v2ApACDNtecJAZ5/image.png\"></a></td><td><a href=\"https://steemit.com/drugwars/@steemitboard/drugwars-early-adopter\">Are you a DrugWars early adopter? Benvenuto in famiglia!</a></td></tr></table>\n\n###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) to get one more award and increased upvotes!",
      "json_metadata": "{\"image\":[\"https://steemitboard.com/img/notify.png\"]}",
      "parent_author": "echo304",
      "parent_permlink": "redux-saga",
      "permlink": "steemitboard-notify-echo304-20190313t115935000z",
      "title": ""
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2019-03-13T11:59:36",
  "trx_id": "eb8dbf441672643881caaa5b5f8d09e4256c0e70",
  "trx_in_block": 6,
  "virtual_op": 0
}
steemdelegated 5.136 SP to @echo304
2018/10/23 09:18:24
delegateeecho304
delegatorsteem
vesting shares8363.355733 VESTS
Transaction InfoBlock #27055582/Trx 5aa642f8af26cc9d991757d25952e96cd5d87966
View Raw JSON Data
{
  "block": 27055582,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "8363.355733 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-10-23T09:18:24",
  "trx_id": "5aa642f8af26cc9d991757d25952e96cd5d87966",
  "trx_in_block": 8,
  "virtual_op": 0
}
steemdelegated 17.617 SP to @echo304
2018/07/24 08:48:12
delegateeecho304
delegatorsteem
vesting shares28688.041255 VESTS
Transaction InfoBlock #24451803/Trx 3dc0ade212eb077da60fea5a7da9c3aa2e3c80ff
View Raw JSON Data
{
  "block": 24451803,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "28688.041255 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-24T08:48:12",
  "trx_id": "3dc0ade212eb077da60fea5a7da9c3aa2e3c80ff",
  "trx_in_block": 55,
  "virtual_op": 0
}
echo304claimed reward balance: 0.027 STEEM, 0.785 SBD, 0.447 SP
2018/07/24 08:28:27
accountecho304
reward sbd0.785 SBD
reward steem0.027 STEEM
reward vests727.880189 VESTS
Transaction InfoBlock #24451408/Trx 7c6af3246c7d49313fd969640f1556c4ded7cdbb
View Raw JSON Data
{
  "block": 24451408,
  "op": [
    "claim_reward_balance",
    {
      "account": "echo304",
      "reward_sbd": "0.785 SBD",
      "reward_steem": "0.027 STEEM",
      "reward_vests": "727.880189 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-24T08:28:27",
  "trx_id": "7c6af3246c7d49313fd969640f1556c4ded7cdbb",
  "trx_in_block": 17,
  "virtual_op": 0
}
cyberdroidflagged (-100.00%) @echo304 / redux-saga
2018/07/23 10:01:03
authorecho304
permlinkredux-saga
votercyberdroid
weight-10000 (-100.00%)
Transaction InfoBlock #24424469/Trx 73d982726078df36af51609b0b1b0007b989fb7c
View Raw JSON Data
{
  "block": 24424469,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "redux-saga",
      "voter": "cyberdroid",
      "weight": -10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-23T10:01:03",
  "trx_id": "73d982726078df36af51609b0b1b0007b989fb7c",
  "trx_in_block": 15,
  "virtual_op": 0
}
steemdelegated 18.066 SP to @echo304
2018/07/21 20:13:18
delegateeecho304
delegatorsteem
vesting shares29419.987372 VESTS
Transaction InfoBlock #24379172/Trx 4d5ceb0bc3d8ef3d33f207cc1ea00a176e4c895d
View Raw JSON Data
{
  "block": 24379172,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "29419.987372 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-21T20:13:18",
  "trx_id": "4d5ceb0bc3d8ef3d33f207cc1ea00a176e4c895d",
  "trx_in_block": 40,
  "virtual_op": 0
}
jupitersaturnflagged (-100.00%) @echo304 / react
2018/07/15 19:54:06
authorecho304
permlinkreact
voterjupitersaturn
weight-10000 (-100.00%)
Transaction InfoBlock #24206109/Trx a3a434a1d2f45f2425181489be29ebd26884a4f2
View Raw JSON Data
{
  "block": 24206109,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "react",
      "voter": "jupitersaturn",
      "weight": -10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-15T19:54:06",
  "trx_id": "a3a434a1d2f45f2425181489be29ebd26884a4f2",
  "trx_in_block": 4,
  "virtual_op": 0
}
jupitersaturnflagged (-100.00%) @echo304 / typescript
2018/07/15 19:53:57
authorecho304
permlinktypescript
voterjupitersaturn
weight-10000 (-100.00%)
Transaction InfoBlock #24206106/Trx 1bab82f727015c6304d7e6b90afaf8fcf35bf2bf
View Raw JSON Data
{
  "block": 24206106,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "typescript",
      "voter": "jupitersaturn",
      "weight": -10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-15T19:53:57",
  "trx_id": "1bab82f727015c6304d7e6b90afaf8fcf35bf2bf",
  "trx_in_block": 20,
  "virtual_op": 0
}
jupitersaturnflagged (-100.00%) @echo304 / redux-saga
2018/07/15 19:53:48
authorecho304
permlinkredux-saga
voterjupitersaturn
weight-10000 (-100.00%)
Transaction InfoBlock #24206103/Trx 354a29c46ab15a15ae3489600b42fcfa1557ee35
View Raw JSON Data
{
  "block": 24206103,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "redux-saga",
      "voter": "jupitersaturn",
      "weight": -10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-07-15T19:53:48",
  "trx_id": "354a29c46ab15a15ae3489600b42fcfa1557ee35",
  "trx_in_block": 35,
  "virtual_op": 0
}
echo304received 0.027 STEEM, 0.785 SBD, 0.447 SP author reward for @echo304 / redux-saga
2018/06/01 14:36:54
authorecho304
permlinkredux-saga
sbd payout0.785 SBD
steem payout0.027 STEEM
vesting payout727.880189 VESTS
Transaction InfoBlock #22943383/Virtual Operation #14
View Raw JSON Data
{
  "block": 22943383,
  "op": [
    "author_reward",
    {
      "author": "echo304",
      "permlink": "redux-saga",
      "sbd_payout": "0.785 SBD",
      "steem_payout": "0.027 STEEM",
      "vesting_payout": "727.880189 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-01T14:36:54",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": 14
}
echo304claimed reward balance: 0.002 STEEM, 0.130 SBD, 0.067 SP
2018/06/01 13:03:33
accountecho304
reward sbd0.130 SBD
reward steem0.002 STEEM
reward vests109.809696 VESTS
Transaction InfoBlock #22941518/Trx 7a10eaff4a3ec69de756a036799c538d38ce7e11
View Raw JSON Data
{
  "block": 22941518,
  "op": [
    "claim_reward_balance",
    {
      "account": "echo304",
      "reward_sbd": "0.130 SBD",
      "reward_steem": "0.002 STEEM",
      "reward_vests": "109.809696 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-06-01T13:03:33",
  "trx_id": "7a10eaff4a3ec69de756a036799c538d38ce7e11",
  "trx_in_block": 59,
  "virtual_op": 0
}
echo304received 0.002 STEEM, 0.130 SBD, 0.067 SP author reward for @echo304 / typescript
2018/05/29 12:32:21
authorecho304
permlinktypescript
sbd payout0.130 SBD
steem payout0.002 STEEM
vesting payout109.809696 VESTS
Transaction InfoBlock #22854514/Virtual Operation #13
View Raw JSON Data
{
  "block": 22854514,
  "op": [
    "author_reward",
    {
      "author": "echo304",
      "permlink": "typescript",
      "sbd_payout": "0.130 SBD",
      "steem_payout": "0.002 STEEM",
      "vesting_payout": "109.809696 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-29T12:32:21",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": 13
}
heejinupvoted (100.00%) @echo304 / redux-saga
2018/05/28 12:36:42
authorecho304
permlinkredux-saga
voterheejin
weight10000 (100.00%)
Transaction InfoBlock #22825805/Trx 107615ee9cc24c953d11f23252d285db329c7750
View Raw JSON Data
{
  "block": 22825805,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "redux-saga",
      "voter": "heejin",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-28T12:36:42",
  "trx_id": "107615ee9cc24c953d11f23252d285db329c7750",
  "trx_in_block": 3,
  "virtual_op": 0
}
big-whalesent 0.001 SBD to @echo304- "Hello Friend , Promote your new post with big-whale . Your post will be more popular and you will find new friends . big-whale provide "Resteem upvote and promo " services . Resteem to more 16,000+ Fo..."
2018/05/26 05:25:15
amount0.001 SBD
frombig-whale
memoHello Friend , Promote your new post with big-whale . Your post will be more popular and you will find new friends . big-whale provide "Resteem upvote and promo " services . Resteem to more 16,000+ Followers , Min 95+ Upvote from different accounts , big-whale Upvote with 100% power . Send 1 SBD or STEEM to @big-whale ( URL as memo ) Service Active
toecho304
Transaction InfoBlock #22759586/Trx 1ef4e171ce86bcc8fbeea081b280f32ace53951c
View Raw JSON Data
{
  "block": 22759586,
  "op": [
    "transfer",
    {
      "amount": "0.001 SBD",
      "from": "big-whale",
      "memo": "Hello Friend , Promote your new post with big-whale . Your post will be more popular and you will find new friends . big-whale provide \"Resteem upvote and promo \" services . Resteem to more 16,000+ Followers , Min 95+ Upvote from different accounts , big-whale Upvote with 100% power . Send 1  SBD or STEEM to @big-whale ( URL as memo ) Service Active",
      "to": "echo304"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-26T05:25:15",
  "trx_id": "1ef4e171ce86bcc8fbeea081b280f32ace53951c",
  "trx_in_block": 4,
  "virtual_op": 0
}
hugewhalesent 0.001 SBD to @echo304- "Promote your post. Your post will be resteemed with over 10.500+ followers , Min 45+ Upvote , Hugewhale Upvote (3100SP) . Your post will be more popular and you will find new friends. Send 0.700 SBD o..."
2018/05/26 02:50:57
amount0.001 SBD
fromhugewhale
memoPromote your post. Your post will be resteemed with over 10.500+ followers , Min 45+ Upvote , Hugewhale Upvote (3100SP) . Your post will be more popular and you will find new friends. Send 0.700 SBD or 1.0 STEEM to @hugewhale ( URL as memo ) Service Active
toecho304
Transaction InfoBlock #22756500/Trx 08738336f8c9a68ac3c1cb6add7bba697f29d6c3
View Raw JSON Data
{
  "block": 22756500,
  "op": [
    "transfer",
    {
      "amount": "0.001 SBD",
      "from": "hugewhale",
      "memo": "Promote your post. Your post will be resteemed with over 10.500+ followers , Min 45+ Upvote , Hugewhale Upvote (3100SP) . Your post will be more popular and you will find new friends. Send 0.700 SBD or 1.0 STEEM to @hugewhale ( URL as memo ) Service Active",
      "to": "echo304"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-26T02:50:57",
  "trx_id": "08738336f8c9a68ac3c1cb6add7bba697f29d6c3",
  "trx_in_block": 14,
  "virtual_op": 0
}
kdjupvoted (50.00%) @echo304 / redux-saga
2018/05/26 02:32:57
authorecho304
permlinkredux-saga
voterkdj
weight5000 (50.00%)
Transaction InfoBlock #22756140/Trx 60dc81f5ab7e06b40c1488a79cd326516169d0c5
View Raw JSON Data
{
  "block": 22756140,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "redux-saga",
      "voter": "kdj",
      "weight": 5000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-26T02:32:57",
  "trx_id": "60dc81f5ab7e06b40c1488a79cd326516169d0c5",
  "trx_in_block": 8,
  "virtual_op": 0
}
wonsamaupvoted (10.00%) @echo304 / redux-saga
2018/05/25 16:40:09
authorecho304
permlinkredux-saga
voterwonsama
weight1000 (10.00%)
Transaction InfoBlock #22744289/Trx b4f6bbfe82d4bc13eea96410ed84f085c6c76cd4
View Raw JSON Data
{
  "block": 22744289,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "redux-saga",
      "voter": "wonsama",
      "weight": 1000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-25T16:40:09",
  "trx_id": "b4f6bbfe82d4bc13eea96410ed84f085c6c76cd4",
  "trx_in_block": 4,
  "virtual_op": 0
}
megabyte77upvoted (100.00%) @echo304 / redux-saga
2018/05/25 15:27:21
authorecho304
permlinkredux-saga
votermegabyte77
weight10000 (100.00%)
Transaction InfoBlock #22742833/Trx 55de0d65ab1d2ab8ddc2d030e46324c6273d4e1f
View Raw JSON Data
{
  "block": 22742833,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "redux-saga",
      "voter": "megabyte77",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-25T15:27:21",
  "trx_id": "55de0d65ab1d2ab8ddc2d030e46324c6273d4e1f",
  "trx_in_block": 46,
  "virtual_op": 0
}
anomalyupvoted (1.00%) @echo304 / redux-saga
2018/05/25 15:09:03
authorecho304
permlinkredux-saga
voteranomaly
weight100 (1.00%)
Transaction InfoBlock #22742467/Trx 67c63794bf214de71d69a3e43c9d94bf06303aa8
View Raw JSON Data
{
  "block": 22742467,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "redux-saga",
      "voter": "anomaly",
      "weight": 100
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-25T15:09:03",
  "trx_id": "67c63794bf214de71d69a3e43c9d94bf06303aa8",
  "trx_in_block": 31,
  "virtual_op": 0
}
davidfnckupvoted (30.00%) @echo304 / redux-saga
2018/05/25 15:04:15
authorecho304
permlinkredux-saga
voterdavidfnck
weight3000 (30.00%)
Transaction InfoBlock #22742371/Trx 0312444d54d7ca2efae5b984a4a6bd6c813b7f05
View Raw JSON Data
{
  "block": 22742371,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "redux-saga",
      "voter": "davidfnck",
      "weight": 3000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-25T15:04:15",
  "trx_id": "0312444d54d7ca2efae5b984a4a6bd6c813b7f05",
  "trx_in_block": 2,
  "virtual_op": 0
}
jinh0729upvoted (4.00%) @echo304 / redux-saga
2018/05/25 14:43:48
authorecho304
permlinkredux-saga
voterjinh0729
weight400 (4.00%)
Transaction InfoBlock #22741962/Trx 1cb030257a20ee96eeec78a371f787cc1388ff19
View Raw JSON Data
{
  "block": 22741962,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "redux-saga",
      "voter": "jinh0729",
      "weight": 400
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-25T14:43:48",
  "trx_id": "1cb030257a20ee96eeec78a371f787cc1388ff19",
  "trx_in_block": 2,
  "virtual_op": 0
}
echo304upvoted (100.00%) @echo304 / redux-saga
2018/05/25 14:40:12
authorecho304
permlinkredux-saga
voterecho304
weight10000 (100.00%)
Transaction InfoBlock #22741890/Trx c6e65ffb4761e5f439ed99b67923e025febe0b18
View Raw JSON Data
{
  "block": 22741890,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "redux-saga",
      "voter": "echo304",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-25T14:40:12",
  "trx_id": "c6e65ffb4761e5f439ed99b67923e025febe0b18",
  "trx_in_block": 43,
  "virtual_op": 0
}
2018/05/25 14:37:24
authorsomesteamer
bodyExcellent post!
json metadata{"tags":["kr-dev"],"app":"steemit/0.1"}
parent authorecho304
parent permlinkredux-saga
permlinkre-echo304-redux-saga-20180525t143746909z
title
Transaction InfoBlock #22741834/Trx 031fd7f4f8c36721a2d0af474a5727895fe0f541
View Raw JSON Data
{
  "block": 22741834,
  "op": [
    "comment",
    {
      "author": "somesteamer",
      "body": "Excellent post!",
      "json_metadata": "{\"tags\":[\"kr-dev\"],\"app\":\"steemit/0.1\"}",
      "parent_author": "echo304",
      "parent_permlink": "redux-saga",
      "permlink": "re-echo304-redux-saga-20180525t143746909z",
      "title": ""
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-25T14:37:24",
  "trx_id": "031fd7f4f8c36721a2d0af474a5727895fe0f541",
  "trx_in_block": 53,
  "virtual_op": 0
}
echo304published a new post: redux-saga
2018/05/25 14:36:54
authorecho304
body![redux-saga.png](https://cdn.steemitimages.com/DQmeZpTQVbymvgqWjspEbFsQcmTu4sTUgGpTGuBaV7egCXA/redux-saga.png) [Redux-saga](https://redux-saga.js.org)는 [Redux](https://redux.js.org/)에서 비동기 액션을 핸들링하기 위한 다양한 라이브러리 중 하나이다. 전형적인 Callback 스타일, Observable을 활용한 라이브러리 등이 있지만 Redux-saga는 ES2015의 [Generator Function](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/function*)를 적극적으로 활용했다. 덕분에 훌륭한 테스트 용이성 제공하고 있다. _*이 포스팅은 Redux-saga에 대한 기본적인 지식을 전제한다_ ### Redux-saga의 무엇이 테스트를 쉽게 만드나 실제적인 테스트 코드 작성 방법을 다루기 전에, Redux-saga의 어떤 요소가 테스트를 용이하게 만드는지 간략하게 정리해보겠다. 이 내용은 공식 문서에도 정리가 되어있으므로, 큰 개념만 설명하고 넘어가도록 한다. Saga는 기본적으로 비동기 액션을 모니터링하는 Watcher Saga와 실제로 비동기 작업을 수행하는 Worker Saga로 구성된다. 그리고 Saga들을 실제적으로 핸들링하여 Redux(Store)와 연결하는 Saga Middleware(이하 미들웨어)가 있다. 그 중 Worker Saga는 `yield` 표현을 이용하여 비동기로 특정 객체를 반환한다. 미들웨어는 (Worker) Saga가 반환하는 Promise나 단순 Object를 핸들링 할 수 있다. 여기서 Promise는 `axios.get(...)` 이나 `$.get(...)` 등과 같은 비동기 작업을 하는 API를 호출한 결과로 반환되는 Promise를 의미한다. 문제는 Promise 객체는 테스트가 어렵다는 점이다. 여기서 미들웨어가 핸들링하는 두 번째 타입인 단순 Object가 등장한다. 해당 Object는 미들웨어로 하여금 특정 작업을 수행하도록 가이드의 역할을 한다. 위에 Promise의 예시와 비교하면 아래와 유사한 객체를 반환하는 셈이다. ```javascript { CALL: { fn: axios.get, args: [...] } } ``` <br/> 첫 번째 시나리오와 대조적으로 Promise를 반환하는 실제 API 호출이 미들웨어로 위임되는 것이다. 미들웨어는 해당 객체를 받아서 적절한 작업을 수행하겠지만, 이 단계를 사용자(개발자)가 신경쓰지 않아도 된다. 이것을 공식 문서에서는 (사이드)이펙트 생성과 (사이드)이펙트 실행의 분리라고 표현했다. > Separation between Effect creation and Effect excution 사용자는 이러한 분리 덕분에 이펙트가 제대로 생성되었는지(가이드 객체가 제대로 반환되었는지)만 테스트하면 되며, 단순 객체의 테스트는 Promise 객체 테스트보다 훨씬 용이하다. ### 무엇을 테스트 하는가 위에서 언급한 Worker Saga는 경우에 따라 상이하나 일반적으로는 아래와 같은 형태를 가진다. ```javascript export function* handleFetchFoo(action) { const { id, otherOption } = action.payload; try { yield put(fooActions.setIsFetching(true)); const foo = yield call(api.fetchFoo, { id: id, ...otherOption }); const fooObject = { wrappedWithObject: true, foo: foo }; // foo를 조작하는 로직 yield put(fooActions.fetchFooSuccess(fooObject)); } catch (error) { yield put(fooActions.fetchFooFailure(error)); } yield put(fooActions.setIsFetching(false)); } ``` 위의 코드를 테스트 가능한(해야할) 작은 조각으로 하나씩 풀어보도록 하자. ```javascript yield put(fooActions.setIsFetching(true)); ``` 비동기 작업(HTTP Request)을 시작하기 전에 동기 액션을 Dispatch 한다. 여기서는 로딩 인디케이터 따위를 표시하기 위한 정보를 업데이트 한다. ```javascript const foo = yield call(api.fetchFoo, { id: id, {...otherOption} }); ``` `fetchFoo`를 위한 (비동기)이펙트를 생성한다. 두 번째 인자로 `fetchFoo` 함수와 함께 호출할 인자를 넘긴다. ```javascript yield put(fooActions.fetchFooSuccess(fooObject)); ``` 예외가 발생하지 않는다면 `fetchFoo`가 성공했다는 동기 액션을 Dispatch 한다. ```javascript catch(error) { yield put(fooActions.fetchFooFailure(error)); } ``` `try` 구문 안의 블록을 실행하다 예외가 발생한 경우 즉시 `fetchFoo`가 실패했다는 동기 액션을 Dispatch 한다. ```javascript yield put(fooActions.setIsFetching(false)); ``` 상기 조건과 관계없이 비동기 작업이 종료되었다는 정보를 업데이트 한다. 이 흐름이 바로 우리가 Redux-saga를 테스트 할 때 검사해야 할 주요 지점이다. 이 지점들의 공통점으로는 모두 `yield` 표현을 사용했다는 점이며, `yield` 표현을 통해서 반환된 값을 테스트 하는 방식으로 진행하게 된다. ### 테스트 코드 ```javascript describe('Foo Saga', () => { describe('handleFetchFoo', () => { it('should fetch and set foo successfully', () => { const id = 'id1'; const otherOption = {}; const iterator = handleFetchFoo( fetchFoo(id, otherOption) ); expect(iterator.next().value).toEqual( put(setIsFetching(true)) ); expect(iterator.next().value).toEqual( call(api.fetchFoo, { id: id, ...otherOption }) ); expect(iterator.next({ value: 'foo' }).value).toEqual( put(fetchFooSuccess({ wrappedWithObject: true , foo: { value: 'foo' } })) ); expect(iterator.next().value).toEqual( put(setIsFetching(false)) ); expect(iterator.next().done).toBeTruthy(); }); }); }); ``` 기본적으로 Saga의 테스트는 Generator Function으로부터 생성된 Iterator가 순차적으로 `yield` 구문을 만나서 반환하는 값을 확인하는 방법으로 진행된다. ```javascript expect(iterator.next().value).toEqual( put(setIsFetching(true)) ); ``` 첫 번째 `yield`가 `setIsFetching` 액션을 `true` 값으로 제대로 반환하는지 테스트 한다. (엄밀하게 말하면 해당 액션을 처리하도록 가이드하는 객체) ```javascript expect(iterator.next().value).toEqual( call(api.fetchFoo, { id: id, ...otherOption }) ); ``` 두 번째 `yield`가 비동기 작업에 대한 이펙트를 제대로 생성/반환하였는지 테스트 한다. ```javascript expect(iterator.next({ value: 'foo' }).value).toEqual( put(fetchFooSuccess({ wrappedWithObject: true , foo: { value: 'foo' } })) ); ``` 비동기 작업이 성공한 경우, `next` 메소드에 특정 값을 전달하여 호출하고 그 값이 `fetchFooSuccess`를 통해 제대로 처리되었는지 테스트 한다. 특히 이 부분이 Redux-saga 테스트 용이성을 단적으로 드러내는 부분이다. Redux-thunk같은 라이브러리를 사용할 경우, 일반적인 비동기 작업을 테스트하기 위해서는 [Sinon.js](http://sinonjs.org/)등을 활용하여 비동기 작업을 수행하는 메소드가 반환할 값을 Stubbing 해주거나 해당 메소드 자체를 Mock으로 만들어야한다. 하지만 Redux-saga는 Generator Function의 `yield` 구문을 통해서 Stub 없이 원하는 값을 주입할 수가 있다. 이 테스트 코드는 해당 값을 주입하고, Worker Saga 안에서 그 값이 다른 객체로 한번 더 감싸졌는지 검사한다. ```javascript expect(iterator.next().value).toEqual( put(setIsFetching(false)) ); ``` 첫 번째 테스트와 마찬가지로 `setIsFetching` 이펙트를 제대로 생성하였는지 테스트 한다. ```javascript expect(iterator.next().done).toBeTruthy(); ``` 해당 Worker Saga로 부터 생성된 Iterator가 종결되었는지 테스트 한다. Saga의 세부적인 과정들을 쪼개어 테스트하는 이 방법은 유용하지만, 개발자가 임의로 값을 주입할 수 있는 지점이 많아 질 경우 테스트 자체가 불안정(Brittle)해질 가능성이 있다. 테스트 코드에서 임의로 주입한 값으로 인해서 실제로 소스 코드가 변경되었음에도 테스트가 실패하지 않는 경우가 그런 것이다. 이에 대한 보완책으로 공식 문서에서는 하나의 Saga를 처음부터 끝까지 실행시킨 후, 수반되는 (사이드)이펙트들을 한번에 검사하는 방법을 제시한다. 이 경우 외부 API를 호출하는 부분만 Sinon 등으로 Stub을 만들고, 나머지 부분은 소스 코드 원형 그대로 테스트에 통과시키는 것이다. ```javascript describe('whole saga', () => { afterEach(() => { sinon.restore(); }); it('should handle success case', async () => { sinon.stub(api, 'fetchFoo').callsFake(() => ({ value: 'foo' })); const dispatched = [] as any; const id = 'id1'; const otherOption = {}; await runSaga( { dispatch: (action) => dispatched.push(action), getState: () => ({}) }, handleFetchFoo, fetchFoo(id, otherOption) ).done; expect(dispatched).toEqual([ { isFetching: true, type: 'SET_IS_FETCHING' }, { id: 'id1', foo: { wrappedWithObject: true, foo: { value: 'foo' } }, type: 'FETCH_FOO_SUCCESS' }, { isFetching: false, type: 'SET_IS_FETCHING' } ]); }); }); ``` 이 방법은 원형 그대로의 소스 코드를 최대한 보존한 채 테스트를 진행할 수 있다는 장점이 있으나, 역시 Saga 내부의 새부적인 로직에 대한 테스트는 어려울 수 있다. 상황에 따라 두가지 방법을 취사 선택하거나, 전체적인 Flow를 두 번째 방법을 활용하여 테스트하고 세부적인 로직을 첫 번째 방법으로 테스트하는 등 개발자의 재량껏 조합해서 사용하면 된다. 여기까지 Redux-saga의 실제적인 테스트 코드 작성 방법에 대해 정리해보았다. 실제 제품에서 사용되는 Saga는 예시의 것보다 훨씬 복잡한 경우가 많을 것이나, 기본적인 방법은 큰 틀을 벗어나지 않는다. _이 글은 필자의 [Medium](https://medium.com/@sangboaklee/redux-saga-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-1fc13f7fd279)에도 게시되었습니다._
json metadata{"tags":["kr-dev","kr","dev","kr-newbie"],"image":["https://cdn.steemitimages.com/DQmeZpTQVbymvgqWjspEbFsQcmTu4sTUgGpTGuBaV7egCXA/redux-saga.png"],"links":["https://redux-saga.js.org","https://redux.js.org/","https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/function*","http://sinonjs.org/","https://medium.com/@sangboaklee/redux-saga-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-1fc13f7fd279"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkkr-dev
permlinkredux-saga
title[웹개발] Redux-saga 테스트 코드 작성하기
Transaction InfoBlock #22741824/Trx 5400f528ca1f93de70f4783329559befcffb9575
View Raw JSON Data
{
  "block": 22741824,
  "op": [
    "comment",
    {
      "author": "echo304",
      "body": "![redux-saga.png](https://cdn.steemitimages.com/DQmeZpTQVbymvgqWjspEbFsQcmTu4sTUgGpTGuBaV7egCXA/redux-saga.png)\n\n[Redux-saga](https://redux-saga.js.org)는 [Redux](https://redux.js.org/)에서 비동기 액션을 핸들링하기 위한 다양한 라이브러리 중 하나이다. 전형적인 Callback 스타일, Observable을 활용한 라이브러리 등이 있지만 Redux-saga는 ES2015의 [Generator Function](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/function*)를 적극적으로 활용했다. 덕분에 훌륭한 테스트 용이성 제공하고 있다. \n\n_*이 포스팅은 Redux-saga에 대한 기본적인 지식을 전제한다_\n\n### Redux-saga의 무엇이 테스트를 쉽게 만드나\n실제적인 테스트 코드 작성 방법을 다루기 전에, Redux-saga의 어떤 요소가 테스트를 용이하게 만드는지 간략하게 정리해보겠다.\n이 내용은 공식 문서에도 정리가 되어있으므로, 큰 개념만 설명하고 넘어가도록 한다. \n\nSaga는 기본적으로 비동기 액션을 모니터링하는 Watcher Saga와 실제로 비동기 작업을 수행하는 Worker Saga로 구성된다.\n그리고 Saga들을 실제적으로 핸들링하여 Redux(Store)와 연결하는 Saga Middleware(이하 미들웨어)가 있다. \n\n그 중 Worker Saga는 `yield` 표현을 이용하여 비동기로 특정 객체를 반환한다.\n미들웨어는 (Worker) Saga가 반환하는 Promise나 단순 Object를 핸들링 할 수 있다. 여기서 Promise는 `axios.get(...)` 이나 `$.get(...)` 등과 같은 비동기 작업을 하는 API를 호출한 결과로 반환되는 Promise를 의미한다.\n문제는 Promise 객체는 테스트가 어렵다는 점이다.\n\n 여기서 미들웨어가 핸들링하는 두 번째 타입인 단순 Object가 등장한다.\n해당 Object는 미들웨어로 하여금 특정 작업을 수행하도록 가이드의 역할을 한다. 위에 Promise의 예시와 비교하면 아래와 유사한 객체를 반환하는 셈이다.\n```javascript\n{ \n  CALL: {\n    fn: axios.get,\n    args: [...]\n  }\n}\n```\n<br/>\n\n첫 번째 시나리오와 대조적으로 Promise를 반환하는 실제 API 호출이 미들웨어로 위임되는 것이다. 미들웨어는 해당 객체를 받아서 적절한 작업을 수행하겠지만, 이 단계를 사용자(개발자)가 신경쓰지 않아도 된다.\n이것을 공식 문서에서는 (사이드)이펙트 생성과 (사이드)이펙트 실행의 분리라고 표현했다.\n> Separation between Effect creation and Effect excution\n\n사용자는 이러한 분리 덕분에 이펙트가 제대로 생성되었는지(가이드 객체가 제대로 반환되었는지)만 테스트하면 되며, 단순 객체의 테스트는 Promise 객체 테스트보다 훨씬 용이하다.\n\n### 무엇을 테스트 하는가\n위에서 언급한 Worker Saga는 경우에 따라 상이하나 일반적으로는 아래와 같은 형태를 가진다.\n```javascript\nexport function* handleFetchFoo(action) {\n  const { id, otherOption } = action.payload;\n\n  try {\n    yield put(fooActions.setIsFetching(true));\n    const foo = yield call(api.fetchFoo, {\n      id: id,\n      ...otherOption\n    });\n\n    const fooObject = {\n      wrappedWithObject: true,\n      foo: foo\n    }; // foo를 조작하는 로직\n\n    yield put(fooActions.fetchFooSuccess(fooObject));\n  } catch (error) {\n    yield put(fooActions.fetchFooFailure(error));\n  }\n  yield put(fooActions.setIsFetching(false));\n}\n```\n위의 코드를 테스트 가능한(해야할) 작은 조각으로 하나씩 풀어보도록 하자.\n\n```javascript\nyield put(fooActions.setIsFetching(true));\n```\n 비동기 작업(HTTP Request)을 시작하기 전에 동기 액션을 Dispatch 한다. 여기서는 로딩 인디케이터 따위를 표시하기 위한 정보를 업데이트 한다.\n\n```javascript\nconst foo = yield call(api.fetchFoo, {\n  id: id,\n  {...otherOption}\n});\n```\n`fetchFoo`를 위한 (비동기)이펙트를 생성한다. 두 번째 인자로 `fetchFoo` 함수와 함께 호출할 인자를 넘긴다.\n\n```javascript\nyield put(fooActions.fetchFooSuccess(fooObject));\n```\n 예외가 발생하지 않는다면 `fetchFoo`가 성공했다는 동기 액션을 Dispatch 한다.\n\n```javascript\ncatch(error) {\n  yield put(fooActions.fetchFooFailure(error));\n}\n```\n `try` 구문 안의 블록을 실행하다 예외가 발생한 경우 즉시 `fetchFoo`가 실패했다는 동기 액션을 Dispatch 한다.\n\n```javascript\nyield put(fooActions.setIsFetching(false));\n```\n 상기 조건과 관계없이 비동기 작업이 종료되었다는 정보를 업데이트 한다.\n\n이 흐름이 바로 우리가 Redux-saga를 테스트 할 때 검사해야 할 주요 지점이다.\n이 지점들의 공통점으로는 모두 `yield` 표현을 사용했다는 점이며, `yield` 표현을 통해서 반환된 값을 테스트 하는 방식으로 진행하게 된다.\n\n### 테스트 코드\n```javascript\ndescribe('Foo Saga', () => {\n  describe('handleFetchFoo', () => {\n    it('should fetch and set foo successfully', () => {\n      const id = 'id1';\n      const otherOption = {};\n      const iterator = handleFetchFoo(\n        fetchFoo(id, otherOption)\n      );\n\n      expect(iterator.next().value).toEqual(\n        put(setIsFetching(true))\n      );\n\n      expect(iterator.next().value).toEqual(\n        call(api.fetchFoo, {\n          id: id,\n          ...otherOption\n        })\n      );\n\n      expect(iterator.next({ value: 'foo' }).value).toEqual(\n        put(fetchFooSuccess({ wrappedWithObject: true , foo: { value: 'foo' } }))\n      );\n\n      expect(iterator.next().value).toEqual(\n        put(setIsFetching(false))\n      );\n\n      expect(iterator.next().done).toBeTruthy();\n    });\n  });\n});\n```\n기본적으로 Saga의 테스트는 Generator Function으로부터 생성된 Iterator가 순차적으로 `yield` 구문을 만나서 반환하는 값을 확인하는 방법으로 진행된다.\n```javascript\nexpect(iterator.next().value).toEqual(\n  put(setIsFetching(true))\n);\n```\n 첫 번째 `yield`가 `setIsFetching` 액션을 `true` 값으로 제대로 반환하는지 테스트 한다. (엄밀하게 말하면 해당 액션을 처리하도록 가이드하는 객체)\n\n```javascript\nexpect(iterator.next().value).toEqual(\n  call(api.fetchFoo, {\n    id: id,\n    ...otherOption\n  })\n);\n```\n 두 번째 `yield`가 비동기 작업에 대한 이펙트를 제대로 생성/반환하였는지 테스트 한다.\n\n```javascript\nexpect(iterator.next({ value: 'foo' }).value).toEqual(\n  put(fetchFooSuccess({ wrappedWithObject: true , foo: { value: 'foo' } }))\n);\n```\n 비동기 작업이 성공한 경우, `next` 메소드에 특정 값을 전달하여 호출하고 그 값이 `fetchFooSuccess`를 통해 제대로 처리되었는지 테스트 한다.\n\n\n특히 이 부분이 Redux-saga 테스트 용이성을 단적으로 드러내는 부분이다.\nRedux-thunk같은 라이브러리를 사용할 경우, 일반적인 비동기 작업을 테스트하기 위해서는 [Sinon.js](http://sinonjs.org/)등을 활용하여 비동기 작업을 수행하는 메소드가 반환할 값을 Stubbing 해주거나 해당 메소드 자체를 Mock으로 만들어야한다.\n\n하지만 Redux-saga는 Generator Function의 `yield` 구문을 통해서 Stub 없이 원하는 값을 주입할 수가 있다.\n이 테스트 코드는 해당 값을 주입하고, Worker Saga 안에서 그 값이 다른 객체로 한번 더 감싸졌는지 검사한다.\n\n```javascript\nexpect(iterator.next().value).toEqual(\n  put(setIsFetching(false))\n);\n```\n 첫 번째 테스트와 마찬가지로 `setIsFetching` 이펙트를 제대로 생성하였는지 테스트 한다.\n\n```javascript\nexpect(iterator.next().done).toBeTruthy();\n```\n 해당 Worker Saga로 부터 생성된 Iterator가 종결되었는지 테스트 한다.\n\nSaga의 세부적인 과정들을 쪼개어 테스트하는 이 방법은 유용하지만, 개발자가 임의로 값을 주입할 수 있는 지점이 많아 질 경우 테스트 자체가 불안정(Brittle)해질 가능성이 있다. 테스트 코드에서 임의로 주입한 값으로 인해서 실제로 소스 코드가 변경되었음에도 테스트가 실패하지 않는 경우가 그런 것이다.\n\n이에 대한 보완책으로 공식 문서에서는 하나의 Saga를 처음부터 끝까지 실행시킨 후, 수반되는 (사이드)이펙트들을 한번에 검사하는 방법을 제시한다.\n이 경우 외부 API를 호출하는 부분만 Sinon 등으로 Stub을 만들고, 나머지 부분은 소스 코드 원형 그대로 테스트에 통과시키는 것이다.\n\n```javascript\ndescribe('whole saga', () => {\n  afterEach(() => {\n    sinon.restore();\n  });\n\n  it('should handle success case', async () => {\n    sinon.stub(api, 'fetchFoo').callsFake(() => ({\n      value: 'foo'\n    }));\n    const dispatched = [] as any;\n    const id = 'id1';\n    const otherOption = {};\n\n    await runSaga(\n      {\n        dispatch: (action) => dispatched.push(action),\n        getState: () => ({})\n      },\n      handleFetchFoo,\n      fetchFoo(id, otherOption)\n    ).done;\n\n    expect(dispatched).toEqual([\n      {\n        isFetching: true,\n        type: 'SET_IS_FETCHING'\n      },\n      {\n        id: 'id1',\n        foo: {\n          wrappedWithObject: true,\n          foo: {\n            value: 'foo'\n          }\n        },\n        type: 'FETCH_FOO_SUCCESS'\n      },\n      {\n        isFetching: false,\n        type: 'SET_IS_FETCHING'\n      }\n    ]);\n  });\n});\n```\n이 방법은 원형 그대로의 소스 코드를 최대한 보존한 채 테스트를 진행할 수 있다는 장점이 있으나, 역시 Saga 내부의 새부적인 로직에 대한 테스트는 어려울 수 있다.\n상황에 따라 두가지 방법을 취사 선택하거나, 전체적인 Flow를 두 번째 방법을 활용하여 테스트하고 세부적인 로직을 첫 번째 방법으로 테스트하는 등 개발자의 재량껏 조합해서 사용하면 된다.\n\n 여기까지 Redux-saga의 실제적인 테스트 코드 작성 방법에 대해 정리해보았다. 실제 제품에서 사용되는 Saga는 예시의 것보다 훨씬 복잡한 경우가 많을 것이나, 기본적인 방법은 큰 틀을 벗어나지 않는다.\n \n_이 글은 필자의 [Medium](https://medium.com/@sangboaklee/redux-saga-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-1fc13f7fd279)에도 게시되었습니다._",
      "json_metadata": "{\"tags\":[\"kr-dev\",\"kr\",\"dev\",\"kr-newbie\"],\"image\":[\"https://cdn.steemitimages.com/DQmeZpTQVbymvgqWjspEbFsQcmTu4sTUgGpTGuBaV7egCXA/redux-saga.png\"],\"links\":[\"https://redux-saga.js.org\",\"https://redux.js.org/\",\"https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/function*\",\"http://sinonjs.org/\",\"https://medium.com/@sangboaklee/redux-saga-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-1fc13f7fd279\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "kr-dev",
      "permlink": "redux-saga",
      "title": "[웹개발] Redux-saga 테스트 코드 작성하기"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-25T14:36:54",
  "trx_id": "5400f528ca1f93de70f4783329559befcffb9575",
  "trx_in_block": 30,
  "virtual_op": 0
}
steemdelegated 18.191 SP to @echo304
2018/05/24 12:34:42
delegateeecho304
delegatorsteem
vesting shares29622.784968 VESTS
Transaction InfoBlock #22710648/Trx 3af00232d35c43b2b6077f06747fdcea14975add
View Raw JSON Data
{
  "block": 22710648,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "29622.784968 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-24T12:34:42",
  "trx_id": "3af00232d35c43b2b6077f06747fdcea14975add",
  "trx_in_block": 38,
  "virtual_op": 0
}
echo304claimed reward balance: 1.009 SBD, 0.420 SP
2018/05/24 12:00:09
accountecho304
reward sbd1.009 SBD
reward steem0.000 STEEM
reward vests683.445255 VESTS
Transaction InfoBlock #22709997/Trx 379a7edd660325a2de4fb565de9c1c18782a07c0
View Raw JSON Data
{
  "block": 22709997,
  "op": [
    "claim_reward_balance",
    {
      "account": "echo304",
      "reward_sbd": "1.009 SBD",
      "reward_steem": "0.000 STEEM",
      "reward_vests": "683.445255 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-24T12:00:09",
  "trx_id": "379a7edd660325a2de4fb565de9c1c18782a07c0",
  "trx_in_block": 67,
  "virtual_op": 0
}
echo304received 1.009 SBD, 0.420 SP author reward for @echo304 / react
2018/05/24 07:31:51
authorecho304
permlinkreact
sbd payout1.009 SBD
steem payout0.000 STEEM
vesting payout683.445255 VESTS
Transaction InfoBlock #22704889/Virtual Operation #7
View Raw JSON Data
{
  "block": 22704889,
  "op": [
    "author_reward",
    {
      "author": "echo304",
      "permlink": "react",
      "sbd_payout": "1.009 SBD",
      "steem_payout": "0.000 STEEM",
      "vesting_payout": "683.445255 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-24T07:31:51",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": 7
}
2018/05/23 09:59:24
authorecho304
permlinktypescript
voterbitcoinparadise
weight2 (0.02%)
Transaction InfoBlock #22679047/Trx 34b5c83be949429fbe548d72d16903f904189248
View Raw JSON Data
{
  "block": 22679047,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "typescript",
      "voter": "bitcoinparadise",
      "weight": 2
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-23T09:59:24",
  "trx_id": "34b5c83be949429fbe548d72d16903f904189248",
  "trx_in_block": 47,
  "virtual_op": 0
}
echo304published a new post: typescript
2018/05/23 09:58:09
authorecho304
body_** 이 글은 필자의 [Medium 포스팅](https://medium.com/@sangboaklee/typescript-%ED%98%84%EC%97%85-%EC%A0%81%EC%9A%A9-%ED%9B%84%EA%B8%B0-caad266c8142)을 포팅한 것 입니다_ ![react_ts.png](https://steemitimages.com/DQmYyuSRctnqYFMnGtXxLbA6ZRGMVTnUuVKLma5afSCfrv9/react_ts.png) <br /> [React](https://reactjs.org/)와 [Redux](https://redux.js.org/) 그리고 [TypeScript](https://www.typescriptlang.org/)는 국내에서도 더이상 생소한 단어가 아니다. 이미 많은 회사와 서비스에서 해당 기술들을 사용하고 있고 활발하게 정보가 공유되고 있다. 이미 진부한 내용일 수도 있으나 실제 서비스 되고 있는 제품에 React + Redux + TypeScript 조합을 적용한 경험을 널리 공유하고자 이 포스팅을 작성하게 되었다. _* 이 글은 기본적인 TypeScript에 대한 배경 지식을 전제로 하고 있습니다._ ## TypeScript 도입에 앞서 아마 현업에서 TypeScript(이하 TS)가 사용된 상당수의 케이스는 프론트엔드 기술로 [Angular](https://angular.io/)를 사용하는 경우가 아닐까 한다. Angular2 이후부터 TS를 권유하기 시작했으니 해당 언어를 실제 제품에 적용할 때는 Angular를 가장 먼저 떠올리게 된다. 하지만 현재 우리 팀의 주된 프론트엔드 기술은 React이었고, TS를 자연스럽게 적용할 수 있는지에 대한 의구심도 가졌다. 또한 자유분방한 JavaScript(이하 JS)를 만끽하던 팀원들이 가지는 타입 시스템에 대한 불신과 거부감 또한 TS를 적용하기에 큰 장벽으로 작용했다. 당시 우리 팀에서 가졌던 의문과 불안요소들을 정리해보았다. 1. **정적 타입 시스템이 정말 필요한가? 상당수의 버그를 잡아준다고 하는데, 타입 시스템의 도입으로 잡을 수 있는 버그가 유의미한 수준인가?** 2. **React 생태계와 TS의 호환성 문제는 없는가?** 3. **코드가 장황(Verbose)해짐으로써 생산성의 저하가 발생하지 않는가?** 4. **기타 마이너한 기술적 우려와 심리적인 저항감([M$](https://www.microsoft.com))** 이런 고민들이 있었지만 결과적으로 TS를 도입하게 되었고, 실제 제품에 적용하고 약 4개월이 지난 지금, 팀원들과의 대화를 통해 정리한 내용과 개인적인 소감을 버무려 사용 경험을 공유해보겠다. ## Happy and Safe Refactoring JS로 대규모 웹 어플리케이션을 개발할 때 종종 경험하는 어려움 중 하나는 바로 **여러 모듈에 걸쳐있는 객체의 리팩토링 작업**이다. 일반적인 타입-컴파일 언어와 달리 JS는 객체 속성의 이름은 변경하는 작업을 할 때 단순한 오타가 발생하더라도 런타임 환경에서만 발견할 수 있다. 개발 중에 테스트를 통해서 발견할 수 있다면 다행이지만, 프로덕션 레벨에서 발견되면 장애로 이어진다. 필자와 팀이 TS를 도입하고 크게만족하는 부분 중 하나가 리팩토링의 용이함이다. 사전에 정의된 객체의 인터페이스를 여러 모듈에 걸쳐서 빈틈없이 적용해두었다면, **인터페이스 정의 한군데만 수정해도 해당 객체의 수정된 부분이 사용되는 모든 파일에서 경고를 띄워주고 컴파일도 되지 않는다.** 특히 **Redux를 이용하여 어플리케이션의 상태관리를 할 경우 하부 State 객체들이 여러 React 컴포넌트에서 참조**하게 되는데, 단순한 State 필드 이름 변경부터 실제 값의 타입을 변경하는 변경까지 TS의 도움으로 안전한 리팩토링을 진행할 수 있었다. 버그를 사전에 잡아준다는 캐치프레이즈에 부정적이었던 필자도 몇차례에 걸친 위와 같은 경험을 통해서, 다양한 상황에서 정적 타입 시스템이 빛을 발할 수 있음을 인정하게 되었다. ## 훌륭한 도구 JS는 언어의 특성상 개발 도구의 도움 받을 수 있는 영역이 제한적이다. C#의 Visual Studio 수준의 지원은 기대하기 어렵다. TS를 사용함으로써 얻을 수 있는 또 다른 큰 이점은 훌륭한 도구의 지원을 받을 수 있다는 점이다. MS에서 개발한 언어인만큼 동 회사에서 개발하여 공개한 Visual Studio Code(이하 VS Code)와의 궁합이 매우 훌륭하다. 물론 C#과 Visual Studio 수준의 기능을 제공하지는 않지만, 타입 시스템을 비롯한 언어적 특성을 십분 활용한 VS Code의 기능은 생산성에 큰 도움을 준다. ![May-21-2018 21-37-33.gif](https://steemitimages.com/DQmRGXMQkxeEpy3guCMnFprU5biuzTwfzLyUqnD2hrDe4pV/May-21-2018%2021-37-33.gif) _향상된 Intellisense_ 위 예시는 VS Code의 Intellisense가 특정 객체의 속성을 자동완성 시켜주고 해당 속성의 타입까지 알려주는 모습이다. 일반적인 JS의 자동완성과는 달리 파일 내에서만 지원하는 것이 아니라, 특정 객체의 타입(인터페이스)가 올바르게 정의되어있다면 정확한 자동완성의 지원을 받을 수 있다. ![May-21-2018 22-18-11.gif](https://steemitimages.com/DQmQL8eDcxQuXWpmAY4iLGG7kEqMrYfjqpadFHSH7y7bFT1/May-21-2018%2022-18-11.gif) _Auto Import_ VS Code가 TS에 대응하여 제공하는 기능 중 하나인 Auto Import는 대규모 웹어플리케이션을 개발하면서 종종 경험하는 불편함 중 하나인 모듈 Import의 번거로움을 일거에 해결해준다. 특히 **Redux를 이용한 어플리케이션에서는 Action을 중심으로 모듈 Import가 빈번하게 발생**하는데, 이런 단순 작업의 로드를 상당 부분 줄여준다. VS Code가 제공하는 많은 기능 중 일부를 예시로 들었으나, 이외에도 다양한 기능들을 제공하고 있으며 대부분의 기능을 추가적인 Extension 설치 없이도 사용할 수 있다는 간편함 또한 장점이다. 웹 프론트엔드 개발을 할 때, 언제나 목마름을 느꼈던 훌륭한 도구에 대한 니즈는 TS와 VS Code를 사용함으로써 상당 부분 해소되었다. ## 기존 JS 생태계와의 호환성도 안정화 단계 React를 비롯한 JS 기반 라이브러리들이 TS에 원활하게 돌아가느냐에 대한 우려도 있었으나, 수개월 동안 사용한 유수의 라이브러리들은 이미 호환성에 대한 이슈가 거의 존재하지 않았다. JS 라이브러리를 TS에서 Import하여 사용하기 위해서는 해당 라이브러리의 타입이 정의된 모듈을 추가로 설치해줘야 한다. ![스크린샷 2018-05-21 오후 11.22.36.png](https://steemitimages.com/DQmaowxTBfan24xafhwVnr5UoBwXXPqqpktdEQAq2rAYzin/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202018-05-21%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.22.36.png) 특정 모듈에 타입 정의 패키지가 존재하는지는 이곳에서 확인이 가능하다. http://definitelytyped.org/ 필자가 처음 TS를 접했던 2016년에는 많은 라이브러리에 타입 정의 패키지가 존재하지 않아 타입 시스템의 도움을 받지 못했으나, 이제는 안정화 단계에 올라서있다고 단언할 수 있겠다. 그리고 React로 개발을 할때, 대부분 사람들이 활용하는 **JSX 문법도 TS에서 훌륭하게 지원**하고 있으므로, 이 부분에 대해서도 문제없다. 물론 타입 시스템을 적용한 React 컴포넌트나 Props 타입과 관련한 지식이 필요하나, **기술적 장벽으로 작용할 수준은 전혀 아니다.** https://www.typescriptlang.org/docs/handbook/jsx.html ## 생산성과의 Trade-Off 정적 타입 시스템을 적극적으로 활용할 경우, JS로 코드를 작성했을 때보다 다소 **코드의 양이 늘어난다는 것은 부정할 수 없는 사실**이다. 이로 인해서 생산성이 저하될 수도 있다는 팀내 우려도 있었다. 실제로 작성하는 코드의 양은 체감이 될 정도로 증가하였다. 하지만 그것으로 인해 생산성이 저하되었다고 단언하기는 힘들다. 위에서 언급한 다양한 기능들의 지원으로 **단순 작업의 속도는 상당부분 개선**되었으며, 오히려 불필요한 **디버깅 시간을 절감**함으로써, 코드 양 증가로 인한 **생산성 저하가 상쇄**되는 느낌이었다. ## 다음 프로젝트도 TypeScript로… 이상으로 현업에서 TS를 적용하면서 마주했던 우려와 실제로 사용하면서 느낀 점들을 정리해보았다. 상당 부분 간소화되어있으나, 이 포스팅을 통해서 TS 적용을 고민하는 팀/개인들이 의사결정을 함에 있어서 도움이 되기를 바라면서 글을 마무리하도록 하겠다. 개인적으로도 팀 전체로도 TS 적용은 성공적이었다고 느꼈으며, 특히 필자는 큰 이변이 없는 한 앞으로도 하게 될 프로젝트는 TS를 사용할 생각이다.
json metadata{"tags":["kr-dev","kr","dev","kr-newbie","typescript"],"image":["https://steemitimages.com/DQmYyuSRctnqYFMnGtXxLbA6ZRGMVTnUuVKLma5afSCfrv9/react_ts.png","https://steemitimages.com/DQmRGXMQkxeEpy3guCMnFprU5biuzTwfzLyUqnD2hrDe4pV/May-21-2018%2021-37-33.gif","https://steemitimages.com/DQmQL8eDcxQuXWpmAY4iLGG7kEqMrYfjqpadFHSH7y7bFT1/May-21-2018%2022-18-11.gif","https://steemitimages.com/DQmaowxTBfan24xafhwVnr5UoBwXXPqqpktdEQAq2rAYzin/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202018-05-21%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.22.36.png"],"links":["https://medium.com/@sangboaklee/typescript-%ED%98%84%EC%97%85-%EC%A0%81%EC%9A%A9-%ED%9B%84%EA%B8%B0-caad266c8142","https://reactjs.org/","https://redux.js.org/","https://www.typescriptlang.org/","https://angular.io/","https://www.microsoft.com","http://definitelytyped.org/","https://www.typescriptlang.org/docs/handbook/jsx.html"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkkr-dev
permlinktypescript
title[개발] TypeScript 현업 적용 후기
Transaction InfoBlock #22679022/Trx 06baed33cdcebe9229d6ae8890405a546b810109
View Raw JSON Data
{
  "block": 22679022,
  "op": [
    "comment",
    {
      "author": "echo304",
      "body": "_** 이 글은 필자의 [Medium 포스팅](https://medium.com/@sangboaklee/typescript-%ED%98%84%EC%97%85-%EC%A0%81%EC%9A%A9-%ED%9B%84%EA%B8%B0-caad266c8142)을 포팅한 것 입니다_\n\n![react_ts.png](https://steemitimages.com/DQmYyuSRctnqYFMnGtXxLbA6ZRGMVTnUuVKLma5afSCfrv9/react_ts.png)\n<br />\n[React](https://reactjs.org/)와 [Redux](https://redux.js.org/) 그리고 [TypeScript](https://www.typescriptlang.org/)는 국내에서도 더이상 생소한 단어가 아니다. 이미 많은 회사와 서비스에서 해당 기술들을 사용하고 있고 활발하게 정보가 공유되고 있다. 이미 진부한 내용일 수도 있으나 실제 서비스 되고 있는 제품에 React + Redux + TypeScript 조합을 적용한 경험을 널리 공유하고자 이 포스팅을 작성하게 되었다.\n\n_* 이 글은 기본적인 TypeScript에 대한 배경 지식을 전제로 하고 있습니다._\n\n## TypeScript 도입에 앞서\n아마 현업에서 TypeScript(이하 TS)가 사용된 상당수의 케이스는 프론트엔드 기술로 [Angular](https://angular.io/)를 사용하는 경우가 아닐까 한다. Angular2 이후부터 TS를 권유하기 시작했으니 해당 언어를 실제 제품에 적용할 때는 Angular를 가장 먼저 떠올리게 된다. 하지만 현재 우리 팀의 주된 프론트엔드 기술은 React이었고, TS를 자연스럽게 적용할 수 있는지에 대한 의구심도 가졌다.\n\n또한 자유분방한 JavaScript(이하 JS)를 만끽하던 팀원들이 가지는 타입 시스템에 대한 불신과 거부감 또한 TS를 적용하기에 큰 장벽으로 작용했다.\n\n당시 우리 팀에서 가졌던 의문과 불안요소들을 정리해보았다.\n\n\n1. **정적 타입 시스템이 정말 필요한가? 상당수의 버그를 잡아준다고 하는데, 타입 시스템의 도입으로 잡을 수 있는 버그가 유의미한 수준인가?**\n2. **React 생태계와 TS의 호환성 문제는 없는가?**\n3. **코드가 장황(Verbose)해짐으로써 생산성의 저하가 발생하지 않는가?**\n4. **기타 마이너한 기술적 우려와 심리적인 저항감([M$](https://www.microsoft.com))**\n\n이런 고민들이 있었지만 결과적으로 TS를 도입하게 되었고, 실제 제품에 적용하고 약 4개월이 지난 지금, 팀원들과의 대화를 통해 정리한 내용과 개인적인 소감을 버무려 사용 경험을 공유해보겠다.\n\n## Happy and Safe Refactoring\nJS로 대규모 웹 어플리케이션을 개발할 때 종종 경험하는 어려움 중 하나는 바로 **여러 모듈에 걸쳐있는 객체의 리팩토링 작업**이다. 일반적인 타입-컴파일 언어와 달리 JS는 객체 속성의 이름은 변경하는 작업을 할 때 단순한 오타가 발생하더라도 런타임 환경에서만 발견할 수 있다. 개발 중에 테스트를 통해서 발견할 수 있다면 다행이지만, 프로덕션 레벨에서 발견되면 장애로 이어진다.\n\n필자와 팀이 TS를 도입하고 크게만족하는 부분 중 하나가 리팩토링의 용이함이다. 사전에 정의된 객체의 인터페이스를 여러 모듈에 걸쳐서 빈틈없이 적용해두었다면, **인터페이스 정의 한군데만 수정해도 해당 객체의 수정된 부분이 사용되는 모든 파일에서 경고를 띄워주고 컴파일도 되지 않는다.**\n\n특히 **Redux를 이용하여 어플리케이션의 상태관리를 할 경우 하부 State 객체들이 여러 React 컴포넌트에서 참조**하게 되는데, 단순한 State 필드 이름 변경부터 실제 값의 타입을 변경하는 변경까지 TS의 도움으로 안전한 리팩토링을 진행할 수 있었다.\n\n버그를 사전에 잡아준다는 캐치프레이즈에 부정적이었던 필자도 몇차례에 걸친 위와 같은 경험을 통해서, 다양한 상황에서 정적 타입 시스템이 빛을 발할 수 있음을 인정하게 되었다.\n\n## 훌륭한 도구\nJS는 언어의 특성상 개발 도구의 도움 받을 수 있는 영역이 제한적이다. C#의 Visual Studio 수준의 지원은 기대하기 어렵다. TS를 사용함으로써 얻을 수 있는 또 다른 큰 이점은 훌륭한 도구의 지원을 받을 수 있다는 점이다.\n\nMS에서 개발한 언어인만큼 동 회사에서 개발하여 공개한 Visual Studio Code(이하 VS Code)와의 궁합이 매우 훌륭하다. 물론 C#과 Visual Studio 수준의 기능을 제공하지는 않지만, 타입 시스템을 비롯한 언어적 특성을 십분 활용한 VS Code의 기능은 생산성에 큰 도움을 준다.\n\n![May-21-2018 21-37-33.gif](https://steemitimages.com/DQmRGXMQkxeEpy3guCMnFprU5biuzTwfzLyUqnD2hrDe4pV/May-21-2018%2021-37-33.gif)\n_향상된 Intellisense_\n\n위 예시는 VS Code의 Intellisense가 특정 객체의 속성을 자동완성 시켜주고 해당 속성의 타입까지 알려주는 모습이다. 일반적인 JS의 자동완성과는 달리 파일 내에서만 지원하는 것이 아니라, 특정 객체의 타입(인터페이스)가 올바르게 정의되어있다면 정확한 자동완성의 지원을 받을 수 있다.\n\n![May-21-2018 22-18-11.gif](https://steemitimages.com/DQmQL8eDcxQuXWpmAY4iLGG7kEqMrYfjqpadFHSH7y7bFT1/May-21-2018%2022-18-11.gif)\n_Auto Import_\n\nVS Code가 TS에 대응하여 제공하는 기능 중 하나인 Auto Import는 대규모 웹어플리케이션을 개발하면서 종종 경험하는 불편함 중 하나인 모듈 Import의 번거로움을 일거에 해결해준다. 특히 **Redux를 이용한 어플리케이션에서는 Action을 중심으로 모듈 Import가 빈번하게 발생**하는데, 이런 단순 작업의 로드를 상당 부분 줄여준다.\n\nVS Code가 제공하는 많은 기능 중 일부를 예시로 들었으나, 이외에도 다양한 기능들을 제공하고 있으며 대부분의 기능을 추가적인 Extension 설치 없이도 사용할 수 있다는 간편함 또한 장점이다.\n\n웹 프론트엔드 개발을 할 때, 언제나 목마름을 느꼈던 훌륭한 도구에 대한 니즈는 TS와 VS Code를 사용함으로써 상당 부분 해소되었다.\n\n## 기존 JS 생태계와의 호환성도 안정화 단계\nReact를 비롯한 JS 기반 라이브러리들이 TS에 원활하게 돌아가느냐에 대한 우려도 있었으나, 수개월 동안 사용한 유수의 라이브러리들은 이미 호환성에 대한 이슈가 거의 존재하지 않았다.\n\nJS 라이브러리를 TS에서 Import하여 사용하기 위해서는 해당 라이브러리의 타입이 정의된 모듈을 추가로 설치해줘야 한다.\n\n![스크린샷 2018-05-21 오후 11.22.36.png](https://steemitimages.com/DQmaowxTBfan24xafhwVnr5UoBwXXPqqpktdEQAq2rAYzin/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202018-05-21%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.22.36.png)\n특정 모듈에 타입 정의 패키지가 존재하는지는 이곳에서 확인이 가능하다.\n\nhttp://definitelytyped.org/\n\n필자가 처음 TS를 접했던 2016년에는 많은 라이브러리에 타입 정의 패키지가 존재하지 않아 타입 시스템의 도움을 받지 못했으나, 이제는 안정화 단계에 올라서있다고 단언할 수 있겠다.\n\n그리고 React로 개발을 할때, 대부분 사람들이 활용하는 **JSX 문법도 TS에서 훌륭하게 지원**하고 있으므로, 이 부분에 대해서도 문제없다. 물론 타입 시스템을 적용한 React 컴포넌트나 Props 타입과 관련한 지식이 필요하나, **기술적 장벽으로 작용할 수준은 전혀 아니다.**\n\nhttps://www.typescriptlang.org/docs/handbook/jsx.html\n\n## 생산성과의 Trade-Off\n정적 타입 시스템을 적극적으로 활용할 경우, JS로 코드를 작성했을 때보다 다소 **코드의 양이 늘어난다는 것은 부정할 수 없는 사실**이다. 이로 인해서 생산성이 저하될 수도 있다는 팀내 우려도 있었다.\n\n실제로 작성하는 코드의 양은 체감이 될 정도로 증가하였다. 하지만 그것으로 인해 생산성이 저하되었다고 단언하기는 힘들다. 위에서 언급한 다양한 기능들의 지원으로 **단순 작업의 속도는 상당부분 개선**되었으며, 오히려 불필요한 **디버깅 시간을 절감**함으로써, 코드 양 증가로 인한 **생산성 저하가 상쇄**되는 느낌이었다.\n\n## 다음 프로젝트도 TypeScript로…\n이상으로 현업에서 TS를 적용하면서 마주했던 우려와 실제로 사용하면서 느낀 점들을 정리해보았다. 상당 부분 간소화되어있으나, 이 포스팅을 통해서 TS 적용을 고민하는 팀/개인들이 의사결정을 함에 있어서 도움이 되기를 바라면서 글을 마무리하도록 하겠다.\n\n개인적으로도 팀 전체로도 TS 적용은 성공적이었다고 느꼈으며, 특히 필자는 큰 이변이 없는 한 앞으로도 하게 될 프로젝트는 TS를 사용할 생각이다.",
      "json_metadata": "{\"tags\":[\"kr-dev\",\"kr\",\"dev\",\"kr-newbie\",\"typescript\"],\"image\":[\"https://steemitimages.com/DQmYyuSRctnqYFMnGtXxLbA6ZRGMVTnUuVKLma5afSCfrv9/react_ts.png\",\"https://steemitimages.com/DQmRGXMQkxeEpy3guCMnFprU5biuzTwfzLyUqnD2hrDe4pV/May-21-2018%2021-37-33.gif\",\"https://steemitimages.com/DQmQL8eDcxQuXWpmAY4iLGG7kEqMrYfjqpadFHSH7y7bFT1/May-21-2018%2022-18-11.gif\",\"https://steemitimages.com/DQmaowxTBfan24xafhwVnr5UoBwXXPqqpktdEQAq2rAYzin/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202018-05-21%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.22.36.png\"],\"links\":[\"https://medium.com/@sangboaklee/typescript-%ED%98%84%EC%97%85-%EC%A0%81%EC%9A%A9-%ED%9B%84%EA%B8%B0-caad266c8142\",\"https://reactjs.org/\",\"https://redux.js.org/\",\"https://www.typescriptlang.org/\",\"https://angular.io/\",\"https://www.microsoft.com\",\"http://definitelytyped.org/\",\"https://www.typescriptlang.org/docs/handbook/jsx.html\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "kr-dev",
      "permlink": "typescript",
      "title": "[개발] TypeScript 현업 적용 후기"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-23T09:58:09",
  "trx_id": "06baed33cdcebe9229d6ae8890405a546b810109",
  "trx_in_block": 38,
  "virtual_op": 0
}
heejinupvoted (100.00%) @echo304 / typescript
2018/05/22 15:48:18
authorecho304
permlinktypescript
voterheejin
weight10000 (100.00%)
Transaction InfoBlock #22657239/Trx 3662669ef9ac3bf486e111f15befd18b9e2c3b1e
View Raw JSON Data
{
  "block": 22657239,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "typescript",
      "voter": "heejin",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-22T15:48:18",
  "trx_id": "3662669ef9ac3bf486e111f15befd18b9e2c3b1e",
  "trx_in_block": 12,
  "virtual_op": 0
}
echo304upvoted (100.00%) @echo304 / typescript
2018/05/22 15:15:33
authorecho304
permlinktypescript
voterecho304
weight10000 (100.00%)
Transaction InfoBlock #22656584/Trx 9edb5457b4ef6e50c4ea57041d49f379778465cc
View Raw JSON Data
{
  "block": 22656584,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "typescript",
      "voter": "echo304",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-22T15:15:33",
  "trx_id": "9edb5457b4ef6e50c4ea57041d49f379778465cc",
  "trx_in_block": 57,
  "virtual_op": 0
}
brainstormotupvoted (100.00%) @echo304 / typescript
2018/05/22 14:29:39
authorecho304
permlinktypescript
voterbrainstormot
weight10000 (100.00%)
Transaction InfoBlock #22655666/Trx b616de4c396a87607a7c10b7f049ebb777d81f37
View Raw JSON Data
{
  "block": 22655666,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "typescript",
      "voter": "brainstormot",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-22T14:29:39",
  "trx_id": "b616de4c396a87607a7c10b7f049ebb777d81f37",
  "trx_in_block": 26,
  "virtual_op": 0
}
jinh0729upvoted (4.00%) @echo304 / typescript
2018/05/22 13:51:36
authorecho304
permlinktypescript
voterjinh0729
weight400 (4.00%)
Transaction InfoBlock #22654905/Trx b88ab842181c3bad4bc9e7573b1a37c5e35e8290
View Raw JSON Data
{
  "block": 22654905,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "typescript",
      "voter": "jinh0729",
      "weight": 400
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-22T13:51:36",
  "trx_id": "b88ab842181c3bad4bc9e7573b1a37c5e35e8290",
  "trx_in_block": 10,
  "virtual_op": 0
}
echo304followed @kdj
2018/05/22 13:26:54
idfollow
json["follow",{"follower":"echo304","following":"kdj","what":["blog"]}]
required auths[]
required posting auths["echo304"]
Transaction InfoBlock #22654411/Trx 0cf5064bd8ca5a40990b707940a264a1d37eb3a2
View Raw JSON Data
{
  "block": 22654411,
  "op": [
    "custom_json",
    {
      "id": "follow",
      "json": "[\"follow\",{\"follower\":\"echo304\",\"following\":\"kdj\",\"what\":[\"blog\"]}]",
      "required_auths": [],
      "required_posting_auths": [
        "echo304"
      ]
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-22T13:26:54",
  "trx_id": "0cf5064bd8ca5a40990b707940a264a1d37eb3a2",
  "trx_in_block": 31,
  "virtual_op": 0
}
idontgnuupvoted (12.00%) @echo304 / typescript
2018/05/22 13:09:21
authorecho304
permlinktypescript
voteridontgnu
weight1200 (12.00%)
Transaction InfoBlock #22654060/Trx 0a60b22e111b6d040779b7cfa7aa6eb6186cd1d9
View Raw JSON Data
{
  "block": 22654060,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "typescript",
      "voter": "idontgnu",
      "weight": 1200
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-22T13:09:21",
  "trx_id": "0a60b22e111b6d040779b7cfa7aa6eb6186cd1d9",
  "trx_in_block": 44,
  "virtual_op": 0
}
2018/05/22 12:56:21
authorecho304
permlinktypescript
voterplgonzalezrx8
weight1200 (12.00%)
Transaction InfoBlock #22653800/Trx e984a1db6c0613e8a67650c1846faa0aaeb607c8
View Raw JSON Data
{
  "block": 22653800,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "typescript",
      "voter": "plgonzalezrx8",
      "weight": 1200
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-22T12:56:21",
  "trx_id": "e984a1db6c0613e8a67650c1846faa0aaeb607c8",
  "trx_in_block": 30,
  "virtual_op": 0
}
2018/05/22 12:49:12
authorecho304
permlinktypescript
votervoyagesofcarla2
weight600 (6.00%)
Transaction InfoBlock #22653657/Trx d8a453ce3bb75328ed784621e9edb08d2eda0202
View Raw JSON Data
{
  "block": 22653657,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "typescript",
      "voter": "voyagesofcarla2",
      "weight": 600
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-22T12:49:12",
  "trx_id": "d8a453ce3bb75328ed784621e9edb08d2eda0202",
  "trx_in_block": 17,
  "virtual_op": 0
}
2018/05/22 12:49:12
authorecho304
permlinktypescript
voterguangzhoulife
weight1200 (12.00%)
Transaction InfoBlock #22653657/Trx f0e113cd995f26bec1eb60f547348e48486d5982
View Raw JSON Data
{
  "block": 22653657,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "typescript",
      "voter": "guangzhoulife",
      "weight": 1200
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-22T12:49:12",
  "trx_id": "f0e113cd995f26bec1eb60f547348e48486d5982",
  "trx_in_block": 16,
  "virtual_op": 0
}
soundworksupvoted (9.29%) @echo304 / typescript
2018/05/22 12:49:12
authorecho304
permlinktypescript
votersoundworks
weight929 (9.29%)
Transaction InfoBlock #22653657/Trx 5441f4afd96b90df6fd6aaa80e645da8f65e3e19
View Raw JSON Data
{
  "block": 22653657,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "typescript",
      "voter": "soundworks",
      "weight": 929
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-22T12:49:12",
  "trx_id": "5441f4afd96b90df6fd6aaa80e645da8f65e3e19",
  "trx_in_block": 15,
  "virtual_op": 0
}
cjdupvoted (12.00%) @echo304 / typescript
2018/05/22 12:49:12
authorecho304
permlinktypescript
votercjd
weight1200 (12.00%)
Transaction InfoBlock #22653657/Trx e1fc477b9a1989b016a7ebca222ce1eff3197394
View Raw JSON Data
{
  "block": 22653657,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "typescript",
      "voter": "cjd",
      "weight": 1200
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-22T12:49:12",
  "trx_id": "e1fc477b9a1989b016a7ebca222ce1eff3197394",
  "trx_in_block": 13,
  "virtual_op": 0
}
umichupvoted (12.00%) @echo304 / typescript
2018/05/22 12:49:06
authorecho304
permlinktypescript
voterumich
weight1200 (12.00%)
Transaction InfoBlock #22653655/Trx e3d3dd6425ad9b6850b7fa8a0ce982f499b49301
View Raw JSON Data
{
  "block": 22653655,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "typescript",
      "voter": "umich",
      "weight": 1200
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-22T12:49:06",
  "trx_id": "e3d3dd6425ad9b6850b7fa8a0ce982f499b49301",
  "trx_in_block": 33,
  "virtual_op": 0
}
echo304published a new post: typescript
2018/05/22 12:32:21
authorecho304
body_** 이 글은 필자의 [Medium 포스팅](https://medium.com/@sangboaklee/typescript-%ED%98%84%EC%97%85-%EC%A0%81%EC%9A%A9-%ED%9B%84%EA%B8%B0-caad266c8142)을 포팅한 것 입니다_ ![react_ts.png](https://steemitimages.com/DQmYyuSRctnqYFMnGtXxLbA6ZRGMVTnUuVKLma5afSCfrv9/react_ts.png) <br /> [React](https://reactjs.org/)와 [Redux](https://redux.js.org/) 그리고 [TypeScript](https://www.typescriptlang.org/)는 국내에서도 더이상 생소한 단어가 아니다. 이미 많은 회사와 서비스에서 해당 기술들을 사용하고 있고 활발하게 정보가 공유되고 있다. 이미 진부한 내용일 수도 있으나 실제 서비스 되고 있는 제품에 React + Redux + TypeScript 조합을 적용한 경험을 널리 공유하고자 이 포스팅을 작성하게 되었다. _* 이 글은 기본적인 TypeScript에 대한 배경 지식을 전제로 하고 있습니다._ ## TypeScript 도입에 앞서 아마 현업에서 TypeScript(이하 TS)가 사용된 상당수의 케이스는 프론트엔드 기술로 [Angular](https://angular.io/)를 사용하는 경우가 아닐까 한다. Angular2 이후부터 TS를 권유하기 시작했으니 해당 언어를 실제 제품에 적용할 때는 Angular를 가장 먼저 떠올리게 된다. 하지만 현재 우리 팀의 주된 프론트엔드 기술은 React이었고, TS를 자연스럽게 적용할 수 있는지에 대한 의구심도 가졌다. 또한 자유분방한 JavaScript(이하 JS)를 만끽하던 팀원들이 가지는 타입 시스템에 대한 불신과 거부감 또한 TS를 적용하기에 큰 장벽으로 작용했다. 당시 우리 팀에서 가졌던 의문과 불안요소들을 정리해보았다. 1. **정적 타입 시스템이 정말 필요한가? 상당수의 버그를 잡아준다고 하는데, 타입 시스템의 도입으로 잡을 수 있는 버그가 유의미한 수준인가?** 2. **React 생태계와 TS의 호환성 문제는 없는가?** 3. **코드가 장황(Verbose)해짐으로써 생산성의 저하가 발생하지 않는가?** 4. **기타 마이너한 기술적 우려와 심리적인 저항감([M$](https://www.microsoft.com))** 이런 고민들이 있었지만 결과적으로 TS를 도입하게 되었고, 실제 제품에 적용하고 약 4개월이 지난 지금, 팀원들과의 대화를 통해 정리한 내용과 개인적인 소감을 버무려 사용 경험을 공유해보겠다. ## Happy and Safe Refactoring JS로 대규모 웹 어플리케이션을 개발할 때 종종 경험하는 어려움 중 하나는 바로 **여러 모듈에 걸쳐있는 객체의 리팩토링 작업**이다. 일반적인 타입-컴파일 언어와 달리 JS는 객체 속성의 이름은 변경하는 작업을 할 때 단순한 오타가 발생하더라도 런타임 환경에서만 발견할 수 있다. 개발 중에 테스트를 통해서 발견할 수 있다면 다행이지만, 프로덕션 레벨에서 발견되면 장애로 이어진다. 필자와 팀이 TS를 도입하고 크게만족하는 부분 중 하나가 리팩토링의 용이함이다. 사전에 정의된 객체의 인터페이스를 여러 모듈에 걸쳐서 빈틈없이 적용해두었다면, **인터페이스 정의 한군데만 수정해도 해당 객체의 수정된 부분이 사용되는 모든 파일에서 경고를 띄워주고 컴파일도 되지 않는다.** 특히 **Redux를 이용하여 어플리케이션의 상태관리를 할 경우 하부 State 객체들이 여러 React 컴포넌트에서 참조**하게 되는데, 단순한 State 필드 이름 변경부터 실제 값의 타입을 변경하는 변경까지 TS의 도움으로 안전한 리팩토링을 진행할 수 있었다. 버그를 사전에 잡아준다는 캐치프레이즈에 부정적이었던 필자도 몇차례에 걸친 위와 같은 경험을 통해서, 다양한 상황에서 정적 타입 시스템이 빛을 발할 수 있음을 인정하게 되었다. ## 훌륭한 도구 JS는 언어의 특성상 개발 도구의 도움 받을 수 있는 영역이 제한적이다. C#의 Visual Studio 수준의 지원은 기대하기 어렵다. TS를 사용함으로써 얻을 수 있는 또 다른 큰 이점은 훌륭한 도구의 지원을 받을 수 있다는 점이다. MS에서 개발한 언어인만큼 동 회사에서 개발하여 공개한 Visual Studio Code(이하 VS Code)와의 궁합이 매우 훌륭하다. 물론 C#과 Visual Studio 수준의 기능을 제공하지는 않지만, 타입 시스템을 비롯한 언어적 특성을 십분 활용한 VS Code의 기능은 생산성에 큰 도움을 준다. ![May-21-2018 21-37-33.gif](https://steemitimages.com/DQmRGXMQkxeEpy3guCMnFprU5biuzTwfzLyUqnD2hrDe4pV/May-21-2018%2021-37-33.gif) _향상된 Intellisense_ 위 예시는 VS Code의 Intellisense가 특정 객체의 속성을 자동완성 시켜주고 해당 속성의 타입까지 알려주는 모습이다. 일반적인 JS의 자동완성과는 달리 파일 내에서만 지원하는 것이 아니라, 특정 객체의 타입(인터페이스)가 올바르게 정의되어있다면 정확한 자동완성의 지원을 받을 수 있다. ![May-21-2018 22-18-11.gif](https://steemitimages.com/DQmQL8eDcxQuXWpmAY4iLGG7kEqMrYfjqpadFHSH7y7bFT1/May-21-2018%2022-18-11.gif) _Auto Import_ VS Code가 TS에 대응하여 제공하는 기능 중 하나인 Auto Import는 대규모 웹어플리케이션을 개발하면서 종종 경험하는 불편함 중 하나인 모듈 Import의 번거로움을 일거에 해결해준다. 특히 **Redux를 이용한 어플리케이션에서는 Action을 중심으로 모듈 Import가 빈번하게 발생**하는데, 이런 단순 작업의 로드를 상당 부분 줄여준다. VS Code가 제공하는 많은 기능 중 일부를 예시로 들었으나, 이외에도 다양한 기능들을 제공하고 있으며 대부분의 기능을 추가적인 Extension 설치 없이도 사용할 수 있다는 간편함 또한 장점이다. 웹 프론트엔드 개발을 할 때, 언제나 목마름을 느꼈던 훌륭한 도구에 대한 니즈는 TS와 VS Code를 사용함으로써 상당 부분 해소되었다. ## 기존 JS 생태계와의 호환성도 안정화 단계 React를 비롯한 JS 기반 라이브러리들이 TS에 원활하게 돌아가느냐에 대한 우려도 있었으나, 수개월 동안 사용한 유수의 라이브러리들은 이미 호환성에 대한 이슈가 거의 존재하지 않았다. JS 라이브러리를 TS에서 Import하여 사용하기 위해서는 해당 라이브러리의 타입이 정의된 모듈을 추가로 설치해줘야 한다. ![스크린샷 2018-05-21 오후 11.22.36.png](https://steemitimages.com/DQmaowxTBfan24xafhwVnr5UoBwXXPqqpktdEQAq2rAYzin/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202018-05-21%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.22.36.png) 특정 모듈에 타입 정의 패키지가 존재하는지는 이곳에서 확인이 가능하다. http://definitelytyped.org/ 필자가 처음 TS를 접했던 2016년에는 많은 라이브러리에 타입 정의 패키지가 존재하지 않아 타입 시스템의 도움을 받지 못했으나, 이제는 안정화 단계에 올라서있다고 단언할 수 있겠다. 그리고 React로 개발을 할때, 대부분 사람들이 활용하는 **JSX 문법도 TS에서 훌륭하게 지원**하고 있으므로, 이 부분에 대해서도 문제없다. 물론 타입 시스템을 적용한 React 컴포넌트나 Props 타입과 관련한 지식이 필요하나, **기술적 장벽으로 작용할 수준은 전혀 아니다.** https://www.typescriptlang.org/docs/handbook/jsx.html ## 생산성과의 Trade-Off 정적 타입 시스템을 적극적으로 활용할 경우, JS로 코드를 작성했을 때보다 다소 **코드의 양이 늘어난다는 것은 부정할 수 없는 사실**이다. 이로 인해서 생산성이 저하될 수도 있다는 팀내 우려도 있었다. 실제로 작성하는 코드의 양은 체감이 될 정도로 증가하였다. 하지만 그것으로 인해 생산성이 저하되었다고 단언하기는 힘들다. 위에서 언급한 다양한 기능들의 지원으로 **단순 작업의 속도는 상당부분 개선**되었으며, 오히려 불필요한 **디버깅 시간을 절감**함으로써, 코드 양 증가로 인한 **생산성 저하가 상쇄**되는 느낌이었다. ## 다음 프로젝트도 TypeScript로… 이상으로 현업에서 TS를 적용하면서 마주했던 우려와 실제로 사용하면서 느낀 점들을 정리해보았다. 상당 부분 간소화되어있으나, 이 포스팅을 통해서 TS 적용을 고민하는 팀/개인들이 의사결정을 함에 있어서 도움이 되기를 바라면서 글을 마무리하도록 하겠다. 개인적으로도 팀 전체로도 TS 적용은 성공적이었다고 느꼈으며, 특히 필자는 큰 이변이 없는 한 앞으로도 하게 될 프로젝트는 TS를 사용할 생각이다.
json metadata{"tags":["kr-dev","kr","dev","kr-newbie"],"image":["https://steemitimages.com/DQmYyuSRctnqYFMnGtXxLbA6ZRGMVTnUuVKLma5afSCfrv9/react_ts.png","https://steemitimages.com/DQmRGXMQkxeEpy3guCMnFprU5biuzTwfzLyUqnD2hrDe4pV/May-21-2018%2021-37-33.gif","https://steemitimages.com/DQmQL8eDcxQuXWpmAY4iLGG7kEqMrYfjqpadFHSH7y7bFT1/May-21-2018%2022-18-11.gif","https://steemitimages.com/DQmaowxTBfan24xafhwVnr5UoBwXXPqqpktdEQAq2rAYzin/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202018-05-21%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.22.36.png"],"links":["https://medium.com/@sangboaklee/typescript-%ED%98%84%EC%97%85-%EC%A0%81%EC%9A%A9-%ED%9B%84%EA%B8%B0-caad266c8142","https://reactjs.org/","https://redux.js.org/","https://www.typescriptlang.org/","https://angular.io/","https://www.microsoft.com","http://definitelytyped.org/","https://www.typescriptlang.org/docs/handbook/jsx.html"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkkr-dev
permlinktypescript
title[개발] TypeScript 현업 적용 후기
Transaction InfoBlock #22653320/Trx 25fd03a84cb043960a164ae6303cc30df4b7b551
View Raw JSON Data
{
  "block": 22653320,
  "op": [
    "comment",
    {
      "author": "echo304",
      "body": "_** 이 글은 필자의 [Medium 포스팅](https://medium.com/@sangboaklee/typescript-%ED%98%84%EC%97%85-%EC%A0%81%EC%9A%A9-%ED%9B%84%EA%B8%B0-caad266c8142)을 포팅한 것 입니다_\n\n![react_ts.png](https://steemitimages.com/DQmYyuSRctnqYFMnGtXxLbA6ZRGMVTnUuVKLma5afSCfrv9/react_ts.png)\n<br />\n[React](https://reactjs.org/)와 [Redux](https://redux.js.org/) 그리고 [TypeScript](https://www.typescriptlang.org/)는 국내에서도 더이상 생소한 단어가 아니다. 이미 많은 회사와 서비스에서 해당 기술들을 사용하고 있고 활발하게 정보가 공유되고 있다. 이미 진부한 내용일 수도 있으나 실제 서비스 되고 있는 제품에 React + Redux + TypeScript 조합을 적용한 경험을 널리 공유하고자 이 포스팅을 작성하게 되었다.\n\n_* 이 글은 기본적인 TypeScript에 대한 배경 지식을 전제로 하고 있습니다._\n\n## TypeScript 도입에 앞서\n아마 현업에서 TypeScript(이하 TS)가 사용된 상당수의 케이스는 프론트엔드 기술로 [Angular](https://angular.io/)를 사용하는 경우가 아닐까 한다. Angular2 이후부터 TS를 권유하기 시작했으니 해당 언어를 실제 제품에 적용할 때는 Angular를 가장 먼저 떠올리게 된다. 하지만 현재 우리 팀의 주된 프론트엔드 기술은 React이었고, TS를 자연스럽게 적용할 수 있는지에 대한 의구심도 가졌다.\n\n또한 자유분방한 JavaScript(이하 JS)를 만끽하던 팀원들이 가지는 타입 시스템에 대한 불신과 거부감 또한 TS를 적용하기에 큰 장벽으로 작용했다.\n\n당시 우리 팀에서 가졌던 의문과 불안요소들을 정리해보았다.\n\n\n1. **정적 타입 시스템이 정말 필요한가? 상당수의 버그를 잡아준다고 하는데, 타입 시스템의 도입으로 잡을 수 있는 버그가 유의미한 수준인가?**\n2. **React 생태계와 TS의 호환성 문제는 없는가?**\n3. **코드가 장황(Verbose)해짐으로써 생산성의 저하가 발생하지 않는가?**\n4. **기타 마이너한 기술적 우려와 심리적인 저항감([M$](https://www.microsoft.com))**\n\n이런 고민들이 있었지만 결과적으로 TS를 도입하게 되었고, 실제 제품에 적용하고 약 4개월이 지난 지금, 팀원들과의 대화를 통해 정리한 내용과 개인적인 소감을 버무려 사용 경험을 공유해보겠다.\n\n## Happy and Safe Refactoring\nJS로 대규모 웹 어플리케이션을 개발할 때 종종 경험하는 어려움 중 하나는 바로 **여러 모듈에 걸쳐있는 객체의 리팩토링 작업**이다. 일반적인 타입-컴파일 언어와 달리 JS는 객체 속성의 이름은 변경하는 작업을 할 때 단순한 오타가 발생하더라도 런타임 환경에서만 발견할 수 있다. 개발 중에 테스트를 통해서 발견할 수 있다면 다행이지만, 프로덕션 레벨에서 발견되면 장애로 이어진다.\n\n필자와 팀이 TS를 도입하고 크게만족하는 부분 중 하나가 리팩토링의 용이함이다. 사전에 정의된 객체의 인터페이스를 여러 모듈에 걸쳐서 빈틈없이 적용해두었다면, **인터페이스 정의 한군데만 수정해도 해당 객체의 수정된 부분이 사용되는 모든 파일에서 경고를 띄워주고 컴파일도 되지 않는다.**\n\n특히 **Redux를 이용하여 어플리케이션의 상태관리를 할 경우 하부 State 객체들이 여러 React 컴포넌트에서 참조**하게 되는데, 단순한 State 필드 이름 변경부터 실제 값의 타입을 변경하는 변경까지 TS의 도움으로 안전한 리팩토링을 진행할 수 있었다.\n\n버그를 사전에 잡아준다는 캐치프레이즈에 부정적이었던 필자도 몇차례에 걸친 위와 같은 경험을 통해서, 다양한 상황에서 정적 타입 시스템이 빛을 발할 수 있음을 인정하게 되었다.\n\n## 훌륭한 도구\nJS는 언어의 특성상 개발 도구의 도움 받을 수 있는 영역이 제한적이다. C#의 Visual Studio 수준의 지원은 기대하기 어렵다. TS를 사용함으로써 얻을 수 있는 또 다른 큰 이점은 훌륭한 도구의 지원을 받을 수 있다는 점이다.\n\nMS에서 개발한 언어인만큼 동 회사에서 개발하여 공개한 Visual Studio Code(이하 VS Code)와의 궁합이 매우 훌륭하다. 물론 C#과 Visual Studio 수준의 기능을 제공하지는 않지만, 타입 시스템을 비롯한 언어적 특성을 십분 활용한 VS Code의 기능은 생산성에 큰 도움을 준다.\n\n![May-21-2018 21-37-33.gif](https://steemitimages.com/DQmRGXMQkxeEpy3guCMnFprU5biuzTwfzLyUqnD2hrDe4pV/May-21-2018%2021-37-33.gif)\n_향상된 Intellisense_\n\n위 예시는 VS Code의 Intellisense가 특정 객체의 속성을 자동완성 시켜주고 해당 속성의 타입까지 알려주는 모습이다. 일반적인 JS의 자동완성과는 달리 파일 내에서만 지원하는 것이 아니라, 특정 객체의 타입(인터페이스)가 올바르게 정의되어있다면 정확한 자동완성의 지원을 받을 수 있다.\n\n![May-21-2018 22-18-11.gif](https://steemitimages.com/DQmQL8eDcxQuXWpmAY4iLGG7kEqMrYfjqpadFHSH7y7bFT1/May-21-2018%2022-18-11.gif)\n_Auto Import_\n\nVS Code가 TS에 대응하여 제공하는 기능 중 하나인 Auto Import는 대규모 웹어플리케이션을 개발하면서 종종 경험하는 불편함 중 하나인 모듈 Import의 번거로움을 일거에 해결해준다. 특히 **Redux를 이용한 어플리케이션에서는 Action을 중심으로 모듈 Import가 빈번하게 발생**하는데, 이런 단순 작업의 로드를 상당 부분 줄여준다.\n\nVS Code가 제공하는 많은 기능 중 일부를 예시로 들었으나, 이외에도 다양한 기능들을 제공하고 있으며 대부분의 기능을 추가적인 Extension 설치 없이도 사용할 수 있다는 간편함 또한 장점이다.\n\n웹 프론트엔드 개발을 할 때, 언제나 목마름을 느꼈던 훌륭한 도구에 대한 니즈는 TS와 VS Code를 사용함으로써 상당 부분 해소되었다.\n\n## 기존 JS 생태계와의 호환성도 안정화 단계\nReact를 비롯한 JS 기반 라이브러리들이 TS에 원활하게 돌아가느냐에 대한 우려도 있었으나, 수개월 동안 사용한 유수의 라이브러리들은 이미 호환성에 대한 이슈가 거의 존재하지 않았다.\n\nJS 라이브러리를 TS에서 Import하여 사용하기 위해서는 해당 라이브러리의 타입이 정의된 모듈을 추가로 설치해줘야 한다.\n\n![스크린샷 2018-05-21 오후 11.22.36.png](https://steemitimages.com/DQmaowxTBfan24xafhwVnr5UoBwXXPqqpktdEQAq2rAYzin/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202018-05-21%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.22.36.png)\n특정 모듈에 타입 정의 패키지가 존재하는지는 이곳에서 확인이 가능하다.\n\nhttp://definitelytyped.org/\n\n필자가 처음 TS를 접했던 2016년에는 많은 라이브러리에 타입 정의 패키지가 존재하지 않아 타입 시스템의 도움을 받지 못했으나, 이제는 안정화 단계에 올라서있다고 단언할 수 있겠다.\n\n그리고 React로 개발을 할때, 대부분 사람들이 활용하는 **JSX 문법도 TS에서 훌륭하게 지원**하고 있으므로, 이 부분에 대해서도 문제없다. 물론 타입 시스템을 적용한 React 컴포넌트나 Props 타입과 관련한 지식이 필요하나, **기술적 장벽으로 작용할 수준은 전혀 아니다.**\n\nhttps://www.typescriptlang.org/docs/handbook/jsx.html\n\n## 생산성과의 Trade-Off\n정적 타입 시스템을 적극적으로 활용할 경우, JS로 코드를 작성했을 때보다 다소 **코드의 양이 늘어난다는 것은 부정할 수 없는 사실**이다. 이로 인해서 생산성이 저하될 수도 있다는 팀내 우려도 있었다.\n\n실제로 작성하는 코드의 양은 체감이 될 정도로 증가하였다. 하지만 그것으로 인해 생산성이 저하되었다고 단언하기는 힘들다. 위에서 언급한 다양한 기능들의 지원으로 **단순 작업의 속도는 상당부분 개선**되었으며, 오히려 불필요한 **디버깅 시간을 절감**함으로써, 코드 양 증가로 인한 **생산성 저하가 상쇄**되는 느낌이었다.\n\n## 다음 프로젝트도 TypeScript로…\n이상으로 현업에서 TS를 적용하면서 마주했던 우려와 실제로 사용하면서 느낀 점들을 정리해보았다. 상당 부분 간소화되어있으나, 이 포스팅을 통해서 TS 적용을 고민하는 팀/개인들이 의사결정을 함에 있어서 도움이 되기를 바라면서 글을 마무리하도록 하겠다.\n\n개인적으로도 팀 전체로도 TS 적용은 성공적이었다고 느꼈으며, 특히 필자는 큰 이변이 없는 한 앞으로도 하게 될 프로젝트는 TS를 사용할 생각이다.",
      "json_metadata": "{\"tags\":[\"kr-dev\",\"kr\",\"dev\",\"kr-newbie\"],\"image\":[\"https://steemitimages.com/DQmYyuSRctnqYFMnGtXxLbA6ZRGMVTnUuVKLma5afSCfrv9/react_ts.png\",\"https://steemitimages.com/DQmRGXMQkxeEpy3guCMnFprU5biuzTwfzLyUqnD2hrDe4pV/May-21-2018%2021-37-33.gif\",\"https://steemitimages.com/DQmQL8eDcxQuXWpmAY4iLGG7kEqMrYfjqpadFHSH7y7bFT1/May-21-2018%2022-18-11.gif\",\"https://steemitimages.com/DQmaowxTBfan24xafhwVnr5UoBwXXPqqpktdEQAq2rAYzin/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202018-05-21%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2011.22.36.png\"],\"links\":[\"https://medium.com/@sangboaklee/typescript-%ED%98%84%EC%97%85-%EC%A0%81%EC%9A%A9-%ED%9B%84%EA%B8%B0-caad266c8142\",\"https://reactjs.org/\",\"https://redux.js.org/\",\"https://www.typescriptlang.org/\",\"https://angular.io/\",\"https://www.microsoft.com\",\"http://definitelytyped.org/\",\"https://www.typescriptlang.org/docs/handbook/jsx.html\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "kr-dev",
      "permlink": "typescript",
      "title": "[개발] TypeScript 현업 적용 후기"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-22T12:32:21",
  "trx_id": "25fd03a84cb043960a164ae6303cc30df4b7b551",
  "trx_in_block": 32,
  "virtual_op": 0
}
kdjupvoted (50.00%) @echo304 / react
2018/05/20 15:12:03
authorecho304
permlinkreact
voterkdj
weight5000 (50.00%)
Transaction InfoBlock #22598922/Trx 9245761737399c953d78aae489a0201ee509d856
View Raw JSON Data
{
  "block": 22598922,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "react",
      "voter": "kdj",
      "weight": 5000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-20T15:12:03",
  "trx_id": "9245761737399c953d78aae489a0201ee509d856",
  "trx_in_block": 40,
  "virtual_op": 0
}
heejinupvoted (100.00%) @echo304 / react
2018/05/18 08:02:03
authorecho304
permlinkreact
voterheejin
weight10000 (100.00%)
Transaction InfoBlock #22532735/Trx 7c164a2ab1cefb47a9a771386af2869f92e3942f
View Raw JSON Data
{
  "block": 22532735,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "react",
      "voter": "heejin",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-18T08:02:03",
  "trx_id": "7c164a2ab1cefb47a9a771386af2869f92e3942f",
  "trx_in_block": 38,
  "virtual_op": 0
}
foodmockgameupvoted (100.00%) @echo304 / react
2018/05/18 01:32:00
authorecho304
permlinkreact
voterfoodmockgame
weight10000 (100.00%)
Transaction InfoBlock #22524934/Trx 084629f09bce2261156d45e2df3c5a8f4de81770
View Raw JSON Data
{
  "block": 22524934,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "react",
      "voter": "foodmockgame",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-18T01:32:00",
  "trx_id": "084629f09bce2261156d45e2df3c5a8f4de81770",
  "trx_in_block": 47,
  "virtual_op": 0
}
echo304upvoted (100.00%) @dorian-lee / 5lvyrd
2018/05/17 13:31:36
authordorian-lee
permlink5lvyrd
voterecho304
weight10000 (100.00%)
Transaction InfoBlock #22510529/Trx 44ab2cacf56b04720600f6d128260e337e94b98c
View Raw JSON Data
{
  "block": 22510529,
  "op": [
    "vote",
    {
      "author": "dorian-lee",
      "permlink": "5lvyrd",
      "voter": "echo304",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-17T13:31:36",
  "trx_id": "44ab2cacf56b04720600f6d128260e337e94b98c",
  "trx_in_block": 2,
  "virtual_op": 0
}
echo304upvoted (100.00%) @asinayo / jabsteem-and-4
2018/05/17 12:38:03
authorasinayo
permlinkjabsteem-and-4
voterecho304
weight10000 (100.00%)
Transaction InfoBlock #22509459/Trx 10ccf401fb5b80996ccabd4c40178994f831184c
View Raw JSON Data
{
  "block": 22509459,
  "op": [
    "vote",
    {
      "author": "asinayo",
      "permlink": "jabsteem-and-4",
      "voter": "echo304",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-17T12:38:03",
  "trx_id": "10ccf401fb5b80996ccabd4c40178994f831184c",
  "trx_in_block": 18,
  "virtual_op": 0
}
brainstormotupvoted (100.00%) @echo304 / react
2018/05/17 11:57:03
authorecho304
permlinkreact
voterbrainstormot
weight10000 (100.00%)
Transaction InfoBlock #22508639/Trx 1c06762f269170eb27e3e6cd33a13857c9c2f141
View Raw JSON Data
{
  "block": 22508639,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "react",
      "voter": "brainstormot",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-17T11:57:03",
  "trx_id": "1c06762f269170eb27e3e6cd33a13857c9c2f141",
  "trx_in_block": 38,
  "virtual_op": 0
}
kezymaupvoted (100.00%) @echo304 / react
2018/05/17 08:02:45
authorecho304
permlinkreact
voterkezyma
weight10000 (100.00%)
Transaction InfoBlock #22503953/Trx 8f88075ca87388b11c34f7bfab4f4fd0c6416a7c
View Raw JSON Data
{
  "block": 22503953,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "react",
      "voter": "kezyma",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-17T08:02:45",
  "trx_id": "8f88075ca87388b11c34f7bfab4f4fd0c6416a7c",
  "trx_in_block": 1,
  "virtual_op": 0
}
echo304upvoted (100.00%) @echo304 / react
2018/05/17 07:35:42
authorecho304
permlinkreact
voterecho304
weight10000 (100.00%)
Transaction InfoBlock #22503412/Trx 7fb2486e797796953d0abe1c47e35f2836192d77
View Raw JSON Data
{
  "block": 22503412,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "react",
      "voter": "echo304",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-17T07:35:42",
  "trx_id": "7fb2486e797796953d0abe1c47e35f2836192d77",
  "trx_in_block": 7,
  "virtual_op": 0
}
2018/05/17 07:32:03
authorcheetah
bodyHi! I am a robot. I just upvoted you! I found similar content that readers might be interested in: https://medium.com/@sangboaklee/react-%ED%85%8C%EC%8A%A4%ED%8C%85-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-1c3719cee5af
json metadata
parent authorecho304
parent permlinkreact
permlinkcheetah-re-echo304react
title
Transaction InfoBlock #22503339/Trx b3f39041bd53d3d8b104b8526d50a5aaedded2a3
View Raw JSON Data
{
  "block": 22503339,
  "op": [
    "comment",
    {
      "author": "cheetah",
      "body": "Hi! I am a robot. I just upvoted you! I found similar content that readers might be interested in:\nhttps://medium.com/@sangboaklee/react-%ED%85%8C%EC%8A%A4%ED%8C%85-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-1c3719cee5af",
      "json_metadata": "",
      "parent_author": "echo304",
      "parent_permlink": "react",
      "permlink": "cheetah-re-echo304react",
      "title": ""
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-17T07:32:03",
  "trx_id": "b3f39041bd53d3d8b104b8526d50a5aaedded2a3",
  "trx_in_block": 22,
  "virtual_op": 0
}
cheetahupvoted (0.08%) @echo304 / react
2018/05/17 07:32:00
authorecho304
permlinkreact
votercheetah
weight8 (0.08%)
Transaction InfoBlock #22503338/Trx 9b4a441239cc3b7891e06e7b16526751e65c5e11
View Raw JSON Data
{
  "block": 22503338,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "react",
      "voter": "cheetah",
      "weight": 8
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-17T07:32:00",
  "trx_id": "9b4a441239cc3b7891e06e7b16526751e65c5e11",
  "trx_in_block": 16,
  "virtual_op": 0
}
ax3upvoted (1.00%) @echo304 / react
2018/05/17 07:32:00
authorecho304
permlinkreact
voterax3
weight100 (1.00%)
Transaction InfoBlock #22503338/Trx 38c408b0b093a1268141bdcbc269e18380b9f4e7
View Raw JSON Data
{
  "block": 22503338,
  "op": [
    "vote",
    {
      "author": "echo304",
      "permlink": "react",
      "voter": "ax3",
      "weight": 100
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-17T07:32:00",
  "trx_id": "38c408b0b093a1268141bdcbc269e18380b9f4e7",
  "trx_in_block": 6,
  "virtual_op": 0
}
echo304published a new post: react
2018/05/17 07:31:51
authorecho304
body** *이 글은 필자의 Medium 포스팅을 포팅한 것 입니다* # Enzyme을 활용하여 React 컴포넌트 테스팅 코드 작성하기 ![enzyme.jpeg](https://steemitimages.com/DQmUaYAPqpMGUzWdXxNaF4uc6u3G44n9uiezxKTivkAp76i/enzyme.jpeg) 이번 포스팅에서는 Airbnb에서 오픈소스화 한 React 테스팅 유틸리티인 [Enzyme](https://github.com/airbnb/enzyme)을 활용하여 React 컴포넌트 테스팅 코드 작성하는 법을 알아보도록 하겠다. >Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components’ output. React는 자체적인 [테스팅 유틸리티](https://reactjs.org/docs/test-utils.html)를 제공하고 있으나, 공식 문서에서도 Airbnb의 Enzyme을 사용하도록 권장하고 있을 정도로 상당히 편리하고 직관적인 API를 제공하고 있다. 세부적인 API reference는 Enzyme 공식 문서를 통해서 확인할 수 있으니, 실제적으로 적용할 수 있는 예제 위주로 글을 풀어나갈 것이며 [Mocha](https://mochajs.org/)와 Chai에 대한 상세한 설명은 생략하도록 하겠다. ## Enzyme 세팅하기 Enzyme은 기존의 테스트 환경에서 `enzyme` 모듈만 추가해주면 간단하게 설정이 가능하다. ``` npm i --save-dev enzyme ``` `enzyme` 모듈 설치 후 테스트 파일에서 필요한 API를 불러와 아래와 같이 컴포넌트 테스트에 활용할 수 있다. ``` /* ... */ import { shallow } from 'enzyme'; describe('<Foo />', () => { it('should render one <div>', () => { const wrapper = shallow(<Foo />); expect(wrapper.find('div')).to.have.lengthOf(1); }); }); ``` <br> ## 렌더링 테스트하기 Enzyme을 통해서(혹은 기본 제공되는 ReactTestUtils을 통해) 테스트하는 가장 기본적인 항목은 해당 컴포넌트가 의도한대로 내용물을 그려주느냐 하는 것이다. 좀 더 쉽게 설명하면 `render()` 메소드가 리턴하는 요소를 테스트하는 것이다. *예시) 기본 중의 기본* ```javascript /* ... */ import React from 'react'; import { shallow } from 'enzyme'; class Foo extends React.Component { _something() { /* DO SOMETHING */ } render() { return ( <div className="foo"> <span className="bar">baz</span> </div> ); } } describe('<Foo />', () => { let wrapper; beforeEach(() => { wrapper = shallow(<Foo />); }); it('should render one <div> with class foo', () => { expect(wrapper.find('div.foo')).to.have.lengthOf(1); }); it('should render one <span> with class bar', () => { expect(wrapper.find('span.bar')).to.have.lengthOf(1); }); it('should render baz inside <span> with class bar', () => { expect(wrapper.find('span.bar').text()).to.be.equal('baz'); }); }); ``` <br> 만약 `render()` 메소드 안에 부모 컴포넌트에서 주어진 `props`에 따라 조건부로 엘리먼트를 그려줘야 한다면 다음과 같이 테스트 할 수 있다. *예시) `props`에 따른 조건부 렌더링 테스트하기* ```javascript /* ... */ class Foo extends React.Component { /* ... */ render() { if (this.props.shouldDrawFoo) { return ( <div className="foo"> foo </div> ); } return ( <div className="bar"> bar </div> ); } } describe('<Foo />', () => { it('should render one <div> with class foo', () => { const wrapper = shallow(<Foo shouldDrawFoo={true} />); // Pass prop inside shallow method call expect(wrapper.find('div.foo').text()).to.be.equal('foo'); }); it('should render one <div> with class bar', () => { const wrapper = shallow(<Foo shouldDrawFoo={false} />); // Pass prop inside shallow method call expect(wrapper.find('div.bar').text()).to.be.equal('bar'); }); }); ``` <br> `div`나 `span` 같은 DOM 엘리먼트 뿐만 아니라 동일한 React 컴포넌트 역시 같은 방법으로 테스트가 가능하다. *예시) 또 다른 컴포넌트를 그리는지 테스트하기* ```javascript /* ... */ import Bar from './bar'; class Foo extends React.Component { /* ... */ render() { return ( <Bar /> ); } } describe('<Foo />', () => { it('should render <Bar />', () => { const wrapper = shallow(<Foo />); expect(wrapper.find(Bar)).to.have.lengthOf(1); }); }); ``` <br> ## Method 테스트하기 Webpack을 이용하여 ES2015의 클래스 문법을 사용하여 React 컴포넌트를 작성할 때, 비즈니스 로직을 포함한 메소드를 만드는 경우가 많다. 컴포넌트를 테스트할 때, 단순한 렌더링 테스트 뿐만 아니라 이러한 메소드 역시 테스트 해줄 필요가 있다. 메소드만 독립적으로 테스트 하는 방법은 크게 두가지가 있다. ### 1. 프로토타입을 활용하는 방법 아시다시피 ES2015의 클래스는 본질적으로 자바스크립트의 프로토타입 상속 체계를 기반으로 만들어진 [Syntactic sugar](http://web-front-end.tistory.com/26)이다. 따라서 해당 클래스 안에 만들어진 메소드는 그 클래스의 프로토타입을 통해 접근하고 테스트 할 수 있다. 아래의 예시를 참고 하도록 하자. *예시) 프로토타입을 이용한 메소드 테스트* ```javascript /* ... */ class Foo extends React.Component { /* ... */ test() { return 'foo'; } render() { /* ... */ } } describe('<Foo />', () => { describe('#test', () => { const testFunc = Foo.prototype.test; it('should return foo', () => { expect(testFunc()).to.be.eq('foo'); }); }); }); ``` <br> 다만 이 방법은 해당 메소드가 `this` 키워드로 컴포넌트의 `state`나 `props`에 접근하고자 할 때 의도한대로 동작을 하지 않는 문제가 있어 추천하지 않는다. *예시) `this` 키워드가 컴포넌트 인스턴스를 가르키지 않는다* ```javascript /* ... */ class Foo extends React.Component { state = { test: 'test' } /* ... */ test() { return this.state.test; } render() { /* ... */ } } describe('<Foo />', () => { describe('#test', () => { const testFunc = Foo.prototype.test; it('should return foo', () => { expect(testFunc()).to.be.eq('foo'); // Error!! this.state is undefined!! }); }); }); ``` <br> Enzyme은 이러한 문제를 위해 해당 React 컴포넌트의 인스턴스로 접근하는 API를 제공하고 있다. ### 2. Enzyme의 instance 메소드 활용하기 컴포넌트의 메소드를 테스트하는 다른 방법으로는 Enzyme의 `instance()` API를 이용하는 것이다. `shallow`나 `mount`를 이용하여 ReactWrapper를 생성한 후에 `instance()` 메소드를 호출해줌으로써 해당 React 컴포넌트의 인스턴스에 접근할 수 있게 된다. 그 후에 해당 인스턴스의 메소드를 테스트하게 되면 `this` 키워드가 의도한대로 작동하게 되어 문제없이 테스트를 진행할 수 있다. *예시) instance API 사용하기* ```javascript /* ... */ class Foo extends React.Component { /* ... */ test() { return 'foo'; } render() { /* ... */ } } describe('<Foo />', () => { describe('#test', () => { const instance = shallow(<Foo />).instance(); // Use instance() API of Enzyme it('should return foo', () => { expect(instance.test()).to.be.eq('foo'); }); }); }); ``` <br> ## Lifecycle 테스트하기 라이프사이클은 React로 어플리케이션을 만들 때, 빈번하게 사용되는 기능인데, 이것 역시 Enzyme으로 손쉽게 테스트 할 수 있다. 필자는 본 섹션의 예시 코드에서 특정 메소드의 호출 여부를 확인할 수 있도록 도와주는 [sinon.js](http://sinonjs.org/)를 활용하도록 하겠다. 예를 들어 아래와 같은 컴포넌트가 있다고 가정하자. *예시) 라이프사이클 훅에서 특정 메소드를 호출하는 컴포넌트* ```javascript class Foo extends React.Component { componentDidMount() { this.props.callback(); } /* ... */ render() { /* ... */ } } ``` <br> `componentDidMount` hook에서 `props`로 주어진 `callback` 함수를 한번 호출하는 컴포넌트이다. `callback` 함수의 동작 자체는 따로 테스트를 하면 될 것이고, 여기서는 해당 컴포넌트가 마운트 된 후에 해당 함수가 호출이 되었는지를 테스트 할 필요가 있다. *예시) 함수에 스파이를 부착하여 넘김으로써 해당 함수의 호출 여부를 테스트 할 수 있다* ```javascript describe('<Foo />', () => { describe('#componentDidMount', () => { it('should return foo', () => { const cb = sinon.spy(); mount(<Foo callback={cb} />); expect(cb.calledOnce).to.be.eq(true); }); }); }); ``` <br> ### mount vs. shallow 그리고 위의 예시에서는 이전의 테스트 코드에서 사용하던 `shallow` API 대신에 `mount`를 사용하였는데, 두 API는 유사한 동작을 하나 `mount`가 좀 더 실제 컴포넌트의 동작과 유사한 수준의 동작을 한다. 공식 문서에서는 테스트 해야할 컴포넌트가 DOM API와 인터랙션하거나 라이프사이클 훅을 모두 활용하는 경우에 `mount` API의 사용을 가이드하고 있다. 특히 라이프사이클 훅 호출 여부는 `mount` API와 `shallow` API가 상이하므로 유의가 필요하다. 간단하게 정리하면 `mount` API는 모든 라이프사이클 훅이 호출되고 `shallow` API는 `componentDidMount`와 `componentDidUpdate`를 제외하고 라이프사이클 훅이 호출된다. 이러한 차이점에 유의하여 선별적으로 API를 사용하도록 하자. ## React 테스트하기… React는 Enzyme과 같은 유틸리티의 도움으로 컴포넌트 단위로 유닛 테스트가 상당히 용이하다. 비록 완벽한 브라우저 상의 테스트는 아닐지라도 이런 유틸리티 덕분에 렌더링과 주요 비즈니스 로직 등을 테스트하기에 부족함이 없기에 React로 만든 웹앱은 테스트가 상당히 용이한 편이다. (Angular 1.x의 테스트와 비교하면…) 상대적으로 적은 노력으로 큰 효과를 얻을 수 있다는 점이 많은 사람들로 하여금 React를 사용하게 만드는 한가지 이유라고 생각한다. <br> ***스팀잇에 처음으로 쓰는 글이 개발 관련 글이네요...아직 익숙하지가 않아서 기존 글을 포팅하는 수준이지만, 계속해서 컨텐츠를 생산해볼까 합니다. 응원 및 조언 부탁드립니다!*
json metadata{"tags":["kr-dev","kr","dev"],"image":["https://steemitimages.com/DQmUaYAPqpMGUzWdXxNaF4uc6u3G44n9uiezxKTivkAp76i/enzyme.jpeg"],"links":["https://github.com/airbnb/enzyme","https://reactjs.org/docs/test-utils.html","https://mochajs.org/","http://web-front-end.tistory.com/26","http://sinonjs.org/"],"app":"steemit/0.1","format":"markdown"}
parent author
parent permlinkkr-dev
permlinkreact
title[개발] React 테스트 코드 작성하기
Transaction InfoBlock #22503335/Trx 0868a0da1273f5b0cf46431eb075c093461320fd
View Raw JSON Data
{
  "block": 22503335,
  "op": [
    "comment",
    {
      "author": "echo304",
      "body": "** *이 글은 필자의 Medium 포스팅을 포팅한 것 입니다*\n\n# Enzyme을 활용하여 React 컴포넌트 테스팅 코드 작성하기\n![enzyme.jpeg](https://steemitimages.com/DQmUaYAPqpMGUzWdXxNaF4uc6u3G44n9uiezxKTivkAp76i/enzyme.jpeg)\n\n이번 포스팅에서는 Airbnb에서 오픈소스화 한 React 테스팅 유틸리티인 [Enzyme](https://github.com/airbnb/enzyme)을 활용하여 React 컴포넌트 테스팅 코드 작성하는 법을 알아보도록 하겠다.\n\n>Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components’ output.\n\nReact는 자체적인 [테스팅 유틸리티](https://reactjs.org/docs/test-utils.html)를 제공하고 있으나, 공식 문서에서도 Airbnb의 Enzyme을 사용하도록 권장하고 있을 정도로 상당히 편리하고 직관적인 API를 제공하고 있다. 세부적인 API reference는 Enzyme 공식 문서를 통해서 확인할 수 있으니, 실제적으로 적용할 수 있는 예제 위주로 글을 풀어나갈 것이며 [Mocha](https://mochajs.org/)와 Chai에 대한 상세한 설명은 생략하도록 하겠다.\n\n## Enzyme 세팅하기\nEnzyme은 기존의 테스트 환경에서 `enzyme` 모듈만 추가해주면 간단하게 설정이 가능하다.\n```\nnpm i --save-dev enzyme\n```\n\n`enzyme` 모듈 설치 후 테스트 파일에서 필요한 API를 불러와 아래와 같이 컴포넌트 테스트에 활용할 수 있다.\n```\n/* ... */\nimport { shallow } from 'enzyme';\n\ndescribe('<Foo />', () => {\n  it('should render one <div>', () => {\n    const wrapper = shallow(<Foo />);\n    expect(wrapper.find('div')).to.have.lengthOf(1);\n  });\n});\n```\n<br>\n\n## 렌더링 테스트하기\nEnzyme을 통해서(혹은 기본 제공되는 ReactTestUtils을 통해) 테스트하는 가장 기본적인 항목은 해당 컴포넌트가 의도한대로 내용물을 그려주느냐 하는 것이다. 좀 더 쉽게 설명하면 `render()` 메소드가 리턴하는 요소를 테스트하는 것이다.\n\n*예시) 기본 중의 기본*\n```javascript\n/* ... */\nimport React from 'react';\nimport { shallow } from 'enzyme';\n\nclass Foo extends React.Component {\n  _something() {\n    /* DO SOMETHING */\n  }\n  \n  render() {\n    return (\n      <div className=\"foo\">\n        <span className=\"bar\">baz</span>\n      </div>\n    );\n  }\n}\n\ndescribe('<Foo />', () => {\n  let wrapper;\n  beforeEach(() => { wrapper = shallow(<Foo />); });\n                    \n  it('should render one <div> with class foo', () => {\n    expect(wrapper.find('div.foo')).to.have.lengthOf(1);\n  });\n  it('should render one <span> with class bar', () => {\n    expect(wrapper.find('span.bar')).to.have.lengthOf(1);\n  });\n  it('should render baz inside <span> with class bar', () => {\n    expect(wrapper.find('span.bar').text()).to.be.equal('baz');\n  });\n});\n```\n<br>\n\n만약 `render()` 메소드 안에 부모 컴포넌트에서 주어진 `props`에 따라 조건부로 엘리먼트를 그려줘야 한다면 다음과 같이 테스트 할 수 있다.\n\n\n*예시) `props`에 따른 조건부 렌더링 테스트하기*\n```javascript\n/* ... */\n\nclass Foo extends React.Component {\n  \n  /* ... */  \n  \n  render() {\n    if (this.props.shouldDrawFoo) {\n      return (\n        <div className=\"foo\">\n          foo\n        </div>\n      );\n    }\n    return (\n      <div className=\"bar\">\n        bar\n      </div>\n    );\n  }\n}\n\ndescribe('<Foo />', () => {\n  it('should render one <div> with class foo', () => {\n    const wrapper = shallow(<Foo shouldDrawFoo={true} />); // Pass prop inside shallow method call\n    expect(wrapper.find('div.foo').text()).to.be.equal('foo');\n  });\n  it('should render one <div> with class bar', () => {\n    const wrapper = shallow(<Foo shouldDrawFoo={false} />); // Pass prop inside shallow method call\n    expect(wrapper.find('div.bar').text()).to.be.equal('bar');\n  });\n});\n```\n<br>\n`div`나 `span` 같은 DOM 엘리먼트 뿐만 아니라 동일한 React 컴포넌트 역시 같은 방법으로 테스트가 가능하다.\n\n\n*예시) 또 다른 컴포넌트를 그리는지 테스트하기*\n```javascript\n/* ... */\nimport Bar from './bar';\n\nclass Foo extends React.Component {\n  \n  /* ... */  \n  \n  render() {\n    return (\n      <Bar />\n    );\n  }\n}\n\ndescribe('<Foo />', () => {\n  it('should render <Bar />', () => {\n    const wrapper = shallow(<Foo />); \n    expect(wrapper.find(Bar)).to.have.lengthOf(1);\n  });\n});\n```\n<br>\n\n## Method 테스트하기\nWebpack을 이용하여 ES2015의 클래스 문법을 사용하여 React 컴포넌트를 작성할 때, 비즈니스 로직을 포함한 메소드를 만드는 경우가 많다. 컴포넌트를 테스트할 때, 단순한 렌더링 테스트 뿐만 아니라 이러한 메소드 역시 테스트 해줄 필요가 있다. 메소드만 독립적으로 테스트 하는 방법은 크게 두가지가 있다.\n\n### 1. 프로토타입을 활용하는 방법\n아시다시피 ES2015의 클래스는 본질적으로 자바스크립트의 프로토타입 상속 체계를 기반으로 만들어진 [Syntactic sugar](http://web-front-end.tistory.com/26)이다. 따라서 해당 클래스 안에 만들어진 메소드는 그 클래스의 프로토타입을 통해 접근하고 테스트 할 수 있다. 아래의 예시를 참고 하도록 하자.\n\n*예시) 프로토타입을 이용한 메소드 테스트*\n```javascript\n/* ... */\n\nclass Foo extends React.Component {\n  \n  /* ... */  \n\n  test() {\n    return 'foo';\n  }\n  \n  render() { /* ... */ }\n}\n      \ndescribe('<Foo />', () => {\n  describe('#test', () => {\n    const testFunc = Foo.prototype.test;\n    it('should return foo', () => {\n      expect(testFunc()).to.be.eq('foo');\n    });\n  });\n});\n```\n<br>\n\n다만 이 방법은 해당 메소드가 `this` 키워드로 컴포넌트의 `state`나 `props`에 접근하고자 할 때 의도한대로 동작을 하지 않는 문제가 있어 추천하지 않는다.\n\n*예시) `this` 키워드가 컴포넌트 인스턴스를 가르키지 않는다*\n```javascript\n/* ... */\n\nclass Foo extends React.Component {\n  state = { test: 'test' }\n  \n  /* ... */  \n\n  test() {\n    return this.state.test;\n  }\n  \n  render() { /* ... */ }\n}\n      \ndescribe('<Foo />', () => {\n  describe('#test', () => {\n    const testFunc = Foo.prototype.test;\n    it('should return foo', () => {\n      expect(testFunc()).to.be.eq('foo'); // Error!! this.state is undefined!!\n    });\n  });\n});\n```\n<br>\nEnzyme은 이러한 문제를 위해 해당 React 컴포넌트의 인스턴스로 접근하는 API를 제공하고 있다.\n\n### 2. Enzyme의 instance 메소드 활용하기\n컴포넌트의 메소드를 테스트하는 다른 방법으로는 Enzyme의 `instance()` API를 이용하는 것이다. `shallow`나 `mount`를 이용하여 ReactWrapper를 생성한 후에 `instance()` 메소드를 호출해줌으로써 해당 React 컴포넌트의 인스턴스에 접근할 수 있게 된다. 그 후에 해당 인스턴스의 메소드를 테스트하게 되면 `this` 키워드가 의도한대로 작동하게 되어 문제없이 테스트를 진행할 수 있다.\n\n*예시) instance API 사용하기*\n```javascript\n/* ... */\n\nclass Foo extends React.Component {\n  \n  /* ... */  \n\n  test() {\n    return 'foo';\n  }\n  \n  render() { /* ... */ }\n}\n      \ndescribe('<Foo />', () => {\n  describe('#test', () => {\n    const instance = shallow(<Foo />).instance(); // Use instance() API of Enzyme\n    it('should return foo', () => {\n      expect(instance.test()).to.be.eq('foo');\n    });\n  });\n});\n```\n<br>\n\n## Lifecycle 테스트하기\n라이프사이클은 React로 어플리케이션을 만들 때, 빈번하게 사용되는 기능인데, 이것 역시 Enzyme으로 손쉽게 테스트 할 수 있다. 필자는 본 섹션의 예시 코드에서 특정 메소드의 호출 여부를 확인할 수 있도록 도와주는 [sinon.js](http://sinonjs.org/)를 활용하도록 하겠다. 예를 들어 아래와 같은 컴포넌트가 있다고 가정하자.\n\n*예시) 라이프사이클 훅에서 특정 메소드를 호출하는 컴포넌트*\n```javascript\nclass Foo extends React.Component {\n  componentDidMount() {\n    this.props.callback();\n  }\n  \n  /* ... */  \n  \n  render() { /* ... */ }\n}\n```\n<br>\n\n`componentDidMount` hook에서 `props`로 주어진 `callback` 함수를 한번 호출하는 컴포넌트이다. `callback` 함수의 동작 자체는 따로 테스트를 하면 될 것이고, 여기서는 해당 컴포넌트가 마운트 된 후에 해당 함수가 호출이 되었는지를 테스트 할 필요가 있다.\n\n*예시) 함수에 스파이를 부착하여 넘김으로써 해당 함수의 호출 여부를 테스트 할 수 있다*\n```javascript\ndescribe('<Foo />', () => {\n  describe('#componentDidMount', () => {\n    it('should return foo', () => {\n      const cb = sinon.spy();\n      mount(<Foo callback={cb} />);\n      expect(cb.calledOnce).to.be.eq(true);\n    });\n  });\n});\n```\n<br>\n### mount vs. shallow\n그리고 위의 예시에서는 이전의 테스트 코드에서 사용하던 `shallow` API 대신에 `mount`를 사용하였는데, 두 API는 유사한 동작을 하나 `mount`가 좀 더 실제 컴포넌트의 동작과 유사한 수준의 동작을 한다. 공식 문서에서는 테스트 해야할 컴포넌트가 DOM API와 인터랙션하거나 라이프사이클 훅을 모두 활용하는 경우에 `mount` API의 사용을 가이드하고 있다.\n\n특히 라이프사이클 훅 호출 여부는 `mount` API와 `shallow` API가 상이하므로 유의가 필요하다. 간단하게 정리하면 `mount` API는 모든 라이프사이클 훅이 호출되고 `shallow` API는 `componentDidMount`와 `componentDidUpdate`를 제외하고 라이프사이클 훅이 호출된다. 이러한 차이점에 유의하여 선별적으로 API를 사용하도록 하자.\n\n## React 테스트하기…\nReact는 Enzyme과 같은 유틸리티의 도움으로 컴포넌트 단위로 유닛 테스트가 상당히 용이하다. 비록 완벽한 브라우저 상의 테스트는 아닐지라도 이런 유틸리티 덕분에 렌더링과 주요 비즈니스 로직 등을 테스트하기에 부족함이 없기에 React로 만든 웹앱은 테스트가 상당히 용이한 편이다. (Angular 1.x의 테스트와 비교하면…) 상대적으로 적은 노력으로 큰 효과를 얻을 수 있다는 점이 많은 사람들로 하여금 React를 사용하게 만드는 한가지 이유라고 생각한다.\n<br>\n\n***스팀잇에 처음으로 쓰는 글이 개발 관련 글이네요...아직 익숙하지가 않아서 기존 글을 포팅하는 수준이지만, 계속해서 컨텐츠를 생산해볼까 합니다. 응원 및 조언 부탁드립니다!*",
      "json_metadata": "{\"tags\":[\"kr-dev\",\"kr\",\"dev\"],\"image\":[\"https://steemitimages.com/DQmUaYAPqpMGUzWdXxNaF4uc6u3G44n9uiezxKTivkAp76i/enzyme.jpeg\"],\"links\":[\"https://github.com/airbnb/enzyme\",\"https://reactjs.org/docs/test-utils.html\",\"https://mochajs.org/\",\"http://web-front-end.tistory.com/26\",\"http://sinonjs.org/\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}",
      "parent_author": "",
      "parent_permlink": "kr-dev",
      "permlink": "react",
      "title": "[개발] React 테스트 코드 작성하기"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-05-17T07:31:51",
  "trx_id": "0868a0da1273f5b0cf46431eb075c093461320fd",
  "trx_in_block": 5,
  "virtual_op": 0
}
big-whalesent 0.001 SBD to @echo304- "want to Re-stem with 9000+ FOLLOWERS | SEND 0.5 OR 1 SBD to @big-whale and give your post a double chance with 9000+ follower _+ 10 min plus up-votes on your post."
2018/03/14 09:47:57
amount0.001 SBD
frombig-whale
memowant to Re-stem with 9000+ FOLLOWERS | SEND 0.5 OR 1 SBD to @big-whale and give your post a double chance with 9000+ follower _+ 10 min plus up-votes on your post.
toecho304
Transaction InfoBlock #20664776/Trx 2e9b5a457d09a66cf89c0e49b3b8bda7ea52b7ce
View Raw JSON Data
{
  "block": 20664776,
  "op": [
    "transfer",
    {
      "amount": "0.001 SBD",
      "from": "big-whale",
      "memo": "want to Re-stem with 9000+ FOLLOWERS | SEND 0.5 OR 1 SBD to @big-whale and give your post a double chance with 9000+ follower _+ 10 min plus up-votes on your post.",
      "to": "echo304"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-03-14T09:47:57",
  "trx_id": "2e9b5a457d09a66cf89c0e49b3b8bda7ea52b7ce",
  "trx_in_block": 60,
  "virtual_op": 0
}
2018/03/13 04:42:00
authorjchoy
permlinkico-finma-guideline
voterecho304
weight10000 (100.00%)
Transaction InfoBlock #20629873/Trx ef95ce0327f4c72a7e273804c93d0658ceb4dc58
View Raw JSON Data
{
  "block": 20629873,
  "op": [
    "vote",
    {
      "author": "jchoy",
      "permlink": "ico-finma-guideline",
      "voter": "echo304",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-03-13T04:42:00",
  "trx_id": "ef95ce0327f4c72a7e273804c93d0658ceb4dc58",
  "trx_in_block": 28,
  "virtual_op": 0
}
steemdelegated 18.682 SP to @echo304
2018/03/13 00:47:09
delegateeecho304
delegatorsteem
vesting shares30422.046573 VESTS
Transaction InfoBlock #20625177/Trx e3618ab9473c3f4293ee799df0ed6b8d7e238eaf
View Raw JSON Data
{
  "block": 20625177,
  "op": [
    "delegate_vesting_shares",
    {
      "delegatee": "echo304",
      "delegator": "steem",
      "vesting_shares": "30422.046573 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2018-03-13T00:47:09",
  "trx_id": "e3618ab9473c3f4293ee799df0ed6b8d7e238eaf",
  "trx_in_block": 46,
  "virtual_op": 0
}

Account Metadata

POSTING JSON METADATA
None
JSON METADATA
None
{
  "posting_json_metadata": {},
  "json_metadata": {}
}

Auth Keys

Owner
Single Signature
Public Keys
STM7heJSTjyiq1ERUuSP1XoWTP6UrHMdpcuVsMwTuHvJ1FzvwPzwd1/1
Active
Single Signature
Public Keys
STM7WE2RE7yynfMLAAjPBXhXk8kRjyYs8kKiB12G2QT38CEBfsret1/1
Posting
Single Signature
Public Keys
STM5nVW7yycrpZjzy63rgo5Cp1bjjewuWbBwb7fTvKUfnQG7R9V1A1/1
Memo
STM6g1WjD4Bh1xXuRTuFy6zBEuxBHZB1HSkbWXYxSFcKnxuG6He5r
{
  "owner": {
    "account_auths": [],
    "key_auths": [
      [
        "STM7heJSTjyiq1ERUuSP1XoWTP6UrHMdpcuVsMwTuHvJ1FzvwPzwd",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "active": {
    "account_auths": [],
    "key_auths": [
      [
        "STM7WE2RE7yynfMLAAjPBXhXk8kRjyYs8kKiB12G2QT38CEBfsret",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "posting": {
    "account_auths": [],
    "key_auths": [
      [
        "STM5nVW7yycrpZjzy63rgo5Cp1bjjewuWbBwb7fTvKUfnQG7R9V1A",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "memo": "STM6g1WjD4Bh1xXuRTuFy6zBEuxBHZB1HSkbWXYxSFcKnxuG6He5r"
}

Witness Votes

0 / 30
No active witness votes.
[]